第16章 基址重定位表

本文最后更新于:2022年5月19日 晚上

PE文件在重定位过程中会用到基址重定位表(Base Relocation Table),本章将学习其结构及操作原理。

16.1 PE重定位

​ 向进程的虚拟内存加载PE文件(EXE/DLL/SYS)时,文件会被加载到PE头的ImageBase所指的地址处。若加载的是DLL (SYS)文件,且在ImageBase位置处已经加载了其他DLL(SYS)文件,那么PE装载器就会将其加载到其他未被占用的空间。这就涉及PE文件重定位的问题,PE重定位是指PE文件无法加载到ImageBase所指位置,而是被加载到其他地址时发生的一系列的处理行为。

使用SDK(Software Development Kit,软件开发工具包)或Visual C++创建PE文件时,EXE 默认的 ImageBase 为00400000, DLL 默认的 ImageBase 为 10000000。此外,
使用DDK(Driver Development Kit,驱动开发工具包)创建的SYS文件默认的ImageBase为 10000。

16.1.1 DLL/SYS

​ 请看图16-1,A.DLL被加载到TEST.EXE进程的10000000地址处。此后,B.DLL试图加载到相同地址(10000000)时,PE装载器将B.DLL加载到另一个尚未被占用的地址(3C000000)处。

image-20211203155325539

16.1.2 EXE

​ 创建好进程后,EXE文件会首先加载到内存,所以在EXE中无须考虑重定位的问题。但是Windows Vista之后的版本引人了ASLR安全机制,每次运行EXE文件都会被加载到随机地址,这样大大增强了系统安全性。

​ 下图是分别运行3次notepad.exe时的截图,可以明显发现,每次运行时程序都被加载到不同地址

image-20211203185112174

ASLR机制也适用于DLL/SYS文件。对于各OS的主要系统DLL,微软会根据不同版本分别赋予不同的ImageBase地址。同一系统的kernel32.dll、user32.dll等会被加栽到自身固有的ImageBase,所以,系统的DLL实际不会发生重定位问题。

Windows Vista/7的系统DLL虽然也拥有自身固有的ImageBase,但是ASLR机制使每次启动时加载的地址都不尽相同。关于ASLR的详细内容请参考后面的章节。

16.2 PE重定位时执行的操作

下面以Windows7的notepad.exe程序为例,看看PE重定位时都发生了什么。如下图所示,
notepad.exe的ImageBase为01000000。

image-20211203185749783

接下来,使用x32dbg运行notepad.exe程序

下图是Windows7中notepad.exe的EP代码部分。在Windows 7的ASLR机制作用下,程序被加载到00DF0000地址处。从图中指令可以看到,方框中进程的内存地址以硬编码形式存在。地址DF3053、DF3B0C是.text节区的IAT区域,地址DFC25C是.data节区的全局变量。每当在x32dbg中重新启动notepad.exe,地址值就随加载地址的不同而改变。像这样,使硬编码在程序中的内存地址随当前加载地址变化而改变的处理过程就是PE重定位。

image-20211203190228302

无法加载到ImageBase地址时,若未进行过PE重定位处理,应用程序就不能正常运行(因发生“内存地址引用错误”,程序异常终止)。

在磁盘文件中,硬编码的地址以ImageBase(01000000)为基准。生成(构建)notepad.exe文件时,由于无法预测程序被实际加载到哪个地址,所以记录硬编码地址时以ImageBase为基准。但在运行的瞬间,经过PE重定位后,这些地址全部以加载地址为基准变换,最后程序得以正常执行而不发生错误。

接下来了解一下PE文件的重定位操作原理。

16.3 PE重定位的基本操作原理

Windows的PE装载器进行PE重定位处理时,基本的操作原理很简单。

PE重定位的基本操作原理

  • 在应用程序中查找硬编码的地址位置。
  • 读取值后,减去ImageBase (VA→RVA)。
  • 加上实际加载地址(RVA→VA),并使用该值替代原来的硬编码地址。

其中最关键的是查找硬编码地址的位置。查找过程中会用到PE文件内部的Relocation Table(重定位表),它是记录硬编码地址偏移(位置)的列表(重定位表是在PE文件构建过程(编译/链接)中提供的)。通过重定位表查找,其实就是指根据PE头的“基址重定位表”项进行的查找。

16.3.1 基址重定位表

