《逆向工程核心原理》第十六章
基址重定位表
- PE在重定位过程会用到基址重定位表(Base Relocation Table)
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。
下图为DLL重定位示意图,A.DLL被加载到TEXT的10000000地址处。此后,B.DLL试图加载到相同地址(10000000)时,PE装载器将B.DLL加载到另一个尚未被占用的地址(3C000000)处。
创建好进程后,EXE文件会首先加载到内存,所以在EXE中无须考虑重定位的问题。但是Windows Vista之后的版本引入了ASLR安全机制,每次运行EXE文件都会被加载到随机地址,这样大大增加了系统安全性。下图为win10中三次用Ollydbg打开notepad.exe(win10 测试ASLR可能需要每次重启电脑,才会改变地址),可以看到EP起始地址不一样,说明运行时程序被随机加载到不同地址。
PE重定位时执行的操作
下面以win10的notepad.exe程序为例,看看PE重定位时都发生了什么。如下图所示,notepad.exe的ImageBase.exe为"00400000"
下图为Ollydbg查看的notepad.exe EP代码部分,从图中指令可以看到,方框中的进程地址都是以硬编码形式存在。这些地址值随加载地址的不同而改变,像这样,使硬编码在程序中的内存地址随当前加载地址变化而改变的处理过程就是PE重定位。
无法加载到ImageBase地址时,若未进行过PE重定位处理,应用程序就不能正常运行(因发生"内存地址引用错误",程序异常终止)
PE重定位操作原理
Windows的PE装载器进行PE重定位处理时,基本的操作原理很简单
- 在应用程序中查找硬编码的地址位置
- 读取值后,减去ImageBase(VA=>RVA)
- 加上实际加载地址(RVA=>VA)
最关键的是查找硬编码地址的位置。查找过程会用到PE文件内部的RelocationTable(重定位表),它是记录硬编码地址偏移(位置)的列表(重定位表是在PE文件构建过程(编译/链接)中提供的)。通过重定位表查找,其实就是指根据PE头的"基址重定位表"项进行的查找。
基址重定位表
基址重定位表位于PE头的DataDirectory数组的第六个元素(数组索引为5),在PE View中查看notepad.exe的基址重定位表地址。
基址重定位表的地址为RVA 0002B000。使用PE View查看该地址,如下图所示设置以RVA地址查看
IMAGE_BASE_RELOCATION结构体
上图的基址重定位表中罗列了硬编码地址的偏移(位置)。读取这张表就能获得准确的硬编码地址偏移。基址重定位表是IMAGE_BASE_RELOCATION结构体数组。IMAGE_BASE_RELOCATION结构体的定义如下
1
2
3
4
5
6typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;IMAGE_BASE_RELOCATION结构体的第一个成员为VirtualAddress,它是一个基准地址(Base Address),实际是RVA值。第二个成员为SizeOfBlock,指重定位快的大小。最后一位TypeOffset数组不是结构体成员,而是以注释的形式存在的,表示在该结构体之下会出现WORD类型的数组,并且该数组元素的值就是硬编码在程序中的地址偏移。
基址重定位表的分析方法
下表列出了notepad.exe部分基址重定位表的部分内容
RVA 数 据 注释 0002B000 00001000 VirtualAddress 0002B004 00000218 SizeOfBlock 0002B008 3000 TypeOffset 0002B00A 3004 TypeOffset 0002B00C 3008 TypeOffset ... ... ... 由IMAGE_BASE_RELOCATION结构体的定义可知,VirtualAddress成员(基准地址)的值为1000,SizeOfBlock成员的值为218。也就是说,上表显示的TypeOffset数组的基准地址(起始地址)为RVA 1000,块的总大小为150(这些块按照基准地址分类,以数组形式存在),块的末端显示为0。TypeOffset值为2个字节(16位)大小,是由4位的Type与12位的Offset合成的。比如TypeOffset值为3000,解析表如下
类型(4位) 偏移(12位) 3 000 高4位用作Type,PE文件中常见的值为3(IMAGE_REL_BASED_HIGHLOW),64位的PE+文件中常见值为A(IMAGE_REL_BASED_DIR64)。
有时为了略去PE装载器的重定位过程,常常把Type值修改为0(IMAGE_REL_BASED_ABSOLUTE)
TypeOffset的低12位才是真正的偏移,该偏移值基于VirtualAddress的偏移。所以程序中的硬编码地址的偏移使用以下公式转换
1
VirtualAddress (1000) + Offset(0000) = 1000(RVA)
下面看一下RVA 1000处是否实际存在要执行PE重定位操作的硬编码地址,notepad.exe被加载到004E0000
EP(VA) - EP(RVA)
地址处,故RVA 1000即为004E1000(VA),该地址储存着地址005048D8,并且该值经过PE重定位发生了变化。
逆向工程核心原理第十七章
从可执行文件中删除.reloc节区
.reloc节区
EXE形式的PE文件中,"基址重定位表"项对运行没什么影响。实际上将其删除后程序仍能正常运行(基址重定位表对DLL/SYS形式的文件来说几乎是必需的)
VC++中生成的PE文件的重定位节区名为.reloc,删除该节区后文件照常运行,且文件大小将缩减(实际上存在这种实用小程序)。.reloc节区一般位于所有节区的最后。
删除.relco节区需要按照以下4个步骤
- 整理.reloc节区头
- 删除.reloc节区
- 修改IMAGE_FILE_HEADER
- 修改IMAGE_OPTIONAL_HEADER
下面以前面用过的Tut.ReverseMe1.exe为例,删除.reloc节区,为了避免删除数据使文件偏移发生变化进而导致修改了错误的地址处数据,这里建议修改顺序按文件偏移低地址到高地址的顺序
修改IMAGE_FILE_HEADER
删除1个节区,需要首先修改IMAGE_FILE_HEADER的Number of Sections成员,可以看到该处偏移为86,用HEdit修改该处值为4
修改IMAGE_OPTIONAL_HEADER
删除一个节区,(进程虚拟内存中)整个映像就随之减少相应大小。映像大小值储存在IMAGE_OPTIONAL_HEADER的Size of Image成员中,从下图可以看到Size of Image值为8000。问题在于,要计算减去多少才能让程序正常运行。根据下面的.reloc节区头的截图可以得知.reloc Virtual Size为272,但是由于Section Alignment值为1000,所以应该减去Section Alignment的整数倍(向上取整)。因此应修改Size of Image为7000,即用HEdit修改D0偏移处为7000即可。
HEdit修改Size of Image
删除.reloc节区头
下图为原exe文件的.reloc节区头截图,由图可知.reloc节区头从文件偏移218处开始,大小为28。使用Hex Editor将区域(218~239)全部用0覆盖,之所以用0覆盖而不直接删除是因为,如果直接删除,那么239之后的偏移都会发生改变,这样以来就需要修改更多的地方来解决这一问题。
删除.reloc节区
从上面的.reloc节区头截图可知.reloc节区文件的起始偏移为3600,用HEdit从3600偏移处到文件末端所有数据删除,这里因为是最后一个节区,所以直接删除对其他节区的文件偏移没有影响。
按以上4个步骤删除.reloc节区后,双击Tut.ReverseMe1.exe,可以发现程序仍能正常运行。