TLS分析
TLS回调函数会先于EP代码的运行
TLS是各线程的独立的数据存储空间。使用TLS技术可以在线程内部独立使用或修改进程的全局数据或静态数据,就像对待自身的局部变量一样。
IMAGE_DATA_DIRECTORY[9]
如果在程序中使用了TLS功能,PE文件的数据目录表第十项就会设置TLS表
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
-
比较重要的成员为
AddressOfCallbacks
,这个值含有TLS
回调函数地址(VA)的数组。这意味着可以向同一程序注册多个TLS回调函数
AddressOfCallbacks
这个数组存储的就是TLS回调函数的地址
进程启动运行时(执行EP代码前)系统会逐一调用存储在该数组中的函数
TLS回调函数
TLS回调函数是指,每当创建/终止进程的线程时会自动调用执行的函数,而创建进程的主线程也会自动调用回调函数,且会在EP代码之前执行
,反调试技术利用的就是这个特征。
IMAGE_TLS_CALLBACK
typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (
PVOID DllHandle,
DWORD Reason,
PVOID Reserved
);
定义TLS回调函数的方法,与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回调函数最后一次被调用执行。
调试TlsTest.exe
再调试带有TLS的程序时,会自动在ntdll.dll模块内部的“System Startup Breakpoint”处暂停,调试器暂停的位置是系统启动断点。然后根据前面的IMAGE_TLS_DIRECTORY获取TLS回调函数的地址,在回调函数的起始地址设置好断点,这样就可以进行TLS回调函数的调试了。
前面我们知道回调函数的首地址是00401000,所以我们在这个地方下个断点,然后F9运行到这。
手工添加TLS回调函数
设计
首先要确定IMAGE_TLS_DIRECTORY结构体与TLS回调函数放到文件的哪个位置。向PE文件添加代码或者数据时。有如下3种方法来查找合适位置:
- 添加到节区末尾的空白区域
- 增加最后一个节区的大小
- 在最后添加新节区
我们采用第二种方法,增加最后一个节区的大小
首先在PE头中修改最后一个节区的大小
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属性
接下来要设置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
最后可以利用OD直接写汇编,最终将程序保存下来就可以了