Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / ASM

Self Deleting Executables

Rate me:
Please Sign up or sign in to vote.
4.67/5 (46 votes)
27 Jan 2010CPOL11 min read 202.5K   1.9K   167   67
How to write a program that deletes itself

Contents

Introduction

I read an interesting article the other day that spoke about the various mechanisms a Win32 application can employ for deleting itself from the disk once execution completes. The basic issue is, of course, that while the module is being executed, the operating system has the file locked. So, something like this will just not work:

C++
TCHAR szModule[MAX_PATH];
GetModuleFileName( NULL, szModule, MAX_PATH );
DeleteFile( szModule );

Of the various options available, the author of the said article suggests the following approach as being the definitive one as it has the added benefit of functioning correctly on all versions of Microsoft Windows (starting with '95).

Now would be a good time to hop over to the article and see what it's about (and while you're there, make sure you look at some of the other articles - pretty neat site). Here's the link:

And, here's the approach, in brief:

  • When it's time to delete ourselves, we first spawn an external process that is guaranteed to exist on all Windows computers (explorer.exe, for example) in the suspended state. We do this by calling CreateProcess, passing CREATE_SUSPENDED for the dwCreationFlags parameter. Note that when a process is launched this way, there's really no telling at what point the primary thread of the process will get suspended. But, it does appear to get suspended long before the entry point gets invoked and, in fact, it occurs even before the Win32 environment for the process has been fully initialized.
  • After this, we get the CONTEXT data (basically, the CPU register state) for the suspended primary thread (from the remote process) via GetThreadContext.
  • We then manipulate the stack pointer (ESP) to allocate some space on the remote stack for storing some of our data (like the path to the executable to be deleted). After this, we plonk the binary code for a local routine that we've written for deleting files over to the remote process (along with the data it needs), by calling WriteProcessMemory.
  • Next, we mess around with the instruction pointer (EIP) so that it points to the binary code we've copied to the remote process, and update the suspended thread's context (via SetThreadContext).
  • And finally, we resume execution of the remote process (via ResumeThread). Since the EIP in the remote thread is now pointing to our code, it executes it; which of course, happily deletes the original executable. And that's it!

That Easy, Eh? Well, Yeah, But...

While this approach does get the job done, the fact that our deletion code executes in the remote process even before Windows has had a chance to initialize it fully, places some restrictions on the kind of APIs that we can invoke. It so turns out that APIs like DeleteFile and ExitProcess do work while the process is in this half-baked state. I figured, I'll modify the approach somewhat so that it allows us to call any API we want from our injected code. Here's what I did:

  • As before, we launch the external process in a suspended state. However, instead of plonking our code at the location that ESP happens to be pointing at when it got suspended, we put it over the executable's entry-point routine, i.e., we replace the remote process' entry point with our own injected code. And when the entry-point code executes, we can be pretty sure that the Win32 environment is fully initialized and primed for use!
  • Figuring out where the entry point of a module lives requires us to parse PE file format structures. In your own program, for example, the following code would give you a pointer to the entry point routine in the process' executable image:
  • C++
    #pragma pack( push, 1 )
    
    struct coff_header
    {
        unsigned short machine;
        unsigned short sections;
        unsigned int timestamp;
        unsigned int symboltable;
        unsigned int symbols;
        unsigned short size_of_opt_header;
        unsigned short characteristics;
    };
    
    struct optional_header
    {
        unsigned short magic;
        char linker_version_major;
        char linker_version_minor;
        unsigned int code_size;
        unsigned int idata_size;
        unsigned int udata_size;
        unsigned int entry_point;
        unsigned int code_base;
    };
    
    #pragma pack( pop )
    
    //
    // get the module address
    //
    
    char *module = (char *)GetModuleHandle( NULL );
    
    //
    // get the sig
    //
    
    int *offset = (int*)( module + 0x3c );
    char *sig = module + *offset;
    
    //
    // get the coff header
    //
    
    coff_header *coff = (coff_header *)( sig + 4 );
    
    //
    // get the optional header
    //
    
    optional_header *opt = (optional_header *)( (char *)coff + sizeof( coff_header ) );
    
    //
    // get the entry point
    //
    
    char *entry_point = (char *)module + opt->entry_point;
  • The entry point that you define, by the way, - main or WinMain - isn't the actual entry point routine. The compiler inserts its own entry point, which in turn calls our function. This entry point, typically, does stuff like CRT initialization and cleanup. In an ANSI console app, for instance, the actual entry point routine is something called mainCRTStartup.

