重定位表的引入
程序加载过程
在win32下,每一个PE文件(其可能由多个子PE文件组成)在运行时,操作系统会给分配一个独立的4GB虚拟内存,内存地址从0x00000000到0xFFFFFFFF。其中低2G为用户程序空间,高2G为操作系统内核空间。并且操作系统会将该文件中数据拉伸成内存中数据,从ImageBase开始,分配SizeOfImage大小空间
当一个主PE文件由很多个子PE文件组成,当我们运行主PE文件时,所有的PE文件共享操作系统分配的一个4GB虚拟空间。如下图我们用OD打开ipmsg.exe程序去查看其模块,可以发现,ipmsg.exe由很多其他PE文件组成,这些文件也叫做模块
在上图中,各PE文件的base也就是其在内存中的起始位置,size也就是其在内存中大小
上图是一个PE文件在4GB虚拟内存中的分布,它由多个PE文件组成。从ImageBase(0x00400000)开始,分配空间SizeOfImage(0x3D000)大小
之后装载需要用到的.dll:把ws2help.dll装载到从ImageBase(0x71A10000)开始,分配空间大小为SizeofImage(0x8000)。其他.dll也是如此
最后把EIP指向EOP(AddressOfEntryPoint),这个程序就可以执行了
注意:我们可以自定义每个PE文件的sizeofimage,具体流程是:打开VC->右键你的项目->setting->选择Link->Category设置为Output->在Base address选项中就可以自定义ImageBase了,之后这个程序编译以后,ImageBase就变成了我们所修改的了
程序编译时的问题
问题一:DLL装载地址冲突
一般情况下一个PE文件自身的子.exe文件的ImageBase不会和别的子.exe文件的ImageBase发生冲突
但是默认情况下DLL的ImageBase为0x10000000,因此如果一个PE文件的多个DLL没有合理的修改分配装载起始地址,就可能出现其ImageBase都是同一个地址,造成装载冲突
注意:如果一个DLL在装载到虚拟内存中时,操作系统发现其他DLL已经占用这块空间了,那么这个DLL会依据模块对齐粒度,往后找空余的空间存入,此时,.dll便有了其内存空间,这个过程,本人称作内存再分配。在程序每次运行时,内存再分配的情况都是不一样的
模块对齐粒度:和结构体的字节对齐一样是为了提高搜索的速度(空间换时间),模块间地址也是要对齐的。模块对齐粒度默认 为0x10000,也就是64K
问题二:编译后的绝对地址
一个.dll或.exePE文件中的全局变量,在程序编译完成后,其全局变量的绝对地址就写入PE文件了。如以下文件中a(人为的全局变量)和%d(系统的全局变量),编译完后地址值都是不变的。
假设如果这个PE文件在装载时,某DLL装载地址冲突,系统会对该.dll文件进行再分配内存空间。由于这个.dll文件的全局变量地址在编译完成后就不再改变,当程序执行时,程序会按这个绝对地址去寻找全局变量使用。而该.dll文件由于装载时再分配内存,就会出现找不到这个全局变量的问题。
不单是全局变量,.DLL中对外提供的函数的地址在编译完后也是不再改变的,而如果此时DLL装载时其分配内存空间发生了改变了,通过这些不变的函数地址也找不到需要的函数了
重定位表引入
所以如果一个PE文件出现了内存装载冲突的情况,那么就需要重定位表,来记录下来,有哪些地方的数据需要做修改、重新定位,保证在内存再分配后,操作系统能正确找到这些数据
比如再问题二中:这个PE文件各子PE文件没有按照其本来ImageBase去装载,而是进行了内存再分配。那么我们写入的全局变量a的存储地址就会发生变化,但是由于硬编码已经生成:A1 30 4A 42 00,那么重定位表就会把30 4A 42 00这个数据的地址记下来,等到运行时操作系统会根据重定位表找到这个数据,做一个重定位修改,即把0x00424a30这个绝对地址修改成它现在所在的绝对地址,保证全局变量a可以被准确找到,修改的操作由操作系统进行负责。
因为一个PE文件的.exe子PE文件一般只有一个,且是最先装载,所以装载位置和其ImageBase是一致的,不需要内存再分配,而.dll子PE文件有很多,所有操作系统需要考虑其装载的位置是不是预期的位置,如果不是,.dll子PE文件就需要提供重定位表
重定位表的位置
找到可选PE头中的最后一个成员数据目录项(有16个元素的结构体数组):找到第6个结构体,就是重定位表的数据目录
下图便是重定位表的位置:
根据重定位表数据目录的VirtualAddress,便可以获取重定位表的地址
重定位表结构
typedef struct _IMAGE_BASE_RELOCATION
{
DWORD VirtualAddress;
DWORD SizeOfBlock;
//在这里还有一堆未知的数据
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION ,* PIMAGE_BASE_RELOCATION;
该结构体虽然叫做重定位表结构体,但实际上本人认为它叫做块结构体更合适
这是因为一个完整重定位表是由多个块组成的,最后一个块的VirtualAddress和SizeOfBlock都是0x00000000,表示重定位表结束。如下图便是一个完整的重定位表:
1.VirtualAddress
宽度为4字节
由于4GB虚拟内存地址需要32位即4字节的数才能完整表示,所以当有10000个地方要修改,就需要记录下这10000个地方的地址,一个地址4字节,共需大小10000 * 4 = 40000字节空间,如此记录,一张表所占内存大小就过大了
因此操作系统会把一个PE文件分页,内存中一个页的大小为0x1000字节,相当于把文件分成了一小页一小页的。
那么如果在一页中有需要重定位的地方,重定位表就会给这个页安排一个块,这个块的VirtualAddress存储了此页的偏移起始地址(RVA)。由于一页的大小只有0x1000字节(4096),所以用12位二进制数就可以表示的下4096个地址,此时记录地址所需要的空间就大大减少了。由于内存对齐的缘故,所以把这个值用16位存放。多出来的高4位可以用来表示其他的含义,低12位表示需要修改的地方相对于所在页的偏移地址故具体项占16位,
2.SizeOfBlock
表示每个块的大小,单位为字节
3.具体项
在结构体中未知的一堆数据中,每两个字节叫一个具体项。
具体项的高4位表示类型:值为3,即0011。一个块中有多少个高4位为0011的具体项,就表示这个块当中有多少个地方需要做重定位修改
当具体项的值为0时,说明这个数据项的2个字节的数据用来做数据对齐用,可以不用修改。因此我们只需要关注高4位值为3的具体项就可以了。
一块中一共有多少个具体项用(此块的SizeofBlock - 4 - 4 )/ 2进行计算
具体项的低12位的值表示要修改的地方相对于所在块的VirtualAddress(也是项所在页)的偏移地址,该值加上该块的VirtualAddress的结果便是要修改的地方的在内存的绝对地址,这个地址上的数据则需要修改做重定位。
综上所述:一页中如果有要重定位的地方,重定位表给此页安排一块(一块对应一页)。此块的VirtualAddress存储此页的起始地址;具体项占16位,高4位表示类型,低12位表示要修改的地方相对于所在页的偏移地址。
页、块、节的关系
一个PE文件(可执行程序)运行时,装入虚拟内存时操作系统会对程序进行分页。重定位表会根据页进行分块。但程序中的节跟分页和分块没有任何关系
我们用LordPE打开一个有重定位表的PE文件,查看重定位表
发现:这个重定位表分了很多块,第175块中记录的是偏移地址为0xAF000的页中要修改重定位的地方,这些要修改的地方在.text节中。第176块中记录的是偏移地址为0xB000的页中要修改重定位的地方,这些要修改的地方在.data节中