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回调函数

    image-20240721205331221

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打印到控制台,然后退出。

img
  • 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种方法来查找合适位置:

  1. 添加到节区末尾的空白区域
  2. 增加最后一个节区的大小
  3. 在最后添加新节区

我们采用第二种方法,增加最后一个节区的大小

首先在PE头中修改最后一个节区的大小

img

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直接写汇编,最终将程序保存下来就可以了