Where Art Thou, Oh! Great Entry Point of the Remote Process?

  • It appears logical, therefore, that we should be able to find the entry point routine in remote processes also in a similar fashion, using ReadProcessMemory. While that is so, finding the equivalent of the module variable in the code given above for remote processes turns out to be trickier than anticipated. The problem is that there is no convenient GetModuleHandle routine that'll work for remote processes.
  • As it turns out, GetModuleHandle returns a virtual address that is valid only within the relevant process' address space. ReadProcessMemory, however, requires real addresses to work with. So the question is, how do we get to know the remote process' base address in memory? The solution, as it turned out, requires us to dig deep into the OS's internals! The credit for this solution goes to Ashkbiz Danehkar whose article called Injective Code Inside Import Table outlines a method for finding this.
  • In brief, the operating system maintains a user-mode data structure for every thread in the system, called the Thread Environment Block (TEB), which describes pretty much everything you'd want to know about the thread, including a pointer to another data structure called the Process Environment Block (PEB) which, as may be apparent, describes processes, including, happily for us, a pointer to the image's base address in memory! These structures are not, however, documented (by Microsoft that is ;). But some very, very clever folks here have managed to figure out the layout for these structures all by themselves!
  • So, all we need to do is:
    • Figure out where the TEB for the primary thread lives in the remote process; this information is stored in the thread's FS register, which is accessible via the GetThreadSelectorEntry API.
    • Read the PEB using the pointer to it in the thread's TEB via ReadProcessMemory.
    • Use the pointer to the image's base address in the PEB, and parse the PE structures till we are left with a reference to the remote process' entry point routine.
    • Phew!

    Here's the code that achieves this:

    C++
    //
    // Gets the address of the entry point routine given a
    // handle to a process and its primary thread.
    //
    
    DWORD GetProcessEntryPointAddress( HANDLE hProcess, HANDLE hThread )
    {
        CONTEXT             context;
        LDT_ENTRY           entry;
        TEB                 teb;
        PEB                 peb;
        DWORD               read;
        DWORD               dwFSBase;
        DWORD               dwImageBase, dwOffset;
        DWORD               dwOptHeaderOffset;
        optional_header     opt;
    
        //
        // get the current thread context
        //
    
        context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
        GetThreadContext( hThread, &context );
    
        //
        // use the segment register value to get a pointer to
        // the TEB
        //
    
        GetThreadSelectorEntry( hThread, context.SegFs, &entry );
        dwFSBase = ( entry.HighWord.Bits.BaseHi << 24 ) |
                         ( entry.HighWord.Bits.BaseMid << 16 ) |
                         ( entry.BaseLow );
    
        //
        // read the teb
        //
    
        ReadProcessMemory( hProcess, (LPCVOID)dwFSBase,
                           &teb, sizeof( TEB ), &read );
    
        //
        // read the peb from the location pointed at by the teb
        //
    
        ReadProcessMemory( hProcess, (LPCVOID)teb.Peb,
                           &peb, sizeof( PEB ), &read );
    
        //
        // figure out where the entry point is located;
        //
    
        dwImageBase = (DWORD)peb.ImageBaseAddress;
        ReadProcessMemory( hProcess, (LPCVOID)( dwImageBase + 0x3c ),
                           &dwOffset, sizeof( DWORD ), &read );
    
        dwOptHeaderOffset = ( dwImageBase + dwOffset + 4 + sizeof( coff_header ) );
        ReadProcessMemory( hProcess, (LPCVOID)dwOptHeaderOffset,
                           &opt, sizeof( optional_header ), &read );
    
        return ( dwImageBase + opt.entry_point );
    }
  • If you're wondering what the weird code initializing dwFSBase means, all I can do is direct you to the documentation for the LDT_ENTRY data structure in MSDN. Structures of this kind are partly the reason why system programmers tend to go bald early in life!

