盗版行为日益猖獗,严重影响到软件开发者和开发商的知识产权及利益,反盗版技术的重要性也越来越引起人们的重视。在反盗版技术中,起最大作用的当属反调试技术。然而传统的反调试技术都存在一个弱点:他们都在程序真正开始执行之后才采取反调试手段。实际上在反调试代码被执行前,调试器有大量的时间来影响程序的执行,甚至可以在程序入口处插入断点命令来调试程序。对于使用C/C++语言编译的程序来说,问题通常会更严重,在执行到main()函数之前,会执行C/C++编译器插入的很大一段代码,这也给调试器带来影响程序执行的机会。
本文讨论一种利用Windows提供的线程访问互斥机制,来实现一种在程序入口之前就执行反调试代码的技术。技术本身不会影响程序的执行,但能有效地防止调试器的调试。
1 TLS技术简介
TLS全称为Thread Local Storage,是Windows为解决一个进程中多个线程同时访问全局变量而提供的机制。TLS可以简单地由操作系统代为完成整个互斥过程,也可以由用户自己编写控制信号量的函数。当进程中的线程访问预先制定的内存空间时,操作系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性[1]。
1.1 TLS回调函数
当用户选择使用自己编写的信号量函数时,在应用程序初始化阶段,系统将要调用一个由用户编写的初始化函数以完成信号量的初始化以及其他的一些初始化工作。此调用必须在程序真正开始执行到入口点之前就完成,以保证程序执行的正确性。
TLS回调函数具有如下的函数原型:
void NTAPI TlsCallBackFunction(PVOID Handle, DWORD Reason, PVOID Reserve);[1]
1.2 TLS的数据结构
Windows的可执行文件为PE格式,在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回调函数地址以及其他一些信息[2]。
2 具体实现及原理
充分利用TLS回调函数在程序入口点之前就能获得程序控制权的特性,在TLS回调函数中进行反调试操作比传统的反调试技术有更好的效果。
2.1 在程序中使用TLS
Microsoft提供的VC编译器都支持直接在程序中使用TLS,下文都将使用VC进行操作。
要在程序中使用TLS,必须为TLS数据单独建一个数据段,用相关数据填充此段,并通知链接器为TLS数据在PE文件头中添加数据。为此,需要在程序源文件中添加如下代码[3]:
#pragma comment(linker, "/INCLUDE:__tls_used")
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK TlsCallBackArray[] = {
TlsCallBackFunction1,
TlsCallBackFunction2,
......
NULL
};
#pragma data_seg()
其中TlsCallBackArray数组中保存了所有的TLS回调函数指针。值得指出的是,数组必须以NULL指针结束,且数组中的每一个回调函数在程序初始化时都会被调用,程序员可按需要添加。但程序员不应当假设操作系统已何种顺序调用回调函数。如此则要求在TLS回调函数中进行反调试操作需要一定的独立性。
2.2 回调函数的具体实现 2.2.1 检测调试器在程序入口插入的断点
PE可执行文件在初始化阶段,PE文件都会被完整地加载进入内存。通过分析PE文件头来获取程序的入口点,并对入口点的前一个或多个字节进行判断,以阻止调试器在程序入口点下断。在下面代码中,使用GetModuleHandle(NULL)来获得应用程序的加载基址;也可以模拟GetModuleHandle()手动读取程序的PEB来得到。但为了程序的通用性,这里仍然使用GetModuleHandle()。获得程序加载基址后,将其强制类转换为相关指针,并进行计算,最终得到程序的入口点在内存中的具体地址[3]:
IMAGE_DOS_HEADER *dos_head=(IMAGE_DOS_HEADER *)GetModuleHandle(NULL);
PIMAGE_NT_HEADERS32 nt_head=(PIMAGE_NT_HEADERS32)((DWORD)dos_head+(DWORD)dos_head->e_lfanew);
BYTE*OEP=(BYTE*)(nt_head->OptionalHeader.AddressOfEntryPoint+(DWORD)dos_head);
下面的代码则通过扫描程序入口点的20字节,判断其中有无调试断点,如有,则退出进程。
for(unsigned long index=0;index<20;index++){
If(OEP[index]==0xcc){
ExitProcess(0);
}
}
需要指出的是,在TLS回调函数执行时,VC运行库msvcrt.dll,mfc.dll等并未载入,不能使用C库的函数。如果有需要使用,应该使用LoadLibrary()函数载入相应的库并使用GetProcAddress()获得函数地址。但此类操作可能会导致调试器的相关事件触发,不建议进行此类操作。
2.2.2 使调试器窗口无效
此处使用FindWindow()查找指定的窗口,并使用SetWindowsLong()将其超类化为不可用,具体代码见下:
HWND hd_od=FindWindow("ollydbg",NULL);
SetWindowLong(hd_od,GWL_STYLE,WS_DISABLED);
.........处理其他类型的调试器
这里需要说明的是,此类方法仅仅对ring3调试器才起作用。如SoftICE,WinDebug之类的内核调试器,因为根本不存在窗口,此种方法对其无效。
2.2.3 为程序执行所必需的元素进行初始化或分配空间
这个操作的目的是为了防止盗版者通过直接将TLS数据清除的方法来避开反调试。当程序正常运行所需要的内存空间或数据必需经过TLS回调函数初始化时,盗版者不可以将程序的TLS数据清除。因为那样做带来的后果是程序运行根本不正常。
另外,如果这种初始化或分配空间的操作分散在各个TLS回调函数中完成,效果会更好。
2.2.4 堵塞输入
此功能主要是为了应对一些未知调试器和一些不在程序入口点下断的调试器。
函数将首先检查user32.dll导出的BlockInput()函数,如果函数代码是被调试器修改过的,那么将直接退出,代码如下[4]:
BYTE *address=(BYTE *)GetProcAddress(LoadLibrary("user32.dll"),"BlockInput");;
bool modify=true;
for(int x=0;x<20;x++){
if(address[x]==0xff&&address[x+1]!=0xff){
modify=false;
break;
}
if(modify) ExitProcess(0);
检查过BlockInput()函数正确性之后,函数将调用BlockInput(TRUE),阻塞用户的鼠标和键盘输入。但函数接下来并不立即取消阻塞,而在main()函数中发起一个异常后再取消阻塞。
这么处理的理由很充分,在main()函数中发起异常将导致调试器捕获异常,并暂停等待用户输入。而此时用户输入是被锁定的,那么程序就相当于被变相锁死了,没有办法继续调试。而当调试器不存在时,代码中的__except()部分将直接获得执行权,并取消阻塞,程序正常运行。main函数中的具体代码如下:
__try{
__asm{
xor eax,eax
div eax,eax
xor eax,eax
}
ExitProcess(0);
}
__except(1,1){
BlockInput(FALSE);
}
值得说明的是,此次在main()函数中调用BlockInput()而不检查也是有原因的。如果此时的BlockInput()函数被调试器修改过,那么取消输入锁定将完全不能工作,那么之后的整个过程也是无法调试的。
如果进一步深入,还可以测试异常返回所用的时间,不论过长或者过短,都能够说明调试器的存在。此处不继续展开了。
2.2.5 创建监视线程
此步目的是为了防止OllyDbg等调试器以进程附加的形式对程序进行调试。程序此步将创建一个子线程,监视调试器窗口的出现,如果发现调试器窗口,将其超类化为不可用。子线程的代码如下:
DWORD WINAPI Monitor(LPVOID s){
while(sign==TRUE){
HWND hd=FindWindow("ollydbg",NULL);
SetWindowLong(hd,GWL_STYLE,WS_DISABLED);
.......处理其它调试器窗口
Sleep(50);
}
return 0;
}
3 实际测试
3.1 测试直接执行
测试方法为直接在资源管理器中双击执行。
3.2 测试用调试器加载
测试方法为分别使用VC自带的调试器加载调试和使用OllyDebug加载生成的程序进行调试。其中OllyDbg使用从看雪论坛下载的OllyDebug 1.10 CHS,并打开了所有的隐藏插件。
测试结果:两者调试的进程都直接退出,且没有输出任何信息。
图3 使用OllyDebug 1.10,打开全部反反调试插件调试,程序直接退出
3.3 测试用调试器附加进程
测试方法为先在资源管理器中双击程序运行,然后打开OllyDebug试图附加进程。
测试结果:OllyDebug窗口直接失效。
4 总 结
通过使用TLS技术作为反调试技术的载体,可以大大增强程序软件的反盗版能力。如果能结合传统技术,将使反调试技术发展至一新高度。