TLS Callbacks
Malware registers Thread Local Storage callbacks that execute before the PE entry point, running anti-debug or unpacking logic that most debuggers miss at startup.
Thread Local Storage (TLS) is a Windows PE feature that lets a binary register callback functions in a special .tls section. The Windows loader calls every registered TLS callback before the process entry point, and again each time a new thread is created. Most debuggers break at WinMain / the PE entry point — by which time TLS callbacks have already run unobserved.
Malware exploits this to:
- Run anti-debugging or anti-analysis checks silently.
- Decrypt or unpack the real payload before the entry point.
- Detect single-step execution by measuring timing inside the callback.
How it works
#include <windows.h>
// The TLS callback — executed before main()
void NTAPI TlsCallback(PVOID hModule, DWORD fdwReason, PVOID pContext)
{
if (fdwReason == DLL_PROCESS_ATTACH) {
// Anti-debug check: PEB.BeingDebugged
PPEB pPeb = (PPEB)__readgsqword(0x60);
if (pPeb->BeingDebugged) {
// Silently corrupt a key global so the main payload fails
ExitProcess(0);
}
// Alternatively: decrypt the payload here
}
}
// Register the callback in the .CRT$XLB section (MSVC)
#pragma comment(linker, "/INCLUDE:_tls_used")
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK p_tls_callback = TlsCallback;
#pragma data_seg()The PE's IMAGE_TLS_DIRECTORY in the optional header points to a null-terminated array of callback pointers. In a hex editor or PE parser, the .tls section and the data directory entry at index 9 are the giveaways.
IMAGE_DIRECTORY_ENTRY_TLS (index 9):
VirtualAddress: 0x00015000
Size: 0x00000018
IMAGE_TLS_DIRECTORY64:
StartAddressOfRawData: ...
EndAddressOfRawData: ...
AddressOfCallBacks: 0x00015010 <- pointer to callback arrayDetection & analysis
During debugging:
- In x64dbg:
Options → Preferences → Events → TLS Callbacks— enable this to break at each callback before the entry point. - In WinDbg:
sxe ldor set a breakpoint on the TLS callback array address before resuming from the loader breakpoint. - IDA: use the
Thread Local Storagesegment view; callbacks appear as cross-references from theAddressOfCallBacksarray.
Static / automated detection:
- YARA: search for
"TLS_CALLBACK"or"TLScallback"strings, or for a non-zeroIMAGE_DIRECTORY_ENTRY_TLSdata directory entry. - PE parsers (pefile, PE-bear): flag any binary with a populated TLS directory that is not a well-known runtime (MSVC, Go, Rust all use TLS legitimately — check the callback count and content).
- Unprotect technique ID: U0124.