前情提要
Demo代码
#include
int tls(){
MessageBox(NULL,"This is TLS CallBack!","TLS Success",MB_OK);
return 0;
}
int main(){
MessageBox(NULL,"This is Main!","Main",MB_OK);
return 0;
}
测试思路
让tls()
中的弹窗先于main()
中的弹窗!关于整个 tls 的原理就不细说了,你需要有些PE格式的基础以及tls的了解
,不然看着很头疼。P.S.实际操作比看别人操作要困难很多,光看是不会进步的
具体方法
使用微软VisualStudio
的话很简单,网上教程很多。先定义一个 TLS 回调函数void NTAPI TLS_CALLBACK
,再添加节区#pragma data_seg (".tls")
就行了!不过我们这篇讲得是手动添加.tls节区以及手动定义回调函数
。因为 MinGW 和 MSVC 编译器编译的 PE 文件格式有所区别,所以需要分这两种情况
编译器对比
- GCC
使用gcc -m32 gcc.c -o gcc
编译。由于 MinGW 的特性使得 GCC 编译的程序含有.tls
节区以及关于程序运行时的 tls 回调函数
- MSVC
使用VC++6.0
编译 Demo 代码。MSVC 编译器编译的程序节区就很少了,并且没有.tls
节以及 tls 回调函数
GCC
前置知识点
一、数据目录表对应的表项及其宏定义
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0.导出表
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1.导入表
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2.资源目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3.异常目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4.安全目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5.重定位基本表
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6.调试目录
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7.描术字串
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8.机器值
#define IMAGE_DIRECTORY_ENTRY_TLS 9.TLS目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10.载入配值目录
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11.绑定输入表
#define IMAGE_DIRECTORY_ENTRY_IAT 12.导入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13.延迟载入描述
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14.COM信息
二、节区 Header 的结构体
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; /* 节区名 */
union {
DWORD PhysicalAddress;
DWORD VirtualSize; /* 节区在内存中占用的大小与文件中的大小不同 */
} Misc;
DWORD VirtualAddress; /* 节区在内存中的偏移 */
DWORD SizeOfRawData; /* 节区在文件在占用的大小 */
DWORD PointerToRawData; /* 节区在文件中的偏移 */
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; /* 节区的属性 */
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
三、TLS 的结构体
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData; /* tls节区的起始地址 */
DWORD EndAddressOfRawData; /* tls节区的最终地址 */
DWORD AddressOfIndex; /* tls节区的索引 */
DWORD AddressOfCallBacks; /* 指向回调函数的指针 */
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32;
四、内存偏移手动转文件偏移
根据节区头部的内存偏移量定位某个 RVA 在哪个节区中,然后利用该节区的文件偏移加上相对于该节区的内存偏移即可得到任意内存偏移相对应的文件偏移
具体操作
首先了解下程序的对齐值,gcc编译的程序内存对齐是0x1000,文件对齐是0x200
然后找到NT Header->Optional Header->Data[9]
,由VirtualAddr
定位到TLS的整个结构体
,要将 RVA 转为文件偏移
根据 TLS 的结构体定义可以知道,tls 节区的内存偏移在 0x408000,转化成文件偏移就是 0x2A00,回调函数的指针指向0x407020,转换成文件偏移就是0x2820
由IDA
看到我们自写的tls()
函数的地址是0x401510
,我们将这个地址调到0x407020对应的文件偏移0x2820后面
,P.S.前面的那两个回调函数应该是运行时相关的,编译器自定义的 tls 回调与我们无关
保存之后运行,可以看到tls函数会先于main函数运行
MSVC
前置知识点
见上方
具体操作
由于 MSVC 编译的程序是没有 tls 节区的所以我们需要添加一个新节区,添加之前先看看文件的对齐值,内存对齐为0x1000,文件对齐为0x1000
,但并不一定所有节区的内存偏移等同于文件偏移,因为可能有的节区内存所占大小大于文件所占大小
,这就导致该节区之后节区的内存和文件偏移不一致
先将NT Header->File Header->NumberOfSection
的数量加1,然后将NT Header->Optional Header->SizeOfImage
的值加0x1000
然后在文件最后添加0x1000个00
,作为新节区
再去修改新节区的头部,偏移值要根据前面的节区填写,比如:前一个节区的内存偏移为0x2B000,文件偏移为0x29000,那么新节区的内存偏移为0x2C000,文件偏移为0x2A000
,注意还需要可写的这个属性
再然后就是填写数据目录表,首先我是把TLS的结构体放在.tls节区起始地址的
,所以NT Header->Optional Header->Data[9]
中的VirtualAddress
填写0x2C000
即 .tls 节区的内存偏移值,然后Size
填写0x18
最后就是填写 TLS 结构体的内容了,自写的 tls 函数地址在0x401020
。TLS 结构体的前三项可以直接填写当前位置的内存偏移,第四项需要注意的是指针类型
双击运行,tls 函数先于 main 函数运行
总结
略微总结一下 PE 文件怎么找到的 TLS 回调函数
- 先是数据目录表找到 TLS 结构体所在的位置
- TLS 结构体会指示 TLS 的起始地址,这个地址通常是 .tls 所在的地址
- TLS 结构体中还会有最重要的回调函数指针,根据指针的地址就能定位到回调函数的地址
关于 .tls 节区的说明,其实并不一定需要 .tls 节区,将 TLS 结构体的Start
和End
地址填写为其它有效内存区域也是可以的。.tls
节区主要是用来存储 tls 回调函数需要用到的一些数据