Thread Local Storage,线程局部存储:各线程独立的数据存储空间。使用TLS技术可以在线程内部独立使用或修改进程的全局数据或静态数据, 就像对待自身的局部变量一样
TLS回调函数常用于反调试,主要是利用了TLS回调函数的调用要先于EP代码的执行
如果开启了TLS功能,PE文件头就会设置TLS表,IMAGE_NT_HEADERS-IMAGE_OPTIONAL_HEADER-IMAGE_DATA_DIRECTORY[9]描述了IMAGE_TLS_DIRECTORY结构体的位置。
IMAGE_TLS_DIRECTORY:
对其中各参数的描述:
StartAddressOfRawData:tls模板在内存中的起始VA,模板是用于创建线程时初始化TLS数据的,可以看到模板中的内容其实就是TLS中创建的变量。
EndAddressOfRawDataL:tls模板在内存中的结束VA
AddressOfIndex:存储TLS索引的位置
AddressOfCallBacks: 指向TLS注册的回调函数的函数指针(地址)数组
SizeOfZeroFill:用于指定非零初始化数据后面的空白空间的大小
Characteristics:属性
逆向过程中比较重要的成员就是AddressOfCallBacks,指向含有TLS回调函数地址的数组,进程在启动运行时,系统会逐一调用储存在该数组中的函数。
每当创建或终止进程的线程时会自动调用执行的函数。当然,创建进程的主线程的时候也会自动调用回调函数,且其执行先于EP代码。反调试技术就是利用的TLS回调函数的这一特征。
参数顺序和定义都是一样的。第一个参数表示模块句柄,第二个参数表示调用TLS回调函数的原因。
对于第二个参数Reaseon:
分别对应了这四种情况
执行效果:
分析:在创建主线程的时候,就调用了回调函数,此时的调用Reason就是1
然后创建子线程的时候,此时又调用了回调函数,此时的调用Reason就是2
之后子线程执行完毕,调用回调函数,调用Reason就是3
最后主线程执行完毕,调用回调函数,调用原因是0
如果使用的调试器是WinDbg,那么默认情况下它就会在系统启动断点(System Startup Breakpoint)处暂停。而像OD这种默认就会在程序的EP处暂停。
下面是设置OD在系统启动断点处暂停。
然后将程序拖入
就会暂停在此处
然后我们通过查看IMAGE_TLS_DIRECTORY结构体的AddressOfCallbacks查看TLS回调函数的地址
从而在对应的函数地址处下断点
查看发现在rdata节区
文件偏移为791C
这个值是以IMAGE_OPTIONAL_HAEDER中的IMAGE_BASE的值为基地址的VA,现在我们查看默认的image_base:
其实一般都是0x400000
然后我们相减得到RVA为0x8114,转换为文件偏移为0x6714:
如下:
然后我们在OD中ctrl+G跳转过去下断点:
然后运行即可调试我们的TLS回调函数
分析汇编可以得到程序的大致过程:
调用函数IsDebuggerPresent检测PEB.BeingDebugged成员得到是否处于调试状态。
然后测试返回值,如果没有在调试状态,那么直接跳转到retn 0xC(加个0xC是为了平衡栈,回调函数有三个参数)进行返回,如果在调试状态,则调用MessageBoxA弹出提示窗口。
其实我们可以给程序手动添加TLS回调函数,具体步骤如下:
1.
下面使用第二种进行讲解步骤
Size of Raw Data你扩展了多少就增加多少
Characteristics如下图:
设置TLS表,让其指向我们增加的IMAGE_TLS_DIRECTORY结构体
设置我们的IMAGE_TLS_DIRECTORY,对照参数进行填写
例:
然后我们先向那个TLS回调函数的地址指向的文件处写上C2 0C 00,对应汇编RETN 0C,为之后我们将程序拖入OD中添加TLS回调函数代码做准备,让其直接先返回
然后我们将这个程序载入OD中调试到TLS回调函数,添加我们的代码(对照上方我们调试程序得到的汇编),最后保存出去即可。
void print_console(char
*
szMsg)
/
/
定义一个函数来向控制台打印字符串
{
HANDLE hStdout
=
GetStdHandle(STD_OUTPUT_HANDLE);
/
/
先于主线程调用执行的TLS回调函数中使用printf可能会发生Runtime Error,可直接调用WriteConsole API
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);
/
/
wsprintfA宽字节的sprintf
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);
}
/
*
注册TLS函数
.CRT$XLX的作用
CRT表示使用C Runtime 机制
X表示表示名随机
L表示TLS Callback section
X也可以换成B~Y任意一个字符
*
/
/
/
存储回调函数地址
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[]
=
{ TLS_CALLBACK1, TLS_CALLBACK2,
0
};
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
;
}
void print_console(char
*
szMsg)
/
/
定义一个函数来向控制台打印字符串
{
HANDLE hStdout
=
GetStdHandle(STD_OUTPUT_HANDLE);
/
/
先于主线程调用执行的TLS回调函数中使用printf可能会发生Runtime Error,可直接调用WriteConsole API
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);
/
/
wsprintfA宽字节的sprintf
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);
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2021-4-23 15:41
被SYJ-Re编辑
,原因: