继续PE系列笔记的更新
PE其它笔记索引可前往:
PE文件笔记一 PE介绍
前面在PE文件笔记十六 代码重定位中学习了代码重定位,接着学习重定位表
重定位表
重定位表的作用
经过前面对导入表和导出表的学习,知道了一个程序运行时一般需要加载多个PE文件
而每个PE文件在内存中的首地址由扩展PE头中的ImageBase决定;当前面的PE文件占用了后面PE文件在内存中位置时,允许后面PE文件重新选择一个基地址。这时候就需要对所有的重定位信息进行修正,而修正的依据就是PE中的重定位表
操作系统会根据重定位表对代码予以纠正,使得代码位置移动后仍然能够正确地运行;也就是代码重定位的工作由操作系统来完成
什么是重定位表
重定位表就是记录代码位置移动后修正重定位信息所需要的一种结构
PS:也就是说当代码位置没有发生移动时,是用不到重定位表的
定位重定位表
定位重定位表原理
像导入表、导出表、重定位表、资源表等表,这些表的起始地址和大小都存储在扩展PE头里的DataDirectory这个数组的成员中,DataDirectory是一个数组,每个数组成员对应一张表
回顾先前的笔记PE文件笔记五 PE文件头之扩展PE头,能得到重定位表对应的下标为5
宏定义 |
值 |
含义 |
IMAGE_DIRECTORY_ENTRY_BASERELOC |
5 |
基地址重定位表 |
即DataDirectory[5]表示重定位表
关于DataDirectory的具体描述在之前的笔记中已经详细说明过了,这里不再赘述:
IMAGE_DATA_DIRECTORY成员 |
数据宽度 |
说明 |
VirtualAddress |
DWORD(4字节) |
表的起始位置(RVA) |
Size |
DWORD(4字节) |
表的大小 |
定位重定位表流程
- 找到扩展PE头的最后一个成员DataDirectory
- 获取DataDirectory[5]
- 通过DataDirectory[5].VirtualAddress得到导入表的RVA
- 将导出表的RVA转换为FOA,在文件中定位到导入表
按流程定位重定位表
要分析的实例
这次分析的实例还是先前的EverEdit.exe
程序在后面的附件中,有需要可以自行取用
找到DataDirectory
使用WinHex打开EverEdit.exe,先找到PE文件头的起始地址:0xF0
再数24个字节(PE文件头标志大小+标准PE头大小),到达扩展PE头:0xF0+24=240+24=264=0x108
然后在数224-128=96个字节(扩展PE头大小减去DataDirectory大小)DataDirectory大小= _IMAGE_DATA_DIRECTORY大小×16=8*16
DataDirectory首地址 = 扩展PE头地址+96=0x108+96=264+96=360=0x168
获取DataDirectory[5]
而重定位表为DataDirectory[5]
重定位表地址 = DataDirectory首地址 + sizeof(IMAGE_DATA_DIRECTORY)×5=0x168+8×5=360+40=400=0x190
DataDirectory[5].VirtualAddress
IMAGE_DATA_DIRECTORY成员 |
值 |
说明 |
VirtualAddress |
0x00281000 |
表的起始位置(RVA) |
Size |
0x00015B68 |
表的大小 |
得到重定位表的RVA
于是得到导出表对应的RVA为:0x281000
RVA转换FOA
但是IMAGE_DATA_DIRECTORY中的VirtualAddress是RVA,需要将其转换成FOA
关于RVA转FOA的内容在 PE文件笔记七 VA与FOA转换中已经详细说明了,这里不再赘述
此时使用PE工具:DIE来进行RVA和FOA的转换,没有使用自己所写的工具
获得了FOA为0x241c00,也就是重定位表的位置了,定位完成
重定位表的结构
定位到了重定位表后自然要了解重定位表的结构才能解读重定位表的内容
与导入表类似,重定位表指针指向的位置是一个数组,而不像导出表一样只有一个结构。这个数组的每一项都是如下结构:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; //重定位内存页的起始RVA
DWORD SizeOfBlock; //重定位块的长度
} IMAGE_BASE_RELOCATION;
重定位表的划分
可以看到,重定位表被分为一个个重定位块
每个重定位块的大小由SizeOfBlock决定,重定位块大小 = SizeOfBlock
为什么重定位表要被分为一个个重定位块?
答:为了节省空间
举个例子:
假设有一堆数据:
0x0080 0610
0x8006 5000
0x0080 0520
0x0080 5210
……
这些数据都是0x0080XXXX的格式,假设数据共有N个
如果按照正常的存储方式,需要N×4 = 4N个字节的空间才能存储下
如果按照基地址+偏移的方式来存储:
即先存储一个基地址:0x0080 0000 (DWORD 4字节)
之后存储的数据只需要存储偏移即可:
0x0080 0610 → 0x0610 (WORD 2字节)
0x8006 5000 → 0x5000 (WORD 2字节)
0x0080 0520 → 0x0520 (WORD 2字节)
0x0080 5210 → 0x5210 (WORD 2字节)
…… → …… - 0x00800000 (WORD 2字节)
此时所需要的空间为:4(基地址占用的空间) + 2×N = 2N+4个字节的空间
对比先前的所需要的空间4N,不难发现,当数据量足够庞大的时候,节省的空间能够接近一半
通过上面的例子,再来理解_IMAGE_BASE_RELOCATION这个结构体
_IMAGE_BASE_RELOCATION成员 |
数据宽度 |
说明 |
VirtualAddress |
DWORD(4字节) |
数据的基地址的RVA |
SizeOfBlock |
DWORD(4字节) |
重定位块的总大小 |
重定位表块是如何设计的
前面知道了重定位表被划分成一块块重定位块以达到节省空间的目的
那么重定位表块是如何设计的?一共有多少个重定位表块?
重定位表块是根据物理内存来设计的
内存是以4KB为单位的,也就是在物理内存上每一个内存的大小划分是4KB,以4KB为一个物理页(这里涉及后面的内存分页的知识)
一个重定位表块对应一个物理页
重定位表块中的数据项(偏移)
前面知道了,重定位表块中SizeOfBlock后的数据部分是作为偏移来使用的,每一个数据项的大小为(WORD)2字节
实际上数据项只有后12位是用来表示偏移的,高4位留作它用
比如:对于一个数据项为:0011 0110 0001 0000 共16位(2字节)
其偏移的数值为:0110 0001 0000 = 0x610
先说说为什么只需要拿12位作为偏移便足矣?
前面提到了物理页的大小是4KB,4KB = 2的12次方 B
也就是说,在一个物理页内的偏移最大也就是4KB = 2的12次方 B
于是只需要使用12位就表示一个物理页内的所有偏移
了解了为什么只需要12位作为偏移后,再来看看高4位的作用
在实际的PE文件中,我们只能看到0和3这两种情况(对应二进制为:0000 和 0011),也就是说这一项要么是对齐用的,要么是需要全部修正的
对于为0的情况,表明该数据为对齐用的填充的垃圾数据,所以不需要对其进行重定位
重定位表的结尾
重定位表的结尾为:找到一项_IMAGE_BASE_RELOCATION,其成员的值均为0
按结构分析重定位表
选中部分为第一个重定位块,以它为例进行分析
将对应的结构填入结构体成员中得到:
_IMAGE_BASE_RELOCATION成员 |
值 |
说明 |
VirtualAddress |
0x00001000 |
数据的基地址的RVA |
SizeOfBlock |
0x00000084 |
重定位块的总大小=以VirtualAddress为基地址的数据的个数×2+8 |
于是得到了:
第一个重定位块的基地址的RVA为:0x1000,将其转换成FOA得到:0x400(转换方法说了很多次了,这里不再赘述)
接着再根据SizeOfBlock算出以VirtualAddress为基地址的数据的个数
个数 = (SizeOfBlock-8)÷2 = (0x84-8)÷2 =(132-8)÷2=124÷2=62个
接下来看需要修正的第一个数据的偏移为:0x3010
先将其转换为二进制得到:0011 0000 0001 0000
高四位为3,表明该数据有效并且需要对它进行重定位
低四位为:0x10,表明该数据的偏移为0x10
最后用基地址+偏移得到 完整的地址为:0x400 + 0x10 =0x410
用WinHex到0x410的位置查看:
不难看出该地址存储的数据是一个VA(虚拟地址),也就是一个绝对地址,所以需要修正
代码实现分析重定位表
// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664
//VA转FOA 32位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
//printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS);
//输出PeEnd
//printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
//printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize, (UINT)sectionArr[i]->SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment;
if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
//printf("SizeInMemory:%X\n", SizeInMemory);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
UINT offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
UINT foa = sectionArr[i]->PointerToRawData + offset;
//printf("foa:%X\n", foa);
return foa;
}
}
}
void getBaseRelocation(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
_IMAGE_DATA_DIRECTORY relocateDataDirectory = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
UINT relocateAddress = VaToFoa32(relocateDataDirectory.VirtualAddress + nt->OptionalHeader.ImageBase, dos, nt, sectionArr);
_IMAGE_BASE_RELOCATION* relocateDirectory = (_IMAGE_BASE_RELOCATION*)((UINT)dos + relocateAddress);
int cnt = 0;
while (true) {
//判断是否到达结尾
if (relocateDirectory->VirtualAddress != 0 && relocateDirectory->SizeOfBlock != 0) {
int num = (relocateDirectory->SizeOfBlock - 8) / 2;
int i;
for (i = 0; i < num-1; i++) {
WORD* offset = (WORD*)((UINT)relocateDirectory+8+2*i);
//高四位为0011即3
if (*offset >= 0x3000) {
printf("base:%X\toffset:%X\n", relocateDirectory->VirtualAddress, *offset-0x3000);
}
}
relocateDirectory =(_IMAGE_BASE_RELOCATION*) ((UINT)relocateDirectory + relocateDirectory->SizeOfBlock);
cnt++;
}
else {
break;
}
}
printf("%d\n", cnt);
}
int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Users\\lyl610abc\\Desktop\\EverEdit\\EverEdit.exe", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);
//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);
//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
//输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
printf("magic:%X\n", *magic);
//根据magic判断为32位程序还是64位程序
switch (*magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
printf("32位程序\n");
//确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS* nt;
//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);
//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}
getBaseRelocation(dos, nt, sectionArr);
break;
}
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
printf("64位程序\n");
//确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS64* nt;
nt = (_IMAGE_NT_HEADERS64*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);
//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}
break;
}
default:
{
printf("error!\n");
break;
}
}
return 0;
}
运行结果
代码说明
解析重定位表的代码并不多,主要在于掌握了结构之后知道其边界和数据含义,就可以通过指针找到对应位置输出即可
总结
- 重定位表只有在代码的位置发生移动时才有效
- 重定位表是为了解决代码重定位的问题而设计的,而重定位表块则是为了节省空间而设计的
- 重定位表块的划分原理在于内存分页
附件
附上本笔记中分析的EverEdit文件:点我下载