TLS反调试的前世今生
From http://www.bhst.org & http://nightx.info
0×00 TLS简述
Thread Local Storage(TLS),是Windows为解决一个进程中多个线程同时访问全局变量而提供的机制。TLS可以简单地由操作系统代为完成整个互斥过程,也可以由用户自己编写控制信号量的函数。当进程中的线程访问预先制定的内存空间时,操作系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性。
而当Coder选择使用自己编写的信号量函数时,在应用程序初始化阶段,系统将要调用一个由用户编写的初始化函数以完成信号量的初始化以及其他的一些初始化工作。此调用必须在程序真正开始执行到入口点之前就完成,以保证程序执行的正确性。
图 1
基于TLS的反调试,原理实为在实际的入口点代码执行之前执行检测调试器代码,实现方式便是使用TLS回调函数实现。通过TLS反调试实现的效果,形如图1,在OD动态调试器加载程序到入口点之前便已经执行反调试代码并退出程序。此外,利用TLS启动时,某些病毒也得以能够在调试器启动之前就开始运行,因为一些调试器是在程序的主入口点处切入的。
0×01 函数原型
TLS回调函数原型如下:
void NTAPI TlsCallBackFunction(PVOID Handle, DWORD Reason, PVOID Reserve);
实现TLS反调试,便是充分利用TLS回调函数在程序入口点之前就能获得程序控制权的特性,使得普通的反调试技术有更好的实际效果。
PE格式中,为TLS数据开辟了一段空间,位置为IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS]。
其中DataDirectory的元素具有如下数据结构:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
TLS的DataDirectory元素,VirtualAddress成员指向一个结构体,结构体中定义了访问需要互斥的内存地址、TLS回调函数地址以及其他一些信息。
0×02 C++下一个TLS-Anti-Debug的Demo
微软提供的VC编译器默认都支持直接在程序中使用TLS,要在程序中使用TLS,首先为TLS数据单独建一个数据段,并用相关数据填充此段,通知链接器为TLS数据在PE文件头中添加数据。
由此,给出第一个利用TLS实现加载前反调试的DEMO:Test1。
代码如下:
- #include “windows.h”
- #include “iostream”
- #include”tlhelp32.h”
- //通知链接器PE文件要创建TLS目录
- #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$XLB”)
- //定义回调函数
- PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
- #pragma data_seg()
- int main()
- {
- MessageBox(NULL,”Main!”,”Test1″,MB_OK);
- return 0;
- }
- //anti-debug1 进程遍历
- 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);
- }
复制代码
图 2
如图2,通过弹窗可以清晰的判断程序执行前首先执行了TLS回调函数。上述代码中,创建TLS段的部分“.CRT$XLB”的含义如下:
.CRT表明是使用C RunTime机制,$后面的XLB中:X表示随机的标识,L表示是TLS callback section,B可以被换成B到Y的任意一个字母,但是不能使用“.CRT$XLA”和“.CRT$XLZ”,因为“.CRT$XLA”和“.CRT$XLZ”是用于tlssup.obj的。
如果想要定义多个TLS回调函数,可以将
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
更改为:
PIMAGE_TLS_CALLBACK p_thread_callback [] = {tls_callback_1, tls_callback_2, tls_callback_3,0};
注意:
VC6.0下以这种方式会发现TLS实现失效,如图3,原因是VC6带的TLSSUP.OBJ有问题,它已定义了回调表的第一项,并且为0,0意味着回调表的结束,因此我们加的函数都不会被调用。
图 3
如不得不在VC6.0下实现TLS,可以尝试自己编写TLSSUP.OBJ,方法如下:
建立一个控制台工程,创建tlssup.c文件并将该文件加入工程。
右键该tlssup.c文件,选择Setting[设置]->C/C++->Gategory->recomliled Headers[预编译的头文件]->Not using precompiled headers[不适用预补偿页眉]。如图4
图 4
// tlssup.c代码:
- #include <windows.h>#include <winnt.h>int _tls_index=0;int _tls_start=0;#pragma data_seg(“.tls$ZZZ”)int _tls_end=0;#pragma data_seg(“.CRT$XLA”)int __xl_a=0;#pragma data_seg(“.CRT$XLZ”)int __xl_z=0;extern PIMAGE_TLS_CALLBACK my_tls_callback[];IMAGE_TLS_DIRECTORY32 _tls_used={(DWORD)&_tls_start,(DWORD)&_tls_end,(DWORD)&_tls_index,(DWORD)my_tls_callback,0,0};
然后,在其它CPP文件中定义my_tls_callbackt即可:
extern “C” PIMAGE_TLS_CALLBACK my_tls_callback[] = {my_tls_callback1,0};
可以有多个回调,但一定要在最后加一个空项,否则很可能出错。
0×03 实现效果
针对上面的DEMO1,编译运行后查看不同加载情况下效果:
使用VS2010编译后直接执行,程序正常运行,如上图2;
使用VS2010自带的调试器加载调试,进程直接退出;
图5使用OllyDbg加载程序调试,进程退出;
图 5
当然,最终反调试效果依赖于TLS回调函数中的反调试代码。
0×04 汇编实现
高级语言可以做的事情,汇编自然也可以做到,给出一种参考的汇编实现Demo:
- .386
- .model flat,stdcall
- option casemap:none
- include \masm32\include\windows.inc
- include \masm32\include\user32.inc
- include \masm32\include\kernel32.inc
- include \masm32\include\ntdll.inc
- includelib \masm32\lib\user32.lib
- includelib \masm32\lib\kernel32.lib
- includelib \masm32\lib\ntdll.lib
- .const
- TLS_CallBackStart dd TlsCallBack0
- szInNormal db ‘正常运行中’,0
- szTitle db ‘Anti-Debug’,0
- szInTls db ‘断点设置出错’,0
- szResult db ‘无法写回到已修改的寄存器’,0
- PUBLIC _tls_used
- _tls_used IMAGE_TLS_DIRECTORY <TLS_Start, TLS_End, dwTLS_Index, TLS_CallBackStart, 0, ?>
- .data?
- dwTLS_Index dd ?
- dwResult dd ?
- OPTION DOTNAME
- ;; 定义一个TLS节
- .tls SEGMENT
- TLS_Start LABEL DWORD
- dd 0100h dup(“slt.”)
- TLS_End LABEL DWORD
- .tls ENDS
- OPTION NODOTNAME
- .code
- ; ;TLS回调函数
- TlsCallBack0 proc Dllhandle:LPVOID,dwReason:DWORD,lpvReserved:LPVOID
- mov eax,dwReason
- cmp eax,DLL_PROCESS_ATTACH ; 在进行加载时被调用
- jnz ExitTlsCallBack0
- invoke GetCurrentProcessId
- invoke OpenProcess,PROCESS_ALL_ACCESS,NULL,eax
- invoke CheckRemoteDebuggerPresent,eax,addr dwResult
- cmp dword ptr dwResult,0
- jne _found
- jmp ExitTlsCallBack0
- _found:
- ;invoke MessageBox,NULL,addr szInTls,addr szTitle,MB_ICONWARNING
- invoke GetCurrentThread
- invoke NtSetInformationThread,eax,11H,NULL,NULL
- ;invoke MessageBox,NULL,addr szResult,addr szTitle,MB_ICONWARNING
- mov eax,ebx
- ExitTlsCallBack0:
- mov dword ptr[TLS_Start],0
- xor eax,eax
- inc eax
- ret
- TlsCallBack0 endp
- Start:
- invoke MessageBox,NULL,addr szInNormal,addr szTitle,MB_OK
- invoke ExitProcess, 1
- end Start
0×05 反反调试之策
针对TLS反调试,给出几种突破方法[注:针对无壳无其他PE加密环境]:
1.使用修改版Ollydbg,此处以ShoutBoy的Jiack版本OD为例,载入程序,直接停在TLS回调函数入口点。而后可以修改代码,跳过反调试部分,保存程序文件,再使用普通OD加载即可。如图6
图 6
2.手工抹掉TLS反调试,使用IDA加载程序,在Function WIndow中发现TlsCallback_0函数。如图7:
函数段:.text
位置:00401190[转换得文件偏移为590]
长度:44[对应10进制68字节]
图 7
通过转换得到文件偏移为590,长度为68字节。用C32asm等十六进制编辑器使用00填充此部分十六进制代码。如图8
图 8
这时执行程序,发现只弹出第二个窗口。载入OD,已经跳过TLS回调函数的反调试部分直接到达EP。如图9
图 9
0×06 一点扩展
利用TLS回调函数在调试器加载前执行Anti-Debug函数保护软件不被恶意修改,关键部分依旧是具体实现反调试部分的代码编写。比如传统的检测断点,检测进程,检测调试器等等。而后,为更好的实现反调试效果,可通过类似加密壳的方式对输入表与输出表等进行加密,更大程度的保护TLS代码不被轻易删除