TLS回调函数
0x01 TLS介绍
TLS(Thread Local Storage,线程局部储存),主要用于给线程独立的传值,由于线程不拥有进程的资源,所以几个同一进程的几个线程需要独立赋值时的需要通过TLS技术。每个线程创建时都会分配一个index所以,这个索引index是全局变量,线程根据index来获取其他线程传过来的返回值。TLS有一个特点,就是它通常在程序EP前就要运行,所以起始TLS才是个程序真正的开始。利用这一特点,可以用来进行的程序的反调试。
一句话介绍:执行于进程或线程的创建与终止,早于EP(Entry Point)
那么我们怎么才能知道一个程序是否含有TLS回调函数呢?
0x02 PE TLS Table
编程中启用了TLS功能,PE头文件中就会设置TLS表
然后找到TLS Table
再找到TLS函数开始的地址
即0x401990
在IDA中验证一下(IDA是能帮你识别出TLS函数的)
0x03 TLS函数调用顺序
假设main函数里写了创建用户线程(ThreadProc)
0x01 DLL_PROCESS_ATTACH 进程启动 Reason=1
进程的主线程调用main()函数前,已经注册的TLS回调函数(TLS_CALLBACK1、TLS_CALLBACK2)会先被调用执行。
0x02 DLL_THREAD_ATTACH 线程启动 Reason=2
所有TLS回调函数完成调用后,main()函数开始调用执行,创建用户线程(ThreadProc)前,TLS回调函数会被再次调用执行。
0x03 DLL_THREAD_DETACH 线程结束 Reason=3
TLS回调函数全部执行完毕后,ThreadProc()线程函数开始调用执行。其执行完毕后Reason=3(DLL_THREAD_DETACH),TLS回调函数被调用执行。
0x04 DLL_PROCESS_DETACH 进程结束 Reason=0
ThreadProc()线程函数执行完毕后,一直在等待线程终止的main()函数(主线程)也会终止。此时Reason=0(DLL_PROCESS__DETACH),TLS回调函数最后一次被调用执行。
【miniLCTF2022】TWIN 复现
0x01 陷阱
main函数是纯纯骗你的,解密出来
后面你能发现一件惊奇的事情,那就是main函数根本就不会被执行!
0x02 找到TLS函数开始的地址
IDA已经帮你解析出了TLS函数,虽然是空的(出题人加了点❀)
也可以像文章开头一样,在PE中就找到了TLS的开始地址。
花指令清不清无所谓。
这个TLS函数中,你能清晰的发现DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH(我已加注释)
void __usercall sub_4019BF(int a1@<ebp>)
{
void *v1; // eax
// Reason==1即DLL_PROCESS_ATTACH
if ( *(_DWORD *)(a1 + 12) == 1 )
{
memset((void *)(a1 - 284), 0, 0x50u);
sub_401930((void *)(a1 - 284));
*(_BYTE *)(a1 - 1) = 0;
*(_BYTE *)(a1 - 1) = NtCurrentPeb()->BeingDebugged; //反调试,得过,设置ZF==1
if ( !*(_BYTE *)(a1 - 1) )
return;
*(_BYTE *)(a1 - 32) = 57;
*(_BYTE *)(a1 - 31) = 51;
*(_BYTE *)(a1 - 30) = 62;
*(_BYTE *)(a1 - 29) = 56;
*(_BYTE *)(a1 - 28) = 0;
Xor_0x7f(a1 - 32);
hObject = CreateFileMappingA(0, 0, 4u, 0, 0x1000u, (LPCSTR)(a1 - 32));
*(_DWORD *)dword_404448 = MapViewOfFile(hObject, 0xF001Fu, 0, 0, 0x1000u);
*(_BYTE *)(a1 - 116) = 47;
*(_BYTE *)(a1 - 115) = 19;
*(_BYTE *)(a1 - 114) = 26;
*(_BYTE *)(a1 - 113) = 30;
*(_BYTE *)(a1 - 112) = 12;
*(_BYTE *)(a1 - 111) = 26;
*(_BYTE *)(a1 - 110) = 95;
*(_BYTE *)(a1 - 109) = 22;
*(_BYTE *)(a1 - 108) = 17;
*(_BYTE *)(a1 - 107) = 15;
*(_BYTE *)(a1 - 106) = 10;
*(_BYTE *)(a1 - 105) = 11;
*(_BYTE *)(a1 - 104) = 95;
*(_BYTE *)(a1 - 103) = 6;
*(_BYTE *)(a1 - 102) = 16;
*(_BYTE *)(a1 - 101) = 10;
*(_BYTE *)(a1 - 100) = 13;
*(_BYTE *)(a1 - 99) = 95;
*(_BYTE *)(a1 - 98) = 25;
*(_BYTE *)(a1 - 97) = 19;
*(_BYTE *)(a1 - 96) = 30;
*(_BYTE *)(a1 - 95) = 24;
*(_BYTE *)(a1 - 94) = 69;
*(_BYTE *)(a1 - 93) = 95;
*(_BYTE *)(a1 - 92) = 0;
v1 = (void *)Xor_0x7f(a1 - 116);
sub_401930(v1);
*(_BYTE *)(a1 - 8) = 90;
*(_BYTE *)(a1 - 7) = 12;
*(_BYTE *)(a1 - 6) = 0;
Xor_0x7f(a1 - 8);
sub_401130((char *)(a1 - 8), dword_404448[0]);
}
// Reason==0 即 DLL_PROCESS_DETACH
if ( !*(_DWORD *)(a1 + 12) )
{
*(_BYTE *)(a1 - 24) = 81;
*(_BYTE *)(a1 - 23) = 80;
*(_BYTE *)(a1 - 22) = 11;
*(_BYTE *)(a1 - 21) = 18;
*(_BYTE *)(a1 - 20) = 15;
*(_BYTE *)(a1 - 19) = 0;
Xor_0x7f(a1 - 24);
CreatTmp();
memset((void *)(a1 - 204), 0, 0x44u);
*(_DWORD *)(a1 - 204) = 68;
CreateProcessA(
(LPCSTR)(a1 - 24),
0,
0,
0,
0,
3u,
0,
0,
(LPSTARTUPINFOA)(a1 - 204),
(LPPROCESS_INFORMATION)(a1 - 136));
*(_BYTE *)(a1 - 44) = 28;
*(_BYTE *)(a1 - 43) = 16;
*(_BYTE *)(a1 - 42) = 13;
*(_BYTE *)(a1 - 41) = 13;
*(_BYTE *)(a1 - 40) = 26;
*(_BYTE *)(a1 - 39) = 28;
*(_BYTE *)(a1 - 38) = 11;
*(_BYTE *)(a1 - 37) = 117;
*(_BYTE *)(a1 - 36) = 0;
*(_BYTE *)(a1 - 16) = 8;
*(_BYTE *)(a1 - 15) = 13;
*(_BYTE *)(a1 - 14) = 16;
*(_BYTE *)(a1 - 13) = 17;
*(_BYTE *)(a1 - 12) = 24;
*(_BYTE *)(a1 - 11) = 117;
*(_BYTE *)(a1 - 10) = 0;
*(_BYTE *)(a1 - 88) = 47;
*(_BYTE *)(a1 - 87) = 19;
*(_BYTE *)(a1 - 86) = 26;
*(_BYTE *)(a1 - 85) = 30;
*(_BYTE *)(a1 - 84) = 12;
*(_BYTE *)(a1 - 83) = 26;
*(_BYTE *)(a1 - 82) = 95;
*(_BYTE *)(a1 - 81) = 28;
*(_BYTE *)(a1 - 80) = 19;
*(_BYTE *)(a1 - 79) = 16;
*(_BYTE *)(a1 - 78) = 12;
*(_BYTE *)(a1 - 77) = 26;
*(_BYTE *)(a1 - 76) = 95;
*(_BYTE *)(a1 - 75) = 11;
*(_BYTE *)(a1 - 74) = 23;
*(_BYTE *)(a1 - 73) = 26;
*(_BYTE *)(a1 - 72) = 95;
*(_BYTE *)(a1 - 71) = 27;
*(_BYTE *)(a1 - 70) = 26;
*(_BYTE *)(a1 - 69) = 29;
*(_BYTE *)(a1 - 68) = 10;
*(_BYTE *)(a1 - 67) = 24;
*(_BYTE *)(a1 - 66) = 24;
*(_BYTE *)(a1 - 65) = 26;
*(_BYTE *)(a1 - 64) = 13;
*(_BYTE *)(a1 - 63) = 95;
*(_BYTE *)(a1 - 62) = 30;
*(_BYTE *)(a1 - 61) = 17;
*(_BYTE *)(a1 - 60) = 27;
*(_BYTE *)(a1 - 59) = 95;
*(_BYTE *)(a1 - 58) = 11;
*(_BYTE *)(a1 - 57) = 13;
*(_BYTE *)(a1 - 56) = 6;
*(_BYTE *)(a1 - 55) = 95;
*(_BYTE *)(a1 - 54) = 30;
*(_BYTE *)(a1 - 53) = 24;
*(_BYTE *)(a1 - 52) = 30;
*(_BYTE *)(a1 - 51) = 22;
*(_BYTE *)(a1 - 50) = 17;
*(_BYTE *)(a1 - 49) = 117;
*(_BYTE *)(a1 - 48) = 0;
sub_401510(a1 - 24, a1 - 136);
if ( dword_404440 == 1 )
{
sub_4012C0(*(_DWORD *)dword_404448 + 20, 5, &unk_40405C);
*(_DWORD *)(a1 - 120) = memcmp((const void *)(*(_DWORD *)dword_404448 + 20), &unk_40402C, 0x14u);
if ( !*(_DWORD *)(a1 - 120) )
{
Xor_0x7f(a1 - 44);
sub_401930((void *)(a1 - 44));
LABEL_12:
CloseHandle(hObject);
return;
}
}
else if ( dword_404440 == -2 )
{
Xor_0x7f(a1 - 88);
sub_401930((void *)(a1 - 88));
goto LABEL_12;
}
Xor_0x7f(a1 - 16);
sub_401930((void *)(a1 - 16));
goto LABEL_12;
}
}
如果不过PEB反调试的话,将不会设置第二个TLS函数,那么后面会得不到正确的flag。
hObject = CreateFileMappingA(0, 0, 4u, 0, 0x1000u, Name_FLAG);创建名字为FLAG的文件映射对象,用于进程间通信。
*(_DWORD *)input = MapViewOfFile(hObject, 0xF001Fu, 0, 0, 0x1000u);存了内存映射文件,便于后面的共享内存。
简而言之,就是创建一个名为FLAG的文件映射对象,把input指向的地址设置成一块共享的内存,这样就可以在子进程里对input这块内存进行修改,实现加密。
来自云之君的Blog
0x03 第一个TLS函数
在TLS函数开始地址处下个断点,开始跟进。
过掉反调试
发现其设置了第二个TLS函数
继续跟进,发现字符串混淆,即对字符串异或0x7f还原
第一个TLS的末尾,开始输入字符,随便输入12345678
第一个TLS函数算是结束了,根据TLS函数的机制,他将会执行第二个TLS函数。
0x04 进入第二个TLS
此时,就可以去第二个TLS开始地址下断点,直接F9跳过去。
我们就能得到第二个TLS函数
获取WriteFile 的地址
HOOK WriteFile地址,到目标函数(其实就是Sub_0x401650,之后可以知道是修改tmp文件中XXTEA的z的右移值)
第二个TLS函数就结束了
0x05 进程结束,进入PROCESS_DETACH
进程结束,根据TLS函数的机制,接下来返回到DLL_PROCESS_DETACH
进入CreatTmp函数
创建子进程tmp文件,可以在同目录下发现已经创建好的tmp文件。
跟进WriteFile函数中,发现了之前HOOK的函数
继续向下调试,能发现很关键的函数
0x06 主进程调试子进程
此时我们要关注Tmp子进程,进入tmp文件,其包含了一个XXTEA,以及一些对Delta修改的指令。
tmp文件中还检测了是否有调试器
我们将IDA的根目录进行修改,改成adi.exe 😋,就能过了。
我们重新调试,在关键点XOR处下断点,就能得到最终的Delta值。
0x07 Cat Flag
第一段flag
已知XXTEA的key和密文(注意key[1]已被赋值为144)
#include <stdio.h>
#include <stdlib.h>
#define delta 0x1C925D64
int main()
{
unsigned int v[5] = { 0x6B7CE328, 0x4841D5DD, 0x963784DC, 0xEF8A3226, 0x776B226};
unsigned int key[4] = {0x12,144,0x56,0x78};
unsigned int sum = 0;
unsigned int y,z,p,rounds,e;
int n = 5;
int i = 0;
rounds = 6 + 52/n;
y = v[0];
sum = rounds*delta;
do
{
e = sum >> 2 & 3;
for(p=n-1;p>0;p--)
{
z = v[p-1];
v[p] -= ((((z>>6)^(y<<2))+((y>>3)^(z<<4))) ^ ((key[(p&3)^e]^z)+(y ^ sum)));
y = v[p];
}
z = v[n-1];
v[0] -= ((((y<<2)^(z>>6))+((z<<4)^(y>>3))))^ ((key[(p^e)&3]^z)+(y ^ sum));
y = v[0];
sum = sum-delta;
}while(--rounds);
for(i=0;i<n;i++)
{
printf("%c%c%c%c",*((char*)&v[i]+0),*((char*)&v[i]+1),*((char*)&v[i]+2),*((char*)&v[i]+3));
}
return 0;
}
//miniLctf{cbda59ff59e
第二段flag
第二个XXTEA函数(sub_4012C0)
#include <stdio.h>
#include <stdlib.h>
#define delta 0x9E3779B9
int main()
{
unsigned int v[5] = {0x9021A921, 0xF53B3060, 0x8E88A84E, 0x43635AD5, 0xAC119239};
unsigned int key[4] = {0x12,0x34,0x56,0x78};
unsigned int sum = 0;
unsigned int y,z,p,rounds,e;
int n = 5;
int i = 0;
rounds = 6 + 52/n;
y = v[0];
sum = rounds*delta;
do
{
e = sum >> 2 & 3;
for(p=n-1;p>0;p--)
{
z = v[p-1];
v[p] -= ((((z>>5)^(y<<2))+((y>>3)^(z<<4))) ^ ((key[(p&3)^e]^z)+(y ^ sum)));
y = v[p];
}
z = v[n-1];
v[0] -= ((((y<<2)^(z>>5))+((z<<4)^(y>>3))))^ ((key[(p^e)&3]^z)+(y ^ sum));
y = v[0];
sum = sum-delta;
}while(--rounds);
for(i=0;i<n;i++)
{
printf("%c%c%c%c",*((char*)&v[i]+0),*((char*)&v[i]+1),*((char*)&v[i]+2),*((char*)&v[i]+3));
}
return 0;
//3e90c91c02e9b40b78b}
}
两段flag组合即为最后的flag
0x08 后话
这题涉及的知识点挺多的,最后还是要感谢P.Z和云之君等大佬的博客和视频,帮助我一步步了解这题的运行过程。感谢这些师傅们,Thanks♪(・ω・)ノ。ORZ。
这题遇到的麻烦也不少:调试时,不知道何原因,tmp子进程一直创建失败,在Process_Exployer软件中也看不到tmp进程的踪影。导致调试时一直卡在WaitForDebugEvent处。但是放到win10虚拟机中,问题又解决了。搞不懂都。
0x09总结知识点
· 理清TLS函数的调用过程,TLS>Main->TLS(这道题Main根本都没执行,加解密与flag的判断,全在TLS函数中)
· 主进程调试子进程,以及其接受异常处理事件,这题秀我一脸了属于是(原来可以酱紫玩)。
· PEB反调试,并不是看到PEB检测就无脑修改ZF标志符,主进程调试子进程,所以子进程中的PEB检测反而不需要过,盲目的过反调试会带来后果。
· HOOK函数,此题HOOK函数修改tmp文件中XXTEA的z的右移位数
· 字符串的混淆
评论区