Get on-the-go access to the latest insights featured on our Trustworthy Computing blogs.
It's been ten years since I first noticed the word "callback" in the Thread Local Storage (TLS) section of the Portable Executable format documentation. Since then, we've seen it used and abused by virus writers, packer vendors, and general mischief-makers (and me, too, of course, as part of my research). During that time, I thought that I had discovered everything that there was to know about it. Apart from the fact that it runs before the main entrypoint, there are other things that it can do:
The execution of TLS callbacks is also platform-specific. If the executable imports only from either ntdll.dll or kernel32.dll, then callbacks will not be called during the "on attach" event when run on Windows XP and later.
That should just about cover it, except for one thing. I was asked recently about some internal details regarding TLS callbacks in DLLs. Since I didn't have any test files easily accessible, I created a new one. It was essentially a do-nothing file that contained a TLS callback. Then I created an .exe file and statically linked my test DLL to it. I tried to run the .exe file, and saw a message that it failed to load. Okay, something wrong in my hand-crafted DLL. The most likely reason is that I put a byte in the wrong place. The simplest way to find out is to step through loading the DLL dynamically, to see where it's failing, so that's what I did. I created a new .exe file, which loaded the DLL dynamically, and then I stepped through the LoadLibrary() function code...
Imagine my surprise when I saw the loader examining the TLS data. Why surprise? Well, because it directly contradicts the existing Portable Executable format documentation. The documentation states that "Statically declared TLS data objects", that is to say, Thread Local Storage callbacks, "can be used only in statically loaded image files. This fact makes it unreliable to use static TLS data in a DLL unless you know that the DLL, or anything statically linked with it, will never be loaded dynamically with the LoadLibrary API function". However, in Windows Vista and later versions, the kernel32 LoadLibrary() function does call Thread Local Storage callbacks in DLLs. Further, the Thread Local Storage callbacks will be called, no matter what is present in the import table. Thus, the DLL can import from ntdll.dll or kernel32.dll or even no DLLs at all (unlike the .exe case described above), and the callbacks will be called!
This is a significant change in behaviour. It also leads to a very neat anti-emulator trick. This behaviour can be detected most easily by an .exe file that co-operates with the DLL. However, it is possible for a DLL to determine if it was loaded statically or dynamically by examining, for example, the value of the stack pointer or the Structured Exception Handler frame list, among other things. That would allow the .exe file to remain unaware of the behaviour, such as the case of an existing DLL being altered to provide this behaviour.
Example code for the DLL file TLS callback looks like this:
inc b [offset l1]...l1: db 0 ;set to 1 on attachexport l1
Example code for the .exe file looks like this:
push offset l2call LoadLibraryApush offset l3push eaxcall GetProcAddressxchg ebx, eaxcall GetVersioncmp al, 6 ;Vista+setnb alcmp [ebx], aljne being_emulated...l2: db "mydll", 0l3: db "l1", 0
In this example, if the callback is called (presumably by the kernel32 LoadLibrary() function), then the value at l1 will be set to one. If the Windows version suggests Windows Vista or later, then a flag will be set to one. If the callback is not called, then the value at l1 will remain zero. If the Windows version suggests Windows XP or earlier, then a flag will be set to zero. If the two results match (either both set or both clear), then the expected behaviour has been demonstrated. Otherwise, the presence of the emulator is revealed.
Oh yes, the problem with my DLL was that the TLS data pointer was off by one byte, resulting in a crash. That's why the DLL didn't load.
- Peter Ferrie