基址重定位表地址位于PE头的DataDirectoiy数组的第六个元素(数组索引为5),如下图所示。
IMAGE_NT_HEADERS\IMAGE_OPTION_HEADER\IMAGE_DATA_DIRECTORY[5]

image-20211203191107789

上图中基址重定位表的地址为RVA 2F000,转换成FOA值为2AE00。

16.3.2 IMAGE_BASE_RELOCATION 结构体

上图的基址重定位表中罗列了硬编码地址的偏移(位置)。读取这张表就能获得准确的硬编码地址偏移。基址重定位表是IMAGE_BASE_RELOCATION结构体数组。

IMAGE_BASE_RELOCATION结构体的定义如下:

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
27
//
// Based relocation format.
//

//@[comment("MVI_tracked")]
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

//
// Based relocation types.
//

#define IMAGE_REL_BASED_ABSOLUTE 0
#define IMAGE_REL_BASED_HIGH 1
#define IMAGE_REL_BASED_LOW 2
#define IMAGE_REL_BASED_HIGHLOW 3
#define IMAGE_REL_BASED_HIGHADJ 4
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_5 5
#define IMAGE_REL_BASED_RESERVED 6
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_7 7
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_8 8
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_9 9
#define IMAGE_REL_BASED_DIR64 10

IMAGE_BASE_RELOCATION结构体的第一个成员为VirtualAddress,它是一个基准地址(BaseAddress),实际是RVA值。第二个成员为SizeOfBlock,指重定位块的大小。最后一项TypeOffset数组不是结构体成员,而是以注释形式存在的,表示在该结构体之下会出现WORD类型的数组,并且该数组元素的值就是硬编码在程序中的地址偏移。

16.3.3 基址重定位表的分析方法

进一步展示重定位表,如下图所示:

image-20211203193715050

由IMAGE_BASE_RELOCATION结构体的定义可知,VirtualAddress成员(基准地址)的值为 1000h,SizeOfBlock成员的值为150h。也就是说,上图中显示的TypeOffset数组的基准地址(起始地址)为RVA 1000,整个重定位块的总大小为150h(这些块按照基准地址分类,以数组形式存在)。块的末端显示为0。

TypeOffset值为2个字节(16位)大小,是由4位的Type与12位的Offset合成的。比如,TypeOffset值为3420,解析如上,高4位用作Type, PE文件中常见的值为3(IMAGE_REL_BASED_HIGHLOW),64位的PE+文件中常见值为A(IMAGE_REL_BASED_DIR64)。

在恶意代码中正常修改文件代码后,有时要修改指向相应区域的重定位表(为了略去PE装载器的重定位过程,常常把Type值修改为0 (IMAGE_REL_BASED_ABSOLUTE))

TypeOffset的低12位是真正的位移,该位移值基于Virtual Address的偏移。所以程序中硬编码地址的偏移使用下面等式换算。

VirtualSize(1000)+Offset(420)=1420(RVA)

下面看一下RVA 1420处是否实际存在要执行PE重定位操作的硬编码地址,如下图所示:

image-20211203195421704

图16-8中notepad.exe被加载到DF0000地址处。故RVA 1420即为VA DF1420,该地址处存储着IAT地址(VA,DF10C4 )。并且该值经过PE重定位而发生了变化。使用相同原理,DF142D、DF1436地址的内容也都是硬编码到程序中的地址值,该偏移可以在表16-2中求得。

TypeOffset项中指向位移的低12位拥有的最大地址值为1000。为了表示更大的地址,要添加1个与其对应的块,由于这些块以数组形式罗列,故称为重定位表。

16.3.4 练习

本小节将通过简单练习进一步加深大家对PE重定位操作原理的理解。练习过程将参照本节开始内容中列出的步骤进行。运行Notepad.exe时,假设它被加载到00DF0000,而不是ImageBase地 址(01000000)中。那此时PE重定位是如何进行的呢?

  1. 查找程序中硬编码地址的位置

程序中使用的硬编码地址的偏移(位置)可以通过基址重定位表查找到(此处使用上面求得的RVA 1420)。该RVA对应的在文件中的偏移量为820。查看该地址,如下所示:

image-20211203210506422

从图中可以看到,RVA 1420地址中存在着程序的硬编码地址值010010C4 (请将该值与上上图中的值(00AF10C4)比较)。

  1. 读取值后,减去ImageBase值(VA→RVA)

