Process hollowing — evidence
Why this document exists
Section titled “Why this document exists”Process hollowing used to write the payload’s sections into the target without
resolving the payload’s import table, so the first call to an imported function
jumped into unmapped memory (0xC0000005). Fixing that exposed a second
limitation (TLS) and, while validating the fix, a third: a relocation bug in
the hollowing fragment itself.
The blocker the TLS work uncovered
Section titled “The blocker the TLS work uncovered”The fragment’s relocation walk had IMAGE_REL_BASED_HIGHLOW (3) and
IMAGE_REL_BASED_DIR64 (10) the wrong way round. An x64 image emits DIR64 for
every absolute address, so the code patched only the low 32 bits of each
relocated pointer. A payload loaded at its preferred base (0x140000000) inside a
target whose base is a high address (e.g. 0x7ff68ad10000) ended up with every
absolute pointer truncated into the low 4 GB — its TLS directory, its IAT and its
CRT’s globals — which is why the payload faulted no matter how much loader state
was reconstructed. With the two constants swapped back the relocated addresses
are exact (measured: AddressOfIndex moved from 0x18ad2f240 — low half only —
to 0x7ff68ad2f240).
The TLS the loader would have provided
Section titled “The TLS the loader would have provided”A CREATE_SUSPENDED target has not run its loader, so the payload’s imported
DLLs are absent; a manually mapped image also never gets a TLS index, a
per-thread TLS block or an OS TLS slot. stub_fragment.rs now reproduces that
part of the loader:
- Let the target’s loader initialize. Patch the original entry point to
jmp $(EB FE), resume, poll until the thread parks there (PEB->Ldris then populated), suspend again. - Load each imported DLL in the target (remote
LoadLibraryAthroughNtCreateThreadEx). - Provision the payload’s TLS. Run
TlsAllocin the target (a throwaway remote thread; the index comes back as its exit status), publish the index where the payload reads_tls_index, allocate a private copy of the.tlstemplate and point the main thread’s TLS slot at it. - Run the payload’s
DLL_PROCESS_ATTACHcallbacks. On the main thread, one at a time, parking it on a dedicatedjmp $(EB FE) stub between calls so each callback runs with a TLS array in its TEB. - Redirect the entry point.
Rip— not justRcx— is set to the payload’s entry. Without that the thread resumed inside the replaced image.
Measured behaviour
Section titled “Measured behaviour”All runs on the development host, svchost.exe as the target, the Rust test
payload (examples/payloads/test_payload.exe):
| Scenario | Result |
|---|---|
| Hollowing, no import resolution (pre-fix baseline) | 0xC0000005 |
| Hollowing, imports resolved, relocations truncated | 0xC0000005 / 0xC0000409 |
| Hollowing, DIR64 relocations applied | TLS write hung on a low-half address |
| Hollowing, relocations + TLS provisioned | payload runs; the stub relays exit 0 |
The last row is the current state: .\dist\test_packed.exe prints its
STUB_MARKER_<hex> and exits with the payload’s own exit code (0). The
end-to-end test still asserts only “the stub did not fail through its own error
path”; the marker itself still needs Process Explorer, because the hollowed
svchost.exe has no console attached.
Remaining work
Section titled “Remaining work”- Runtime verification of the TLS callbacks is pending. The callbacks are now
walked and invoked on the main thread (parked on a
jmp $stub between calls, so each has a TLS array in its TEB), and a malformed callback array fails loudly through the fragment’s diagnostics. What has not been measured on the measurement host yet is that a callback actually fires and that the payload’s exit code is unaffected — the#[ignore]end-to-end test still asserts only that the stub did not fail through its own error path. - The end-to-end test could assert the payload’s exit code once the marker question above is solved in a way that survives a console-less target.
References
Section titled “References”src/engine/imports.rs— the shared import resolver.src/techniques/process_hollowing/stub_fragment.rs— relocations, TLS provisioning and the entry-point hand-off.- Syscall layer — the syscall layer the hollowing
fragment calls through (
nt_create_thread_exis what replacedCreateRemoteThread).