基址重定位概念
当向程序的虚拟内存加载PE文件时,文件会被加载到ImageBase所指向的地址。
对EXE文件来说,EXE文件会首先加载到内存,每个文件总是使用独立的虚拟地址空间,这就意味着EXE文件不用考虑基址重定位问题;
对于DLL文件来说,多个DLL文件使用调用其本身的EXE文件的地址空间,不能保证ImageBase所指向的地址没有被其他DLL文件占用,所以DLL文件当中必须包含重定位信息,也就是说,本来A.DLL被加载到M.EXE进程的00100000地址处,但是此处加载了B.DLL文件,PE装载器将A.DLL文件加载到其他还未被占用的地址处(00850000)处。
对于系统的DLL来说实际上不会发生重定位,因为同一系统的kernel32.dll、user32.dll等会被加载到自身固有的ImageBase。
PE重定位的操作原理
Windows的PE装载器进行PE重定位处理的操作原理流程如下:
在应用程序当中查找硬编码位置
读取之后减去ImageBase,也就是用VA-基址得到RVA
加上实际加载地址得到真正的VA
其中最关键的就是找到硬编码的位置,而要找到硬编码的位置,首先要找到基址重定位表,该表位于.reloc区段,找到基址重定位表的的正确打开方式是通过数据目录表的IMAGE_DIRECTORY_ENTRY_BASERELOC条目查找
基址重定位表
基址重定位表的地址位于PE头的数据目录表(DataDirectory)的第六个元素,位于安全表的后面,
我们看看重定位表的结构体定义:
typedef struct _IMAGE_BASE_RELOCATION
{
DWORD VirtualAddress; //基准地址(Base Address)
DWORD SizeOfBlock; //重定位块大小(一个IMAGE_BASE_RELOCATION结构的大小)
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只负责4KB大小分页内的重定位信息。因此结构中的VirtualAddress总是0x1000的倍数。IMAGE_BASE_RELOCATION结构体的第一个成员就VirtualAddress是RVA的值。第二个成员SizeOfBlock就是表示重定位块的大小。
需要注意的一点是,最后一项TypeOffset数组其实不是结构体成员,而是以注释形式存在,表示这个结构体下面会出现WORD类型的数组,该数组元素的值就是硬编码在程序当中的偏移。
只是这么说,还是有点难以理解,我们用win7的notepad程序来说明一下(可能有人要问,为什么这里使用EXE文件来演示,之前不是说了EXE文件不用考虑基址重定位问题,而且为什么这次我们选择win7的notepad而非之前的winxp的notepad?这里需要说明的是,因为Windows Vista之后的版本引入了ASLR机制,使程序运行起来更安全,每次运行EXE文件都会被加载到随机地址,也就是地址随机化。win7有这个机制而winxp没有,也就是说winxp下的EXE文件不用考虑基址重定位问题,而win7是需要考虑的):
用到的工具有:OD,PEview
基址重定位表IMAGE_BASE_RELOCATION,位于PE头的DataDirectory数组的第六个元素,由下图可知,其地址为0002F000
我们用PEview去RVA2F0000看看
如上图我们看到,VirtualAddress(RVA)的值为1000,SizeOfBlock的值为150,即TypeOffset数组的基准地址为RVA 1000。其中,TypeOffset值大小为2字节,由4位的Type和12位的Offset组成,比如TypeOffset数组的第一个元素值为3420,其中高4位的值“3”(IMAGE_REL_BASED_HIGHLOW)为Type,一般为PE文件,而64位的PE文件常见值为“A”,低12位才是真正的位移、其值为420,下面公式计算程序中硬编码地址的偏移:
VirtualAddress + Offset = 1000 + 420 = 1420(RVA)
OD将程序加载到了00400000处,接下来我们在OD里面看看00401420处是否有要进行重定位的硬编码地址:
如上图所示,0x4010C4需要执行PE重定位操作,接下来我们来验证看看。
刚才我们计算了第一条硬编码地址的偏移1420,我们在PEview中看看:
偏移1420处保存的地址为010010C4,这是本来应该加载的地址,但是由于重定位,
实际加载地址为0x400000+10C4 = 0x4010C4。
我们看看上面OD第一条重定位的地址,0x4010C4,没问题。