[TOC]
反盗版技术中,起最大作用的当属反调试技术。然而传统的反调试技术都存在一个弱点:他们都在程序真正开始执行之后才采取反调试手段。实际上在反调试代码被执行前,调试器有大量的时间来影响程序的执行,甚至可以在程序入口处插入断点命令来调试程序。对于使用C/C++语言编译的程序来说,问题通常会更严重,在执行到main()函数之前,会执行C/C++编译器插入的很大一段代码,这也给调试器带来影响程序执行的机会。
TLS设计的本意,是为了解决多线程程序中变量同步的问题,是Thread Local Storage的缩写,意为线程本地存储。
线程本身有独立于其他线程的栈空间,因此线程中的局部变量不用考虑同步问题。
多线程同步问题在于对全局变量的访问,TLS在操作系统的支持下,通过把全局变量打包到一个特殊的节,当每次创建线程时把这个节中的数据当做副本,拷贝到进程空闲的地址空间中。
以后线程可以像访问局部变量一样访问该异于其他线程的全局变量的副本,而不用加同步控制。
对线程本地存储的全局变量的测试代码
#include "stdafx.h"
#include
#include
#include
using namespace std;
// 定义静态TLS全局变量
__declspec(thread) int value =0xcccccccc;__
DWORD WINAPI NewThread ( LPVOID lParam )
{
// 设置子线程value,并不影响其他线程
value = *((int*)lParam);
while(1)
{
printf("%d\n",value);
Sleep(1000);
}
return 0 ;
}
#define THREAD_NUM 3
int main(int argc, char* argv[])
{
int arry[3]={1,2,3};
// 设置主线程静态TLS的value为5
value = 5 ;
// 创建子线程
HANDLE hThread[THREAD_NUM];
for (int loop = 0; loop < THREAD_NUM; loop++)
{
hThread[loop] = CreateThread ( NULL, 0, NewThread, &arry[loop], 0, NULL );
Sleep(1000);
}
// 等待直到子线程结束
WaitForMultipleObjects(THREAD_NUM, hThread, TRUE, INFINITE);
return 0;
}
运行后可看到因value被当作一个副本copy到3个不同的线程,因此在线程对其值进行改变,其它线程中的变量会出现变化。
而这个将全部变量的数值copy到不同的线程的过程是在整个线程运行前执行的[包括主线程],因此可在这个线程TLS回调函数中进行一些调试检测,判定进程是否正在被调试,如果发现调试,进行退出或其它操作。
TLS可以简单地由操作系统代为完成整个互斥过程,也可以由用户自己编写控制信号量的函数。
当进程中的线程访问预先制定的内存空间时,操作系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性。
而当Coder选择使用自己编写的信号量函数时,在应用程序初始化阶段,系统将要调用一个由用户编写的初始化函数以完成信号量的初始化以及其他的一些初始化工作。
此调用必须在程序真正开始执行到入口点之前就完成,以保证程序执行的正确性。
基于TLS的反调试,原理为在实际的入口点代码执行之前执行检测调试器代码,实现方式便是使用TLS回调函数实现。
在OD动态调试器加载程序到入口点之前便已经执行反调试代码并退出程序。此外,利用TLS启动时,某些病毒也得以能够在调试器启动之前就开始运行,因为一些调试器是在程序的主入口点处切入的。
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回调函数地址以及其他一些信息。
微软提供的VC编译器默认都支持直接在程序中使用TLS,要在程序中使用TLS,首先为TLS数据单独建一个数据段,并用相关数据填充此段,通知链接器为TLS数据在PE文件头中添加数据。
#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);
}
PE可执行文件在初始化阶段,PE文件都会被完整地加载进入内存。通过分析PE文件头来获取程序的入口点,并对入口点的前一个或多个字节进行判断
[为了检测其代码是否被设置断点,恶意软件可以查找指令操作码0xcc]
,以阻止调试器在程序入口点下断。
在下面代码中,使用GetModuleHandle(NULL)来获得应用程序的加载基址;也可以模拟GetModuleHandle()手动读取程序的PEB来得到。但为了程序的通用性,这里仍然使用GetModuleHandle()。获得程序加载基址后,将其强制类转换为相关指针,并进行计算,最终得到程序的入口点在内存中的具体地址:
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()获得函数地址。但此类操作可能会导致调试器的相关事件触发,不建议进行此类操作.
可以计算自身的校验和,如果校验和发生变化,正在被调试,并且其代码内部已被放置断点。
可以利用代码运行时动态检查系统运行时间来判定是否存在调试器
在Windows中,如果程序目前处于被调试状态的话,系统调用isDebuggerPresent将返回1,否则返回0。这个系统API调用简单检查PEB进程环境块中的一个标志位,当调试器正在运行时该标志位被置1。
直接通过PEB进程环境块的第二个字节就可以完成这项检查,读取代码:
mov eax,fs:[30h]
move eax,byte [eax+2]
test eax,eax
jne @DdebuggerDetected
在上面的代码中,eax被设置为PEB(进程环境块),然后访问PEB的第二个字节,并将该字节的内容移入eax。通过查看eax是否为零,即可完成这项检测。如果为零,则不存在调试器;否则,说明存在一个调试器。
如果某个进程为提前运行的调试器所创建的,那么系统就会给ntdll.dll中的堆操作例程设置某些标志,这些标志分别是
FLG_HEAP_ENABLE_TAIL_CHECK
FLG_HEAP_ENABLE_FREE_CHECK
FLG_HEAP_VALIDATE_PARAMETERS
我们可以通过下列代码来检查这些标志:
mov eax,fs:[30h]
mov eax,[eax+68h]
and eax,0x70
test eax,eax
jne @DebuggerDetected
在上面的代码中,我们还是访问PEB,然后通过将PEB的地址加上偏移量68h到达堆操作例程所使用的这些标志的起始位置,通过检查这些标志就能知道是否存在调试器。
检查堆头部内诸如ForceFlags之类的标志也能检测是否有调试器在运行,如下所示:
mov eax,fs:[30h]
mov eax,[eax+18h] ;process heap
mov eax,[eax+10h] ;heap flags
test eax,eax
jne @DebuggerDetected
上面的代码向我们展示了如何通过PEB的偏移量来访问进程的堆及堆标志,通过检查这些内容,我们就能知道Force标志是否已经被当前运行的调试器提前设置为1了。
另一种检测调试器的方法是,使用NtQueryInformationProcess这个系统API调用。
可以将ProcessInformationClass设为7来调用该函数,这样会引用ProcessDebugPort.
如果该进程正在被调试的话,该函数将返回-1。示例代码如下
push 0
push 4
push offset isdebugged
push 7 ;ProcessDebugPort
push -1
call NtQueryInformationProcess
test eax,eax
jne @ExitError
cmp isdebugged,0
jne @DebuggerDetected
在本例中,首先把NtQueryInformationProcess的参数压入堆栈。
这些参数介绍如下:
第一个是句柄(在本例中是0)
第二个是进程信息的长度(在本例中为4字节)
第三个是一个变量,用于返回是否存在调试器的信息。如果该值为非零值,那么说明该进程正运行在一个调试器下;否则,说明一切正常
第四个是进程信息类别(在本例中是7,表示ProcessDebugPort)
最后一个参数是返回长度。
使用这些参数调用NtQueryInformationProcess后的返回值位于isdebugged中。随后测试该返回值是否为0即可。
另外,还有其他一些检测调试器的方法,如检查设备列表是否含有调试器的名称,检查是否存在用于调试器的注册表键,以及通过扫描内存以检查其中是否含有调试器的代码等。
另一种非常类似于EPO的方法是,通知PE加载器通过PE头部中的线程局部存储器(TLS)表项来引用程序的入口点。这会导致首先执行TLS中的代码,而不是先去读取程序的入口点。因此,TLS在程序启动就可以完成反调试所需检测。从TLS启动时,使得病毒得以能够在调试器启动之前就开始运行,因为一些调试器是在程序的主入口点处切入的。