TLS 回调函数

简单介绍

TLS(Thread Local Storage, 线程局部存储)回调函数,TLS 回调函数的调用运行要先于 EP 代码的执行,并且每次创建或结束线程都会再次调用,故常用于反调试。

函数定义

1
2
3
4
typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK)(
PVOID DllHandle,
DWORD Reason,
PVOID Reserved);

其与 DllMain() 函数的定义十分相似。

细节

TLS 和 TLS 的特征

TLS 是各线程的独立的数据存储空间,使用 TLS 技术可以在线程内部独立使用或修改进程的全局数据或静态数据。

若启用 TLS 功能,PE 头文件中会设置 TLS 表(TLS Table)项目:

启用TLS功能

未启用则是这样的:

未启用

并且如果使用 IDA 打开,会在 Exports 选项卡看到 TlsCallBack 字样:

IDA中的TLS

TLS 调用原因(Reason)

1
2
3
4
5
//winnt.h
#define DLL_PROCESS_ATTACH 1 //进程创建
#define DLL_THREAD_ATTACH 2 //线程创建
#define DLL_THREAD_DETACH 3 //线程销毁
#define DLL_PROCESS_DETACH 0 //进程销毁

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
//本代码在vs2019x86平台上通过测试
#include<stdio.h>
#include<Windows.h>
#pragma comment(linker,"/INCLUDE:__tls_used")
void NTAPI TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
printf("TLStest,Reason=%d\n",Reason);
}
#pragma data_seg(".CRT$XLY")
PIMAGE_TLS_CALLBACK tls_callback_func = TLS_CALLBACK;
#pragma data_seg()
DWORD WINAPI ThreadProc(LPVOID lParam)
{
printf("Thread start\n");
printf("Thread stop\n");
}
int main()
{
printf("main start\n");
auto hThread=CreateThread(NULL,0,ThreadProc,NULL,0,NULL);
WaitForSingleObject(hThread, 1);
CloseHandle(hThread);
printf("main stop\n");
}

对以上代码的解释:

  • #pragma comment(linker,"/INCLUDE:__tls_used")作用是告知编译器使用 TLS 回调函数。其中,在 x86 环境下,使用__tls_used,而在x64环境下使用_tls_used

  • TLS_CALLBACK这个函数即是我们定义的 TLS 回调函数,遵照上面的函数定义即可。

  • #pragma data_seg(".CRT$XLY")#pragma data_seg()作用是注册回调函数,如果回调函数只有 1 个,则可以以上述形式编写代码即可,如果回调函数有多个,则可以声明一个数组,如PIMAGE_TLS_CALLBACK tls_callback_func[] = {TLS_CALLBACK1, TLS_CALLBACK2, 0};注意数组最后一个元素必须为 0 。WaitForSingleObject这个函数作用是等待线程运行,否则 main 进程会先于 ThreadProc 线程结束,有可能 ThreadProc 线程还未运行,进程就结束了,运行结果就不太清晰

  • 如果编译选项中开启了 /MT ,则 TLS 中的 printf 函数可能会出现错误,此时可以使用如下代码来防止出现问题:

    1
    2
    3
    4
    char string[80]={0};
    wsprintfA(string,"TLStest,reason=%d\n",Reason);
    HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
    WriteConsoleA(hStdout, string, strlen(string), NULL, NULL);
  • 运行结果如下,可以看出其 Reason 对应的各种情况:

    运行结果

    如何调试 TLS 回调函数

    直接使用调试器打开带 TLS 回调函数的程序,则无法调试 TLS 回调函数,因为 TLS 早在 EP 代码开始前就被执行了,因此需要在调试器中打开System breakpoint选项,从而使程序暂停在系统启动断点System Startup Breakpoint处。有的调试器也提供”暂停在 TLS 回调函数处”的选项。