commit a068c1c05c2fe92533965fd1eda87986d94ff613
parent d69495a0717a9abaec8ab18ea0cfe3f50cc80cbd
Author: DAEMON <zer0sec.xp@icloud.com>
Date: Sat, 3 Oct 2026 18:51:29 +0100
Add three process-injection sheets under exploitation
Partial batch 1 of the ired.team integration: process hollowing, classic
DLL injection and thread execution hijacking, filed
subcategory 'Process Injection'. The exploitation category promised
'injection, upload, and shell delivery' and shipped four sheets, none
about injection.
Written to the handling in the design doc's section 2, since ired.team
publishes no licence: no prose copied, commands and call sequences
reproduced as the technical facts they are, screenshots hotlinked at the
pinned SHA with per-image credit in each figcaption. Each sheet carries
a Detection section, which is this vault's own addition and the reason
these are derived works rather than restatements.
Batch 1 is specced at nine sheets; the remaining six are not written and
this does not complete it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat:
3 files changed, 478 insertions(+), 0 deletions(-)
diff --git a/src/content/sheets/exploitation/dll-injection.md b/src/content/sheets/exploitation/dll-injection.md
@@ -0,0 +1,118 @@
+---
+title: "Classic DLL Injection"
+description: "Writing a DLL path into a remote process and calling LoadLibrary on it — the oldest injection primitive, and the one every EDR expects."
+category: exploitation
+subcategory: "Process Injection"
+tags: [process-injection, dll, windows, t1055]
+tools: [process-hacker, procmon, sysmon, process-explorer]
+difficulty: beginner
+updated: 2026-10-03
+upstreamName: "ired.team"
+upstreamUrl: "https://ired.team/offensive-security/code-injection-process-injection/dll-injection"
+upstreamAuthor: "Mantvydas Baranauskas"
+upstreamLicense: none
+upstreamRelation: derived
+references:
+ - name: "MITRE ATT&CK T1055.001 — Dynamic-link Library Injection"
+ url: "https://attack.mitre.org/techniques/T1055/001/"
+ license: none
+ relation: inspired
+ note: "Technique ID and the data sources the Detection section is built around."
+---
+
+## What this covers
+
+The baseline technique: get a handle to a process, allocate a few bytes in it, write the
+path of a DLL on disk, and start a thread at `LoadLibraryA` with that path as the argument.
+The target's own loader does the rest — mapping the DLL, resolving its imports, and calling
+`DllMain`.
+
+It is the easiest injection to write and the easiest to catch. It is worth knowing properly
+anyway, because every more sophisticated technique on this list exists to avoid one of its
+three giveaways: the DLL on disk, the `CreateRemoteThread`, and the module appearing in the
+target's module list.
+
+## Prerequisites
+
+- A handle with `PROCESS_CREATE_THREAD`, `PROCESS_VM_OPERATION` and `PROCESS_VM_WRITE`.
+ Against a process in another session or owned by another user, that means
+ `SeDebugPrivilege` — so in practice, local administrator.
+- Architecture must match: a 64-bit process cannot load a 32-bit DLL.
+- **The DLL must exist on disk** at a path the target process can read. This is the
+ technique's defining weakness — there is a file to find, hash and submit.
+
+## Walkthrough
+
+| # | Call | Purpose | Artefact |
+|---|---|---|---|
+| 1 | `OpenProcess` | handle to the target | Sysmon event 10, `GrantedAccess 0x1F3FFF` or similar |
+| 2 | `VirtualAllocEx(..., MEM_COMMIT, PAGE_READWRITE)` | space for the path string | small RW private allocation — not suspicious alone |
+| 3 | `WriteProcessMemory` | the DLL path | cross-process write |
+| 4 | `GetModuleHandle("kernel32")` + `GetProcAddress("LoadLibraryA")` | the start routine | — |
+| 5 | `CreateRemoteThread(..., LoadLibraryA, pPath, ...)` | the loader runs in the target | **Sysmon event 8** — the loudest part |
+| 6 | target's loader maps the DLL, calls `DllMain` | execution | Sysmon event 7 (`ImageLoad`) naming your DLL |
+
+Note that the allocation here is `PAGE_READWRITE`, not RWX: you are writing a *string*, not
+code. Scanners hunting private executable memory do not find this one — the module shows up
+properly mapped, image-backed, in the module list. That is the trade: it is quiet in memory
+and loud everywhere else.
+
+### What it looks like from the outside
+
+The injected DLL executes with the host's identity and token, so whatever it spawns inherits
+the host's lineage. A payload that runs a shell produces a process tree that makes no sense
+for the host application.
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/inject-dll.png"
+ alt="Process tree showing notepad.exe as the parent of rundll32.exe, which in turn parents a cmd.exe"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption>The giveaway in the tree: notepad parents rundll32, which parents cmd.exe.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/inject-dll-procmon.png"
+ alt="Procmon event list showing the injected DLL being opened and loaded by the target process"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption>The same thing in Procmon — the injected DLL is read from disk by the host.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+## Detection
+
+This is the technique detection was built for, so there are several independent catches and
+you do not need all of them.
+
+**Sysmon event 8, `CreateRemoteThread`.** A remote thread whose `StartAddress` resolves to
+`kernel32!LoadLibraryA` or `LoadLibraryW` is close to conclusive. Legitimate uses of
+`CreateRemoteThread` exist — debuggers, some injectors in AV products themselves — so
+baseline them rather than alerting blind.
+
+**Sysmon event 7, `ImageLoad`.** The DLL is a real mapped module, so it is named in full.
+Flag modules loaded from user-writable paths (`%TEMP%`, `%APPDATA%`, `C:\Users\Public`),
+unsigned modules in signed processes, and any module whose load is not explained by the
+host's import table or a known plugin directory.
+
+**Sysmon event 10, `ProcessAccess`.** `GrantedAccess` containing `PROCESS_CREATE_THREAD`
+(`0x0002`) together with `PROCESS_VM_WRITE` (`0x0020`) is the access combination this
+technique needs and little else does.
+
+**Module-list review.** `Process Hacker` → Modules, or `listdlls`, shows the DLL plainly.
+Compare each loaded module against the expected set for that binary; a `rundll32` or an
+unknown DLL inside `notepad.exe` is not ambiguous.
+
+**On disk.** There is a file. Hash it, and check its path against application allowlists.
+Of the techniques on this page, this is the only one where the payload is recoverable from
+the filesystem after the fact — which makes it the best case for the responder and the worst
+for the operator.
+
+## References
+
+- [MITRE ATT&CK T1055.001](https://attack.mitre.org/techniques/T1055/001/)
+- [MITRE ATT&CK T1055.002](https://attack.mitre.org/techniques/T1055/002/) — portable
+ executable injection, the variant with no DLL on disk and no module-list entry
+- [Thread execution hijacking](/sheets/exploitation/thread-execution-hijacking) — starting
+ code without `CreateRemoteThread`
diff --git a/src/content/sheets/exploitation/process-hollowing.md b/src/content/sheets/exploitation/process-hollowing.md
@@ -0,0 +1,185 @@
+---
+title: "Process Hollowing"
+description: "Unmapping a suspended process's image and running a different PE from its base address, including the relocation fixups that make the swap work."
+category: exploitation
+subcategory: "Process Injection"
+tags: [process-injection, evasion, windows, maldev, t1055]
+tools: [windbg, process-hacker, pe-bear, sysmon]
+difficulty: advanced
+updated: 2026-10-03
+upstreamName: "ired.team"
+upstreamUrl: "https://ired.team/offensive-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations"
+upstreamAuthor: "Mantvydas Baranauskas"
+upstreamLicense: none
+upstreamRelation: derived
+references:
+ - name: "MITRE ATT&CK T1055.012 — Process Hollowing"
+ url: "https://attack.mitre.org/techniques/T1055/012/"
+ license: none
+ relation: inspired
+ note: "Technique ID, and the detection data sources the Detection section is organised around."
+---
+
+## What it does
+
+You start a legitimate process suspended, unmap the image it was built around, write a
+different PE into its address space, repair that PE's absolute addresses for wherever it
+actually landed, point the thread's entry at the new code, and resume. The process keeps the
+image path, PID and command line it started with, so anything that identifies a process by
+its path sees the host and not the payload.
+
+The word "hollowing" covers a family of related things. This sheet is the strict form —
+`NtUnmapViewOfSection` against the host's own base address, then a full manual PE load into
+the hole. The variants that skip the unmap are different techniques with different
+artefacts: see [thread execution hijacking](/sheets/exploitation/thread-execution-hijacking),
+and module stomping, which overwrites a legitimately loaded module instead.
+
+## Prerequisites
+
+- `SeDebugPrivilege` is not required for a process you create yourself, which is the whole
+ appeal — you own the handle from `CreateProcess`, so there is no `OpenProcess` against a
+ process you do not own.
+- Architecture must match. A 32-bit payload into a 64-bit host fails at the relocation
+ stage, usually confusingly.
+- The payload PE needs a relocation table if it cannot be placed at its preferred
+ `ImageBase`. A binary linked `/FIXED` has no `.reloc` section and can only be hollowed in
+ where its preferred base is free, which in practice it is not.
+
+## Walkthrough
+
+The sequence, with what each call is for and what it leaves behind. This is the shape of the
+technique rather than a buildable loader — the artefact column is the part that matters for
+the Detection section below.
+
+| # | Call | Purpose | Artefact |
+|---|---|---|---|
+| 1 | `CreateProcessA(..., CREATE_SUSPENDED, ...)` | start the host, thread never runs | process created with no thread activity; Sysmon event 1 with a normal image path |
+| 2 | `GetThreadContext` | recover the PEB address from the thread context | — |
+| 3 | `ReadProcessMemory` at PEB+8 | read `ImageBaseAddress` | cross-process read of own child |
+| 4 | `NtUnmapViewOfSection(hProc, base)` | drop the host image | **the strong signal** — a process whose base address has no mapped image |
+| 5 | `VirtualAllocEx(..., MEM_COMMIT\|MEM_RESERVE, PAGE_EXECUTABLE_READWRITE)` | reserve `SizeOfImage` for the payload | RWX private commit, image-backed nowhere |
+| 6 | `WriteProcessMemory` ×N | headers, then each section at its `VirtualAddress` | repeated cross-process writes |
+| 7 | relocation fixups | patch absolute addresses by the delta | — |
+| 8 | `SetThreadContext` | point `RCX`/`EAX` at the new entry point | thread entry outside any module |
+| 9 | `ResumeThread` | run it | execution begins from private memory |
+
+### Finding `ImageBaseAddress`
+
+The PEB holds the image base eight bytes in on x86 (`0x10` on x64). In WinDBG the host's PEB
+is visible directly, which is the cheapest way to confirm your offsets are right before
+trusting them in code.
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/Screenshot%20from%202019-04-28%2016-36-33.png"
+ alt="WinDBG output locating the suspended host process's PEB at address 0100e000"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption>The host's PEB located in WinDBG, here at <code>0100e000</code>.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/Screenshot%20from%202019-04-28%2016-38-29.png"
+ alt="WinDBG showing the ImageBaseAddress field eight bytes past the PEB base"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption><code>ImageBaseAddress</code> sits eight bytes past the PEB on x86.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+### The unmap
+
+Before the unmap, the host's base address holds its own image. This is worth looking at in a
+debugger once, because it is exactly the state that a defender's memory scan compares
+against.
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/Screenshot%20from%202019-04-28%2016-50-46.png"
+ alt="Debugger view of the host image still mapped at its ImageBaseAddress before hollowing"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption>The host image still mapped at its base, before the unmap.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/Peek%202019-04-28%2016-53.gif"
+ alt="Debugger showing the base address empty after NtUnmapViewOfSection, the image no longer present"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption>After <code>NtUnmapViewOfSection</code>: the base address is empty.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+Re-allocating at the host's old base often fails with `ERROR_INVALID_ADDRESS` even though
+the region reads as unmapped, so implementations generally let the allocator choose. That
+choice is what forces the relocation work in the next step — if you could always land on the
+payload's preferred `ImageBase`, there would be nothing to fix up.
+
+### Relocations
+
+Allocating anywhere other than the payload's preferred `ImageBase` means every absolute
+address baked into the image is wrong by a fixed delta:
+
+```text
+delta = allocated_base - payload_optional_header.ImageBase
+```
+
+Walk `IMAGE_DIRECTORY_ENTRY_BASERELOC`. Each `IMAGE_BASE_RELOCATION` block covers a 4 KB
+page and is followed by 16-bit entries; the top 4 bits are the type, the low 12 are the
+offset into that page. For each entry of type `IMAGE_REL_BASED_HIGHLOW` (x86) or
+`IMAGE_REL_BASED_DIR64` (x64), add the delta to the pointer at that address. Types
+`ABSOLUTE` (0) are padding and are skipped.
+
+Reading the section headers correctly is the step that silently ruins the rest, so check them
+against a PE parser rather than trusting your struct offsets.
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/Screenshot%20from%202019-04-28%2017-08-28.png"
+ alt="Section header values read by the loader shown side by side with the same values in a PE parser"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption>Section header values cross-checked against a PE parser.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+## Detection
+
+Hollowing is noisy once you know what to compare. The technique's own requirements create
+the signals.
+
+**Image/memory mismatch.** The decisive artefact: the process's base address either has no
+mapped image, or has one whose contents do not match the file on disk at the path the
+process claims. Nothing legitimate unmaps its own primary image. Compare the in-memory PE
+headers at `ImageBaseAddress` against the file at the process's image path — a mismatch in
+`SizeOfImage`, section names, or entry point is conclusive.
+
+**Private RWX in a fresh process.** Step 5 commits a `PAGE_EXECUTE_READWRITE` region that is
+not backed by any file. Enumerate committed regions with `VirtualQueryEx` and flag
+`MEM_PRIVATE` + executable. Volatility's `malfind` finds this directly; so does a
+`Get-ForeignThread`-style scan.
+
+**Thread entry outside a module.** After `SetThreadContext` the thread's start address is in
+private memory. `NtQueryInformationThread(ThreadQuerySetWin32StartAddress)` compared against
+the loaded module list separates injected threads from legitimate ones.
+
+**API sequence.** `CreateProcess(CREATE_SUSPENDED)` → `NtUnmapViewOfSection` →
+`VirtualAllocEx` → `WriteProcessMemory` → `SetThreadContext` → `ResumeThread` against the
+same handle is a near-unambiguous pattern. Most EDR hooks at least `NtUnmapViewOfSection`
+and `NtSetContextThread` for exactly this reason.
+
+**Sysmon.** Event 1 shows a process whose parent makes no sense for it (calc.exe spawned by
+a build output). Event 8 (`CreateRemoteThread`) does **not** fire — hollowing resumes an
+existing thread, which is part of why it was popular — so do not rely on it. Event 10
+(`ProcessAccess`) with `GrantedAccess` including `0x0008`/`0x0020` (`VM_WRITE`,
+`VM_OPERATION`) is the usable one.
+
+**Behavioural.** A suspended process that lives for more than a few milliseconds before
+resuming is unusual. So is a process whose loaded-module list is missing the DLLs its stated
+image imports.
+
+## References
+
+- [MITRE ATT&CK T1055.012 — Process Hollowing](https://attack.mitre.org/techniques/T1055/012/)
+- [Thread execution hijacking](/sheets/exploitation/thread-execution-hijacking) — the
+ no-unmap variant
diff --git a/src/content/sheets/exploitation/thread-execution-hijacking.md b/src/content/sheets/exploitation/thread-execution-hijacking.md
@@ -0,0 +1,175 @@
+---
+title: "Thread Execution Hijacking"
+description: "Suspending an existing thread and repointing its instruction pointer at your code, so nothing new is ever created to notice."
+category: exploitation
+subcategory: "Process Injection"
+tags: [process-injection, evasion, windows, t1055, entry-point]
+tools: [windbg, process-hacker, sysmon, volatility]
+difficulty: advanced
+updated: 2026-10-03
+upstreamName: "ired.team"
+upstreamUrl: "https://ired.team/offensive-security/code-injection-process-injection/injecting-to-remote-process-via-thread-hijacking"
+upstreamAuthor: "Mantvydas Baranauskas"
+upstreamLicense: none
+upstreamRelation: derived
+references:
+ - name: "AddressOfEntryPoint code injection"
+ url: "https://ired.team/offensive-security/code-injection-process-injection/addressofentrypoint-code-injection"
+ author: "Mantvydas Baranauskas"
+ license: none
+ relation: derived
+ note: "The suspended-process variant that overwrites the image entry point instead of repointing RIP."
+ - name: "AddressOfEntryPoint injection without VirtualAllocEx RWX"
+ url: "https://ired.team/offensive-security/code-injection-process-injection/addressofentrypoint-code-injection-without-virtualallocex-rwx"
+ author: "Mantvydas Baranauskas"
+ license: none
+ relation: derived
+ note: "The variant that avoids allocating a fresh RWX region; folded in below with the caveat about transient page protection."
+ - name: "MITRE ATT&CK T1055.003 — Thread Execution Hijacking"
+ url: "https://attack.mitre.org/techniques/T1055/003/"
+ license: none
+ relation: inspired
+ note: "Technique ID and detection data sources."
+---
+
+## What this covers
+
+Instead of creating a thread to run your code, you take one that already exists. Suspend it,
+read its register context, point its instruction pointer at shellcode you have written into
+the process, and resume. The thread belongs to the target, was started by the target, and
+appears in no remote-thread telemetry.
+
+Two variants are folded in here because they are the same idea at different moments:
+
+- **Running-thread hijack** — against a live process, grab its main thread and repoint `RIP`.
+- **AddressOfEntryPoint** — against a process you created suspended, overwrite the image's
+ entry point so the thread runs your code when first resumed. No context manipulation
+ needed, because the thread has not started yet.
+
+## Prerequisites
+
+- `PROCESS_VM_OPERATION`, `PROCESS_VM_WRITE` on the process, and `THREAD_SUSPEND_RESUME`,
+ `THREAD_GET_CONTEXT`, `THREAD_SET_CONTEXT` on the thread.
+- Shellcode must be position-independent. You are jumping into it with whatever register
+ state the hijacked thread happened to have.
+- **Restore or accept the loss.** Save the original context before you change it. Code that
+ does not return to the saved `RIP` kills the host thread, which is often the difference
+ between a quiet injection and an application that visibly hangs.
+
+## Walkthrough — running-thread hijack
+
+| # | Call | Purpose | Artefact |
+|---|---|---|---|
+| 1 | `OpenProcess` | handle to the target | Sysmon 10 |
+| 2 | `VirtualAllocEx(..., PAGE_EXECUTE_READWRITE)` | space for shellcode | private RWX region |
+| 3 | `WriteProcessMemory` | the shellcode | cross-process write |
+| 4 | `CreateToolhelp32Snapshot` / `Thread32First` | find a thread of the target | thread enumeration |
+| 5 | `OpenThread(THREAD_ALL_ACCESS, ...)` | handle to that thread | — |
+| 6 | `SuspendThread` | freeze it | a suspended thread in a running process |
+| 7 | `GetThreadContext` | capture registers | — |
+| 8 | set `ctx.Rip = shellcodeAddress` | redirect | **thread with RIP outside any module** |
+| 9 | `SetThreadContext` | apply | EDR commonly hooks `NtSetContextThread` |
+| 10 | `ResumeThread` | run | execution from private memory |
+
+Finding the target's thread is step one in practice — you need a thread ID, usually the
+process's main thread.
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28609%29.png"
+ alt="Thread listing for the target notepad process with its main thread ID identified"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption>Locating the target's main thread ID.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+The captured context is what you must preserve — it holds the CPU registers as the thread
+was executing, and it is the only route back to normal operation.
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28612%29.png"
+ alt="Retrieved thread context structure showing the hijacked thread's CPU register values"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption>The hijacked thread's captured register context.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+After the context is set, `RIP` points into the allocated region rather than into any loaded
+module — which is simultaneously the point of the technique and the way it is caught.
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28613%29.png"
+ alt="Thread context showing RIP now set to the shellcode address inside the target process memory"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption><code>RIP</code> repointed to the shellcode address in the target's memory.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+<figure class="shot">
+ <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28617%29.png"
+ alt="Shellcode executing after the hijacked thread is resumed, with a shell returned"
+ loading="lazy" referrerpolicy="no-referrer">
+ <figcaption>Execution after <code>ResumeThread</code>.
+ <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
+ </figcaption>
+</figure>
+
+## Walkthrough — AddressOfEntryPoint
+
+Against a process started `CREATE_SUSPENDED`, the thread exists but has not executed, so
+there is no context to juggle:
+
+1. `CreateProcessA(..., CREATE_SUSPENDED, ...)`.
+2. Read the PEB to find `ImageBaseAddress`, parse the headers, compute
+ `ImageBase + OptionalHeader.AddressOfEntryPoint`.
+3. `WriteProcessMemory` your shellcode directly over the entry point.
+4. `ResumeThread`.
+
+**On the "without RWX" framing.** This variant is often presented as avoiding RWX memory,
+and it does avoid allocating a *fresh* RWX region — you are writing into the image's own
+executable section. But `WriteProcessMemory` has to make that page writable to write it, so
+an executable page does transiently become writable regardless. The honest claim is that
+there is no new private RWX allocation to find afterwards, not that no write-to-executable
+ever happens. A hook on `NtProtectVirtualMemory` still sees it.
+
+The upside for the operator is real though: the shellcode lives inside an image-backed,
+on-disk-matching region, so `malfind`-style private-RWX hunting misses it entirely. The
+catch moves to comparing the in-memory entry point against the file on disk.
+
+## Detection
+
+**Thread start address vs module list.** `NtQueryInformationThread` with
+`ThreadQuerySetWin32StartAddress`, compared against the loaded modules, finds any thread
+beginning in unbacked memory. Works on the running-thread variant.
+
+**`NtSetContextThread` on a foreign thread.** Setting the context of a thread in another
+process is rare outside debuggers. Most EDR products hook this specifically, because it is
+the one call the running-thread variant cannot avoid.
+
+**No Sysmon event 8.** Worth stating plainly: hijacking creates no remote thread, so
+`CreateRemoteThread` telemetry never fires. A detection strategy resting on event 8 does not
+see this technique at all. Event 10 (`ProcessAccess`) is the one that fires, with
+`GrantedAccess` including `PROCESS_VM_WRITE`.
+
+**Suspend/resume pattern.** `SuspendThread` on a thread you do not own, followed within
+milliseconds by `SetThreadContext` and `ResumeThread`, is the signature. Thread suspension
+counts are visible in `Process Hacker`.
+
+**Entry-point integrity.** For the AddressOfEntryPoint variant, read the first bytes at
+`ImageBase + AddressOfEntryPoint` in memory and compare with the same offset in the file on
+disk. A mismatch in a process that has just started is conclusive, and it is the only check
+that catches this variant reliably.
+
+**Private RWX.** Applies to the running-thread variant only — step 2 leaves a
+`MEM_PRIVATE` executable region. `malfind`, or `VirtualQueryEx` enumeration looking for
+committed executable private pages.
+
+## References
+
+- [MITRE ATT&CK T1055.003](https://attack.mitre.org/techniques/T1055/003/)
+- [Process hollowing](/sheets/exploitation/process-hollowing) — the variant that unmaps the
+ host image first
+- [MITRE ATT&CK T1055.004](https://attack.mitre.org/techniques/T1055/004/) — APC queue
+ injection, the other route to running code on a thread you did not create