一、PE重定位
PE文件在重定位过程中会用到基址重定位表(Base Relocation Table)。
向进程的虚拟内存加载PE文件(EXE/DLL/SYS)时,文件会被加载到PE头的ImageBase所指的地址处。若加载的是dll(sys)文件,且在ImageBase位置处已经加载了其他的dll(sys)文件,那么PE装载器就会将其加载到其他未被占用的空间。
使用SDK或Visual C++创建PE文件时,EXE默认的ImageBase为00400000,DLL默认的ImageBase为10000000。此外使用DDK(驱动开发工具包)创建SYS文件默认的ImageBase为10000
DLL/SYS
A.DLL被加载到TEST.EXE进程的10000000地址处。此后,B.DLL试图加载到相同地址时,PE装载器将B.DLL加载到另一个尚未被占用的地址(3C000000)处。
EXE
创建好进程后,EXE文件会首先加载到内存,所以在EXE中无须考虑重定位的问题。但是windows中引入了ASLR安全机制,每次运行EXE文件都会被加载到随机地址,这样大大增强了系统安全性。
我们分别运行3次ConsoleApplication4.exe(32位)时截图,可以明显发现,每次运行时程序都被加载到不同的地址。
三次运行结果发现,入口地址均不一样。但是这是建立在重启电脑的前提下,如果不重启电脑,每次执行的结果都是一致的。
Windows系统对已加载的exe基地址进行了缓存优化。若exe文件未修改且系统未重启,基地址可能被复用以减少性能开销
ASLR机制也适用于DLL/SYS文件。对于各OS的主要系统DLL,微软会根据不同版本分别赋予不同的ImageBase地址。同一系统的kernel32.dll、user32.dll等会被加载到自身固有的ImageBase,所以,系统的DLL实际不会发生重定位问题。
二、PE重定位时执行的操作
接下来我们写一个小程序ConsoleApplication4.exe为例,看看PE重定位到底发生了什么。使用OllyDbg运行我们的程序。
我们的程序在ASLR机制的作用下,被加载到00500000地址处,从图中指令可以看到,方框中进程的内存地址与小辣椒对应的文件数据并不统一,这是为什么呢?像这种使硬编码在程序中的内存地址随当前加载地址变化而变化的处理过程就是PE重定位。
无法加载到ImageBase地址时,若未进行过PE重定位处理,应用程序就不能正常运行(因发生“内存地址引用错误”,程序异常终止)。
我们在对比以上程序的内存地址与硬编码地址后,归纳整理如下
文件(ImageBase:00400000) 进程内存(加载地址:00500000) 00403160 00503160 0040315C 0050315C 00403158 00503158 00403154 00503154 00403150 00503150 0040314C 0050314C 00403178 00503178 硬编码地址以ImageBase为基准。生成ConsoleApplication4.exe文件时,由于无法预测程序被实际加载到哪个地址,所以记录硬编码地址时以ImageBase为基准。但是在运行的瞬间,经过PE重定位后,这些地址全部以加载地址为基准变换,最后程序得以正常执行而不发生错误。
三、PE重定位操作原理
Windows的PE装载器进行PE重定位处理时,基本的操作原理很简单。
PE重定位的基本操作原理
- 在应用程序中查找硬编码的地址位置
- 读取值后,减去ImageBase
- 加上实际加载地址
其中最关键的是查找硬编码地址的位置。查找过程中会用到PE文件内部的Relocation Table(重定位表),它是记录硬编码地址偏移(位置)的列表(重定位表是在PE文件构建过程中提供的)。通过重定位表查找,其实就是指根据PE头的“基址重定位表”项进行的查找。
基址重定位表
基址重定位表地址位于PE头的DataDirectory数组的第六个元素(数组索引为5)
如图基址重定位表的地址为RVA 5000,计算得到文件偏移为 5000 - 5000 + 2200 = 2200
IMAGE_BASE_RELOCATION结构体
基址重定位表中罗列了硬编码地址的偏移。读取这张表就能获取准确的硬编码地址偏移。基址重定位表是IMAGE_BASE_RELOCATION结构体数组。
IMAGE_BASE_RELOCATION结构体的第一个成员为VirtualAddress,它是一个基准地址,实际是RVA值。第二个成员为SizeOfBlock,指重定位块的大小。最后一项TypeOffset数组不是结构体成员,而是以注释形式存在的,表示在该结构体之下会出现WORD类型的数组,并且该数组元素的值就是硬编码在程序中的地址偏移。
基址重定位表的分析方法
基址重定位表的部分内容
文件偏移 | RVA | 数据 | 注释 |
---|---|---|---|
2200 | 5000 | 00001000 | VirtualAddress |
2204 | 5004 | 00000160 | SizeOfBlock |
2208 | 5008 | 3006 | TypeOffset |
220A | 500A | 300B | TypeOffset |
220C | 500C | 3013 | TypeOffset |
220E | 500E | 301E | TypeOffset |
… | … | … | … |
由IMAGE_BASE_RELOCATION结构体的定义可知,VirtualAddress成员的值为1000,SizeOfBlock成员的值为160。也就是说,TypeOffset数组的基准地址(起始地址)为RVA 1000,块的总大小为160.块的末端显示为0.TypeOffset值为2个字节大小,是由4位的Type与12位的Offset合成的。比如,TypeOffset值为3006,解析如下
类型(4位) | 偏移(12位) |
---|---|
3 | 006 |
高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的偏移。所以程序中硬编码地址的偏移使用下面等式换算
VirtualSize(1000) + Offset(006) = 1006(RVA)
练习
现在我们可以结合之前od与小辣椒的数据进行分析
地址值 | 文件偏移 | 对应RVA | TypeOffset |
---|---|---|---|
00403160 | 06BE | 12BE | 32BE |
0040315C | 06C4 | 12C4 | 32C4 |
00403158 | 06CA | 12CA | 32CA |
四、ASLR机制
基本原理
地址空间布局随机化(Address Space Layout Randomization,ASLR)是一种通过随机化关键内存区域的基址来防御内存攻击的安全机制。其核心思想是打破攻击者对内存布局的预测能力,例如栈、堆、共享库和可执行模块的地址不再是固定值,而是每次程序启动时动态分配。
visual studio中可以通过属性 – 链接器 – 高级 – 随机基址。进行ASLR属性的开启关闭。
通过查看NT头 – Optional Header – DllCharacteristics 中值为 0040代表开启ASLR
工作流程
1. 静态随机化
在程序加载阶段,操作系统为以下区域分配随机基址:
- 主可执行模块(如.exe或ELF文件)
- 动态链接库(DLL/.so)
- 栈空间
- 堆空间
基址=默认基址+R×页面大小
其中R为随机数,页面大小通常为4KB或2MB。
2. 动态随机化
运行时通过以下方式增强随机性:
- 堆内存分配时添加随机偏移(如Windows的HeapAlloc)
- 栈帧地址随机化(通过插入随机填充字节)
- 共享库重定位(如Linux的mmap随机化)
实现机制
1. 操作系统支持
- Windows:自Vista起全面支持ASLR,要求程序启用/DYNAMICBASE链接选项
- Linux:通过内核参数kernel.randomize_va_space控制(0关闭/1部分开启/2完全开启)
2. 硬件协同
依赖内存管理单元(MMU)的分页机制,将虚拟地址随机映射到物理地址。例如:
// Linux内核代码片段(简化)
unsigned long random_offset = get_random_int() & 0x00FFFFFF;
load_address = default_base | random_offset;
3. 安全增强
- High Entropy ASLR(Windows 8+):将随机熵从8位提升至24位
- PIE(Position-Independent Executable):使可执行文件自身支持重定位
优缺点分析
优势 | 局限性 |
---|---|
增加漏洞利用难度 | 无法防御信息泄露攻击 |
与其他机制(如DEP)形成纵深防御 | 可能增加内存碎片 |
对性能影响通常小于1% | 需程序/编译器支持(如PIE) |
绕过方法示例
- 通过内存泄露获取基址
- 暴力破解低熵ASLR(如早期Android实现)
- 利用未随机化区域(如部分JIT编译代码)