在学习逆向的过程中,大家可能了解到利用TLS,可以实现反调试,因为TLS回调函数会在入口函数之前执行,但是可能不知道具体的原理,以及TLS是个什么东西,通过下面的介绍,你将会了解TLS的原理以及用法。
什么是线程局部存储
线程局部存储(Thread Local Storage),又简称TLS, 设计的目的是解决多线程之间变量的同步问题,程序员通过使用TLS机制,可以实现让程序拥有全局变量,但在不同的线程里却对应有不同的值,也就是说,进程中的所有线程都可以拥有全局变量,但这些变量其实是特定对某个线程才有意义的。
线程局部存储共分动态和静态两种,分类的主要依据是: 线程局部存储的数据所用空间,操作系统完成的是动态申请还是静态分配
动态线程局部存储
动态TLS存在以下四个API函数
TlsAlloc
TlsGetValue
TlsSetValue
TlsFree
应用程序或DLL在合适的时候会调用这四个函数,通过索引对进程中每个线程的存储区进行统一操作。他们位于动态链接库文件kernel32.dll
下面通过一个例子(不用动态TLS和使用动态TLS)来说明一下TLS的作用以及这4个API的用法
例子(不用TLS):
#include <stdio.h>
#include <windows.h>
int num;
//线程函数模板
DWORD WINAPI ThreadFunc(LPVOID lParam)
{
num = 5;
for (int i = 0; i < 5; i++)
{
printf("当前线程ID: %d, num = %d\n", GetCurrentThreadId(), num);
num--;
}
return 0;
}
int main()
{
HANDLE hThread[4] = {};
for (int i = 0; i < 4; i++)
{
hThread[i] = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
}
WaitForMultipleObjects(4, hThread, TRUE, INFINITE);
printf("All threads has been destroyed!\n");
for (int i = 0; i < 4; i++)
{
CloseHandle(hThread[i]);
}
getchar();
return 0;
}
期望的输出是每个线程num的输出都是 5 4 3 2 1,但是结果却是
使用了动态TLS例子
#include <stdio.h>
#include <windows.h>
int num;
//线程函数模板
DWORD WINAPI ThreadFunc(LPVOID lParam)
{
TlsSetValue(num, (LPVOID)5); //设置某个线程该索引处对应的值
for (int i = 0; i < 5; i++)
{
printf("当前线程ID: %d, num = %d\n", GetCurrentThreadId(), TlsGetValue(num));
TlsSetValue(num, (LPVOID)((DWORD)TlsGetValue(num) - 1));
}
return 0;
}
int main()
{
HANDLE hThread[4] = {};
num = TlsAlloc(); //分配一个索引
if (num == TLS_OUT_OF_INDEXES)
{
printf("分配失败!\n");
return 0;
}
for (int i = 0; i < 4; i++)
{
hThread[i] = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
}
WaitForMultipleObjects(4, hThread, TRUE, INFINITE);
printf("All threads has been destroyed!\n");
TlsFree(num); //释放线程Tls分配的空间
for (int i = 0; i < 4; i++)
{
CloseHandle(hThread[i]);
}
getchar();
return 0;
}
输出结果为
可以看出TLS为线程绑定了数据,到了这里,大家也应该明白TLS 为啥叫线程局部存储了吧,,,就是把数据绑定到线程中,线程在哪,数据就在哪
线程局部存储的具体实现原理
线程局部存储机制的示意图
进程环境块的0X44偏移处的数据为 ULONG TlsBitmapBits[2] 共64位,每个位可以是0或者1,分别代表未使用和已使用
线程环境块TEB的0XE10偏移处为 PVOID TlsSlot[0X40]; 是一个PVOID类型的数组,长度为64,数组的索引号对应着进程标志位的位索引
什么意思呢?我画张图,再结合上面的代码,大家就明白了
我们通过TlsAlloc是分配一个索引,上面的代码中,num就是2,然后在PEB的TlsBitmapBits的索引是2的位就会被设置成1
然后我们通过TlsSetValue设置就是根据索引,将对应线程的TlsSlot数组中对应的索引设置成对应的数据,如上代码中就设置成5了,然后用TlsGetValue是得到成对应的值
TlsFree是释放这个索引对应的线程的空间,将PEB中TlsBitmapBits的对应位设置为0
静态线程局部存储
静态线程局部存储是操作系统提供的另外一种线程与数据绑定的技术。它与动态TLS的区别在于,通过静态线程局部存储指定的数据无需使用专门的API函数,随意在易用性上会更好一些。
静态线程局部存储预先将变量定义在PE文件内部,一般使用.tls节存储(也不一定),对于相关API的调用由操作系统来完成。这种方式的有点就是从高级语言程序员角度来看更简单了。这种实现方式使得TLS数据的定义与初始化就像程序中使用普通的静态变量那样。
对静态TLS变量的定义不需要想动态线程局部存储一样,调用相关API,只需要做如下声明即可:
_declspec(thread) int tlsFlag=1;
为了支持这种编程模式。PE中的.tls节会包含以下信息:
1、初始化数据
2、用于每个线程初始化和终止的回调函数
3、TLS索引
可执行代码访问静态TLS数据一般需要经过一下几个步骤:
1.在链接的时候,连接器设置TLS目录中的AddressOfIndex字段。这个字段指向一个位置,在这个位置保存程序用到的TLS索引。
2.当创建线程是,加载器通过将线程环境块TEB的地址放入FS寄存器来传递线程的TLS数组地址。距TEB开头0x2c的位置处的字段ThreadLocalStoragePointer指向TLS数组。
3.加载器将TLS索引值保存到AddressOfIndex字段指向的位置处。
4.可执行代码获取TLS索引以及TLS数组的位置。
5.可执行代码将索引乘以4,并将该值作为这个数组内的偏移来使用。通过以上方法获取给定程序和模块的TLS数据区的地址。每个线程拥有他自己的TLS数据区,但这对于线程是透明的,它并不需要知道怎为单个线程分配数据的。
6.单个的TLS数据对象都位于TLS数据区的某个固定偏移处,因此可以用这种方式访问。
静态的TLS主要的应用是TLS回调函数。它可以在入口函数之前执行,所以可以做很多事情,比如反调试,加解密啥的。
下面 写代码 实验一下
#include <stdio.h>
#include <windows.h>
__declspec(thread) int value = 0X12345678;
#define THREAD_NUM 3
//线程函数模板
DWORD WINAPI ThreadFunc(LPVOID lParam)
{
printf("线程ID ->%d, value = %#X\n", GetCurrentThreadId(), value);
return 0;
}
int main()
{
HANDLE hThreads[THREAD_NUM] = {};
value = 5;
for (int i = 0; i < THREAD_NUM; i++)
{
hThreads[i] = CreateThread(NULL, 0, ThreadFunc, 0, 0, 0);
}
WaitForMultipleObjects(THREAD_NUM, hThreads, TRUE, INFINITE);
getchar();
return 0;
}
这里的运行结果是:
正是因为静态线程局部存储的原因,使得每个线程都有一份value的拷贝,所以在这里即使主线程将value赋值为5,在其他线程当中value的值仍然是0X12345678
我们将生成的exe文件拖入到CFF Explorer中,查看其TLS 目录
StartAddressOfRawData 4025A4
EndAddressOfRawData 4025AC
AddressOfindex 403380
AddressOfCallBacks 4020EC
SizeOfZeroFill 0
Characteristics 00300000
在每个线程中都对应的着value的拷贝,那么他的拷贝原数据是在哪的,
这里StartAddressOfRawData 和 EndAddressOfRawData 之间就是拷贝的源数据,针对这个程序来说,我们找到这个StartAddressOfRawData在文件中对应的偏移
我们发现果然是这样,在代码中写的value的值0X12345678就在这里存着
那么线程是怎么找到其对应的value 的,在动态线程局部存储中,是在TEB 偏移E10处(TLS槽)存着,但静态线程局部存储不是这样的
我们用OD调试,找到线程函数那里
在TEB的2C处存的一个指针, 这个指针指向一个TLS槽(注意这里不是TEB中的那个TLS槽,而是自己分配的空间)
对于这个程序来说,索引是0,结构是这样的
我们在看看回调函数,由于程序中没有定义回调函数,所以AddressOfCallBacks指向的数据是0
下面我们重写代码添加回调函数
#include <stdio.h>
#include <windows.h>
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
__declspec(thread) int value = 0X12345678;
#define THREAD_NUM 3
void NTAPI tls_callback(PVOID h, DWORD reason, PVOID pv)
{
if (reason == DLL_PROCESS_ATTACH)
{
MessageBox(NULL, "线程局部存储回调函数", "测试", MB_OK);
}
return;
}
//线程函数模板
DWORD WINAPI ThreadFunc(LPVOID lParam)
{
printf("线程ID ->%d, value = %#X\n", GetCurrentThreadId(), value);
return 0;
}
int main()
{
HANDLE hThreads[THREAD_NUM] = {};
value = 5;
for (int i = 0; i < THREAD_NUM; i++)
{
hThreads[i] = CreateThread(NULL, 0, ThreadFunc, 0, 0, 0);
}
WaitForMultipleObjects(THREAD_NUM, hThreads, TRUE, INFINITE);
getchar();
return 0;
}
EXTERN_C
#pragma data_seg (".CRT$XLB")
PIMAGE_TLS_CALLBACK _tls_callback = tls_callback;
#pragma data_seg ()
至于为啥子这样添加反调试,请参考https://www.jianshu.com/p/841d360777de
我们运行一下程序,会先弹出窗口,在打印
我们来到AddressOfCallBacks指向的地方
将程序拖入到OD中
会发现程序先执行TLS回调函数,然后再执行main函数
(补充,,利用这一点可以实现很多事情,比如:加解密,反调试啥的)
参考资料:
《Windows PE权威指南》
https://blog.youkuaiyun.com/lixiangminghate/article/details/46770635
https://www.jianshu.com/p/841d360777de