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回调函数要执行需要经历下面三个步骤
- 在链接(link)时,链接器要在PE文件中创建TLS目录
- 在创建线程时,加载器(loader)会从TEB(Thread Environment Block,线程环境块,通过FS段寄存器可以获取TEB的位置)中获取一个指向TLS回调函数数组的指针
- 如果在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);
#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
EXTERN_C
#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 };
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中
_initterm
参考文章:
程序入口点与main函数
initterm
在main前执行函数
全局构造函数执行
在Windows平台中,执行我们手写的main函数之前,系统会执行一段mainCRTStartup代码,其中不同版本的实现也是不同的
其对系统的堆栈、全局变量、命令行参数、环境变量等进行初始化操作,而_init_term就是对全局变量进行初始化的函数,其在main函数前执行
并且对于函数指针数组
其中__xc_a和__xc_z是两个函数指针
1 2 3 4 5 6 7
| _CRTALLOC(".CRT$XCA") _PVFV __xc_a[] = {NULL}; _CRTALLOC(".CRT$XCZ") _PVFV __xc_z[] = {NULL};
#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) void foo() { std::cout << "hello" << std::endl; } typedef void(__cdecl* _PVFV)(); __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;
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")
func *before1 = before_main1;
#pragma data_seg(".CRT$XCU")
func *before2 = before_main2;
#pragma data_seg()
void main() { _onexit(after_main); cout<<"this's main start"<<endl; }
|
但是只能在Debug\X86下能得到想要的结果