探索黑客技术攻防,实战研究与安全创新

导航菜单

TLS反调试的前世今生

0x00TLS简述

ThreadLocalStorage(TLS),是Windows为解决一个进程中多个线程同时访问全局变量而提供的机制。TLS可以简单地由操作系统代为完成整个互斥过程,也可以由用户自己编写控制信号量的函数。当进程中的线程访问预先制定的内存空间时,操作系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性。

而当Coder选择使用自己编写的信号量函数时,在应用程序初始化阶段,系统将要调用一个由用户编写的初始化函数以完成信号量的初始化以及其他的一些初始化工作。此调用必须在程序真正开始执行到入口点之前就完成,以保证程序执行的正确性。

F10.png

基于TLS的反调试,原理实为在实际的入口点代码执行之前执行检测调试器代码,实现方式便是使用TLS回调函数实现。通过TLS反调试实现的效果,形如图1,在OD动态调试器加载程序到入口点之前便已经执行反调试代码并退出程序。此外,利用TLS启动时,某些病毒也得以能够在调试器启动之前就开始运行,因为一些调试器是在程序的主入口点处切入的。

0x01函数原型

TLS回调函数原型如下:


voidNTAPITlsCallBackFunction(PVOIDHandle,DWORDReason,PVOIDReserve);


实现TLS反调试,便是充分利用TLS回调函数在程序入口点之前就能获得程序控制权的特性,使得普通的反调试技术有更好的实际效果。

PE格式中,为TLS数据开辟了一段空间,位置为IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS]。

其中DataDirectory的元素具有如下数据结构:


typedefstruct_IMAGE_DATA_DIRECTORY{
DWORDVirtualAddress;
DWORDSize;
}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;


TLS的DataDirectory元素,VirtualAddress成员指向一个结构体,结构体中定义了访问需要互斥的内存地址、TLS回调函数地址以及其他一些信息。

0x02C++下一个TLS-Anti-Debug的Demo微软提供的VC编译器默认都支持直接在程序中使用TLS,要在程序中使用TLS,首先为TLS数据单独建一个数据段,并用相关数据填充此段,通知链接器为TLS数据在PE文件头中添加数据。

由此,给出第一个利用TLS实现加载前反调试的DEMO:Test1。

代码如下:


