Early Bird APC injection
Create a target suspended, write the payload into it and queue an APC before it runs, so the payload executes before the target’s entry point and the hooks around it.
Metadata
Section titled “Metadata”| ATT&CK | T1055.004 (Process Injection: Asynchronous Procedure Call) |
| Stability | Experimental |
| Category | Execution |
| YAML key | early_bird |
| Introduced in | unreleased (Early Bird injection) |
What it does
Section titled “What it does”Creates a legitimate process suspended, writes the payload into it and queues an APC to its main thread before the process has run, so the payload executes during the target’s own initialization — before the target’s entry point, and before the anti-malware hooks placed around it.
YAML parameters
Section titled “YAML parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
payload | string | no | the only declared payload | Name of the payload in build.payloads to run |
target | string | no | C:\Windows\System32\svchost.exe | Process to create, suspend and inject into |
wait_ms | integer | no | 0 | Bounded wait, in milliseconds, for the target to exit; 0 does not wait |
The payload must be format: shellcode; plan::validate rejects a pe, dll
or dotnet payload for this technique at build time, and checks that the
target’s architecture matches build.output.arch.
YAML example
Section titled “YAML example”runtime: - technique: early_bird params: target: "C:\\Windows\\System32\\svchost.exe" wait_ms: 0How it works
Section titled “How it works”sequenceDiagram
participant L as Loader
participant T as Target (suspended)
L->>T: CreateProcessA(CREATE_SUSPENDED)
L->>T: nt_alloc_vm RW, nt_write_vm payload, nt_protect_vm RX
L->>T: nt_queue_apc (user APC, first in the queue)
L->>T: nt_resume_thread
T->>T: LdrInitializeThunk -> NtTestAlert flushes the queue
T->>T: payload runs before the entry point
T-->>L: target keeps running (or exits)
1. Create and prepare the target
Section titled “1. Create and prepare the target”CreateProcessAthe target withCREATE_SUSPENDED: its main thread is parked before ntdll’sLdrInitializeThunkhas run.nt_alloc_vmaPAGE_READWRITEregion in the target (never RWX),nt_write_vmthe payload into it, thennt_protect_vmit to read+execute.
2. Queue the APC and resume
Section titled “2. Queue the APC and resume”nt_queue_apcthe payload as a user APC on the target’s main thread while it is still suspended, so the APC is first in the queue.nt_resume_thread. The target’s loader flushes the APC queue during its own initialization (LdrInitializeThunk→NtTestAlert) and delivers the APC; the payload therefore runs before the target’s entry point.
The last step is the technique’s selling point: the payload runs before the target’s own code, and before the user-mode hooks that antivirus products place around the entry point.
Return value and wait_ms
Section titled “Return value and wait_ms”The payload runs in another process, so the loader cannot read the payload’s own
exit code back. wait_ms bounds how long the loader waits for the target to
exit, and the returned code follows this table:
wait_ms | Target’s fate | execute_payload returns |
|---|---|---|
0 (default) | not waited for | 259 (STILL_ACTIVE) |
> 0 | exits inside the window | the target’s exit status (e.g. 0xC000001D for a payload that crashed it) |
> 0 | still running when the window expires | 259 (STILL_ACTIVE) |
259 therefore means “the target’s exit was not observed”: either the plan did
not ask for a wait, or the target is still alive — which is what a real payload
(a beacon) does. A target that genuinely exits with 259 is indistinguishable;
the debug log (build.stub.debug: true) prints the observed status, and is the
authority.
Not waiting by default is deliberate: it keeps the technique chainable (the
loader walks on to the next step) and never blocks on a target that a real
payload keeps alive forever. on_success/on_fail and the following steps
behave as for any other Execution technique.
Caveats
Section titled “Caveats”- The payload’s
build.payloads.*.argsdo not reach the target. For in-process techniques the runner callspatch_command_line(), which patches the loader’s PEB (and kernelbase’s cached pointer); a process created byCreateProcessAhas its own PEB. The target’s command line is exactly whatCreateProcessAwas given — its own path — and the injected shellcode inherits that. - The APC carries no payload arguments. All queued arguments are null, so a payload that expects a configuration block cannot be described by this technique (the same limitation the other shellcode runners have).
syscalls.mode: nonemapsnt_queue_apctoQueueUserAPC, which takes a single pointer-sized argument;mode: indirectcallsNtQueueApcThreadwith the three arguments the native call has. The payload is entered as the APC routine and ignores both shapes.mode: indirectis x64-only, like the rest of the syscall layer.
What it evades
Section titled “What it evades”- The payload never touches disk in cleartext and never runs under its own image name.
- The APC is delivered by a legitimate process’s own loader thread, before the target’s entry point and before the hooks placed around it.
- The region is never RWX: it is written
PAGE_READWRITEand only then made read+execute.
What detects it
Section titled “What detects it”- Kernel callbacks (
PsSetCreateProcessNotifyRoutineExand the process/image callbacks) see the cross-process allocation, write, protection change and the APC queued against a thread that has never run. - ETW-TI reports the cross-process write into a freshly created process
(
NtWriteVirtualMemoryagainst a target-process handle). - Static:
CreateProcessAis always imported (a loader entry point), andQueueUserAPCis imported insyscalls.mode: none(the syscall layer removes it inmode: indirect). - Queuing an APC to a suspended thread before its process has started is itself a behavioral signature.
Measurements
Section titled “Measurements”- Measured end to end (x64 and x86): a two-byte
ud2payload plus a boundedwait_msmakes the loader relay the target’s0xC000001D, which only happens if the APC was delivered. The static baseline (362 KB x64) is indocs/measurements.md. - Target choice matters. The 32-bit
notepad.exeof a recent Windows build is a launcher shim:QueueUserAPCon its thread fails withERROR_INVALID_HANDLE(0x80070006), while the 64-bit one andcmd.exe(both architectures) work. The defaultsvchost.exeis a real image; when a run fails on an unusual target, suspect the target before the technique.
Implementation notes
Section titled “Implementation notes”- The fragment (
stub_fragment.rs) receives the target path, the wait bound and every diagnostic throughrender_fragment_for, all rebuilt from obfuscated bytes, so no target string and no message reaches the artifact’s.rdatain cleartext. CreateProcessAandGetExitCodeProcessstay Win32: creating the target and reading its exit status are loader entry points, not kernel calls the stub issues. Everything kernel-facing goes through thent_*helpers (nt_alloc_vm,nt_write_vm,nt_protect_vm,nt_queue_apc,nt_resume_thread,nt_wait_for_single_object,nt_close).- On a failure after the target was created, the thread is resumed best-effort and both handles are closed, so a broken deployment cannot leave a suspended, payload-carrying process behind.
- The APC routine is the payload itself: it must be position-independent code
that returns to the APC dispatcher, calls
ExitProcess/ExitThread, or never returns. A payload that returns leaves the target running normally.
References
Section titled “References”- MITRE ATT&CK T1055.004 — Process Injection: Asynchronous Procedure Call.
- ired.team — “Early Bird APC Queue Code Injection”.