Passing Data to the Remote Process

  • Now that we know where the entry point lives in the remote process, it should be really straightforward, right? Wrong! There still is that itsy bitsy problem of figuring out how we are to pass data to the remote process!
  • The routine that deletes our executable looks like this:

    C++
    #pragma pack(push, 1)
    
    //
    //  Structure to inject into remote process. Contains
    //  function pointers and code to execute.
    //
    
    typedef struct _SELFDEL
    {
        HANDLE  hParent;                // parent process handle
    
    
        FARPROC fnWaitForSingleObject;
        FARPROC fnCloseHandle;
        FARPROC fnDeleteFile;
        FARPROC fnSleep;
        FARPROC fnExitProcess;
        FARPROC fnRemoveDirectory;
        FARPROC fnGetLastError;
        FARPROC fnLoadLibrary;
        FARPROC fnGetProcAddress;
        BOOL    fRemDir;
        TCHAR   szFileName[MAX_PATH];   // file to delete
    
    } SELFDEL;
    
    #pragma pack(pop)
    
    //
    //  Routine to execute in remote process.
    //
    
    void remote_thread(SELFDEL *remote)
    {
        // wait for parent process to terminate
    
        remote->fnWaitForSingleObject(remote->hParent, INFINITE);
        remote->fnCloseHandle(remote->hParent);
    
        // try to delete the executable file
    
        while(!remote->fnDeleteFile(remote->szFileName))
        {
            // failed - try again in one second's time
    
            remote->fnSleep(1000);
        }
    
        // finished! exit so that we don't execute garbage code
    
        remote->fnExitProcess(0);
    }
  • As you might have noticed, the function remote_thread makes all system calls via function pointers, instead of calling them directly. This is done because, in the normal course, the compiler generates tiny stubs whenever calls to routines in dynamically loaded DLLs are made from a program. This stub jumps to a function pointer stored in a table initialized by the operating system's loader at runtime. Since we don't want these fancy stubs generated for code that is meant to be injected into a remote process, we deal exclusively with function pointers.

    Fortunately for us, the system APIs (in kernel32, user32 etc.) always get loaded at the same virtual address in all processes. So, all we need to do is initialize a data structure with pointers to all the system calls we want to make from the remote process, and pass this structure along also. With our entry-point overwrite strategy, of course, how are we to do this? To make a long story short, I settled for the following approach.

  • First, I modified remote_thread to look like this:
  • C++
    //
    //  Routine to execute in remote process.
    //
    
    void remote_thread()
    {
        //
        // this will get replaced with a
        // real pointer to the data when it
        // gets injected into the remote
        // process
        //
    
        SELFDEL *remote = (SELFDEL *)0xFFFFFFFF;
    
        //
        // wait for parent process to terminate
        //
    
        remote->fnWaitForSingleObject(remote->hParent, INFINITE);
        remote->fnCloseHandle(remote->hParent);
    
        //
        // try to delete the executable file
        //
    
        while(!remote->fnDeleteFile(remote->szFileName))
        {
            //
            // failed - try again in one second's time
            //
    
            remote->fnSleep(1000);
        }
    
        //
        // finished! exit so that we don't execute garbage code
        //
    
        remote->fnExitProcess(0);
    }
  • I then converted this into shellcode. If you've never heard of the term shellcode before, then here's a quick primer on what it is. If you know what it is already, then feel free to skip the next section.

Shellcode?

