练习
我们先运行HelloTls.exe文件
然后我们使用OD调试这个程序
发现已经和之前打开弹出的内容不同了,这是因为程序运行EP代码之前先调用了TLS回调函数,而这个函数中存在反调试代码,使程序在被调试时弹出”Debugger Detected!”。
TLS原理分析
TLS是各线程的独立的数据存储空间。使用TLS技术可以在线程内部独立使用或修改进程的全局数据或静态数据,就像对待自身的局部变量一样。
IMAGE_DATA_DIRECTORY[9]
如果在程序中使用了TLS功能,PE文件的数据目录表第十项就会设置TLS表,RVA为9310
IMAGE_TLS_DIRECTORY
typedef struct _IMAGE_TLS_DIRECTORY32
{
DWORD StartAddressOfRawData;
DWORD EndAddressOfRawData;
PDWORD AddressOfIndex;
PIMAGE_TLS_CALLBACK *AddressOfCallBacks;
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32
IMAGE_TLS_DIRECTORY有两种版本,分别为32位版本和64位版本
这里面比较重要的是AddressOfCallbacks,这个值指向含有TLS回调函数地址(VA)的数组,这就意味着可以向同一程序注册多个TLS回调函数
回调函数地址数组
该数组存储的就是TLS回调函数的地址。进程启动运行时,也就是EP代码执行前系统会逐一调用存储在该数组中的函数。
TLS回调函数
TLS回调函数是指,每当创建/终止进程的线程时会自动调用执行的函数,而创建进程的主线程也会自动调用回调函数,且会在EP代码之前执行,反调试技术利用的就是这个特征。
IMAGE_TLS_CALLBACK
typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (
PVOID DllHandle,
DWORD Reason,
PVOID Reserved
);
仔细观察函数定义,我们发现这个函数与DllMain()函数的定义类似。
DllHandle为模块句柄,即加载地址
Reason为调用TLS回调函数的原因
#define DLL_PROCESS_ATTACH 1
#define DLL_THREAD_ATTACH 2
#define DLL_THREAD_DETACH 3
#define DLL_PROCESS_DETACH 0
练习-TlsTest.exe
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#pragma comment(linker, "/INCLUDE:__tls_used")
void print_console(char* szMsg)
{
HANDLE hStdout = GetStdHandle(STD_OUT1PUT_HANDLE);
WriteConsoleA(hStdout, szMsg, strlen(szMsg), NULL, NULL);
}
void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
char szMsg[80] = { 0, };
wsprintfA(szMsg, "TLS_CALLBACK1() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
print_console(szMsg);
}
void NTAPI TLS_CALLBACK2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
char szMsg[80] = { 0, };
wsprintfA(szMsg, "TLS_CALLBACK2() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
print_console(szMsg);
}
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1, TLS_CALLBACK2, 0 };
#pragma data_seg()
DWORD WINAPI ThreadProc(LPVOID lParam)
{
print_console("ThreadProc() start\n");
print_console("ThreadProc() end\n");
return 0;
}
int main(void)
{
HANDLE hThread = NULL;
print_console("main() start\n");
hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
WaitForSingleObject(hThread, 60 * 1000);
CloseHandle(hThread);
print_console("main() end\n");
return 0;
}
在这份代码里,先注册了2个TLS回调函数(TLS_CALLBACK1、TLS_CALLBACK2)。它们的操作就是把DllHandle和Reason打印到控制台,然后退出。
DLL_PROCESS_ATTACH
进程的主线程调用main()函数前,已经注册的TLS回调函数TLS_CALLBACK1、TLS_CALLBACK2会先被执行调用,此时Reason为1
DLL_THREAD_ATTACH
所有TLS回调函数完成调用后,main()函数开始执行,创建用户线程(ThreadProc)前,TLS回调函数会被再次调用执行,此时Reason为2
DLL_THREAD_DETACH
TLS回调函数全部执行完后,ThreadProc开始执行,在执行完后,Reason为3,此时TLS回调函数被调用执行
DLL_PROCESS_DETACH
main()主线程也会终止,此时Reason为0,TLS回调函数最后一次被调用执行。
调试TLS回调函数
如果直接使用调试器打开带有TLS回调函数的程序,则无法调试TLS回调函数,因为TLS回调函数会在EP代码之前执行,所以我们在调试代码之前需要对OD的设置进行一些修改
此时再调试带有TLS的程序时,会自动在ntdll.dll模块内部的“System Startup Breakpoint”处暂停,调试器暂停的位置是系统启动断点。然后根据前面的IMAGE_TLS_DIRECTORY获取TLS回调函数的地址,在回调函数的起始地址设置好断点,这样就可以进行TLS回调函数的调试了。
前面我们知道回调函数的首地址是00401000,所以我们在这个地方下个断点,然后F9运行到这。
代码逻辑比较清晰,通过IsDebuggerPresent的API来进行反调试,我们通过修改flags中的Z位置1,就可以绕过反调试了
手工添加TLS回调函数
设计规划
首先要确定IMAGE_TLS_DIRECTORY结构体与TLS回调函数放到文件的哪个位置。向PE文件添加代码或者数据时。有如下3种方法来查找合适位置:
添加到节区末尾的空白区域
增加最后一个节区的大小
在最后添加新节区
我们采用第二种方法,增加最后一个节区的大小。
Pointer to Raw Data=9000
Size of Raw Data=600
所以PE头中定义的文件整体大小为9600
我们用010editor打开hello.exe,然后在尾部插入200h个字节
编辑PE文件头
将.rcrs节区头中的Size of Raw Data=800,Characteristics=E0000060
在原有属性的基础上新增加了IMAGE_SCN_CNT_CODE|IMAGE_SCN_MEM_EXECUTE|IMAGE_SCN_MEM_WRITE属性
IMAGEE_DATA_DIRECTORY[9]
接下来要设置TLS表的值
我们的TLS表放在9600的位置,大小为24个字节(0x18),9600是文件偏移,转化成RVA就是1 E600
设置IMAGE_TLS_DIRECTORY结构体
我们在9600位置开始填入数据,大小为0x18个字节,把从9618开始后面0x18字节,0x18/4=6个地址,刚好赋给IMAGE_TLS_DIRECTORY,在9630位置设置函数地址,存放在Address of Callbacks指向的回调函数数组中,其中C20C是机器码,对应汇编就是 RETN 0C,也就是说这个函数只进行了平衡堆栈的操作,TLS有三个参数,所以retn 0xc
编写TLS回调函数
可以利用OD直接写汇编,最终将程序保存下来就可以了
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 767778848@qq.com