第45章 TLS回调函数

本文最后更新于:2022年5月27日 下午

第45章TLS回调函数

代码逆向分析领域中,TLS(Thread Local Storage,线程局部存储)回调函数(Callback Function)常用于反调试,本章将学习TLS回调函数的相关知识。TLS回调函数的调用运行要先于EP代码的执行,该特征使它可以作为一种反调试技术使用。下面通过练习示例来了解有关TLS回调函数的内容。

所有练习示例在Windows XP & 7(32位)中通过测试。

45.1 练习 #1: HelloTls.exe

运行练习程序文件(HelloTls.exe),弹出一个消息框,单击“确定”按钮后,程序终止运行,如图45-1所示。

下面使用OllyDbg调试练习示例程序。在OllyDbg调试器中打开并运行HelloTls.exe文件,弹岀如图45-2所示的消息对话框。

如图45-2所示,消息对话框中显示的内容与程序正常运行时显示的内容不同。单击“确定”按钮HelloTls.exe进程随即终止,如图45-3所示。

正常运行与调试运行中出现不同行为的原因在于,程序运行EP代码前先调用了TLS回调函数,而该回调函数中含有反调试代码,使程序在被调试时弹出“DebuggerDetected!”消息对话框。如果不理解这一原理,调试将无法继续。以上练习示例虽然简单,但却很好地描述了TLS回调函 数的行为特征。接下来讲TLS与TLS回调函数的相关知识,学习其工作原理。

45.2 TLS

讲解TLS回调函数前,先简单了解一下有关TLS的知识。TLS是各线程的独立的数据存储空间。使用TLS技术可在线程内部独立使用或修改进程的全局数据或静态数据,就像对待自身的局部变量一样(编程中这种功能非常有用)。

关于TLS更详细的介绍请参考以下网址:

http://msdn.microsoft.com/en-us/library/ms686749(VS.85).aspx

45.2.1 IMAGE_DATA_DIRECTORY[9]

若在编程中启用了TLS功能,PE头文件中就会设置TLS表(TLS Table)项目,如下图所示(IMAGE_NT_HEADERS-IMAGE_OPTIONAL_HEADER-IMAGE_DATA_DIRECTORY[9])。

如图45-4所示,IMAGE_TLS_DIRECTORY结构体位于RVA 9310地址处。

45.2.2 IMAGE TLS DIRECTORY

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
typedef struct _IMAGE_TLS_DIRECTORY64 {
ULONGLONG StartAddressOfRawData;
ULONGLONG EndAddressOfRawData;
ULONGLONG AddressOfIndex; // PDWORD
ULONGLONG AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *;
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY64;
typedef IMAGE_TLS_DIRECTORY64 * PIMAGE_TLS_DIRECTORY64;

typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData;
DWORD EndAddressOfRawData;
DWORD AddressOfIndex; // PDWORD
DWORD AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;

#ifdef _WIN64
typedef IMAGE_THUNK_DATA64 IMAGE_THUNK_DATA;
typedef PIMAGE_THUNK_DATA64 PIMAGE_THUNK_DATA;
typedef IMAGE_TLS_DIRECTORY64 IMAGE_TLS_DIRECTORY;
typedef PIMAGE_TLS_DIRECTORY64 PIMAGE_TLS_DIRECTORY;
#else
typedef IMAGE_THUNK_DATA32 IMAGE_THUNK_DATA;
typedef PIMAGE_THUNK_DATA32 PIMAGE_THUNK_DATA;
typedef IMAGE_TLS_DIRECTORY32 IMAGE_TLS_DIRECTORY;
typedef PIMAGE_TLS_DIRECTORY32 PIMAGE_TLS_DIRECTORY;
#endif

IMAGE_TLS_DIRECTORY结构体有2种版本,分别为32位版本与64位版本,以上练习示例中使用的是32位版本的结构体(大小为18h)。使用PEView工具査看IMAGE_TLS_DIRECTORY结构体(RVA: 9310),其各成员如图45-5所示。

代码逆向分析中涉及的比较重要的成员为Address Of Callbacks,该值指向含有TLS回调函数地址(VA)的数组。这意味着可以向同一程序注册多个TLS回调函数(数组以NULL值结束)。

45.2.3回调函数地址数组

图45-6就是TLS回调函数地址数组。

该数组中实际存储的就是TLS回调函数的地址。进程启动运行时,(执行EP代码前)系统会逐一调用存储在该数组中的函数。请注意,虽然以上练习示例中仅注册了 1个TLS函数(地址为401000),但其实我们可以通过修改程序注册多个TLS函数。

45.3 TLS回调函数

接下来从技术层面简单整理之前介绍的TLS回调函数相关内容。

所谓TLS回调函数是指,每当创建/终止进程的线程时会自动调用执行的函数。有意思的是,创建进程的主线程时也会自动调用回调函数,且其调用执行先于EP代码。反调试技术利用的就是TLS回调函数的这一特征。

请注意,创建或终止某线程时,TLS回调函数都会自动调用执行,前后共2次(原意即为此)。执行进程的主线程(运行进程的EP代码)前,TLS回调函数会先被调用执行,许多逆向分析人员将该特征应用于程序的反调试技术。

IMAGE_TLS_CALLBACK

TLS回调函数的定义如代码45-2所示

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

仔细观察TLS回调函数的定义可以发现,它与DllMain()函数的定义类似。代码45-3是DllMain()函数的定义。

1
2
3
4
5
BOOL WINAPI DllMain(
HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpvReserved
)

观察以上2个函数可以发现,它们的参数顺序与含义都是一样的。其中,参数DllHandle为模块句柄(即加载地址),参数Reason表示调用TLS回调函数的原因,具体原因有4种,如代码45-4所示。

1
2
3
4
#define DLL_PROCESS_ATTACH   1    
#define DLL_THREAD_ATTACH 2
#define DLL_THREAD_DETACH 3
#define DLL_PROCESS_DETACH 0

要想准确理解TLS回调函数的工作原理(在哪个时间点调用哪个回调函数),最好的方法就是亲自创建。接下来做第二个练习示例,以进一步学习TLS回调函数的工作原理。

45.4 练习 #2: TlsTest.exe

TlsTest.exe程序是使用Visual C++编写的,它向各位充分展现了注册TLS回调函数的方法。代 码45-5(TlsTest.cpp)是TlsTest.exe程序的源代码。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <windows.h>

#pragma comment(linker, "/INCLUDE:__tls_used") //告诉链接器要使用TLS

void print_console(char* szMsg)
{
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);

WriteConsoleA(hStdout, szMsg, strlen(szMsg), NULL, NULL);
}

