TLS(Thread Local Storage 线程局部存储)html
一个进程中的每一个线程在访问同一个线程局部存储时,访问到的都是独立的绑定于该线程的数据块。在PEB(进程环境块)中TLS存储槽共64个(位于PEB的TlsBitmapBits字段中,共64位(bit)微软保证每一个进程最少拥有64个索引可用,若是须要,系统还会为线程提供更多的索引)。每一个TEB(线程环境块)偏移0x2C处是一个PVOID THreadLocalStoragePointer指针,存放着当前进程TLS副本地址。TLS对每一个线程均可见,而且这个值对于全部的线程都是相同的,操做这个索引就表明操做了全部线程的相同索引。程序员
typedef struct _IMAGE_TLS_DIRECTORY32 {
ULONG StartAddressOfRawData; // TLS模板的起始位置
ULONG EndAddressOfRawData; // TLS模板的结束地址
ULONG AddressOfIndex; // TLS索引的位置
ULONG AddressOfCallBacks; // TLS回调函数数组指针
ULONG SizeOfZeroFill; // 填充0
ULONG Characteristics; // 保留
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;windows
IMAGE_TLS_DIRECTORY32.AddressOfIndex 用于保存TLS索引的位置,索引的具体值由loader决定。数组
IMAGE_TLS_DIRECTORY32.AddressOfCallBacks 这是一个指针,它指向TLS回调函数组成的数组。这个数组是以NULL结尾的,所以,若是没有回调函数的话,这个字段指向的位置应该是4字节的0。数据结构
TLS回调函数:是线程创建和退出时的回调函数,包括主线程和其它线程。程序能够经过PE文件的方式提供一个或多个TLS回调函数,用以支持对TLS数组进行附加的初始化和终止操做,这种操做相似于面向对象程序设计中的构造函数和析构函数。回调函数的原型与DLL入口函数参数相同:typedef VOID (NTAPI*PIMAGE_TLS_CALLBACK) (PVOID DllHandle, DWORD Reason, PVOID Reserved);框架
TLS数据初始化和TLS回调函数调用都在入口点以前执行,也就是说TLS是程序最开始运行的地方。函数
下面附上Anti-Attach代码DEMO工具
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
// TLS被挂接次数
DWORD g_numAttachCount = 0;
#define MAX_NUM_OF_THREADS 1
// TLS回调函数
void __stdcall TlsCallBack(PVOID DllHandle,ULONG Reason,PVOID Reserved)
{
if ( Reason == DLL_PROCESS_ATTACH )
{
g_numAttachCount = 1;
}
else if ( Reason == DLL_THREAD_ATTACH )
{
// 进程有多个线程建立
g_numAttachCount++;
if ( g_numAttachCount > MAX_NUM_OF_THREADS )
{
MessageBox(NULL, _T("有人调戏?"), NULL, MB_OK);
ExitProcess(-1);
}
}
}
// TLS索引的位置
DWORD TlsIndex = 0;
// 回调函数数组
PIMAGE_TLS_CALLBACK TlsCallBackArr[] = { (PIMAGE_TLS_CALLBACK)TlsCallBack, NULL };
// 定义_tls_used
// 参考VC安装的目录C定义源码: X:\Program Files\Microsoft Visual Studio 9.0\VC\crt\src\tlssup.c
extern "C"
{
IMAGE_TLS_DIRECTORY _tls_used={
0, // start of tls data
0, // end of tls data
(ULONG)(PULONG)&TlsIndex, // address of tls_index
(ULONG)TlsCallBackArr, // pointer to call back array
0, // size of tls zero fill
0 // characteristics
};
}
int main()
{
DWORD numCount = 0;
while ( true )
{
Sleep( 1000 );
printf( "等待OD Attach: %d\n", numCount++ );
}
return 0;
}
OllyDbg使用Attack的方式载入这个例子将会暂停在入口点。因为TLS回调函数是在实际的入口点执行以前被调用的,OllyDbg应该配置一下使其在TLS回调被调用以前中断在实际的loader。
能够经过选择选项->调试选项->事件->第一次中断于->系统断点来设置中断于ntdll.dll内的实际loader代码。这样设置之后,OllyDbg将会中断在位于执行TLS回调的ntdll!LdrpRunInitializeRoutines()以前的ntdll!_LdrpInitializeProcess(),这时就能够在回调例程中下断并跟踪了。如,在内存映像的.text代码段上设置内存访问断点,能够断在TLS回调函数。
参考文献: http://www.pediy.com/kssd/pediy07/pediy7-658.htm
<<Windows PE 权威指南>> 戚利
<<加密与解密第三版>> 段钢测试
看雪论坛加密
转载于:http://hi.baidu.com/cppcoffee/item/ac6f0c173119e105b88a1a38 感谢原做者为咱们带来美妙绝伦的文章
盗版行为日益猖獗,严重影响到软件开发者和开发商的知识产权及利益,反盗版技术的重要性也愈来愈引发人们的重视。在反盗版技术中,起最大做用的当属反调试技术。然而传统的反调试技术都存在一个弱点:他们都在程序真正开始执行以后才采起反调试手段。实际上在反调试代码被执行前,调试器有大量的时间来影响程序的执行,甚至能够在程序入口处插入断点命令来调试程序。对于使用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技术做为反调试技术的载体,能够大大加强程序软件的反盗版能力。若是能结合传统技术,将使反调试技术发展至一新高度。
===============================================
在上篇中,咱们会为读者介绍恶意软件经常使用来的反仿真技术。本文中,咱们将向读者介绍恶意软件用以阻碍对其进行逆向工程的各类反调试技术,以帮助读者很好的理解这些技术,从而可以更有效地对恶意软件进行动态检测和分析。
1、反调试技术
反调试技术是一种常见的反检测技术,由于恶意软件老是企图监视本身的代码以检测是否本身正在被调试。为作到这一点,恶意软件能够检查本身代码是否被设置了断点,或者直接经过系统调用来检测调试器。
1.断点
为了检测其代码是否被设置断点,恶意软件能够查找指令操做码0xcc(调试器会使用该指令在断点处取得恶意软件的控制权),它会引发一个SIGTRAP。若是恶意软件代码自己创建了一个单独的处理程序的话,恶意软件也能够设置伪断点。用这种方法恶意软件能够在被设置断点的状况下继续执行其指令。
恶意软件也能够设法覆盖断点,例若有的病毒采用了反向解密循环来覆盖病毒中的断点。相反,还有的病毒则使用汉明码自我纠正自身的代码。汉明码使得程序能够检测并修改错误,可是在这里却使病毒可以检测并清除在它的代码中的断点。
2.计算校验和
恶意软件也能够计算自身的校验和,若是校验和发生变化,那么病毒会假定它正在被调试,而且其代码内部已被放置断点。VAMPiRE是一款抗反调试工具,可用来逃避断点的检测。VaMPiRE经过在内存中维护一张断点表来达到目的,该表记录已被设置的全部断点。该程序由一个页故障处理程序(PFH),一个通用保护故障处理程序(GPFH),一个单步处理程序和一个框架API组成。当一个断点被触发的时候,控制权要么传给PFH(处理设置在代码、数据或者内存映射I/O中的断点),要么传给GPFH(处理遗留的I/O断点)。单步处理程序用于存放断点,使断点能够屡次使用。
3.检测调试器
在Linux系统上检测调试器有一个简单的方法,只要调用Ptrace便可,由于对于一个特定的进程而言没法连续地调用Ptrace两次以上。在Windows中,若是程序目前处于被调试状态的话,系统调用isDebuggerPresent将返回1,不然返回0。这个系统调用简单检查一个标志位,当调试器正在运行时该标志位被置1。直接经过进程环境块的第二个字节就能够完成这项检查,如下代码为你们展现的就是这种技术:
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这个系统调用。咱们能够将ProcessInformationClass设为7来调用该函数,这样会引用ProcessDebugPort,若是该进程正在被调试的话,该函数将返回-1。示例代码以下所示。
push 0push 4push offset isdebuggedpush 7 ;ProcessDebugPortpush -1call NtQueryInformationProcesstest eax, eaxjne @ExitErrorcmp isdebugged, 0jne @DebuggerDetected
在本例中,首先把NtQueryInformationProcess的参数压入堆栈。这些参数介绍以下:第一个是句柄(在本例中是0),第二个是进程信息的长度(在本例中为4字节),接下来是进程信息类别(在本例中是7,表示ProcessDebugPort),下一个是一个变量,用于返回是否存在调试器的信息。若是该值为非零值,那么说明该进程正运行在一个调试器下;不然,说明一切正常。最后一个参数是返回长度。使用这些参数调用NtQueryInformationProcess后的返回值位于isdebugged中。随后测试该返回值是否为0便可。
另外,还有其余一些检测调试器的方法,如检查设备列表是否含有调试器的名称,检查是否存在用于调试器的注册表键,以及经过扫描内存以检查其中是否含有调试器的代码等。
另外一种很是相似于EPO的方法是,通知PE加载器经过PE头部中的线程局部存储器(TLS)表项来引用程序的入口点。这会致使首先执行TLS中的代码,而不是先去读取程序的入口点。所以,TLS在程序启动就能够完成反调试所需检测。从TLS启动时,使得病毒得以可以在调试器启动以前就开始运行,由于一些调试器是在程序的主入口点处切入的。