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(DriverDevelopment Kit,驱动开发工具包)创建的SYS文件默认的ImageBase为10000。
如图:A.DLL
被加载到TEST.EXE进程的10000000地址处。此后,B.DLL
试图加载到相同地址(10000000)时,PE装载器将B.DLL加载到另一个尚未被占用的地址(3C000000)处
创建好进程后,EXE文件会首先加载到内存,所以在EXE中无须考虑重定位的问题。但是
WindowsVista之后的版本引入了ASLR安全机制,每次运行EXE文件都会被加载到随机地址,这样大大增强了系统安全性。
ASLR机制也适用于DLL/SYS文件。对于各OS的主要系统DLL,微软会根据不同版本分别赋予不同的ImageBase地址。同一系统的kernel32.dll、user32.dll等会被加载到自身固有的ImageBase,所以,系统的DLL实际不会发生重定位问题。
以 notepad.exe
为例,看看PE重定位时发生了什么,使用010查看可以发现其基址 ImageBase
是 01000000h
然后用OD运行
在ASLR 的机制下,发现程序被加载到了 00b60000
位置处
从图中指令可以看到,方框中进程的内存地址以硬编码形式存在。地址 B610FC
、b61100
是 .text
节区的IAT区域,地址 b6C0A4
是.data
节区的全局变量。每当在OllyDbg中重启notepad.exe(Restart(Ctrl+F2)),地址值就随加载地址的不同而改变。像这样,使硬编码在程序中的内存地址随当前加载地址变化而改变的处理过程就是PE重定位。
无法加载到 ImageBase
地址时,若未进行过PE重定位处理,应用程序就不能正常运行(因发生“内存地址引用错误”,程序异常终止)
原理很简单
基址重定位表地址位于PE头的DataDirectory
数组的第六个元素(数组索引I为5)
可以看到基址重定位表的地址为 RVA 2F000
上图的基址重定位表中罗列了硬编码地址的偏移(位置)。读取这张表就能获得准确的硬编码地址偏移。基址重定位表是 IMAGE_BASE_RELOCATION
结构体数组。
//
// Based relocation format.
//
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_MIPS_JMPADDR 5
#define IMAGE_REL_BASED_MIPS_JMPADDR16 9
#define IMAGE_REL_BASED_IA64_IMM64 9
#define IMAGE_REL_BASED_DIR64 10
IMAGE_BASE_RELOCATION
结构体的第一个成员为 VirtualAddress
,它是一个基准地址(BaseAddress),实际是RVA值。第二个成员为SizeOfBlock,指重定位块的大小。
最后一项TypeOffset数组不是结构体成员,而是以注释形式存在的,表示在该结构体之下会出现WORD类型的数组,并且该数组元素的值就是硬编码在程序中的地址偏移。
010 Editor
显示的是文件中的原始数据,需要将 RVA 转换为文件偏移地址(FOA),所以要查看RVA 21000h
就看FOA 2AE00h
PEview可以直接看RVA
FOA(文件偏移地址) | 数据 | 注释 |
---|---|---|
2AE00 | 00001000 | VirtualAddress |
2AE04 | 00000150 | SizeOfBlock |
2AE08 | 3420 | TypeOffset |
2AE0A | 342D | TypeOffset |
2AE0C | 3436 | TypeOffset |
由 IMAGE_BASE_RELOCATION
结构体的定义可知,VirtualAddress
成员(基准地址的值为 1000h
,SizeOfBlock成员的值为 150h
。也就是说,表中显示的TypeOffest数组的基准地址(起始地址)为RVA 1000,块的总大小为150(这些块按照基准地址分类,以数组形式存在)。块的末端显示为0。TypeOffset值为2个字节(16位)大小,是由4位的Type与12位的Offset合成的。比如,TypeOffset值为3420,解析如表所示。
虽然010里面没有显示出
type
但是元数据里面是有的
类型(4位) | 偏移(12位) |
---|---|
3 | 420 |
高4位用作Type,PE文件中常见的值为3(IMAGE_REL_BASED_HIGHLOW),64位的PE+文件中常见值为A(IMAGE_REL_BASED_DIR64)。
在恶意代码中正常修改文件代码后,有时要修改指向相应区域的重定位表(为了略去PE装载器的重定位过程,常常把Type值修改为0(IMAGEREL_BASED_ABSOLUTE))。
TypeOffset的低12位是真正的位移,该位移值基于VirtualAddress
的偏移。所以程序中硬编码地址的偏移使用下面等式换算。
查看一下 RVA 1420
处是否实际存在要执行PE重定位操作的硬编码地址
上图中 notepad.exe
被加载到 b60000
地址处。故 RVA 1420
即为 VA b61420
,该地址处存储着IAT地址(VA,b610C4
)。并且该值经过PE重定位而发生了变化。使用相同原理,b6142D、b61436
地址的内容也都是硬编码到程序中的地址值,该偏移可以在表中求得。
TypeOffset项中指向位移的低12位拥有的最大地址值为1000。为了表示更大的地址,要添加1个与其对应的块,由于这些块以数组形式罗列,故称为重定位表。
运行 Notepad.exe
时,假设它被加载到 00b60000
,而不是 ImageBase
地址(01000000
)中。那此时PE重定位是如何进行的呢?
程序中使用的硬编码地址的偏移(位置)可以通过基址重定位表查找到(此处使用上面求得的 RVA 1420
。使用PEView查看RVA 1420
地址中的内容,如图所示。
这里存在着程序硬编码地址值 010010c4
与下图中的 00b610c4
进行比较
对于程序内硬编码的地址(010010C4
),PE装载器都做如上处理,根据实际加载的内存地址修正后,将得到的值(00b610C4
)覆盖到同一位置。对一个 IMAGE_BASE_RELOCATION
结构体的所有 TypeOffset
都重复上述过程,且对与 RVA 1000~2000
地址区域对应的所有硬编码地址都要进行PE重定位处理。若 TypeOffset
值为0,则表明一个 IMAGE_BASE_RELOCATION
结构体结束。
对重定位表中出现的所有IMAGEBASE_RELOCATION
结构体都重复上述处理后,就完成了对进程内存区域相应的所有硬编码地址的PE重定位。重定位表以NULL结构体结束(即IMAGE_BASE_RELOCATION
结构体成员的值全部为NULL)。
以上就是PE重定位的操作原理与重定位表结构体的相关内容。