#includewindows.h
#includeiostream
#includetlhelp32.h
//通知链接器PE文件要创建TLS目录
#pragmacomment(linker,/INCLUDE:__tls_used)
voidlookupprocess(void);
voidDebugger(void);
voidNTAPItls_callback(PVOIDh,DWORDreason,PVOIDpv)
{
lookupprocess();
Debugger();
MessageBox(NULL,NotMain!,Test1,MB_OK);
return;
}
//创建TLS段
#pragmadata_seg(.CRT$XLB)
//定义回调函数
PIMAGE_TLS_CALLBACKp_thread_callback=tls_callback;
#pragmadata_seg()
intmain()
{
MessageBox(NULL,Main!,Test1,MB_OK);
return0;
}
//anti-debug1进程遍历
voidlookupprocess()
{
PROCESSENTRY32pe32;
pe32.dwSize=sizeof(pe32);
HANDLEhProcessSnap=::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
BOOLbMore=::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
voidDebugger(void)
{
intresult=0;
__asm{
mov
eax,dwordptrfs:[30h]//TEB偏移30H处
movzxeax,byteptrds:[eax+2h]//取PEB中BeingDebug,若为1则被调试
mov
}
result,eax
if(result)exit(0);
}


F11.png

如图2,通过弹窗可以清晰的判断程序执行前首先执行了TLS回调函数。上述代码中,创建TLS段的部分.CRT$XLB的含义如下:.CRT表明是使用CRunTime机制,$后面的XLB中:X表示随机的标识,L表示是TLScallbacksection,B可以被换成B到Y的任意一个字母,但是不能使用.CRT$XLA和.CRT$XLZ,

因为.CRT$XLA和.CRT$XLZ是用于tlssup.obj的。

如果想要定义多个TLS回调函数,可以将

PIMAGE_TLS_CALLBACKp_thread_callback=tls_callback;


更改为:


PIMAGE_TLS_CALLBACKp_thread_callback[]={tls_callback_1,tls_callback_2,tls_callback_3,0};


注意:

VC6.0下以这种方式会发现TLS实现失效,如图3,原因是VC6带的TLSSUP.OBJ有问题,它已定义了回调表的第一项,并且为0,0意味着回调表的结束,因此我们加的函数都不会被调用.

F12.png

如不得不在VC6.0下实现TLS,可以尝试自己编写TLSSUP.OBJ,方法如下:建立一个控制台工程,创建tlssup.c文件并将该文件加入工程。右键该tlssup.c文件,选择Setting[设置]-C/C++-Gategory-PrecomliledHeaders[预编译的头文件]-Notusingprecompiledheaders[不适用预补偿页眉]。如图4

F13.png

图4


//tlssup.c代码:
#includewindows.h
#includwinnt.h
int_tls_index=0;
int_tls_start=0;
#pragmadata_seg(.tls$ZZZ)
int_tls_end=0;
#pragmadata_seg(.CRT$XLA)
int__xl_a=0;
#pragmadata_seg(.CRT$XLZ)
int__xl_z=0;
externPIMAGE_TLS_CALLBACKmy_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即可:

externCPIMAGE_TLS_CALLBACKmy_tls_callback[]={my_tls_callback1,0};

可以有多个回调,但一定要在最后加一个空项,否则很可能出错。

0x03实现效果

针对上面的DEMO1,编译运行后查看不同加载情况下效果:

使用VS2010编译后直接执行,程序正常运行,如上图2;

使用VS2010自带的调试器加载调试,进程直接退出;

图5使用OllyDbg加载程序调试,进程退出;

F14.png

图5

当然,最终反调试效果依赖于TLS回调函数中的反调试代码。

0x04汇编实现

高级语言可以做的事情,汇编自然也可以做到,给出一种参考的汇编实现Demo:


.386
.modelflat,stdcall
optioncasemap: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_CallBackStartddTlsCallBack0
szInNormaldb正常运行中,0
szTitledbAnti-Debug,0
szInTlsdb断点设置出错,0
szResultdb无法写回到已修改的寄存器,0
PUBLIC_tls_used
_tls_usedIMAGE_TLS_DIRECTORY<TLS_Start,TLS_End,dwTLS_Index,TLS_CallBackStart,
0,?
.data?
dwTLS_Indexdd?
dwResultdd?
OPTIONDOTNAME
;;定义一个TLS节
.tlsSEGMENT
TLS_StartLABELDWORD
dd0100hdup(slt.)
TLS_EndLABELDWORD
.tlsENDS
OPTIONNODOTNAME
.code
;;TLS回调函数
TlsCallBack0procDllhandle:LPVOID,dwReason:DWORD,lpvReserved:LPVOID
mov
cmp
jnz
eax,dwReason
eax,DLL_PROCESS_ATTACH;在进行加载时被调用
ExitTlsCallBack0
invokeGetCurrentProcessId
invokeOpenProcess,PROCESS_ALL_ACCESS,NULL,eax
invokeCheckRemoteDebuggerPresent,eax,addrdwResult
cmp
jne
dwordptrdwResult,0
_found
jmpExitTlsCallBack0
_found:
;invokeMessageBox,NULL,addrszInTls,addrszTitle,MB_ICONWARNING
invokeGetCurrentThread
invokeNtSetInformationThread,eax,11H,NULL,NULL
;invokeMessageBox,NULL,addrszResult,addrszTitle,MB_ICONWARNING
mov
eax,ebx
ExitTlsCallBack0:
mov
xor
inc
ret
dwordptr[TLS_Start],0
eax,eax
eax
TlsCallBack0endp
Start:
invokeMessageBox,NULL,addrszInNormal,addrszTitle,MB_OK
invokeExitProcess,1
endStart


0x05反反调试之策

针对TLS反调试,给出几种突破方法[注:针对无壳无其他PE加密环境]:

1.使用修改版Ollydbg,此处以ShoutBoy的Jiack版本OD为例,载入程序,直接停在TLS

回调函数入口点。而后可以修改代码,跳过反调试部分,保存程序文件,再使用普通OD加

载即可。如图6

F15.png

图6

2.手工抹掉TLS反调试,使用IDA加载程序,在FunctionWIndow中发现TlsCallback_0函数。如图7:

函数段:.text位置:00401190[转换得文件偏移为590]长度:44[对应10进制68字节]

F16.png

图7

通过转换得到文件偏移为590,长度为68字节。用C32asm等十六进制编辑器使用00填充此部分十六进制代码。如图8

F17.png

这时执行程序,发现只弹出第二个窗口。载入OD,已经跳过TLS回调函数的反调试部分直接到达EP。如图9

F18.png

0x06一点扩展利用TLS回调函数在调试器加载前执行Anti-Debug函数保护软件不被恶意修改,关键部分依旧是具体实现反调试部分的代码编写。比如传统的检测断点,检测进程,检测调试器等等。

而后,为更好的实现反调试效果,可通过类似加密壳的方式对输入表与输出表等进行加密,更大程度的保护TLS代码不被轻易删除。