不得不说这个标题写的有点大,大到涉及到编译连接的内容,我还是努力把他写好吧。这里只讨论TLS静态存储,T不讨论TLS动态存储,毕竟跟反调试关系不大。
TLS设计的本意,是为了解决多线程程序中变量同步的问题,是Thread Local Storage的缩写,意为线程本地存储。线程本身有独立于其他线程的栈空间,因此线程中的局部变量不用考虑同步问题。多线程同步问题在于对全局变量的访问,TLS在操作系统的支持下,通过把全局变量打包到一个特殊的节,当每次创建线程时把这个节中的数据当做副本,拷贝到进程空闲的地址空间中。以后线程可以像访问局部变量一样访问该异于其他线程的全局变量的副本,而不用加同步控制。下面将由浅入深的介绍TLS静态存储。
#include "stdafx.h"
#include
#include
#include
using namespace std;
// 定义静态TLS全局变量
__declspec(thread) int value =0xcccccccc;
DWORD WINAPI NewThread ( LPVOID lParam )
{
// 设置子线程value,并不影响其他线程
value = *((int*)lParam);
while(1)
{
printf("%d\n",value);
Sleep(1000);
}
return 0 ;
}
#define THREAD_NUM 3
int main(int argc, char* argv[])
{
int arry[3]={1,2,3};
// 设置主线程静态TLS的value为5
value = 5 ;
// 创建子线程
HANDLE hThread[THREAD_NUM];
for (int loop = 0; loop < THREAD_NUM; loop++)
{
hThread[loop] = CreateThread ( NULL, 0, NewThread, &arry[loop], 0, NULL );
Sleep(1000);
}
// 等待直到子线程结束
WaitForMultipleObjects(THREAD_NUM, hThread, TRUE, INFINITE);
return 0;
}
这段代码对疑似全局变量value进行了4次修改。按照以往的经验,value的最终值应该为3,即屏幕的输出应该是12333333333...,事实却是:
由于value非常规的定义方式,导致程序意想不到的输出,那么来看下不同方式下生成的exe有什么区别:
/*__declspec(thread)*/ int value =0xcccccccc; VS __declspec(thread) int value =0xcccccccc;
左图是未加__declspec(thread)生成的exe的PE结构的数据目录及节区,右图是加了__declspec(thread)生成的exe的数据目录及节区。两者相较区别有二:
1).数据目录TLS项非空;2).增加了一个TLS节区。
来看下M$对__declspec(thread)的解释,看着是生成一个:
引自:https://technet.microsoft.com/zh-cn/library/9w1sdazb
__declspec( thread ) declarator
Remarks
Thread Local Storage (TLS) is the mechanism by which each thread in a multithreaded process allocates storage for thread-specific data.
In standard multithreaded programs, data is shared among all threads of a given process, whereas thread local storage is the mechanism
for allocating per-thread data. For a complete discussion of threads, see Multithreading.
TLS以_IMAGE_TLS_DIRECTORY32作为管理结构,通过TLS数据目录项在进程空间中定位:
以下为进程内存映像:
地址 大小 属主 区段 包含 类型 访问 初始访问 已映射为
00400000 00001000 tsl PE 文件头 Imag R RWE
00401000 00010000 tsl .textbss 代码,数据 Imag R RWE
00411000 00004000 tsl .text SFX Imag R RWE
00415000 00002000 tsl .rdata Imag R RWE
00417000 00001000 tsl .data Imag R RWE
00418000 00001000 tsl .idata 输入表 Imag R RWE
00419000 00001000 tsl .tls Imag R RWE
0041A000 00001000 tsl .rsrc 资源 Imag R RWE
以下为部分数据目录项:
004001A0 00000000 DD 00000000 ; Global Ptr address = 0x0
004001A4 00000000 DD 00000000 ; Must be 0
004001A8 D0640100 DD 000164D0 ; TLS Table address = 0x164D0 ---> TLS结构在内存中的RVA,TLS存储在内存:0x004164D0处,位于.rdata节
004001AC 18000000 DD 00000018 ; TLS Table size = 18 (24.)
004001B0 00000000 DD 00000000 ; Load Config Table address = 0x0
004001B4 00000000 DD 00000000 ; Load Config Table size = 0x0
定位到 0x004164D0处结合_IMAGE_TLS_DIRECTORY32结构的定义,看看各字段的值:
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData; //TLS数值模板的起始地址:0x419000,位于.tls节
DWORD EndAddressOfRawData; //TLS数值模板的结束地址:0x419208,位于.tls节
DWORD AddressOfIndex; //TLS索引的位置0x41713C,位于data节
DWORD AddressOfCallBacks; //TLS回调函数数组指针(已NULL结尾):0x415720,位于rdata节
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32;
004164C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
004164D0 >00 90 41 00 08 92 41 00 3C 71 41 00 20 57 41 00 .怉.扐.
还记得前面说过每个线程运行前会把.tls节中的数据拷贝一份作为副本传到进程空闲的地址空间中吗?拷贝的模板,即数据来源的起止由_IMAGE_TLS_DIRECTORY32!StartAddressOfRawData/_IMAGE_TLS_DIRECTORY32!EndAddressOfRawData指定。这么说,代码中__declspec(thread) value的原始值也能在这个区间中找到了?事实的确如此:0x419000<0x419100<419208
00419100 00 00 00 00 CC CC CC CC 00 00 00 00 00 00 00 00 ....烫烫........
现在已经知道每个线程会有一个.tls节数据的副本,那么问题来了,各个线程如何去搜索这个副本?
额,记得windows给创建线程时会创建TEB块,用于存放各线程信息,而TEB!ThreadLocalStoragePointer(距离fs指向的内存起始处0x2C)正是指向TLS副本的指针。可以通过反汇编看下线程如何取值:
0041153D mov eax,dword ptr [__tls_index (417138h)] //
00411542 mov ecx,dword ptr fs:[2Ch]
00411549 mov edx,dword ptr [ecx+eax*4] //ThreadLocalStoragePointer确切的说是一个2级指针
0041154C mov dword ptr [edx+104h],5
运行后寄存器的值:
EAX = 00000000 EBX = 7EFDE000 ECX = 004C5520 EDX = 004C5560 ESI = 00000000 EDI = 0018FF30 EIP = 004114C6 ESP = 0018FE2C EBP = 0018FF30
edx的值就是线程从.tls节中拷来的副本所在地址,0x104正好是value在副本中的偏移,在.tls节中value位于0x419104,距离.tls节区首地址_IMAGE_TLS_DIRECTORY32!StartAddressOfRawData:0x419000正好是0x104。当创建其他几个线程后,取value的地址时,edx的值依次为:
EDX = 00587128
EDX = 005875E0
EDX = 00587A98
可知,包括主线程在内每个线程的TLS副本地址不同,这又是前面:"还记得前面说过每个线程运行前会把.tls节中的数据拷贝一份作为副本传到进程空闲的地址空间中吗"这句话的另一辅证。
再来看看TLS回调函数数组指针0x415720处:
00415720 >00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
空空如也,也对,代码中的确没有设置回调函数。接下去添加一个回调试试:
#pragma comment(linker, "/INCLUDE:__tls_used")
//The /INCLUDE option tells the linker to add a specified symbol to the symbol table.
void NTAPI tls_callback(PVOID h, DWORD reason, PVOID pv)
{
MessageBox(NULL,"Not Main!","Test1",MB_OK);
return;
}
//创建TLS段
#pragma data_seg(".CRT$XLT")
//Specifies the data segment where initialized variables are stored in the .obj file.
/*example:
#pragma data_seg(".my_data1")
int j = 1; // stored in "my_data1"
*/
//定义回调函数
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
#pragma data_seg()
还记得在.rdata节中有一个_IMAGE_TLS_DIRECTORY32结构吗?这是由C运行时库定义的变量。该变量名为_tls_used,源代码位于tlssup.c文件中(与Visual Studio一起发布)。 _tls_used标准的声明方式如下所示:
_CRTALLOC(".rdata$T")
//变量_tls_used存储在.rdata区 这与上面查看PE内存映射的结果相同
const IMAGE_TLS_DIRECTORY _tls_used =
{
(ULONG)(ULONG_PTR) &_tls_start, // start of tls data
(ULONG)(ULONG_PTR) &_tls_end, // end of tls data
(ULONG)(ULONG_PTR) &_tls_index, // address of tls_index
(ULONG)(ULONG_PTR)(&__xl_a+1), // pointer to call back array
(ULONG) 0, //size of tls zero fill
(ULONG) 0 //characteristics
};
通过#pragma comment(Linker,"INCLUDE:_tls_used")显示的申明crt库中定义的_tls_used变量,以便向_tls_used!AddressOfCallBacks域添加回调函数。
[为了使用CRT提供的TLS回调支持,需要我们声明一个存放在以“.CRT$XLx“为名的节里面,这里x是一个位于A和Z之间的字母。需要如此奇怪的节名是因为TLS回调指针需要进行内存排序的原因。为了理解这种特殊声明的作用,需要首先明白编译器和链接器是如何组织PE文件中的数据的。PE文件中,除了头部数据,其它均是分不同节存储的,节就是具有相同属性(也保护属性)集合的内存区域。关键字__declspec(allocate(“section-name”))告诉编译器在最终PE文件中其作用域内的内容放在指定的节内。编译器额外支持将相似名字的节合并为一个大节的功能。该功能通过使用节名前缀+$+任意字符串 的形式来激活。编译器将合并具有相同节名前缀的节为一个大节。编译器对于相似节采用字典顺序进行合并(对$后的字符串进行排序)。这意味着在内存中,位于节“.CRT$XLB”中的变量将在位于节“.CRT$XLA”中变量位置的后面,但是在位于节“.CRT$XLZ”中的变量的前面。 C运行时库利用编译器的这一特性来创建一个以NULL结尾的TLS回调数组(将节“.CRT$XLZ”中放置一个NULL指针)。因此为了保证声明的函数指针位于TLS回调数组内部,必须将它放在节“.CRT$XLx”中。] []间内容引自参考资料3。
好的,至此编译链接,看下能不能找到TLS回调函数?
004164DC >00 90 41 00 08 92 41 00 38 71 41 00 20 57 41 00 .怉.扐.8qA. WA.
00415720 >1E 10 41 00 00 00 00 00 00 00 00 00 00 00 00 00 A.............
0041101E /E9 9D030000 jmp tsl.tls_callback
TLS回调函数指针数组中目前有一项,指向0x41101E,这是一个跳转地址跳转到0x4113c0(因为程序是Debug版,启用了增量连接,函数调用都先经过一个ILT表,在从ILT表跳转到真正的函数入口)。从PE的内存映像来看,这是.text节区的内容,应该是段代码的地址。来看看可执行程序的map文件设置的加载地址:
0002:000003c0 ?tls_callback@@YGXPAXK0@Z 004113c0 f tsl.obj
0002:00000430 ?NewThread@@YGKPAX@Z 00411430 f tsl.obj
0002:00000500 _main 00411500 f tsl.obj
#include "stdafx.h"
#include
#include
#include
using namespace std;
#pragma comment(linker, "/INCLUDE:__tls_used")
void lookupprocess(void);
void Debugger(void);
void NTAPI tls_callback(PVOID h, DWORD reason, PVOID pv)
{
lookupprocess();
Debugger();
MessageBox(NULL,"Not Main!","Test1",MB_OK);
return;
}
//创建TLS段
#pragma data_seg(".CRT$XLT")
//定义回调函数
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
#pragma data_seg()
// 定义静态TLS全局变量
__declspec(thread) int value =0xcccccccc;
DWORD WINAPI NewThread ( LPVOID lParam )
{
// 设置子线程value,并不影响其他线程
value = *((int*)lParam);
while(1)
{
printf("%d\n",value);
Sleep(1000);
}
return 0 ;
}
#define THREAD_NUM 3
int main(int argc, char* argv[])
{
int arry[3]={1,2,3};
// 设置主线程静态TLS的value为5
value = 5 ;
// 创建子线程
HANDLE hThread[THREAD_NUM];
for (int loop = 0; loop < THREAD_NUM; loop++)
{
hThread[loop] = CreateThread ( NULL, 0, NewThread, &arry[loop], 0, NULL );
Sleep(20000);
}
// 等待直到子线程结束
WaitForMultipleObjects(THREAD_NUM, hThread, TRUE, INFINITE);
return 0;
}
void lookupprocess()
{
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
BOOL bMore = ::Process32First(hProcessSnap,&pe32);
while(bMore)
{
strlwr(pe32.szExeFile);
if (!strcmp(pe32.szExeFile,"ollyice.exe"))
{
exit(0);
}
if (!strcmp(pe32.szExeFile,"ollydbg.exe"))
{
exit(0);
}
if (!strcmp(pe32.szExeFile,"peid.exe"))
{
exit(0);
}
if (!strcmp(pe32.szExeFile,"idaq.exe"))
{
exit(0);
}
bMore = ::Process32Next(hProcessSnap,&pe32);
}
::CloseHandle(hProcessSnap);
}
//anti-debug2
void Debugger(void)
{
int result=0;
__asm
{
mov eax, dword ptr fs:[30h]//TEB偏移30H处
movzx eax, byte ptr ds:[eax+2h]//取PEB中BeingDebug,若为1则被调试
mov result,eax
}
if (result)
exit(0);
}
这个反调试比较简单,把反调试的逻辑直接加在TLS回调中,因此对应的反反调试也比较简单:定位PE文件中,把TLS回调函数的指针全部改为0即可。
PE头在内存中的偏移和文件中的偏移一致,因此文件0x1B8处就是TLS结构的RVA:0x16518,位于.rdata节,距离节首偏移:0x16518-0x15000=0x1518。.rdata在文件中的偏移为0x3E00,因此TLS结够在文件偏移0x5318处:
00005310h: 00 00 00 00 00 00 00 00 00 90 41 00 08 92 41 00 ; .........怉..扐.
00005320h: 38 71 41 00 20 57 41 00 00 00 00 00 00 00 00 00 ; 8qA. WA.........
0x415720(怎么看都是一个VA)落在.rdata节中,距离节首0x720B,在文件中的偏移为:0x3E00+0x720=0x4520:
00004510h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................
00004520h: 1E 10 41 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ..A.............
00004530h: 00 00 00 00 AA 2C 9A 55 00 00 00 00 02 00 00 00 ; ....?歎........
0x4520处保存的就是回调函数指针,把他置零,然后对比置零前后调试结果,VS果断可以调试运行了~:
补上.rdata节的节信息:
00400240 2E 72 64 61>ASCII ".rdata" ; SECTION
00400248 411F0000 DD 00001F41 ; VirtualSize = 1F41 (8001.)
0040024C 00500100 DD 00015000 ; VirtualAddress = 0x15000
00400250 00200000 DD 00002000 ; SizeOfRawData = 2000 (8192.)
00400254 003E0000 DD 00003E00 ; PointerToRawData = 0x3E00
.text节的节信息:
00400218 2E 74 65 78>ASCII ".text" ; SECTION
00400220 6D390000 DD 0000396D ; VirtualSize = 396D (14701.)
00400224 00100100 DD 00011000 ; VirtualAddress = 0x11000
00400228 003A0000 DD 00003A00 ; SizeOfRawData = 3A00 (14848.)
0040022C 00040000 DD 00000400 ; PointerToRawData = 0x400
参考资料:
Windows PE权威指南
TLS反调试的前世今生
线程局部存储TLS