PE
用010Editor解析PE头
首先打开模板
然后按 Alt + 4 即可
效果如图
PE头
以上是参考图
1.DOS部分
1 | typedef struct _IMAE_DOS_HEADER// 偏移, 意义 |
1.WORD e_magic(0x00处)
● 对应PE文件的开头,是PE文件DOS头的标识符”MZ”→0x5A4D
○ 对应Winnt.h头文件中宏定义: #define IMAGE_DOS_SIGNATURE 0x4D5A // MZ
2.LONG e_lfanew(0x3C处)
● 对应PE文件0x3C处指向NT头在文件中的偏移(默认0xB0),即32位及以上系统文件头在文件中真正的偏移
2.NT头
“NT” 在这里是 “New Technology” 的缩写,完整的单词是 “New Technology”。 它们的内核相比之前的 MS-DOS 和 Windows 95/98 等系统有了重大的改进和升级。
文件头结构体
IMAGE_FILE_HEADER FileHeader
- MAGE_FILE_HEADER:描述磁盘上PE文件的相关信息。
- 定位文件头地址:DOS头中的e_lfanew+4(位于NT头标识的地址+0x4)
1 | // 文件头结构体: _IMAGE_FILE_HEADER |
选项头结构体
IMAGE_OPTIONAL_HEADER(区分32位和64位)
- IMAGE_FILE_HEADER::以供操作系统加载PE文件使用,必选。
- 定位选项头地址:DOS头中的e_lfanew+4+0x14(文件头大小)。
1 | // 32位选项头结构体:_IMAGE_OPTIONAL_HEADER |
WORD DllCharacteristics;一些介绍
- IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE(0x0040):指示DLL支持ASLR(地址空间布局随机化),使得每次加载时的地址空间布局都不同,增加了系统的安全性。
- IMAGE_DLLCHARACTERISTICS_NX_COMPAT(0x0100):指示DLL支持DEP(数据执行保护),这样操作系统就可以防止恶意代码在内存中执行。
- IMAGE_DLLCHARACTERISTICS_NO_SEH(0x0400):指示DLL没有安全异常处理器(SEH),这意味着该DLL可能不支持异常处理。
- IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE(0x8000):指示DLL是Terminal Server Aware,即它能够识别并适应终端服务器环境。
- IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA(0x0020):指示DLL需要使用高熵虚拟地址(High Entropy VA),这是一种更高级别的ASLR。
- IMAGE_DLLCHARACTERISTICS_APPCONTAINER(0x1000):指示DLL运行在AppContainer环境中,这是Windows 8及更新版本引入的一种应用沙箱机制。
节表重点成员–数据目录 IMAGE_DATA_DIRECTORY
- 数据目录用来描述PE中各个表的位置及大小信息,重点表:导出表、导入表、重定位表、资源表。
1 | // 数据目录 _IMAGE_DATA_DIRECTORY结构体 |
参数1VirtualAddress指定了数据块的相对虚拟地址(RVA),因为当exe在处理导入表的时候,已经映射进进程内存了,取值RVA更方便。
Size则指定了该数据块的大小,有时并不是该类型数据的总大小,可能只是该类型数据一个数据项的大小。这两个成员(主要是VirtualAddress)成为了定位各种表的关键,所以一定要知道每个数组元素所指向的数据块类型,以下表格就是它的对应关系:
图: IMAGE_DATA_DIRECTORY数组元素项图析
IMP(导入表):导入表用来描述模块调用的API列表,位于PE数据目录中的第二项即DataDirectory[1],其中记录了导入表的地址和大小,VirtualAddress指向IMAGE_IMPORT_DESCRIPTOR 结构体数组,这个结构体数组中的每个元素对应着一个dll文件,以全0作为最后一个元素结尾。程序产生调用会生成CALL指令,两大问题及解决思路如下:
- 1.地址存放问题:出于运行环境等因素考虑,导入函数的地址不能为固定地址。所以在exe中保存导入函数的相关信息,系统和链接器对其进行约定:链接器在生成exe的时候,为所有调用API的地方填写一个间接地址,当程序运行起来后,相应地址则会被写入真正API的地址,此区域即为IAT表(导入地址表);
- 2.exe如何存储导入dll及其函数信息:dll与函数是一对多的关系,原则上应该设计为多方填写1方信息的数据关系,但考虑到数据较多的情况,遍历不便。反过来设计为1方存储多方信息的数据结构,虽然会造成插入删除的不方便,但是考虑到exe加载dll的实际场景,无插入删除需求,所以应该设计为后者结构更贴合遍历查询需求。
1 | // IMAGE_IMPORT_DESCRIPTOR 导入表结构,以全0(20个0)结尾 |
- IAT(导入地址表):IMP中的FirstThunk指向IAT表。
- INT(导入名称表):IMP中的OriginalFirstThunk指向INT表,也是DataDirectory[12]项。
- PE加载前,IAT和INT都指向_IMAGE_IMPORT_BY_NAME结构体
1 | / IMAGE_THUNK_DATA结构体汇总只有一个联合体, |
- PE加载后,IAT有变:加载后的IAT每一项存储的是所对应的导入函数地址。
3.节表
(1)节表总概
- 节表:描述PE文件与内存之间的映射关系,由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节(每个节占用0x28B),说明PE文件的指定内容拷贝至内存的哪个位置、拷贝大小及内存属性的设置。结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一。节表总是被存放在紧接在PE文件头的地方。
- 节表大小 = FileHeader.NumberOfSections(节数量)* IMAGE_SECTION_HEADER 结构体。
IMAGE_SECTION_HEADER 结构体
1 | // IMAGE_SECTION_HEADER 节表结构体,大小40B |
一句话解释就是:
在文件偏移为 DWORD PointerToRawData 处映射 DWORD SizeOfRawData 大小的内容到内存,映射到的内存地址是 DWORD VirtualAddress ,所占的内存大小为 DWORD VirtualSize
下面是Characteristics的属性介绍
节表置于选项头之后,节表首地址 计算方法:
- (1)选项头的地址 + 选项头的大小;
- (2) e_lfanew+4+0x14(文件头大小)+0xE0(32位选项头大小)。
Dump:
我们知道,一个可执行程序从硬盘加载到内存到运行,需要一个拉伸的过程,那么我们可以反过来,能否可以在运行起来的内存里面,还原出一个exe?
这样的一个操作就叫做dump
步骤就是:
PE头拷贝下来,然后节按照节表的定义的区域,拷下来
值得注意的是:
我们最好使用OD,X96dbg先将程序运行到OEP,然后再Dump,这是为什么呢?
原因就是:程序一旦跑起来,程序如果有全局变量,那么就有可能全局变量被赋值,一旦被赋值,如果程序启动时有检测全局变量是否为初始值,不是就退出程序的话,那么Dump下来的程序就是不可用的
以下记录一次Dump之旅,主角:扫雷
首先用x96dbg打开扫雷,运行到oep,然后分析
选中PE头,然后按下 CTRL C 复制,然后打开一个新的文件,再按下CTRL B粘贴
然后拷贝节表
接下来是节:
选中的那一段翻译过来就是,在内存中偏移为0x1000处的地址拷贝0x3A56个字节(这是实际大小,没有对齐),复制到文件偏移为0x400的地址,粘贴0x3c00个字节
但是由于是dump,我们便要按照对齐后的大小,即复制粘贴0x3c00大小的数据
节表注入
新增节:
需要这几个步骤:
- 节表个数加1
- 节表添加一项
- 添加节数据
- 修改SizeOfImage
举例子:
目标:把一个exe塞进节表
过程:
首先修改节表个数
这里改为4(原来是3)
然后添加节表
- 这里要注意的是,新加的PointrtToRawData也就是文件偏移就等于上一个节表的PointrtToRawData+SizeOfRawData
- SizeOfRawData要与FileAlignment对齐,例如我这个的例子是0x200
- 新加的VirtualAddress是等于上一个节表的VirtualAddress加上VirtualSize与SectionAlignment对齐后的值
之前我这里犯了一个错误,添加完数据后,我想反正0x1000也是0x200的倍数,不如直接让SizeOfRawData和0x1000对齐得了,结果反而出错了,推测是从文件映射到内存根本没那么多数据,系统找不到,因此就无法启动
再继续添加节数据:
直接复制粘贴exe的数据进去即可:
修改SizeOfImage
新的SizeOfImage = VirtualAddress + VirtualSize与SectionAlignment对齐后的值
但是值得一提的是,新增节可能不会奏效,因为可能在节表那一段没有足够多的空间给我塞下一个新的节表,有可能存储着其他重要数据,而他们是不能够覆盖的
用PETools可以添加节:
扩展最后一个节:
- 修改节表
- 添加节数据
- 修改SizeOfImage
1.修改节表
例如我想在原来节的基础上再添加0x3000的数据,首先将两个参数修改一下
就直接先在文件的末尾添加0x3000个字节的0x00,然后再从这个表里复制全部内容到原来的文件末尾处即可
完成添加
导入表:
导出表是给别人用的函数的清单,相当于饭店提供的菜单
导入表相当于客人点菜选择的菜品单。上面记录了exe要使用的dll和dll中的函数
数据目录第一项伟导出表,第二项表为导入表
1 | typedef struct _IMAGE_IMPORT_DESCRIPTOR { |
定位导入表:
和之前的好多表一样,VirtualAddress 指向多个一样的导入表结构。
最后 sizeOf ( IMAGE_IMPORT_DESCRIPTOR) 个 0 代表导入表结束
导入表的union只用第二个 OriginalFirstThunk,时间戳 TimeDateStamp 记录了当前程序编译生成的时间
Name 是指向 dll 名的 RVA,只记录了一个 dll 名,所以导入表会有若干张,顺序无缝存放,一张导入表就是一个要使用的 dll,以全0作为结尾
OriginalFirstThunk(导入表结构第一个成员)指向一张叫 INT 的表(import name table,导入名称表),存的是若干个 IMAGE_THUNK_DATA 结构,最后以0结尾作结束标志。
FirstThunk(导入表结构最后一个成员)也指向一张表,IAT表(import address table,导入地址表),找到这张表有两种方式,一种就是通过导入表这里找到,第二种就是通过数据目录表,倒数第三个项就是指向的 IAT 表,IMAGE_DIRECTORY_ENTRY_IAT,也是以0结束。在文件加载前,这两个表存的内容完全一致,都是存储的IMAGE_THUNK_DATA 结构(IMAGE_THUNK_DATA32)(详见下下图)。注意虽然一致,但 INT 表和 IAT 表是两块不同的空间,分别记录的都是程序中使用到的 dll 函数,不使用的 dll 函数不会记录。
PE文件加载前:
这里要注意的是,NTDLL和Kernel32会提前将函数地址放入IAT
上文提到的 IMAGE_THUNK_DATA 结构其实只是一个四字节的 union,如下图。
在加载前,IMAGE_THUNK_DATA 只存一个 RVA,这个 RVA 指向上图的IMAGE_IMPORT_BY_NAME 结构(具体结构格式在下图也有)
在这个结构中,
BYTE Name[1];
使用的技巧是所谓的 “结构体变长数组”,这种技巧通常用于存储可变长度的数据。尽管声明为BYTE Name[1];
,实际上这个字段可以容纳一个以 null 终止的字符串,而字符串的长度可以是任意的。这是因为在PE文件的导入表中,函数名称并不是固定长度的,每个函数名称的长度都不同。因此,为了有效地存储这些不定长度的字符串,使用了变长数组的技巧。结构体只定义了一个字节的数组,但在实际使用时,根据函数名称的长度动态分配所需的内存,然后将函数名称存储在这个内存块中,以 null 终止字符串。
这种方法允许节省内存,因为不需要为每个结构分配固定大小的缓冲区以容纳字符串,而可以根据实际需要进行动态分配。在C/C++中,这种技巧在很多情况下用于处理可变长度的数据,例如字符串数组,以提高内存使用效率。在实际使用时,程序员通常会动态分配足够的内存以存储实际的字符串,然后将字符串内容复制到该内存中,以确保字符串正确存储和 null 终止。
在加载后,IAT 表变为存储函数的地址了,所以才有文章最前面提到的,在调用 MassageBox 文件加载前call间接寻址找的是一个字符串,而文件加载后这个间接寻址变为了函数的真正地址。而 INT 表不变。
IAT 表在文件加载完成后系统会调用 GetProcAddress() 函数,就是做的我们前面写过的根据导出表函数序号或函数名字找到函数地址的功能。系统会循环 INT 表,根据表内的名字或序号调用 GetProcAddr() 得到地址,依次添加到 IAT 表中。
注意 IMAGE_IMPORT_BY_NAME 中的 Hint 不是导出序号,而是当前这个函数在导出表函数地址表中的索引。但基本没用,所以不一定是准确的,可以全部为0。而Name并不是一字节,而是以’\0’结尾的不定长字符串。
1 | typedef struct _IMAGE_THUNK_DATA32 { |
哎,被一个基础的指针指向搞迷糊了,一个指针类型指向一个结构,取了星才是这个结构本身。
注意,IMAGE_IMPORT_BY_NAME 中的 Name 并不只有 1 个字节,而是一直遍历到元素为 ‘\0’ 为止,OriginalFirstThunk 和 FirstThunk 的遍历 具体详见下图(这俩的遍历都一样的,其实都是遍历的 IMAGE_THUNK_DATA)
下面就是定位导入表的过程:
第一层循环遍历所有导入表,以0结尾
第二层循环先处理OriginalFirstThunk,其中的表项IMAGE_THUNK_DATA32就当做一个DWORD 四字节数据处理即可,为0则表结束
表项IMAGE_THUNK_DATA32 有可能是个序号,也有可能是个偏移,根据图中说明来处理。
从测试的结果来看,INT表就算是没有,貌似也不会咋样,但是IAT表是一定要有的
因为就算INT表没有东西,也可以从IAT表拿Dll的名字
1 | while(取出导入表一项,且不为全0) |
绑定导入表:
(比较新的Windows版本基本废弃使用绑定导入表了 )
1.关于绑定导入
一般情况下,在程序加载前IAT表和INT表中的内容相同,都是程序引用的dll中的函数的函数名或序号;
加载完成后IAT表中将替换为函数的真正地址;
如果dll文件被更新了,那绑定导入表的时间戳和dll文件的时间戳就不一样了,内容肯定变了,就需要修正序号函数名称函数地址表,绑定导入表就这作用
但在加载前IAT表中直接写绝对地址是可以实现的;
加载前在IAT表中保存绝对地址的优点:启动程序快;
在启动程序时需要:申请4gb内存空间、贴exe、贴dll、将IAT表修复为地址等等;
如果直接用绝对地址,则省去了修复IAT表的操作;
缺点:
dll重定位时,如果dll没能占据自身ImageBase处的地址,则需要修复绝对地址;
dll被修改时,dll被修改,IAT表中对应的函数地址可能被改,需要修复函数地址;
例如windows提供的一些程序就使用了这种方式,比如记事本;
这种方式称为“绑定导入“;
2.如何判断绑定导入
在导入表中结构中有个属性:TimeDateStamp;
该属性表示时间戳;
如果值为0则表示当前的dll的函数没有被绑定,在程序加载时会调用系统函数获取函数地址;
如果值为-1则表示当前的dll的函数已经绑定,而且绑定的时间存在另外一张表里;那张表就是绑定导入表;
3.绑定导入表
PE加载EXE相关的DLL时,首先会根据IMAGE_IMPORT_DESCRIPTOR结构中的TimeDateStamp来判断是否要重新计算IAT表中的地址。
TimeDateStamp == 0 未绑定
TimeDateStamp == -1 已绑定 真正的绑定时间为IMAGE_BOUND_IMPORT_DESCRIPTOR的TimeDateStamp
绑定导入表位于数据目录的第12项;
绑定导入表的结构:
1 | typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR { |
TimeDateStamp ->时间戳;用来判断是否和绑定的dll是同一个版本;也就是看时间戳和dll的pe头中的时间戳是否一样;
OffsetModuleName ->dll的名字;注意保存的既不是RVA也不是FOA;
dll的名字计算公式为:第一个DESCRIPTOR的值+OffsetModuleName;
NumberOfModuleForwarderRefs ->当前dll另外依赖的dll数量;因为dll也可能依赖dll;
绑定导入表结构后面紧跟的并不一定是下一个绑定导入表;
如果NumberOfModuleForwarderRefs为N则还有N个另外的结构;
该结构也是用来描述dll的;
结构如下:
1 | typedef struct _IMAGE_BOUND_FORWARDER_REF { |
前两个属性和绑定导入表意义一样;
第三个属性Reserved为保留字段没鸟用
绑定导入表的结构图:
注意:
当IMAGE_BOUND_IMPORT_DESCRIPTOR结构中的TimeDateStamp与DLL文件标准PE头中的TimeDateStamp值不相符时,
或者DLL需要重新定位的时候,就会重新计算IAT中的值.
导入表注入
凡是记录在导入表的dll,都会被系统加载到进程
步骤很简单:
1.找到一片宽阔的区域(没有存数据的区域)
2.把位于可选头存导入表的地址(注意时ROV)改到找到的没有存数据的区域
3.将原来的导入表复制到新的区域,
4.加上自己的导入表数据
按照下面这个来
有个问题就是如果我用扫雷,发现注入不成功,原因未知
(已解决,属性要改下,最好每个节都给最高权限就不会有这些问题)
LoadPE
已经写好了,放在E盘
[C_LoadPe.rar](E:\viusal studio document\科锐\PE\LoadPe\C_LoadPe.rar)
(前提:没有TLS表)
主要就是解决 导入表修复问题
因为LoadLibrary只是简单的把文件映射到内存,IAT表此时还是和INT表是一个东西,并没有填上真正的地址
1 |
|
导出表:
按照序号导出的时候:
导出表存的不是下面这个形式:
索引 | 地址 |
---|---|
1 | 0x11223344 |
2 | 0x44556677 |
3 | 0x11223366 |
导入表是没有索引的,直接存地址:
0x11223344 |
---|
0x11223344 |
0x55661122 |
那没有索引咋定位呢?回存一个base
例如第一个编码是0x1000,第二个是0x1001,第三个是0x1002
那么base就是0x1000,然后按照顺序存,例如0x1000就存在第0个,0x1002就存在第二个
当然如果编码分别是0x1000,0x2000,0x3000
那么base仍然是0x1000,序号为0x2000的与0x1000的中间会隔着0x1000的空数据
这就是空间换时间
如果是名称导出,系统自动给名称编个序号,本质上也是根据序号去找
数据目录结构
1 | typedef struct IMAGE_DIRECTIRY_ENTRY_EXPORT { |
IMAGE_EXPORT_DIRECTORY
1 | typedef struct _IMAGE_EXPORT_DIRECTORY { |
AddressOfFunctions 存的是导出函数的地址
AddressOfNames 指的是按照名字导出的函数的个数
AddressOfNameOrdinals 指的是按照名字导出的序号表,序号个数和NumberOfNames是一致的
那么不按序号导出的情况时,序号个数不是和NumberOfNames相等吗?难道不按序号导出就不配拥有序号?
当然不是,序号还是会有的,去AddressOfFunctions去找,对应的索引加上Base就是对应的序号值
a. 导出函数地址表:
- 该表中元素宽度为4个字节
- 该表中存储所有导出函数的地址
- 该表中个数由NumberOfFunctions决定
- 该表项中的值是RVA,需要加上ImageBase才是函数真正的地址。
b. 导出函数名称表:
- 该表中的元素宽度为4个字节。
- 该表中存储所有以名字导出函数的名字的RVA(以 /0结尾的字符串)
- 导出名称表是排序过的,有利于折半查找。(ASCLL码排序)
c. 导出函数序号表:
- 该表中的元素是2个字节
- 该表中存储的是对应函数地址表的下标。
- 该表中个数由导出序号的最大值 - 最小值 决定,没有的序号内容以 NULL 填充(所以会造成如果序号值相差过大,会影响Dll的体积)
(三)为什么分成3张表?
- 函数导出的个数与函数名个数未必一样,所以需要将导出函数地址和导出函数名称表分开。
- 函数地址表并不是一定大于函数名称表的。因为一个相同的函数可能存在多个不同的名字。
模拟GetProcAddress
重要性:加壳的代码很容易被人dump,因此为了防御都会想办法抹除导入表,内存和文件中都没有。
但是程序运行起来的时候,总要加载所需DLL,攻击者会在GetProcAddress下断点,将每次的数据都保存下来,从而恢复导入表。
但是,如果这个函数以及内部完全由自己实现的话,攻击者就不得不看代码进行还原。
反Dump:目前很多防止Dump都使用了API模拟,不调用API,将IAT表填上自己模拟的API地址。
原理:根据模块句柄和函数的名称或则序号来获取该API所在的地址。
(一)步骤:
- 定位到导出名称表
- 获取导出名称表个数
- 遍历比较字符串
- 获取对应的序号
- 获取序号对应的地址,(从地址表中获取)
(二)函数转发处理
判断转发函数:判断获取到的函数地址表地址是否在 导入名称表 + 导出表Size (导出表)范围内
处理流程:
- 拆解字符串。LoadLibaray(DLL名称)
- 调用自己,获取名字的地址。
导出表应用
(一)IAT表反向查询
VA(内存中的call地址) => RVA(转换成基于当前模块的偏移地址) => 遍历模块() =>遍历导出表 => 遍历导出地址表,判断RVA是否命中 =>遍历序号表 =>从名称表获取函数名称
不用考虑函数转发问题,因为函数转发的地址会命中另一个模块
静态反汇编只能看导入表,动态调试才能查看导出表
(二)IDA显示函数名称
通过导入表反向查询函数名称,如果将PE文件中导入函数名称表中的地址按照计划调换位置,然后动态运行时再通过内存访问换回来,那么IDA作为静态调试只能通过PE中记录的函数名称去导入表信息遍历,这样只能获取错误的API。
IAT被填入了错误的API地址,而IDA进行IAT反向查询,实际上显示的是正确的,但是运行时程序会调用修改后的API。
可以选用参数个数相同,但是功能不同的API,攻击者使用IDA反汇编看到的汇编代码调用了一个API,但实际上程序的调用了是另一个API,起到了混淆的作用。
这样实际上就是对IDA做反汇编对抗,IDA没有办法检查API的正确性。
重定位表
在数据目录表的第五个(从0开始算)
简介:
定义:记录需要绝对地址修正的表,大多数绝对地址如果imagebase变化的话就无法使用,需要修正程序所调用的那些绝对地址。
修正方法:需要重定位的地址 + 偏移(当前基址 - PE的基址)
开了随机基址的程序才需要重定位,而DLL通常都有重定位表,因为不一定能够加载到DLL指定的ImageBase上。
为什么需要重定位呢?不是已经有导出表了,里面就有函数的偏移呀
因为导出表里面记录的偏移,也就是AddressOfFunctions,不包含dll里面不导出的函数地址,仅仅只有导出函数的地址,如果没有重定位表,那么这些地址将得不到修正,在dll也就无法调用这些不导出的函数,会影响Dll的正常功能
修的应该是那些绝对地址的,重定位表会指出需要修改的机器码的地址,然后修改即可,会改变机器码
OS如何判定是否重定位?
- 先查看随机地址标志,标志开启,地址重定位
- 再查看数据目录项 5 是否位NULL,不为NULL,基址重定位。
重定位表结构
重定位表描述待修复的值所在的地方,这个值是一个RVA。数据目录处的Size字段有用,是重定位表的总大小。
重定位表位于数据目录第3项。
1 | typedef struct _IMAGE_BASE_RELOCATION { |
VirtualAddress
- 这个虚拟地址是一组重定位数据的开始RVA地址,只有重定位项的有效数据加上这个值才是重定位数据真正的RVA地址
SizeOfBlock
- 它是当前重定位块的总大小,因为VirtualAddress和SizeOfBlock都是4字节的,所以(SizeOfBlock - 8)才是该块所有重定位项的大小,(SizeOfBlock - 8) / 2就是该块所有重定位项的数目。
TypeOffset[1]
- 重定位项在该结构中没有体现出来,他的位置是紧挨着这个结构的,可以把他当作一个数组,宽度为2字节。表示该地址处有一个地址需要进行重定位
- 每一个重定位项分为两个部分:高4位和低12位
- 高4位表示了重定位数据的类型(0x00没有任何作用仅仅用作数据填充,为了4字节对齐,说明这个地址不需要修正。0x03表示这个数据是重定位数据,需要修正。0x0A出现在64位程序中,也是需要修正的地址),
- 低12位就是重定位数据相对于VirtualAddress的偏移,也就是上面所说的有效数据。之所以是12位,是因为12位的大小足够表示该块中的所有地址(每一个数据块表示一个页中的所有重定位数据,一个页的大小位0x1000)。
修正方法:被重定位处原来的地址 + 偏移(当前基址 - PE的基址):(VA - ImageBase) + NewImageBase
重定位表只是记录了修哪里,以及怎么修,并不会记录修多少,因为既然是随机基址,那么基址的值就不固定,必须每次软件起来才能确定。
注意:.reloc: 的节一般用于存储重定位表,但是不作为定位重定位表的依据,应使用数据目录定位。
这个讲得不清不楚的,但是还是要说一下,并不是因为IAT表里面的地址需要被修正,而是调用某函数的时候,会先要FF 15 Call到IAT表,然后再Call IAT表里面真正的函数地址
所以 FF 15 Call的 IAT表的地址需要被修正
学完壳,再回来看看,感觉好奇怪
TLS表
TLS: Thread local storage 线程局部存储
显式TLS:
用Windows提供的API
TlsAlloc
:- 函数原型:
DWORD TlsAlloc(void);
- 功能:分配一个新的 TLS 索引,该索引可用于在当前进程的所有线程中访问线程局部存储。
- 返回值:如果成功,则返回一个 TLS 索引;如果失败,则返回
TLS_OUT_OF_INDEXES
(-1)。 - 示例用法:通常在程序初始化时调用
TlsAlloc
来分配一个 TLS 索引,然后将该索引保存在全局变量中以备后续使用。
- 函数原型:
TlsSetValue
:- 函数原型:
BOOL TlsSetValue(DWORD dwTlsIndex, LPVOID lpTlsValue);
- 功能:将数据存储到当前线程的指定 TLS 索引处。
- 参数:
dwTlsIndex
:要设置值的 TLS 索引。lpTlsValue
:要存储的数据指针。
- 返回值:如果成功,则返回非零值;如果失败,则返回零。
- 示例用法:在当前线程中调用
TlsSetValue
来存储线程私有数据。
- 函数原型:
TlsGetValue
:- 函数原型:
LPVOID TlsGetValue(DWORD dwTlsIndex);
- 功能:从当前线程的指定 TLS 索引处获取数据。
- 参数:
dwTlsIndex
:要获取值的 TLS 索引。
- 返回值:如果成功,则返回存储在 TLS 索引处的数据指针;如果失败,则返回
NULL
。 - 示例用法:在当前线程中调用
TlsGetValue
来获取之前存储的线程私有数据。
- 函数原型:
TlsFree
:- 函数原型:
BOOL TlsFree(DWORD dwTlsIndex);
- 功能:释放指定的 TLS 索引。
- 参数:
dwTlsIndex
:要释放的 TLS 索引。
- 返回值:如果成功,则返回非零值;如果失败,则返回零。
- 示例用法:在程序退出或不再需要某个 TLS 索引时调用
TlsFree
来释放该索引。
- 函数原型:
代码如下:
1 |
|
从运行的结果可以知道,主线程调用的TlsSetValue的值不能被子线程读到,同理,子线程设的值也不会被主线程读到
我们来看看,TlsSetValue是怎么干活的
[ebp+0x8]是第二个参数,[ebp+0xC]是第一个参数
发现把eax,也就是第二个参数,丢进了dword ptr[edi+esi*4+0xE10],edi是fs:[0x18] (即TEB)
用Windbg查出来,+0xE10存的是TlsSlots
发现是一个DWORD的,有64个元素的数组
TLS是用来存啥的?如果是TlsSlots是DWORD数组,那可以存字符串吗
在 Windows 中,TLS 可以用来存储各种类型的数据,包括整数、指针、结构体等。TLS 通常被用于存储线程特定的数据或者线程需要独立访问的数据,比如线程相关的配置信息、状态信息、线程私有的全局变量等。
(超过四字节就存地址呗 )
TlsAlloc
函数的作用是在 TLS (Thread Local Storage) 索引数组中找到一个空闲的位置,并返回该位置的索引值。
隐式TLS:
变量:
1 |
|
发现主线程是可以读取的,但是不能改变
为啥会这样呢,我们用汇编看看怎么个事
对应着这个
g_dwVal = 0x111111;
大概长这样,随便画的
0x104是一个定死的值,具体为啥,没说
TLS表:
1 | typedef struct _IMAGE_TLS_DIRECTORY32 { |
PE头里面的 TLS 表,使用隐式TLS的时候才存在该表,数据目录第 9(下标) 项
StartAddressOfRawData 和 EndAddressOfRawData 决定一个范围,创建线程的时候拷贝该范围数据到TEB结构体中。
1 | __declspec(thread) DWORD g_dwVal = 0x12345678; |
加了一句这个,编译器就会在PE中留一块内存,专门存放这玩意
当进程启动后,就会拷贝到TEB,然后自己去TEB里面拿,TEB是每个线程独有的
这样就能保证每个线程都只能操控自己的变量
总结就是: tls就是把pe的tls数据复制到线程结构体里,没了
TLS_Callback TLS回调函数
TLS_Callback
TLS_Callback(Thread-local storage,线程本地存储)是WindowsAPI下的一个回调函数,它会在程序进程开始,结束以及线程开始,结束的时候调用。
TLS在逆向中
TLS_Callback在逆向中有一个特点: 先于main函数执行/在main结束后执行。因此也经常被用作反调试,或者是藏匿变量修改,以防止动态和静态分析。
1 |
|
如何跟踪 Tls回调函数??
我们跟踪到Tls表
根据前面提到的:
1 | typedef struct _IMAGE_TLS_DIRECTORY32 { |
对应的AddressOfCallBacks为0x417720,这是一个(VA)地址,我们直接用IDA去跟
果然发现了回调函数数组
资源表:
解析资源表
这是一个嵌套的结构
1 | //资源目录头 |
我们来试着解析一波资源表:
就以这个程序为例子:
从9A00开始,就是资源表
前面0xC个字节对我们来说不重要
NumberOfNamedEntries;在这里是0,NumberOfIdEntries在这里是7
接下来是柔性数组,是_IMAGE_RESOURCE_DIRECTORY_ENTRY结构
1 | IMAGE_RESOURCE_DIRECTORY_ENTRY |
这个结构体总共是两个DWORD的大小,第一个DWORD最高位不为1,则该DWORD就是代表ID值,否则就是代表字符串的偏移
我们可以看到,图片里共有七个这样的结构,他们的ID分别是3 4 5 6 9 E 0x18,这个其实是资源的种类,可以用VS查看下
随便拿一个ID为3的,即RT_ICON
_IMAGE_RESOURCE_DIRECTORY_ENTRY结构中解析出ID号为3,最高位为1,是一个文件夹,要按照 _IMAGE_RESOURCE_DIRECTORY进行解析
解析到RT_MENU的结果(unicode编码)