TLS和main前的初始化

TLS回调函数和_initterm

TLS回调函数

在介绍TLS回调函数之前,可以先了解一下TLS

[TLS及其分类][https://www.cnblogs.com/revercc/p/14348817.html]

Thread Local Storage(TLS),线程局部存储:各线程独立的数据存储空间。使用TLS技术可以在线程内部独立使用或修改进程的全局数据或静态数据, 就像对待自身的局部变量一样

TLS回调函数

参考文章:

TLS_callback

TLS_callback

反调试和TLS回调函数

TLS回调函数要执行需要经历下面三个步骤

  1. 在链接(link)时,链接器要在PE文件中创建TLS目录
  2. 在创建线程时,加载器(loader)会从TEB(Thread Environment Block,线程环境块,通过FS段寄存器可以获取TEB的位置)中获取一个指向TLS回调函数数组的指针
  3. 如果在TLS回调函数数组不是一个空的数组,加载器就会顺序执行这个数组中的各个回调函数

TLS回调函数用于反调试,主要是利用于TLS回调函数的调用要先于EP代码的执行,也就是在主线程创建之前先执行TLS回调函数。

TLS回调函数是每当创建或终止进程的线程时会自动调用执行的函数。创建进程的主线程的时候也会自动调用回调函数

从上图中,我们可以看出TLS回调函数和DllMain的定义是类似的,其中第一个参数表示模块句柄,第二个参数表示调用TLS回调函数的原因

Reason的类型有如下四种,下面是定义

图片描述

DLL_PROCESS_ATTACH-1:新进程创建时,在初始化主线程时执行

DLL_THREAD_ATTACH-2:在新线程创建时执行,但不包括主线程

DLL_THREAD_DETACH-3:指所有线程终止时执行,但不包括主线程

DLL_PROCESS_DETACH-0:进程终止时执行

TLS回调函数的实现

下面的代码是基于Release版的X86程序。接下来说一下这个程序的问题,使用Release\X64,无法成功调用TLS回调函数,而使用Debug\X64,Debug\X86只能在主线程创建前调用,而在主线程结束之后没有调用

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
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>  
#include <windows.h>
#include <iostream>
void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD dwReason, PVOID Reserved);//TLS回调函数的声明

#ifdef _M_IX86 //判断当前编译模式是32位还是64位,然后通知链接器要创建TLS目录

#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") //对应64位模式,通知编译器存在tls段
#endif

EXTERN_C
/*
创建一个TLS段,注册TLS函数
.CRTXLX的作用:
CRT表示使用C Runtime机制
X表示表示名随机
L表示TLS CallBack Section
X可以换成B~Y其中任意一个字符
*/

#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif
#pragma data_seg()
#pragma const_seg()
PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK1,0 };//TLS_CALLBACK数组存储回调函数,以0结尾

//根据TLS回调函数的定义创建TLS回调函数
void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
printf("%d\n",Reason);
if (IsDebuggerPresent())
{
printf("TLS_CALLBACK: Debugger Detected!\n");
}
else
{
printf("TLS_CALLBACK: No Debugger Present!\n");
}
}

int main(int argc, char* argv[])
{
return 0;
}

效果

ida中

ida中TLS回调函数

_initterm

参考文章:

程序入口点与main函数

initterm

在main前执行函数

全局构造函数执行

在Windows平台中,执行我们手写的main函数之前,系统会执行一段mainCRTStartup代码,其中不同版本的实现也是不同的

其对系统的堆栈、全局变量、命令行参数、环境变量等进行初始化操作,而_init_term就是对全局变量进行初始化的函数,其在main函数前执行

并且对于函数指针数组

其中__xc_a和__xc_z是两个函数指针

1
2
3
4
5
6
7
//__xc_a, __xc_z的定义
_CRTALLOC(".CRT$XCA") _PVFV __xc_a[] = {NULL};
_CRTALLOC(".CRT$XCZ") _PVFV __xc_z[] = {NULL};

//_CRTALLOC的定义
#pragma section(".CRT$XCA", long, read)
#pragma section(".CRT$XCZ", long, read)

而initterm函数的内容如下

1
2
3
4
5
6
7
8
9
static void __cdecl _initterm (_PVFV * pfbegin,_PVFV * pfend)
{
while ( pfbegin < pfend )
{
if ( *pfbegin != NULL )
(**pfbegin)();
++pfbegin;
}
}

PVFV的定义

1
typedef void (__cdecl *_PVFV)();

可以看出initterm就是遍历上面两个函数指针之间的所有函数并执行

我们有两种办法在这两个函数指针之间添加函数

构造函数

全局对象的构造函数会被加入到a和z之间,注意下面的例子是Release的,使用Debug模式编译出来的也是一样效果,但是在ida中a和z之间还有很多0进行填充,如下两图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<string.h>
#include<iostream>

class data
{
public:
data();
};

data::data()
{
printf("hello\n");
}

data test;

int main()
{
printf("world");
return 0;
}

通过全局对象的构造函数在cpp的初始化中添加函数

从ida的注释定义也可以看出是函数指针以及其含义

1
void (__fastcall *pre_cpp_initializer)()

创建段

注意:以下代码只有在Debug模式下可以得到想要的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#define SECNAME ".CRT$XCG"
#pragma section(SECNAME,long,read)//创建段,在__xc_a和__xc_z之间
void foo()
{
std::cout << "hello" << std::endl;
}
typedef void(__cdecl* _PVFV)();//定义和__xc_a和__xc_z一样类型的函数
__declspec(allocate(SECNAME)) _PVFV dummy[] = { foo };//在创建的段中生成数据,我们这里将函数的地址存储进去
int main()
{
printf("world");
return 0;
}

效果

因为我们添加的是xcg,所以只看对于cpp的xca和xcz之间的

xca段的地址

xcz段的地址

添加的xcg段的地址

可以发现这是介于两函数指针之间的,所以是会被遍历到并且执行的

而我们想在C的全局对象初始化数组中添加函数只需要修改段的名称为.CRT$XIG

也可以按照上述链接中的代码进行

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
44
45
46
#include <iostream>

using namespace std;

//1.通过段名称“.CRT$XIU”,“.CRT$XCU”把函数表放在“C/C++初始化函数表”中
//通过特殊的段名称“.CRT$XIU”,“.CRT$XCU”,
//链接器会把before1表放在“C初始化函数表”中,类似这样
//[__xi_a, ..., before1(xiu), ..., __xi_z].
//同理,before2表会被链接器放在“C++初始化函数表”中,象这样
//[__xc_a, ..., before2(xcu), ..., __xc_z],

int before_main1()
{
printf("before_main1()\n");

return 0;
}
int before_main2()
{
printf("before_main2()\n");

return 0;
}
int after_main()
{
printf("after_main()\n");
return 0;
}

typedef int func();

#pragma data_seg(".CRT$XIU") //用#pragma data_seg建立一个新的数据段并定义共享数据
//static func * before1[] = { before_main1 };
func *before1 = before_main1;

#pragma data_seg(".CRT$XCU")
//static func * before2[] = { before_main2 };
func *before2 = before_main2;

#pragma data_seg()

void main()
{
_onexit(after_main);
cout<<"this's main start"<<endl;
}

但是只能在Debug\X86下能得到想要的结果