第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 |
|
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 |
|
仔细观察TLS回调函数的定义可以发现,它与DllMain()函数的定义类似。代码45-3是DllMain()函数的定义。
1 |
|
观察以上2个函数可以发现,它们的参数顺序与含义都是一样的。其中,参数DllHandle为模块句柄(即加载地址),参数Reason表示调用TLS回调函数的原因,具体原因有4种,如代码45-4所示。
1 |
|
要想准确理解TLS回调函数的工作原理(在哪个时间点调用哪个回调函数),最好的方法就是亲自创建。接下来做第二个练习示例,以进一步学习TLS回调函数的工作原理。
45.4 练习 #2: TlsTest.exe
TlsTest.exe程序是使用Visual C++编写的,它向各位充分展现了注册TLS回调函数的方法。代 码45-5(TlsTest.cpp)是TlsTest.exe程序的源代码。
1 |
|
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回调函数常用于反调试,请各位务必掌握本章知识。