所在位置
重定位表处于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() { 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。
我将在之后的文章中给出这么做的代码实现。