010010C4-01000000=000010C4

  1. 加上实际加载地址(RVA→VA)

00010C4+00DF0000=00DF10C4

对于程序内硬编码的地址(010010C4),PE装载器都做如上处理,根据实际加载的内存地址修正后,将得到的值(00DF10C4)覆盖到同一位置。对一个IMAGE_BASE_RELOCATION结构体的所有TypeOffset都重复上述过程,且对与RVA 1000~2000地址区域对应的所有硬编码地址都要进行PE重定位处理(参考图16-7)。若TypeOffset值为0,则表明一个IMAGE_BASE_RELOCATION结构体结束。

对重定位表中出现的所有IMAGE_BASE_RELOCATION结构体都重复上述处理后,就完成了对进程内存区域相应的所有硬编码地址的PE重定位。重定位表以NULL结构体结束(即IMAGE_BASE_RELOCATION结构体成员的值全部为NULL)。

以上就是PE重定位的操作原理与重定位表结构体的相关内容。

自实现代码解析

代码如下:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
VOID printRelocation() {
//Original buffer
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
ReadPEFile(FILEPATH_IN, &pFileBuffer);
pDosHeader = (PIMAGE_DOS_HEADER) pFileBuffer;
pNTHeader = (PIMAGE_NT_HEADERS) ((DWORD) pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER) (((DWORD) pNTHeader) + 4);
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32) ((DWORD) pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pSectionHeader = (PIMAGE_SECTION_HEADER) ((DWORD) pOptionHeader + pPEHeader->SizeOfOptionalHeader);

DWORD relocation_foa = (DWORD) pFileBuffer + RVAtoFOA(pFileBuffer, pOptionHeader->DataDirectory[5].VirtualAddress);
PIMAGE_BASE_RELOCATION pimageBaseRelocation = (PIMAGE_BASE_RELOCATION) relocation_foa;
int num = 0;
PIMAGE_BASE_RELOCATION ptemp1ImageBaseRelocation = pimageBaseRelocation;
PIMAGE_BASE_RELOCATION ptemp2ImageBaseRelocation;
while (TRUE) {
DWORD relocation_table_foa = RVAtoFOA(pFileBuffer, ptemp1ImageBaseRelocation->VirtualAddress);
PIMAGE_BASE_RELOCATION ptemp2ImageBaseRelocation = (PIMAGE_BASE_RELOCATION) ((DWORD) ptemp1ImageBaseRelocation +
ptemp1ImageBaseRelocation->SizeOfBlock);
if (ptemp2ImageBaseRelocation->SizeOfBlock == 0 && ptemp1ImageBaseRelocation->VirtualAddress == 0) { break; }
else {
ptemp1ImageBaseRelocation = ptemp2ImageBaseRelocation;
num++;
}
}
//Reuse ptemp1ImageBaseRelocation and ptemp2ImageBaseRelocation
ptemp1ImageBaseRelocation = pimageBaseRelocation;
for (int i = 0; i < num; i++) {
ptemp2ImageBaseRelocation = (PIMAGE_BASE_RELOCATION) ((DWORD) ptemp1ImageBaseRelocation + ptemp1ImageBaseRelocation->SizeOfBlock);
PWORD pRelocationData = (DWORD) ptemp1ImageBaseRelocation;
WORD total_in_block = (ptemp1ImageBaseRelocation->SizeOfBlock - 8) / 2;
printf("Section %lx h\n", ptemp1ImageBaseRelocation->VirtualAddress);
printf("----------------------------------\n");
for (int j = 0; j < total_in_block; j++) {
WORD data = *(PWORD) (8 + (DWORD) (pRelocationData + j));
// Get three bit
int flag = data >> 12; //Flag is 3(need to be fixed) or 0
//Remove the front 4 bits;
WORD address = data << 4;
address = address >> 4;
printf("Item %d: The RVA is %x, the attribute value is %d\n", j + 1, address, flag);

}
ptemp1ImageBaseRelocation = ptemp2ImageBaseRelocation;
}
}

运行结果:

image-20211203211859176

参考

《逆向工程核心原理》 第16章


第16章 基址重定位表
https://m0ck1ng-b1rd.github.io/1999/02/14/逆向工程核心原理/第16章 基址重定位表/
作者
何语灵
发布于
1999年2月14日
许可协议