void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
char szMsg[80] = {0,};
wsprintfA(szMsg, "TLS_CALLBACK1() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
print_console(szMsg);
}

void NTAPI TLS_CALLBACK2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
char szMsg[80] = {0,};
wsprintfA(szMsg, "TLS_CALLBACK2() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
print_console(szMsg);
}

/*
注册TLS函数

但是只规定了回调函数的地址以及函数在那个节区

#pragma comment(linker,"/INCLUDE:__tls_used")这条语句的作用就是告诉链接器.CRT$XLY里有回调函数的地址,来调用吧

.CRT$XLX的作用
CRT表示使用C Runtime 机制
X表示表示名随机 L表示TLS Callback section
X也可以换成B~Y任意一个字符

*/
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1, TLS_CALLBACK2, 0 };
#pragma data_seg()

DWORD WINAPI ThreadProc(LPVOID lParam)
{
print_console("ThreadProc() start\n");

print_console("ThreadProc() end\n");

return 0;
}

int main(void)
{
HANDLE hThread = NULL;

print_console("main() start\n");

hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
WaitForSingleObject(hThread, 60*1000);
CloseHandle(hThread);

print_console("main() end\n");

return 0;
}

TlsTest.cpp源代码中注册了2个TLS回调函数(TLS_CALLBACK1、TLS_CALLBACK2)。它们也非常简单,只是将DllHandle与Reason这2个参数的值输出到控制台,然后终止退出。main()函数也非常简单,创建用户线程(ThreadProc)后终止,main()与ThreadProc()内部分别将函数开始/终止日志输岀到控制台。图45-7是TlsTest.exe程序运行的画面。

下面分别讲解各函数调用顺序。

45.4.1 DLL_PROCESS_ATTACH

进程的主线程调用main()函数前,已经注册的TLS回调函数(TLS_CALLBACK1、TLS_CALLBACK2)会先被调用执行,此时Reason的值为 1(DLL_PROCESS_ATTACH)。

45.4.2 DLL_THREAD_ATTACH

所有TLS回调函数完成调用后,main()函数开始调用执行,创建用户线程(ThreadProc)前,TLS回调函数会被再次调用执行,此时Reason=2(DLL_THREAD_ATTACH)。

45.4.3 DLL_THREAD_DETACH

TLS回调函数全部执行完毕后,ThreadProc()线程函数开始调用执行。其执行完毕后Reason=3(DLL_THREAD_DETACH),TLS回调函数被调用执行。

