第45章:TLS回调函数

TLS(Thread Local Storage,线程局部存储)回调函数(Callback Function)常用于反调试。

TLS 回调函数的调用运行要先于 EP 代码的执行。它是各线程独立的数据存储空间,可修改进程的全局/静态数据。

若在编程中启用了 TLS,PE 头文件中会设置 TLS 项目,即:IMAGE_TLS_Directory

其中比较重要的成员是:AddressOfCallBacks  它指向回调函数数组地址

自己找一下试试:

同时获取到相应的节区信息,计算: 9310 - 8000 + 6600 = 7910 .

第四个元素即为 AddressOfCallBacks .

408114 明显超出了文件的大小,因此判断是加上了 ImageBase (即 VA ),在节区头查看 ImageBase 大小为 400000 .

因此 RVA 为 8114 ,8114 - 8000 + 6600 = 6714 .

参数 Reason 表示调用 TLS 回调函数的原因:

TLS设计的本意,是为了解决多线程程序中变量同步的问题,是 Thread Local Storage 的缩写,意为线程本地存储。线程本身有独立于其他线程的栈空间,因此线程中的局部变量不用考虑同步问题。多线程同步问题在于对全局变量的访问,TLS在操作系统的支持下,通过把全局变量打包到一个特殊的节,当每次创建线程时把这个节中的数据当做副本,拷贝到进程空闲的地址空间中。以后线程可以像访问局部变量一样访问该异于其他线程的全局变量的副本,而不用加同步控制。

第二个程序的代码( TlsTest.cpp ):

#include <windows.h>

#pragma comment(linker, "/INCLUDE:__tls_used")// 通过#pragma comment(Linker,"INCLUDE:_tls_used")显示的申明crt库中定义的_tls_used变量
                            // 以便向_tls_used!AddressOfCallBacks域添加回调函数。                              
void print_console(char* szMsg)
{
    HANDLE hStdout = GetStdHandle(STD_OUTPUT_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
", 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
", DllHandle, Reason);
    print_console(szMsg);
}

#pragma data_seg(".CRT$XLX")   //CRT 表明使用 C RunTime 机制,X 表示标识名随机,L 表示 TLS callback section, X 为 B-Y 之间的任意一个字母
    PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1, TLS_CALLBACK2, 0 }; // 注意,此处将自定义函数放入了 TLS 表中。
#pragma data_seg()

DWORD WINAPI ThreadProc(LPVOID lParam)
{
    print_console("ThreadProc() start
");

    print_console("ThreadProc() end
");

    return 0;
}

int main(void)
{
    HANDLE hThread = NULL;

    print_console("main() start
");

    hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
    WaitForSingleObject(hThread, 60*1000);
    CloseHandle(hThread);

    print_console("main() end
");

    return 0;
}

程序运行截图:

① DLL_Process_ATTACH  在主线程调用 Main()函数前,已经注册的两个函数会被调用执行。

② DLL_Thread_ATTACH   在 TLS 函数完成后,main()函数开始执行。在创建用户线程前(若有),TLS 回调函数会再次被执行。

③ DLL_Thread_Detach     TLS 函数执行完后,线程函数开始调用执行,线程函数结束后(若有)再次调用 TLS 函数。

④DLL_Process_Detach     main()函数结束后,再次调用 TLS 函数。

若不在主函数中调用 CreateThread 函数,则只会执行两次 TLS 。 

使用 win xp sp3 进行实际调试:

使用 OD ,将断点设置为 TLS 断点,程序会断在此处:

对比代码,并观察程序名可知,程序被断在了 TLS_CallBack1 处。继续运行程序至返回:

可以看到,通过间接调用,实现了 TLS CallBack1 的调用。继续执行程序:

可以看到,函数执行到了第一次执行的位置,表明会循环执行,直至函数执行完:

手工添加 TLS 回调函数

①将 TLS 结构体及 TLS 回调函数放在最后一个节区 or 其它节区的空白地区 or 添加新节区。

属性增加了  Write (方便在调试时编写代码),Execute(可执行),CNT_CODE(区段包含代码)。

②设置 TLS 表

③设置 IMAGE_TLS_DIRECTORY 结构体

StartAddressOfRawData:tls模板在内存中的起始VA,模板是用于创建线程时初始化TLS数据的;
EndAddressOfRawDataL:tls模板在内存中的结束VA;
AddressOfIndex:存储TLS索引的位置;

这三个值都可以指向 NULL 区域。

第四个值 CallBack 指向一个数组,以 四个字节的 NULL 结尾。数组每四个字节指向一个地址,存储着函数。

有意思的是,在使用 StudyPe+ x86 修改后,在 win xp sp3 上无法运行,但是在 Win 10 上可以。经查证:

使用软件进行修改时,增加最后一个节区 200 字节时,会自动将 VirtualSize 增大 1000,就算自己修改后回去后,可选头中的 ImageSize 还是自动增大 1000。

因此如果自己改变了 VirtualSize ,那么会造成 ImageSize 比实际大 1000,在 xp 上无法运行。

原文地址:https://www.cnblogs.com/Rev-omi/p/13561456.html