tls-callback-anti-analysis
What It Does
On Windows, PE files can declare a Thread-Local Storage (TLS) directory. The loader walks the AddressOfCallBacks array and invokes each function pointer before the executable's normal entry point (main / WinMain). Malware uses this to execute anti-debug, anti-VM, or decryption logic before most EDR user-mode hooks are in place. Analysts debugging from entry0 or main entirely miss this code.
Detection / Fingerprint
- PE32+ with
IMAGE_DIRECTORY_ENTRY_TLSpresent (DataDirectory index 9). AddressOfCallBacksis non-zero and points to an array of function pointer VAs in.rdataor.data.- Standard MSVC output has 0–2 TLS callbacks. Counts > 2 are suspicious; counts > 4 are strongly indicative of anti-analysis logic.
- The callback functions are small (often < 64 bytes), do not reference CRT startup, and may call
IsDebuggerPresent,NtQueryInformationProcess, orCheckRemoteDebugger.
Implementation Patterns Observed
Observed in 0b6a849a68a4 (single sample)
- TLS directory at RVA
0x53bb80(file offset0x53bb80) inside.rdata. AddressOfCallBacks = 0x1404f9338.- Array contains 8 entries — 4× typical compiler output.
- Callbacks terminate with a
0x0sentinel. ^[raw/analyses/0b6a849a68a48f7301c3459a7771378e458e2d5debce9376be350784c61b72b7/dump_tls.py]
Build toolchain pattern
- MSVC
__declspec(thread)orTlsAllocdoes NOT produce this pattern. The callbacks are explicitly linked via.CRT$XLB/.CRT$XLCsections or by patching theTLS Directoryat link time. - Some packers / crypters inject a TLS directory and callback array into a previously unprotected PE.
Reproduce on Your Own VMs
Toolchain: Visual Studio 2022, x64 Release, C++.
Steps:
- Create any C++ project.
- Add a
.cppwith the TLS callback:
// tls_callback.cpp
#pragma comment(linker, "/INCLUDE:__tls_used")
#pragma section(".CRT$XLB", read)
extern "C" __declspec(allocate(".CRT$XLB"))
PIMAGE_TLS_CALLBACK TlsCallbackArray[] = {
[](PVOID hModule, DWORD Reason, PVOID Reserved) {
if (Reason == DLL_PROCESS_ATTACH) {
// Pre-main code
if (IsDebuggerPresent()) {
ExitProcess(0xBAD);
}
}
},
nullptr // sentinel
};
- Build with
/guard:cf /GS /MT. - Verify with
rabin2 -I build\MyApp.exe— checkhavecode true, then inspect TLS directory. - Set a breakpoint on
TlsCallbackArray[0]beforemain. It fires first.
Verification step:
python3 -c "import pefile; pe = pefile.PE('build/MyApp.exe'); print([hex(c) for c in pe.DIRECTORY_ENTRY_TLS.struct.AddressOfCallBacks or []])"
Should show the callback VA, confirming linkage.
Defensive Countermeasures
- EDR / hook timing: User-mode hooks placed at
kernel32!LoadLibraryA,ntdll!NtCreateThread, etc. are installed after TLS callbacks execute. Kernel callbacks (PsSetCreateProcessNotifyRoutine) fire much earlier — but they lack user-mode context. - Detection rule: Alert on PE loads where
pe.tls_callbacks > 2andpe.tls_callbacks[>0]resolves to code inside.rdataor.textbut outside the CRT main region. - Hunting: YARA for
IMAGE_DIRECTORY_ENTRY_TLSwithAddressOfCallBacks != 0andNumberOfRvaAndSizes == 16.
Pages Where Observed
- /intel/analyses/0b6a849a68a48f7301c3459a7771378e458e2d5debce9376be350784c61b72b7.html — 8-entry TLS array in 5.9 MB MSVC C++ PE32+, static-only analysis
- unclassified-pe32plus — entity tracking samples with this pattern