45.4.4 DLL_PROCESS_DETACH

ThreadProc()线程函数执行完毕后,一直在等待线程终止的main()函数(主线程)也会终止。

此时Reason=0(DLL_PROCESS_DETACH), TLS回调函数最后一次被调用执行。以上TlsTest.exe练习示例中,2个TLS回调函数分别被调用执行了4次,总共为8次。现在我们已经对TLS回调函数的注册及工作原理有了深入了解。接下来学习其调试方法。

TlsTest.cpp源文件中并未使用printf()函数,因为开启特定编译选项(/MT)编译源 程序时,先于主线程调用执行的TLS回调函数中可能发生Run-Time Error(运行时错误)。此时可以直接调用WriteConsole() API来以防万一。

45.5 调试TLS回调函数

若直接使用调试器打开带有TLS回调函数的程序,则无法调试TLS回调函数,因为TLS回调函数在EP代码之前就被调用执行了。练习示例#1文件(HdloTls.exe)中,TLS回调函数内部还含有反调试代码,这使程序调试无法继续。如图45-8所示,此时修改OllyDbg选项就可以调试TLS回调函数。

然后重启调试器重新调试HelloTls.exe,调试器就会在ntdll.dll模块内部的“System Startup Breakpoint”处暂停,如图45-9所示。

调试器暂停的位置即是系统启动断点(System Startup Breakpoint)0在OllyDbg调试器的默认设置下,调试器会在EP处暂停,而WinDbg调试器默认在系统启动断点暂停。

参考图45-5与图45-6获取TLS回调函数的地址,然后在回调函数的起始地址设置好断点,这样就可以调试TLS回调函数了。

使用特定调试器插件(如Oily Advanced)时,存在一个“暂停在TLS回调函数”的选项,使用起来更加方便。此外,最新版本的OllyDbg(版本2.0以上)默认提供“暂停在TLS回调函数”的选项,如图45-10所示

请各位亲自调试HelloTls.exe的TLS回调函数。

45.6 手工添加TLS回调函数

我比较喜欢翻看并修改PE文件,借助几种工具(OllyDbg、PEView、HxD),我们可以随心所欲地修改PE文件。本节的目标是直接修改Hello.exe文件(PE文件),为其添加TLS回调函数,使之与前面介绍的HelloTls.exe练习文件具有类似的行为功能。下面向大家介绍手工修改PE文件并添加TLS回调函数的过程。

随心所欲地修改PE文件前,需要了解PE文件格式相关知识,并通过大量练习来熟悉它们。此外,不同版本WindowsOS的PE装栽器的行为动作会有细微差别,反复练习即可逐渐掌握。

45.6.1修改前的原程序

修改前的原程序为Hello.exe,它非常简单,运行时弹出一个消息框,然后终止退岀,如图45-11所示。

我们的目标是手工修改原程序文件,添加TLS回调函数,使之与HelloTls.exe具有类似行为。

45.6.2设计规划

首先要确定IMAGE_TLS_DIRECTORY结构体与TLS回调函数放到文件的哪个位置。向某个 PE文件添加代码或数据时,有如下3种方法来查找合适位置:

第一,添加到节区末尾的空白区域。

第二,增加最后一个节区的大小。

第三,在最后添加新节区。

这里采用第二种方法,即增加最后一个节区的大小(参考图45-14)。使用PEView查看Hello.exe文件最后一个节区(.rsrc)的节区头(请注意,Hello.exe的Section Alignments1000,File Alignment=200),如图45-12所示。

可以看到,最后一个节区(.rsrc)的Pointer to Raw Data=9000,Size of Raw Data=200。所以PE头中定义的文件整体大小为9200。考虑到要添加的代码与数据的大小,我们将最后一个节区的大小增加200(文件的大小增加到9400)。使用010Editor工具打开Hello.exe文件,移动光标至最后位置,在菜单栏中选择Edit-Insert bytes菜单,打开插入字节对话框。如图45-13所示,向Bytecount中输入200,单击OK按钮后,即从光标的当前位置新添加了200h个字节(即512个字节)。

图45-12中Virtual Size为1B4,PE装载器会按照Section Alignment值对齐该值,即加载到内存中的大小为1000(一定要理解好这个关系)。所以将节区的文件大小增加200后,实际Virtual Size值变为3B4,它比加栽到内存中的尺寸1000要小,所以不需要再单独增大Virtual Size的值。不增大Virtual Size的值同样不会影响增加的200h字节被复制到内存镜像中(Virtual Size只会只会影响对齐后的大小!!!)。

45.6.3编辑PE文件头

.rsrc节区头

