TLS回调函数–一文看懂(详)
学习的时候看到了很多博客,虽然大同小异,但是受益匪浅。
在此总结一下(缝补匠),并以一道CTF例题总结
耐心看就会懂,可能需要一些PE文件前置知识。
另外在搜索资料过程中,发现一些师傅的代码只能在x86下生成才有效,因此更新了一下代码,使其可以运行在x86/x64下运行。
文章目录
一、初识TLS
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回调函数地址的数组,进程在启动运行时,系统会逐一调用储存在该数组中的函数。
二、TLS回调函数
每当创建或终止进程的线程时会自动调用执行的函数。当然,创建进程的主线程的时候也会自动调用回调函数,且其执行先于EP代码。反调试技术就是利用的TLS回调函数的这一特征。
参数顺序和定义都是一样的。第一个参数表示模块句柄,第二个参数表示调用TLS回调函数的原因。
对于第二个参数Reaseon:
调用原因有四种<重要>
#define DLL_PROCESS_ATTACH 1
#define DLL_THREAD_ATTACH 2
#define DLL_THREAD_DETACH 3
#define DLL_PROCESS_ATTACH 0
主线程调用main前调用TLS回调函数,调用原因为DLL_PROCESS_ATTACH
子线程启动前调用TLS,原因为DLL_THREAD_ATTACH
子线程结束后调用TLS,原因为DLL_THREAD_DETACH
主线程结束后调用TLS 原因为DLL_PROCESS_DETACH
三、实例代码 x86/64
下述代码可以用x64/x86模式下进行调试.
x86和x64下对于Tls的使用有所不同,如以下代码所示。
#include <windows.h>
//
/
//
//告知连接器使用TLS,在x86和x64链接方式有所不同
#ifdef _WIN64
#pragma comment(linker,"/INCLUDE:_tls_used")
#else
#pragma comment(linker,"/INCLUDE:__tls_used")
#endif // _WIN64
/
//
void print_console(const 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);
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任意一个字符
*/
extern "C"
#ifdef _WIN64
#pragma const_seg(".CRT$XLX")
const
#else
#pragma data_seg(".CRT$XLX")
#endif
/
//
//存储回调函数地址
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;
}
运行结果:
四、Tls反调试
#include <Windows.h>
#include <stdio.h>
// linker spec 通知链接器PE文件要创建TLS目录
#ifdef _M_IX86
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
#else
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback")
#endif
void NTAPI __stdcall TLS_CALLBACK(PVOID DllHandle, DWORD dwReason, PVOID Reserved)
{
if (IsDebuggerPresent())
{
MessageBoxW(NULL, L" TLS_CALLBACK: 请勿调试本程序 !", L"TLS Callback", MB_ICONSTOP);
ExitProcess(0);
}
}
// 创建TLS段
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
PIMAGE_TLS_CALLBACK _tls_callback = TLS_CALLBACK;
#else
#pragma data_seg (".CRT$XLB")
PIMAGE_TLS_CALLBACK _tls_callback = TLS_CALLBACK;
#endif
int main(int argc, char* argv[])
{
system("pause");
return 0;
}
五、调试Tls回调函数
如何找到Tls
若在编程中启用了TLS功能,PE头文件中就会设置TLS表(IMAGE_NT_HEARDERS->IMAGE_OPTIONAL_HEADER->IMAGE_DATA_DIRECTORY[9])
可以看到TLS Table的RVA是00009310,找到对应位置如下
TLS Table中比较重要的成员为AddressOfCallbacks,该值指向含有TLS回调函数地址(VA)的数据(一个程序中可以注册多个TLS回调函数)
Ollydbg
-
修改OllyDbg(2.0)选项就可以调试TLS回调函数。
-
重启应用程序,程序会暂停在TLS回调函数,
ida.pro
在函数窗口中可以直接找到TLS回调函数
六、手动去除TLS函数
此时,在IDA中的导出窗口中,我们可以看到两个TLS函数以及start
下面就进行 TLS的手动去除
1.首先使用PEview打开目标文件
找到了TLS在头部的位置,在winhex中打开,找到1B0位置
将这一块填充为0
2.之后再找到TLS在数据段里的位置,并在winhex里将其填充为0
很明显,就是这一段
之后就可以保存文件,大功告成了
七、手工添加TLS回调函数
使用的工具
- 010editor
- OllyDgbg
规划设计
手动添加TLS,需要知道IMAGE_TLS_DIRECTORY结构体与TLS回调函数放到PE文件的哪些位置。向某个PE文件添加代码或者数据时候有如下三种来查找合适的位置。
- 增加最后一个节区域的大小
- 添加到节区域末尾的空白处
- 在最后添加新节区
本次使用第一种增加最后一个节区的大小。
手动添加TLS回调函数
更改节区大小
使用010editor可以看到最后一个节区的基本信息,Pointer to Raw Date = 9000,size of Raw Data =200.所以PE头中定义的文件整体大小为9200h字节。
本次实验我们将最后一个节区增加200h字节,使最后一个节区大小为9400h。
更改节区属性
节区的属性更改为可读可写可执行。
增加200h字节后VA,是按照section Alignment值对其,因该值未超过1000,所以该值不需要改变。
扩大节区200个字节
在9200处添加200个字节大小的空间。
在新增加的空间中增加TLS结构体
做完以上基本信息后,需要在9200地址处创建IMAGE_TLS_DIRECTORY结构体,AddressOfCallBacks成员的值为VA 40c280.(文件偏移为9280h)。他是Array of TLS callback function的起始地址。只要把TLS回调函数的地址9290h放入到40c280回调函数数组中(文件偏移为9280h),即可成功注册TLS回调函数。
利用OllyDebug的汇编功能,在程序的回调函数地址处编写回调函数。在下图中按空格键,编写汇编语言。
利用OD编写回调函数代码
由上文中介绍的IMAGE_TLS_CALLBACK代码中可以知道,在esp+8的位置是reason参数。跟踪函数可以知道FS:[30]处指向PEB函数地址。通过PEB的结构体可以知道eax+2 指向了PEB.Beingbugged。
将编写的程序另存为新文件
将修改的内存全部选中,右键单击代码-弹出选择—保存文件菜单。另存为Manual_hello_Tls1.exe
八、CTF re [NewStarCTF 2023 公开赛道]k00h_slT
[题目链接](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]k00h_slT)
Main
进入主函数,发现输出和输入函数,将其重命名 (快捷键N)
sub_401950 -> printf
sub_401990 -> scanf
重点分析StartAddress
函数(其中的sub_401500
是base64编码)
StartAddress
发现并没有做什么,Tab查看汇编。
发现存在IDA未检测出的灰色区域,其原因是在函数结束之前retn
了.
将.text:00401773
C后,发现是花指令。用nop
的方式一直解决到函数结束即可。其间藏了个死循环,若不解决无法动态调试。解决后F5
查看反汇编代码如下图所示。
其中的sub_401500
是base64编码.
TlsCallback_0
发现函数列表中还有 TlsCallback_0
。
分析可以知道:
case0
: checkcase1
: 反调试case2
: 注意到144=0x90
所以这里检测的是Tlscallback
函数自身有没有被nop
若有则修改一些关键数据 所以我们不能nop
来反调试case3
: 这里其实是对sub401500
中调用的函数进行了修改
看到VirtualProtect
函数就知道有修改代码的可能。
关于Tls_callback
参数:
Tls_callback
的第二个参数Reason
可以有四个值
分别是DLL_PROCESS_ATTACH
、DLL_THREAD_ATTACH
、DLL_THREAD_DETACH
、DLL_PROCESS_DETACH
因此对应顺序为 1-2-3-0
整个程序流程:
- 首先程序启动,先通过
case 1
检测是否存在调试器,如果存在则直接卡死进程 - 然后通过main函数中的
CreateThread
创建了一个线程,然后由于有WaitForSingleObject
的存在,main
函数卡在这条指令等待线程执行结束。线程进入后先通过case 2
的nop
检测,然后对4A7000
进行修改。
动调进入sub_4013B0
运行到此处即可得到key
的值 - 随后反调试之进程检测,然后调用了
sub_401500
,此时这个函数的内容对应的是sub_401170
函数即base64
函数。 - 调用完后退出线程的时候进入
case 3
,sub_401500
的call被修改成为了sub_4013B0
- 退出线程后,进入main函数中的
sub_401500
,进行了xxtea
的加密。下面比较不相同因此不会触发假flag的提示。 - 程序所有进程退出,进入了
Tls
的case 0
,case 0
是最后的check
函数,通过检测后程序输出Congratulations
。
动调过程
其中会碰到.text:004017A9 jmp loc_4017A9
,直接跳到下一句就好。
在case3
和主函数sub_401500
处下断点,进入观察再经过修改后的sub_401500
中的call
被修改成为了sub_4013B0
P命令
创建一个函数,发现是xxtea
加密。
密文在case0
中可以找到,可同时找到base64
变种密码表
找key:
加密解密过程
加密
- 改变
base64
密码表 - 进行魔改
base64
加密 xxtea
加密
解密
- 找到魔改密码表和
key
xxtea
解密- 魔改
base64
解密
#include <stdio.h>
#include <stdint.h>
#define DELTA 0x9e3779b9
#define MX (((z>>5^y<<3) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))
void btea(uint32_t *v, int n, uint32_t const key[4])
{
uint32_t y, z, sum;
unsigned p, rounds, e;
if (n > 1) /* Coding Part */
{
rounds = 6 + 52/n;
sum = 0;
z = v[n-1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for (p=0; p<n-1; p++)
{
y = v[p+1];
z = v[p] += MX;
}
y = v[0];
z = v[n-1] += MX;
}
while (--rounds);
}
else if (n < -1) /* Decoding Part */
{
n = -n;
rounds = 6 + 52/n;
sum = rounds*DELTA;
y = v[0];
do
{
e = (sum >> 2) & 3;
for (p=n-1; p>0; p--)
{
z = v[p-1];
y = v[p] -= MX;
}
z = v[n-1];
y = v[0] -= MX;
sum -= DELTA;
}
while (--rounds);
}
}
int main()
{
uint32_t v[]= {0x3400A0D0, 0xB23CFFEB, 0xCDE69111, 0x032D0771, 0xFA1D9E6C, 0x9D15360A, 0x933EBF03, 0x9F12DDA6,
0x8C58DDA1, 0x46BEE3E0, 0x04476F65, 0x3C44CEF9};
uint32_t const k[4]= {0x75,0x404,0xBF,0X2652};
int n= -12;
btea(v, n, k);
for(int i=0;i<12;i++)
for(int j=0;j<4;j++){
printf("%c",(v[i]>>(8*j))&0xff);
}
return 0;
}
拿到base变表(一样的动调提取即可 )加密后的
48Pt4WXo+yhqh0GzFAbRg0XqgFhq4UOl+UwqL8wUHy6rEFSQ
用base64变表解一下就可以得到flag