shellcode is the technical term (in security circles) for binary machine code that is typically used in exploits as the payload. Here's a quick and dirty way of generating the shellcode from the obj file generated when you compile your source files. In our case, we are interested in whipping the shellcode up for the remote_thread routine. Here's what you've got to do:

  • First, compile your source file (in our case, selfdel.c) using the /c command line option. This causes the compiler to skip the linking step.
  • cl /nologo /c selfdel.c
  • Now, use the utility dumpbin to disassemble your obj file like so:
  • dumpbin /disasm:bytes selfdel.obj > s.asm

    This produces a file called s.asm that looks like this:

    ASM
    Microsoft (R) COFF/PE Dumper Version 8.00.50727.42
    Copyright (C) Microsoft Corporation. All rights reserved.
    
    
    Dump of file selfdel.obj
    
    File Type: COFF OBJECT
    
    _remote_thread:
      00000000: 55                 push        ebp
      00000001: 8B EC              mov         ebp,esp
      00000003: 83 EC 10           sub         esp,10h
      00000006: 53                 push        ebx
      00000007: C7 45 F0 FF FF FF  mov         dword ptr [ebp-10h],0FFFFFFFFh
                FF
      0000000E: 8B 45 F0           mov         eax,dword ptr [ebp-10h]
    
    ...... more stuff like this ......
    
      000000D2: C3                 ret ; <-- this marks the end of the
                                             assembly dump for _remote_thread
    
    ...... even more stuff like this ......
  • The line _remote_thread: marks the beginning of the assembly dump for the remote_thread routine, and the line containing the ret statement indicates the end of the routine. Open s.asm in VS.NET 2002/2003/2005, and delete everything except the stuff that falls between _remote_thread: and ret. Delete the line containing _remote_thread: as well, so you end up with something that looks like this:
  • ASM
      00000000: 55                 push        ebp
      00000001: 8B EC              mov         ebp,esp
      00000003: 83 EC 10           sub         esp,10h
      00000006: 53                 push        ebx
      00000007: C7 45 F0 FF FF FF  mov         dword ptr [ebp-10h],0FFFFFFFFh
                FF
      0000000E: 8B 45 F0           mov         eax,dword ptr [ebp-10h]
    
    ...... more stuff like this ......
    
      000000D2: C3                 ret
  • What you're left with is a file that can be divided into three logical columns.
    • All the stuff till and including the ':' is the byte-offset for that instruction. So when you see the number '00000003', it indicates that that instruction is 3 bytes away from the beginning of the routine.
    • This is followed by the machine code for that instruction with one or more trailing white space characters.
    • All the stuff after the machine code is the assembly instruction.
  • We are interested in the second column, which we extract out using some nifty regular expressions in Visual Studio's Find/Replace dialog. Open the Find/Replace dialog in Visual Studio, and run the following expressions in the given order:
  • Sl. No.FindReplaceDescription
    1<nobr>[0-9A-F]+\::b+{[0-9A-F:b]+}.*</nobr>\1Strips the first and the third column from the file.
    2^:b+nothingRemoves leading white space. Ensure that there is absolutely nothing in the "Replace with" textbox.
    3:b+$nothingRemoves trailing white space. Again, ensure that there is absolutely nothing in the "Replace with" textbox.
    4\nspaceRemoves all new line characters from the file so there's just one line in the file. Enter a single space character in the "Replace with" textbox.
    5space<nobr>', '\\x</nobr>Replaces all space characters with the literal ', '\x.
  • Now, type this at the beginning of the line:
  • C++
    char shellcode[] = { '\x

    And finally, type this at the end of the line:

    C++
    ' };

    After all of this, you should end up with something that looks like this:

    C++
    char shellcode[] = {
        '\x55', '\x8B', '\xEC', '\x83', '\xEC',
        '\x10', '\x53', '\xC7', '\x45', '\xF0',
        '\xFF', '\xFF', '\xFF', '\xFF', '\x8B',
    
        // ...... more stuff like this ......
    
    
        '\x5B', '\x8B', '\xE5', '\x5D', '\xC3'
    };

    Phew!

What Do We Do With this Shellcode?

  • On converting remote_thread into shellcode, we are left with something that looks like this (this is just representative shellcode, and not the one that got generated for the routine shown above):
    C++
    char shellcode[] = {
        '\x55', '\x8B', '\xEC', '\x83', '\xEC',
        '\x10', '\x53', '\xC7', '\x45', '\xF0',
        '\xFF', '\xFF', '\xFF', '\xFF',   // replace these 4 bytes
    
                                          // with actual address
    
    
        '\x8B', '\x45', '\xF0', '\x8B', '\x48',
        '\x20', '\x89', '\x4D', '\xF4', '\x8B',
        '\x55', '\xF0', '\x8B', '\x42', '\x24',
        '\x89', '\x45', '\xFC', '\x6A', '\xFF', ... more shell code here

    As it turns out in our case, the value 0xFFFFFFFF that we initialized the pointer remote with in remote_thread shows up the exact same way in the shellcode also. Since we know where the entry point lives in the remote process, all we need to do is to first replace 0xFFFFFFFF in the shellcode with the actual pointer to the data before over-writing the entry point. Here's how this looks:

    C++
    STARTUPINFO             si = { sizeof(si) };
    PROCESS_INFORMATION     pi;
    SELFDEL                 local;
    DWORD                   data;
    TCHAR                   szExe[MAX_PATH] = _T( "explorer.exe" );
    DWORD                   process_entry;
    
    //
    // this shellcode self-deletes and then shows a messagebox
    //
    
    char shellcode[] = {
        '\x55', '\x8B', '\xEC', '\x83',
        '\xEC', '\x10', '\x53', '\xC7',
    
        '\xFF', '\xFF', '\xFF', '\xFF',   // replace these 4 bytes
    
                                          // with actual address
    
        '\x8B', '\x45', '\xF0', '\x8B',
        '\x48', '\x20', '\x89', '\x4D',
    
    
        ... snipped lots of meaningless shellcode here! ...
    
        '\xFF', '\xD0', '\x5B', '\x8B',
        '\xE5', '\x5D', '\xC3'
    
    };
    
    //
    // initialize the SELFDEL object
    //
    
    local.fnWaitForSingleObject     = (FARPROC)WaitForSingleObject;
    local.fnCloseHandle             = (FARPROC)CloseHandle;
    local.fnDeleteFile              = (FARPROC)DeleteFile;
    local.fnSleep                   = (FARPROC)Sleep;
    local.fnExitProcess             = (FARPROC)ExitProcess;
    local.fnRemoveDirectory         = (FARPROC)RemoveDirectory;
    local.fnGetLastError            = (FARPROC)GetLastError;
    local.fnLoadLibrary             = (FARPROC)LoadLibrary;
    local.fnGetProcAddress          = (FARPROC)GetProcAddress;
    
    //
    // Give remote process a copy of our own process handle
    //
    
    DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
        pi.hProcess, &local.hParent, 0, FALSE, 0);
    GetModuleFileName(0, local.szFileName, MAX_PATH);
    
    //
    // get the process's entry point address
    //
    
    process_entry = GetProcessEntryPointAddress( pi.hProcess, pi.hThread );
    
    //
    // replace the address of the data inside the
    // shellcode (bytes 10 to 13)
    //
    
    data = process_entry + sizeof( shellcode );
    shellcode[13] = (char)( data >> 24 );
    shellcode[12] = (char)( ( data >> 16 ) & 0xFF );
    shellcode[11] = (char)( ( data >> 8 ) & 0xFF );
    shellcode[10] = (char)( data & 0xFF );
    
    //
    // copy our code+data at the exe's entry-point
    //
    
    VirtualProtectEx( pi.hProcess,
                      (PVOID)process_entry,
                      sizeof( local ) + sizeof( shellcode ),
                      PAGE_EXECUTE_READWRITE,
                      &oldProt );
    WriteProcessMemory( pi.hProcess,
                        (PVOID)process_entry,
                        shellcode,
                        sizeof( shellcode ), 0);
    WriteProcessMemory( pi.hProcess,
                        (PVOID)data,
                        &local,
                        sizeof( local ), 0);
    
    //
    // Let the process continue
    //
    
    ResumeThread(pi.hThread);

    There! That's all there is to it.

Please find the code for a self-deleting executable (that among other things, also displays a message box from the remote process' hijacked entry point) here.

Conclusion

You'll probably never have to write a program that deletes itself, in your career; but there are a few nifty, if somewhat esoteric, tricks in there, eh?! One commercial program that does this sort of thing is the helper program that you download when you use the Copilot service. However, I suspect that program uses a different technique that is probably a lot more straightforward and a gazillion times less interesting ;). Briefly, an implementation note in Copilot's functional specification document states that self-deletion is a simple matter of:

  • embedding a small EXE as a resource into your main program
  • extracting this EXE to a temporary location, passing FILE_FLAG_DELETE_ON_CLOSE to CreateFile
  • and having the small EXE wait till the main EXE exits, before deleting it

The FILE_FLAG_DELETE_ON_CLOSE flag should cause the OS to delete the small EXE when all open handles to it are closed. That's really neat, but hey, where's the fun in that?!

Revision history

  • January 07, 2007: Article first published
  • January 27, 2010: Fixed typo in the search/replace strings for generating shellcode

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Microsoft
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
SuggestionA better solution (with source code) Pin
Elmue8-Jul-15 9:18
Elmue8-Jul-15 9:18 
QuestionNice Article! Pin
rgawdzik29-Sep-12 19:19
rgawdzik29-Sep-12 19:19 
AnswerRe: Nice Article! Pin
Rajasekharan Vengalil29-Sep-12 20:29
Rajasekharan Vengalil29-Sep-12 20:29 
GeneralMy vote of 5 Pin
gndnet27-Jun-12 21:57
gndnet27-Jun-12 21:57 
GeneralRe: My vote of 5 Pin
Rajasekharan Vengalil29-Sep-12 20:29
Rajasekharan Vengalil29-Sep-12 20:29 
Questionbatch rocks! Pin
K4HVDs14-Apr-12 21:28
K4HVDs14-Apr-12 21:28 
GeneralMy vote of 5 Pin
K4HVDs14-Apr-12 21:24
K4HVDs14-Apr-12 21:24 
GeneralRe: My vote of 5 Pin
Rajasekharan Vengalil29-Sep-12 20:29
Rajasekharan Vengalil29-Sep-12 20:29 
GeneralMy vote of 5 Pin
Mukit, Ataul17-Jun-11 5:17
Mukit, Ataul17-Jun-11 5:17 
GeneralRe: My vote of 5 Pin
Rajasekharan Vengalil8-Dec-11 20:53
Rajasekharan Vengalil8-Dec-11 20:53 
GeneralA slightly different approach [modified] Pin
Absolute Zero7-Apr-11 14:05
Absolute Zero7-Apr-11 14:05 
GeneralRe: A slightly different approach Pin
Absolute Zero8-Apr-11 0:01
Absolute Zero8-Apr-11 0:01 
GeneralRe: A slightly different approach Pin
Rajasekharan Vengalil8-Apr-11 0:52
Rajasekharan Vengalil8-Apr-11 0:52 
GeneralRe: A slightly different approach Pin
Rajasekharan Vengalil8-Apr-11 0:47
Rajasekharan Vengalil8-Apr-11 0:47 
GeneralRe: A slightly different approach Pin
Absolute Zero8-Apr-11 2:52
Absolute Zero8-Apr-11 2:52 
GeneralRe: A slightly different approach Pin
Rajasekharan Vengalil9-Apr-11 5:14
Rajasekharan Vengalil9-Apr-11 5:14 
GeneralRe: A slightly different approach Pin
rogndeb1-Jun-11 17:05
rogndeb1-Jun-11 17:05 
GeneralRe: A slightly different approach (64 bit) Pin
TahoeKid15-Jun-11 10:20
TahoeKid15-Jun-11 10:20 
Questionreadprocessmemory with teb fails with error 998 Pin
himanshupareek13-Dec-10 19:56
professionalhimanshupareek13-Dec-10 19:56 
AnswerRe: readprocessmemory with teb fails with error 998 Pin
Rajasekharan Vengalil13-Dec-10 22:21
Rajasekharan Vengalil13-Dec-10 22:21 
GeneralRe: readprocessmemory with teb fails with error 998 Pin
himanshupareek14-Dec-10 1:08
professionalhimanshupareek14-Dec-10 1:08 
GeneralRe: readprocessmemory with teb fails with error 998 Pin
Rajasekharan Vengalil14-Dec-10 1:11
Rajasekharan Vengalil14-Dec-10 1:11 
GeneralRe: readprocessmemory with teb fails with error 998 Pin
himanshupareek14-Dec-10 21:45
professionalhimanshupareek14-Dec-10 21:45 
GeneralMy vote of 5 Pin
nenfa30-Jul-10 4:20
nenfa30-Jul-10 4:20 
GeneralRe: My vote of 5 Pin
Rajasekharan Vengalil1-Nov-10 18:05
Rajasekharan Vengalil1-Nov-10 18:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.