概述:TLS(TLSDirectory) 的说明和使用
0x01 TLS
TLS全称线程局部存储器,它用来保存变量或回调函数。
TLS里面的变量和回调函数都在程序入口点(AddressOfEntry)之前执行,也就是说程序在被调试时,还没有在入口点处断下来之前,TLS中的变量和回调函数就已经执行完了,所以TLS可以用作反调试之类的操作。
TLS中的变量单独存在于每个独立的线程当中,每个线程中对该变量的操作都不会影响到其他线程中的TLS变量。
TLS变量的创建方法有两种方式,分别是动态方式和静态方式,动态方法会用到TlsAlloc、TlsFree、TlsSetValue、TlsGetValue这几个函数来操作变量,静态方法会用声明__declspec (thread) int xx = 1;这样的方式来创建。需要注意的是静态创建的TLS变量不能用于DLL动态库中。
理论说的多了未免有点枯燥,先写个实例吧,说明变量和回调函数的创建,这样明白的更透彻一点。
0x02 TLS实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 __declspec (thread)int g_nNum = 0x11111111 ; __declspec (thread)char g_szStr[] = "TLS g_nNum = 0x%p ...\r\n" ;void NTAPI t_TlsCallBack_A (PVOID DllHandle, DWORD Reason, PVOID Red) { if (DLL_THREAD_DETACH == Reason) { printf ("t_TlsCallBack_A -> ThreadDetach!\r\n" ); return ; } }void NTAPI t_TlsCallBack_B (PVOID DllHandle, DWORD Reason, PVOID Red) { if (DLL_THREAD_DETACH == Reason) { printf ("t_TlsCallBack_B -> ThreadDetach!\r\n" ); return ; } }#pragma data_seg(".CRT$XLB" ) PIMAGE_TLS_CALLBACK p_thread_callback[] = { t_TlsCallBack_A, t_TlsCallBack_B, NULL };#pragma data_seg() DWORD WINAPI t_ThreadFun (PVOID pParam) { printf ("t_Thread -> first printf:" ); printf (g_szStr, g_nNum); g_nNum = 0x2222222 ; printf ("t_Thread -> second printf:" ); printf (g_szStr, g_nNum); return 0 ; }int _tmain() { printf ("_tmain -> TlsDemo.exe is running...\r\n\r\n" ); CreateThread (NULL , 0 , t_ThreadFun, NULL , 0 , 0 ); Sleep (100 ); printf ("\r\n" ); CreateThread (NULL , 0 , t_ThreadFun, NULL , 0 , 0 ); system ("pause" ); return 0 ; }
首先我们看注册TLS变量的方式,本例子使用的是静态方法也就是__declspec (thread)int xx = 1;这样的方式来创建,而注册回调函数相对比较麻烦点,我们要声明一个#pragma data_seg(“.CRT$XLB”)这样的宏定义将回调函数包起来,CRT表明使用C RunTime机制,X表示标识名随机,L表示TLS callback section,B可以是B-Y之间任意的字母。
我们还看见回调函数会在线程被终止时调用,并且调用的顺序跟注册回调函数时相关,其实回调函数不止会在线程终止时调用,还有以下几种情况会被调用。
具体的参见代码吧,打印结果如下:
可以看见两个线程对g_nNum的操作其实是互不影响的,在程序运行到入口点之前,g_nNum已经被初始化了,以后每开辟一条新的线程,系统都会拷贝一份TLS变量的副本到该线程中。
0x03 TLS 解析
接下来对TLS在PE文件中的结构进行解析。TLS在PE中数据目录表的第10位。
通常一个包含了TLS表的程序,它就会拥有.tls段,这个段里面保存了变量和回调函数的数据,但是TLS表本身的结构体一般存在于.rdata段内。本文的例子程序的TLS表RVA是0x2200,通过转换为offset得到0x1000,我们到0x1000处看看十六进制再对比结构体字段就可以解析出TLS表了。
1 2 3 4 5 6 7 8 9 typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; DWORD EndAddressOfRawData; PDWORD AddressOfIndex; PIMAGE_TLS_CALLBACK *AddressOfCallBacks; DWORD SizeOfZeroFill; DWORD Characteristics; } IMAGE_TLS_DIRECTORY32
以上述代码为例:编译 x64-debug 版本后分析如下所示:
可以看到 .tls
段的起始地址为 E200h
。 也就是.tls 段的偏移在 E200h位置,跳转到 E200h 看一下。
从 E2000h 开始就可以使用 _IMAGE_TLS_DIRECTORY32 结构体读取 tls 表了。
在 tls 段内存中就能看到 TLS 变量了