daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

thread-execution-hijacking.md (9370B)


      1 ---
      2 title: "Thread Execution Hijacking"
      3 description: "Suspending an existing thread and repointing its instruction pointer at your code, so nothing new is ever created to notice."
      4 category: exploitation
      5 subcategory: "Process Injection"
      6 tags: [process-injection, evasion, windows, t1055, entry-point]
      7 tools: [windbg, process-hacker, sysmon, volatility]
      8 difficulty: advanced
      9 updated: 2026-10-03
     10 upstreamName: "ired.team"
     11 upstreamUrl: "https://ired.team/offensive-security/code-injection-process-injection/injecting-to-remote-process-via-thread-hijacking"
     12 upstreamAuthor: "Mantvydas Baranauskas"
     13 upstreamLicense: none
     14 upstreamRelation: derived
     15 references:
     16   - name: "AddressOfEntryPoint code injection"
     17     url: "https://ired.team/offensive-security/code-injection-process-injection/addressofentrypoint-code-injection"
     18     author: "Mantvydas Baranauskas"
     19     license: none
     20     relation: derived
     21     note: "The suspended-process variant that overwrites the image entry point instead of repointing RIP."
     22   - name: "AddressOfEntryPoint injection without VirtualAllocEx RWX"
     23     url: "https://ired.team/offensive-security/code-injection-process-injection/addressofentrypoint-code-injection-without-virtualallocex-rwx"
     24     author: "Mantvydas Baranauskas"
     25     license: none
     26     relation: derived
     27     note: "The variant that avoids allocating a fresh RWX region; folded in below with the caveat about transient page protection."
     28   - name: "MITRE ATT&CK T1055.003 — Thread Execution Hijacking"
     29     url: "https://attack.mitre.org/techniques/T1055/003/"
     30     license: none
     31     relation: inspired
     32     note: "Technique ID and detection data sources."
     33 ---
     34 
     35 ## What this covers
     36 
     37 Instead of creating a thread to run your code, you take one that already exists. Suspend it,
     38 read its register context, point its instruction pointer at shellcode you have written into
     39 the process, and resume. The thread belongs to the target, was started by the target, and
     40 appears in no remote-thread telemetry.
     41 
     42 Two variants are folded in here because they are the same idea at different moments:
     43 
     44 - **Running-thread hijack** — against a live process, grab its main thread and repoint `RIP`.
     45 - **AddressOfEntryPoint** — against a process you created suspended, overwrite the image's
     46   entry point so the thread runs your code when first resumed. No context manipulation
     47   needed, because the thread has not started yet.
     48 
     49 ## Prerequisites
     50 
     51 - `PROCESS_VM_OPERATION`, `PROCESS_VM_WRITE` on the process, and `THREAD_SUSPEND_RESUME`,
     52   `THREAD_GET_CONTEXT`, `THREAD_SET_CONTEXT` on the thread.
     53 - Shellcode must be position-independent. You are jumping into it with whatever register
     54   state the hijacked thread happened to have.
     55 - **Restore or accept the loss.** Save the original context before you change it. Code that
     56   does not return to the saved `RIP` kills the host thread, which is often the difference
     57   between a quiet injection and an application that visibly hangs.
     58 
     59 ## Walkthrough — running-thread hijack
     60 
     61 | # | Call | Purpose | Artefact |
     62 |---|---|---|---|
     63 | 1 | `OpenProcess` | handle to the target | Sysmon 10 |
     64 | 2 | `VirtualAllocEx(..., PAGE_EXECUTE_READWRITE)` | space for shellcode | private RWX region |
     65 | 3 | `WriteProcessMemory` | the shellcode | cross-process write |
     66 | 4 | `CreateToolhelp32Snapshot` / `Thread32First` | find a thread of the target | thread enumeration |
     67 | 5 | `OpenThread(THREAD_ALL_ACCESS, ...)` | handle to that thread | — |
     68 | 6 | `SuspendThread` | freeze it | a suspended thread in a running process |
     69 | 7 | `GetThreadContext` | capture registers | — |
     70 | 8 | set `ctx.Rip = shellcodeAddress` | redirect | **thread with RIP outside any module** |
     71 | 9 | `SetThreadContext` | apply | EDR commonly hooks `NtSetContextThread` |
     72 | 10 | `ResumeThread` | run | execution from private memory |
     73 
     74 Finding the target's thread is step one in practice — you need a thread ID, usually the
     75 process's main thread.
     76 
     77 <figure class="shot">
     78   <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28609%29.png"
     79        alt="Thread listing for the target notepad process with its main thread ID identified"
     80        loading="lazy" referrerpolicy="no-referrer">
     81   <figcaption>Locating the target's main thread ID.
     82     <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
     83   </figcaption>
     84 </figure>
     85 
     86 The captured context is what you must preserve — it holds the CPU registers as the thread
     87 was executing, and it is the only route back to normal operation.
     88 
     89 <figure class="shot">
     90   <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28612%29.png"
     91        alt="Retrieved thread context structure showing the hijacked thread's CPU register values"
     92        loading="lazy" referrerpolicy="no-referrer">
     93   <figcaption>The hijacked thread's captured register context.
     94     <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
     95   </figcaption>
     96 </figure>
     97 
     98 After the context is set, `RIP` points into the allocated region rather than into any loaded
     99 module — which is simultaneously the point of the technique and the way it is caught.
    100 
    101 <figure class="shot">
    102   <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28613%29.png"
    103        alt="Thread context showing RIP now set to the shellcode address inside the target process memory"
    104        loading="lazy" referrerpolicy="no-referrer">
    105   <figcaption><code>RIP</code> repointed to the shellcode address in the target's memory.
    106     <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
    107   </figcaption>
    108 </figure>
    109 
    110 <figure class="shot">
    111   <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28617%29.png"
    112        alt="Shellcode executing after the hijacked thread is resumed, with a shell returned"
    113        loading="lazy" referrerpolicy="no-referrer">
    114   <figcaption>Execution after <code>ResumeThread</code>.
    115     <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
    116   </figcaption>
    117 </figure>
    118 
    119 ## Walkthrough — AddressOfEntryPoint
    120 
    121 Against a process started `CREATE_SUSPENDED`, the thread exists but has not executed, so
    122 there is no context to juggle:
    123 
    124 1. `CreateProcessA(..., CREATE_SUSPENDED, ...)`.
    125 2. Read the PEB to find `ImageBaseAddress`, parse the headers, compute
    126    `ImageBase + OptionalHeader.AddressOfEntryPoint`.
    127 3. `WriteProcessMemory` your shellcode directly over the entry point.
    128 4. `ResumeThread`.
    129 
    130 **On the "without RWX" framing.** This variant is often presented as avoiding RWX memory,
    131 and it does avoid allocating a *fresh* RWX region — you are writing into the image's own
    132 executable section. But `WriteProcessMemory` has to make that page writable to write it, so
    133 an executable page does transiently become writable regardless. The honest claim is that
    134 there is no new private RWX allocation to find afterwards, not that no write-to-executable
    135 ever happens. A hook on `NtProtectVirtualMemory` still sees it.
    136 
    137 The upside for the operator is real though: the shellcode lives inside an image-backed,
    138 on-disk-matching region, so `malfind`-style private-RWX hunting misses it entirely. The
    139 catch moves to comparing the in-memory entry point against the file on disk.
    140 
    141 ## Detection
    142 
    143 **Thread start address vs module list.** `NtQueryInformationThread` with
    144 `ThreadQuerySetWin32StartAddress`, compared against the loaded modules, finds any thread
    145 beginning in unbacked memory. Works on the running-thread variant.
    146 
    147 **`NtSetContextThread` on a foreign thread.** Setting the context of a thread in another
    148 process is rare outside debuggers. Most EDR products hook this specifically, because it is
    149 the one call the running-thread variant cannot avoid.
    150 
    151 **No Sysmon event 8.** Worth stating plainly: hijacking creates no remote thread, so
    152 `CreateRemoteThread` telemetry never fires. A detection strategy resting on event 8 does not
    153 see this technique at all. Event 10 (`ProcessAccess`) is the one that fires, with
    154 `GrantedAccess` including `PROCESS_VM_WRITE`.
    155 
    156 **Suspend/resume pattern.** `SuspendThread` on a thread you do not own, followed within
    157 milliseconds by `SetThreadContext` and `ResumeThread`, is the signature. Thread suspension
    158 counts are visible in `Process Hacker`.
    159 
    160 **Entry-point integrity.** For the AddressOfEntryPoint variant, read the first bytes at
    161 `ImageBase + AddressOfEntryPoint` in memory and compare with the same offset in the file on
    162 disk. A mismatch in a process that has just started is conclusive, and it is the only check
    163 that catches this variant reliably.
    164 
    165 **Private RWX.** Applies to the running-thread variant only — step 2 leaves a
    166 `MEM_PRIVATE` executable region. `malfind`, or `VirtualQueryEx` enumeration looking for
    167 committed executable private pages.
    168 
    169 ## References
    170 
    171 - [MITRE ATT&CK T1055.003](https://attack.mitre.org/techniques/T1055/003/)
    172 - [Process hollowing](/sheets/exploitation/process-hollowing) — the variant that unmaps the
    173   host image first
    174 - [MITRE ATT&CK T1055.004](https://attack.mitre.org/techniques/T1055/004/) — APC queue
    175   injection, the other route to running code on a thread you did not create