PE结构(6)重定位表

所在位置

重定位表处于OP头的数据目录页数组的第六张表,即 PE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBERSOF_DIRECTORY_ENTRY]; 数组的第五个元素。

1
2
3
4
typedef struct _PE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
}PE_DATA_DIRECTORY, *lpPe_DATA_DIRECTORY;

该目录页的VirtualAddress字段指向了重定位表所在RVA。

结构

重定位表结构如下:

1
2
3
4
5
6
typedef struct _DATA_RELOCATION_DIRECTORY {
DWORD VirtualAddress;
DWORD SizeOfBlock;
//原则上该成员不属于此结构体
WORD TypeOffset[1];
}DATA_RELOCATION_DIRECTORY, * PDATA_RELOCATION_DIRECTORY;

由于重定位表在一个PE文件上不止存在一张,事实上存在多张。这多张重定位表之间的间隔并不固定,所幸有SizeOfBlock字段用于确认某张重定位表占用空间的大小。

假设第一张重定位表处于地址A,则A + SizeOfBlock 即为第二张重定位表所在地址。

若一张重定位表的 VirtualAddress 和 SizeOfBlock 均为0,则代表已没有新的重定位表。

基于此原理,遍历所有重定位表具有可行性。

代码演示遍历重定位表

主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main3() {
//将指定文件加载到内存,并解析PE信息到PEFILE结构体
PEFILE peFile{ 0 };
FILE* pFile = fopen("D:\\TestDLL.dll", "rb");
int fileLength = LoadFileToMemory(pFile, &peFile);
fclose(pFile);

if (peFile.h_pe == nullptr) {
return 0;
}

//获取重定位表
PDATA_RELOCATION_DIRECTORY pRelocationTable = nullptr;
GetRelocationTable(&pRelocationTable, &peFile);
if (pRelocationTable == nullptr) {
return 0;
}

PrintRelocationTable(pRelocationTable, &peFile);

return 0;
}

打印函数:

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
//打印重定位表内容
void PrintRelocationTable(PDATA_RELOCATION_DIRECTORY pRelocationTable, lpPEFILE lpPefile) {
printf("%s\n", "输出重定位表内容.");
DWORD virtualAddress = pRelocationTable->VirtualAddress;
DWORD sizeOfBlock = pRelocationTable->SizeOfBlock;
int i = 1;
while (virtualAddress != 0 && sizeOfBlock != 0) {
printf("当前第 %d 张重定位表内容:\n", i);
printf("大表地址:0x%x \n", virtualAddress);
int elementsCount = (sizeOfBlock - 8) / 2;
printf("小表项数量:%d \n", elementsCount);
for (int i = 0; i < elementsCount; i++) {
WORD element = pRelocationTable->TypeOffset[i];
BYTE high4 = element >> 12;
WORD low12 = element & 0x0FFF;
DWORD rva = virtualAddress + low12;
printf("当前第%d项, 偏移:0x%x, 标记:0x%x, RVA:0x%x, FOA:0x%x \n",
i + 1, low12, high4, rva, Rva2Foa(rva, lpPefile));
}
printf("****************************************\n");
pRelocationTable = (PDATA_RELOCATION_DIRECTORY)((DWORD)pRelocationTable + sizeOfBlock);
virtualAddress = pRelocationTable->VirtualAddress;
sizeOfBlock = pRelocationTable->SizeOfBlock;
i++;
}
}

重定位表作用

代码在编译时,虽然会尽可能使用相对地址(RVA)作为汇编硬编码的操作数,但是在使用某些资源(如全局变量)时,会将PE文件的ImageBase与该变量的RVA相加后直接硬编码在程序中。

若程序每次运行时的ImageBase永远不发生变更,那这样的编码方式并不会出现问题。

可实际上程序运行,尤其是DLL这类模块型文件被装载时,由于ImageBase有冲突,经常要发生临时变更ImageBase的情景。此时程序文件中硬编码的语句,已无法寻址获取到原内存空间上的数据,这类语句执行就会发生问题。

此时我们的重定位表就要闪亮登场了!

记录所有变更

最朴素的一种想法就是,如果在编译时,记录下所有“写死”的地方,当ImageBase发生变更时,就对这些记录的地址进行修正,问题不就解决了嘛?

因为ImageBase无论怎么变更,与原ImageBase相比较时,总能算得一个offset;我们预先记录了硬编码的操作数所在的地址,当ImageBase发生变更时,就对这些地址上的数加上这个offset。这就能确保程序仍然可以访问正确的内存地址。

重定位表所做的,正是这么一回事。

结构解析

1
2
3
4
5
6
typedef struct _DATA_RELOCATION_DIRECTORY {
DWORD VirtualAddress;
DWORD SizeOfBlock;
//原则上该成员不属于此结构体
WORD TypeOffset[1];
}DATA_RELOCATION_DIRECTORY, * PDATA_RELOCATION_DIRECTORY;

一张重定位表中,VirtualAddress 记录了一个页地址(某内存页的起始地址),从SizeOfBlock之后的内存中,每两字节记录一个偏移地址。

聪明如我的读者,应该已经想到如何计算有多少个偏移地址了吧?
_DATA_RELOCATION_DIRECTORY 结构体是按一字节内存对齐的,这意味着所有数据是紧凑排列的。
用 (SizeOfBlock - 8) / 2 的公式,即可求得所有偏移量的数量了,有了数量即可遍历该结构体。

用页地址 + 偏移地址的方式,即可找到某个需要修改的操作数所在的地址,32位系统中将该值按四字节读取后加上需要追加的offset即可完成修复工作。

然而还有一个坑

或许你对内存结构颇有研究,会提出一个问题:“一页内存只有4096字节,理论上偏移量只需要12位bit即可存储,那么两字节存储是不是浪费了4bit的数据宽度呢?”

可惜Microsoft技高一筹,早就想到了这个问题。

因此每个偏移量的16位bit上,高4位被用于标记位,低12位才是偏移量。

只有高4位bit的值等于3时,才说明该地址需要被修复。

这意味着如果你想模拟OS修复重定位表的话,必须分开处理每个TypeOffset的高4bit与低12bit。

我将在之后的文章中给出这么做的代码实现。


PE结构(6)重定位表
http://dubhehub.github.io/blogs/2024050500180037226.html
Author
Sin
Posted on
May 5, 2024
Licensed under