TLS回调函数

TLS回调函数是个啥

TLS全称Thread Local Storage,中文“线程局部存储”。
TLS是各线程的独立的数据存储空间,使用TLS技术可在线程内部独立使用或修改进程的全局数据或静态数据,就像对待自身的局部变量一样。
Buuuuuuuuut,我们今天要来看一下TLS其他的用处。

TLS的特性与利用

TLS的基本样式

TLS的代码实现基本样式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <windows.h>
#include <iostream>
#pragma comment(linker, "/INCLUDE:__tls_used")//x64模式下写_tls_used

VOID NTAPI TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
//在这里写你想做的事情
}

//它能跑,你就别管它在干啥
EXTERN_C
#pragma const_seg(".CRT$XLB")
const PIMAGE_TLS_CALLBACK tlsCalls[] = { TLS_CALLBACK, 0 };
#pragma const_seg()

int main()
{

return 0;
}

每一句都是什么意思呢?不知道,背下来就好了。
P.S.x64模式下需要禁用优化。

TLS被调用的原因

TLS会因为以下4种原因被调用:

即进程、线程的启动前和结束后。
这种特性决定了它经常被用于反调试,你的程序会在main()函数之前就被莫名其妙地中止了。

测试你的TLS回调函数

运行下面的代码,看看你的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <windows.h>
#include <iostream>
#pragma comment(linker, "/INCLUDE:__tls_used")

VOID NTAPI TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
if (Reason == DLL_PROCESS_ATTACH)
{
printf("running TLSCallBack before main()\n");
}
if (Reason == DLL_PROCESS_DETACH)
{
printf("running TLSCallBack after main()\n");
}
if (Reason == DLL_THREAD_ATTACH)
{
printf("running TLSCallBack before thread\n");
}
if (Reason == DLL_THREAD_DETACH)
{
printf("running TLSCallBack after thread\n");
}
}

EXTERN_C
#pragma const_seg(".CRT$XLB")
const PIMAGE_TLS_CALLBACK tlsCalls[] = { TLS_CALLBACK, 0 };
#pragma const_seg()

VOID my_thread()
{
printf("thread begin\n");
printf("thread end\n");
}

int main()
{
printf("main() begin\n");
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)my_thread, NULL, NULL, NULL);
WaitForSingleObject(hThread, 1000);
printf("main() end\n");
return 0;
}

理论上应该会输出4遍running TLSCallBack balabala。然鹅,我这里只能输出成这样:

利用TLS藏东西

我们在写代码的时候总有一些事情是不想让别人发现的,比如srand()之类的,或者把真正的加密函数藏起来。我们就可以把它放在TLS中运行。如果你的TLS是可以在main()之后运行的。那么你可以藏成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <windows.h>
#include <iostream>
#pragma comment(linker, "/INCLUDE:__tls_used")

VOID NTAPI TLS_CALLBACK0(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
if (Reason == DLL_PROCESS_ATTACH)
{
//first half of encryption code
}
if (Reason == DLL_PROCESS_DETACH)
{
//last half of encryption code
exit(0);
}
}

VOID NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
//change something that will be used in the last half of encryption code
ExitProcess(-1);
}

EXTERN_C
#pragma const_seg(".CRT$XLB")
const PIMAGE_TLS_CALLBACK tlsCalls[] = { TLS_CALLBACK0, TLS_CALLBACK1, 0 };
#pragma const_seg()

int main()
{
//fake encryption code
return 0;
}

这样程序在执行完TLS_CALLBACK1后会因为ExitProcess(-1)而结束进程,即跳过整个main()函数去执行TLS_CALLBACK0。
如果你像我一样,main()函数后面无法运行TLS,那么你可以藏成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <windows.h>
#include <iostream>
#pragma comment(linker, "/INCLUDE:__tls_used")

VOID NTAPI TLS_CALLBACK0(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
if (Reason == DLL_PROCESS_ATTACH)
{
//first half of encryption code
}
}

VOID NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
if (Reason == DLL_THREAD_ATTACH)
{
//last half of encryption code
exit(0);
}
}

EXTERN_C
#pragma const_seg(".CRT$XLB")
const PIMAGE_TLS_CALLBACK tlsCalls[] = { TLS_CALLBACK0, TLS_CALLBACK1, 0 };
#pragma const_seg()

void my_thread()
{
//fake encryption code
}

int main()
{
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)my_thread, NULL, NULL, NULL);
WaitForSingleObject(hThread, -1);//这样此句之后的东西就不会被执行了
return 0;
}

由于exit(0)的存在,整个函数不会执行线程中的任何一行代码。

邪恶的想法

上述写法已经可以让我们神不知鬼不觉地绕过展示出来的代码而去执行藏起来的东西。如果再在真正的加密函数中用上Windows系统编程特有的try-except模块,那么你将创造出一个IDA里处处都是假代码的、丧心病狂的、恶心至极的题目。hiahiahiahia…

TLS的防治

识别TLS的存在

我们可以在main()函数的第一行代码/创建线程的代码处下个断点,然后动调,发现程序退出了/输出了或者进行操作了都是TLS存在的特征。

解决TLS的问题

想多了,我不会。不过好消息是IDA会。
在IDA的函数窗口搜索TLS就会显示出IDA帮你分析出来的TLS回调函数:

然后我们就可以去里面慢慢读了。

结束语

怎么做恶心的题不太会,但怎么出恶心的题倒是会了…
阿巴阿巴阿巴…