请参考图45-12,分别修改.rsrc节区头中Size of Raw Data与Characteristics的值,即Size of Raw Data=400、Characteristics=E0000060,如图45-15所示。

在原有属性的基础上新增加了 IMAGE_SCN_CNT_CODE|IMAGE_SCN_MEM_EXECUTE|IMAGE_SCN_MEMWRITE属性。

由于要在扩展区域内创建IMAGE_TLS_DIRECTORY结构体与TLS回调函数,所 以需要向该节区添加 IMAGE_SCN_CNT_CODE|IMAGE_SCN_MEM_EXECUTE 属性。此外,还必须向包含IMAGE_TLS_DIRECTORY结构体的节区添加IMAGE_SCN_MEM WRITE属性,才能保证正常运行。

IMAGE_DATA_DIRECTORY[9]

接下来要设置TLS表(IMAGE_NT_HEADERS-IMAGE_OPTIONAL_HEADER-IMAGE_DATA_DIRECOTRY[9])的值。从图45-14中可以看到,扩展区域的起始地址为9200(文件偏移)。在PEView中查看该地址为C200(RVA地址),我们将从该地址处创建IMAGE_TLS_DIRECTORY结构体。因此修改PE文件头中的IMAGE_DATA_DIRECTORY[9],如图45-17所示(RVA=C200, Size=18)。

修改后用PEView工具查看,如图45-18所示。

45.6.4 设置 IMAGE_TLS_DIRECTORY 结构体

接下来设置IMAGE_TLS_DIRECTORY结构体,只要把TLS回调函数注册到其中即可。编辑设置IMAGE_TLS_DIRECTORY结构体,如图45-19所示。

我们在文件偏移9200(RVA C200)地址处创建了 IMAGE_TLS_DIRECTORY结构体。AddressOfCallbacks成员的值为VA 40C224(文件偏移9224),它是Array ofTLS Callback Function(TLS回调函数数组)的起始地址。只要把TLS回调函数的地址(40C230)放入该数组(VA:40C224,Offset: 9224),即可成功注册TLS回调函数。使用PEView工具查看设置后的IMAGE_TLS_DIRECTORY结构体,如图45-20所示。

先向TLS回调函数写入“C2 0C00 - RETN 0C”命令,即在TLS回调函数中不执行任何操作,直接返回。

TLS回调函数的返回指令不是RETN,而是RETN 0C指令,因为函数有3个参数(大小为0C),所以需要修正栈,修正大小为0C。现在运行修改后的Hello.exe文件,若修改没有问题,则能正常运行。

45.6.5 编写TLS回调函数

上述准备工作全部完成后,接下来编写TLS回调函数3利用OllyDbg的汇编功能,从40C230地址处开始编写反调试代码,如图45-21所示。

如图45-21所示,编写好TLS回调函数后,将修改的代码与数据全部选中(40C230~40C291),在鼠标右键中依次选择Copy to executable-Selection - Save file菜单,保存为ManualHelloTls.exe文件。

下面简单讲解TLS回调函数的代码。Reason参数值为1(DLL_PROCESS_ATTACH)时,检 查PEB.BeingDebugged成员,若处于调试状态,则弹岀消息框(MessageBoxA)后终止并退岀进程(ExitProcess)。阅读代码时,参考代码注释就很容易把握代码结构。此外还要注意,传递给MessageBoxA()函数的2个字符串参数分别存储在40C270与40C280地址处。

MessageBoxA()与 ExitProcess() API 的 IAT 地址(分别为4080E8与408028)使用原 Hello.exe 的 IAT 中的即可。在OllyDbg 的 Assemble 对话框中,以 “CALL user32 .MessageBoxA”、“CALLKemel32.ExitProcess” 形式输入就可以了。OllyDbg调试器会

自动求得API的地址并输入结果。如果要调用的API不在IAT中,那么编写代码时要复杂得多。

45.6.6最终完成

在OllyDbg中打开并运行上面编写的ManualHelloTls.exe文件时,弹岀“Debugger Detected!”消息框,如图45-22所示,单击“确定”后,程序终止运行,这表明手工添加TLS回调函数成功通过手动方式向PE文件添加TLS回调函数的练习到此结束。

45.7小结

本章我们学习了TLS回调函数的工作原理及具体实现方法,并了解了其调试方法。TLS回调函数常用于反调试,请各位务必掌握本章知识。


第45章 TLS回调函数
https://m0ck1ng-b1rd.github.io/1999/04/05/逆向工程核心原理/第45章 TLS回调函数/
作者
何语灵
发布于
1999年4月5日
许可协议