简述
代码逆向分析领域中,TLS(Thread Local Storage,线程局部存储)回调函数(Callback Function)常用反调试。TLS回调函数的调用运行要先于EP代码的执行,该特征使它可以作为一种反调试技术的使用。
TLS
TLS是各线程的独立的数据存储空间,使用TLS技术可在线程内部独立使用或修改进程的全局数据或静态数据,就像对待自身的局部变量一样。
IMAGE_DATA_DIRECTORY[9]
若在编程中启用了TLS
功能,PE头文件中就会设置TLS
表(TLS Table
)项目,如下(IMAGE_NT_HEADERS-IMAGE_OPTIONAL_HEADER-IMAGE_DATA_DIRECTORY[9]
)
typedef struct _IMAGE_TLS_DIRECTORY64 {
ULONGLONG StartAddressOfRawData;
ULONGLONG EndAddressOfRawData;
ULONGLONG AddressOfIndex; // PDWORD
ULONGLONG AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *;
DWORD SizeOfZeroFill;
union {
DWORD Characteristics;
struct {
DWORD Reserved0 : 20;
DWORD Alignment : 4;
DWORD Reserved1 : 8;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
} IMAGE_TLS_DIRECTORY64;
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData;
DWORD EndAddressOfRawData;
DWORD AddressOfIndex; // PDWORD
DWORD AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *
DWORD SizeOfZeroFill;
union {
DWORD Characteristics;
struct {
DWORD Reserved0 : 20;
DWORD Alignment : 4;
DWORD Reserved1 : 8;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;
_IMAGE_TLS_DIRECTORY32
结构体有2种版本,分别为32位版本和64位版本。
代码逆向分析中涉及的比较重要的成员AddressOfCallbacks
,该值指向含有TLS回调函数地方(VA)的数组。这意味着可以向同一程序注册多个TLS
回调函数
回调函数地址数组
该数组中实际存储的就是TLS回调函数的地址。进程启动运行时,(执行EP代码前)系统会逐一调用存储在该数组中的函数。
可以看出此数组有两个回调函数(地址为4113B1和41129E),我们可以通过修改程序注册多个TLS函数
TLS回调函数
接下来就是从技术层面简单整理之前介绍的TLS回调函数相关内容。
所谓TLS回调函数是指,每当创建/终止进程的线程时会自动调用执行的函数。有意思的是,创建进程的主线程时,也会自动调用回调函数,且其调用执行先于EP代码。反调试技术利用的就是TLS回调函数的这一特征。
请注意,创建或终止某线程时,TLS回调函数都会自动调用执行,前后共2次(原意即为此)。
执行进程的主线程(运行线程的EP代码)前,TLS回调函数会被先调用执行,许多逆向分析人员将该特征应用于程序的反调试技术。
IMAGE_TLS_CALLBACK
typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (
PVOID DllHandle,
DWORD Reason,
PVOID Reserved
);
仔细观察TLS回调函数的定义可以发现,它与DllMain的定义类似。
BOOL WINAPI DllMain(
__in HINSTANCE hModule,
__in DWORD fdwReason,
__in LPVOID lpReserved
);
观察以上2个函数可以发现,它们的参数顺序与含义都是一样的。其实,参数DllMain为模块句柄(即加载地址),参数fdwReason表示调用TLS回调函数的原因,具体原因有四种,如下所示
#define DLL_PROCESS_ATTACH 1
#define DLL_THREAD_ATTACH 2
#define DLL_THREAD_DETACH 3
#define DLL_PROCESS_ATTACH 0
接下来用例子讲解这四种原因
#include<Windows.h>
#pragma comment(linker,"/INCLUDE:__tls_used")
void print_console(const char* szMsg)
{
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
WriteConsole(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 lPram) {
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");
system("pause");
return 0;
}
代码讲解:
#pragma comment(linker,"/INCLUDE:__tls_used")
告知链接器使用TLS
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1,TLS_CALLBACK2,0 };
#pragma data_seg()
注册TLS
函数,.CRT$XLX
的作用:CRT
表示使用C Runtime
机制,X
表示表示名随机,L
表示TLS Callback section
,X
也可以换成B~Y
任意一个字符
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1,TLS_CALLBACK2,0 };
存储回调函数地址
DLL_PROCESS_ATTACH
进程的主线程调用main函数前,已经注册的TLS
回调函数(TLS_CALLBACK1,TLS_CALLBACK2
)会先被调用执行,此时Reason的值为1(DLL_PROCESS_ATTACH
)
DLL_THREAD_ATTACH
所有的TLS
回调函数执行完后,main()函数开始调用执行,创建用户线程(ThreadProc
)前,TLS
回调函数会被再次调用执行,此时Reason的值为2(DLL_THREAD_ATTACH
)
DLL_THREAD_DETACH
TLS
回调函数全部 执行完毕后,ThreadProc()
线程函数开始调用执行。其执行完毕后Reason
=3(DLL_THREAD_DETACH
),TLS
回调函数被调用执行
DLL_PROCESS_DETACH
ThreadProc()
线程函数执行完毕后,一直在等待线程终止的main函数(主线程)也会终止。此时Reason
=0(DLL_PROCESS_ATTACH
),TLS
回调函数最后一次被调用执行。
补充:
源文件中并未使用printf()
函数,因为开启特定编译选项(/MT)
编译源程序,先于主程序调用执行的TLS
回调函数中可能发生Run-time Error
(运行时错误)。此时可以直接调用WriteConsole()
API来以防万一。
调试TLS回调函数
若直接使用调试器打开带有TLS回调函数的程序,则无法调试TLS回调函数,因为TLS在EP代码之前就被调用执行了,如果TLS函数内部含有反调试代码,这使程序直接无法继续。需要如下操作选项----->调试设置---->事件------->点击 系统断点
调试器暂停的位置即是系统启动断点,在OD调试器的默认设置下,调试器会在EP处暂停,而WinDbg调试器默认在系统启动断点暂停。
补充
DLL
程序入口点函数:DllMain
,注意:大小写是区别的(仅导出资源的DLL可以没有DllMain函数)
函数原型:
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
return TRUE;
}
hModule参数:指向DLL本身的实例句柄;
ul_reason_for_call参数:指明了DLL被调用的原因,可以有以下4个取值:
1.DLL_PROCESS_ATTACH:
当DLL被进程 <<第一次>> 调用时,导致DllMain函数被调用,
同时ul_reason_for_call的值为DLL_PROCESS_ATTACH,
如果同一个进程后来再次调用此DLL时,操作系统只会增加DLL的使用次数,
不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。
2.DLL_PROCESS_DETACH:
当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的ul_reason_for_call值是DLL_PROCESS_DETACH。
★如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。
3.DLL_THREAD_ATTACH:
当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,
并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。
新创建的线程负责执行这次的DLL的DllMain函数,
只有当所有的DLL都处理完这一通知后,系统才允许线程开始执行它的线程函数。
4.DLL_THREAD_DETACH:
如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),
系统查看当前映射到进程空间中的所有DLL文件映像,
并用DLL_THREAD_DETACH来调用DllMain函数,
通知所有的DLL去执行线程级的清理工作。
★注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,
系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
③lpReserved参数:保留,目前没什么意义。