接着学习PE结构解析。
写的过程中总结一下常用到的基础知识:
基地址(ImageBase):当PE文件通过Windows加载器载入内存后,内存中的版本称为模块,映射文件的起始地址称为模块句柄,可通过模块句柄访问内存中其他数据结构,这个内存起始地址就称为基地址。
虚拟地址(VA):在Windows系统中,PE文件被系统加载到内存后,每个程序都有自己的虚拟空间,这个虚拟空间的内存地址称为虚拟地址。
相对虚拟地址(RVA):可执行文件中,有许多地方需要指定内存中的地址。例如,应用全局变量时需要指定它的地址。为了避免在PE文件中出现绝对内存地址引入了相对虚拟地址,它就是在内存中相对于PE文件载入地址的偏移量。
它们之间的关系:虚拟地址(VA) = 基地址(Image Base)+相对虚拟地址(RVA)
文件偏移地址(Offset):当PE文件存储在磁盘中时,某个数据的位置相对于文件头的偏移量称为文件偏移地址(File Offset)。文件偏移地址从PE文件的第一个字节开始计数,起始值为0。
数据目录表是PE中比较重要的一个部分,其也是一个结构。微软在Microsoft Virtual Studio在对其结构又定义。
结构如下:
用loadPE打开一个PE文件,点击目录,即可看到数据目录表,再点击每个表对应可以展开的按钮,即可看到相应参数对应的值,
比如点开输入表,可看到调用了哪些动态链接库,又在动态链接库里调用了哪些API:
为了加深理解,下面对一些重要的表进行学习解析。
解析这些表之前,先写一个地址转换函数,就是将相对虚拟地址(RVA)转换为文件偏移地址(Offset)。
那么为什么要写这样一个函数呢?因为一些PE文件为了减小体积,磁盘对齐值不是一个内存页1000h,而是200h。当这类文件被映射到内存中后,同一数据相对于文件头的偏移量在内存中和磁盘文件是不同的,这样就出现了文件偏移地址和虚拟地址的转换问题。当然,那些磁盘对齐值与内存对齐值相同的区块,同一数据在磁盘文件中的偏移与在内存中的偏移相同,因此不需要转换。
如图,当文件被映射到内存中时,MS-DOS头,PE头和块表的偏移位置都没有改变,但是当区块被映射到内存中后,其偏移地址就发生了改变。
文件偏移地址(Offset)为add1 ,相对虚拟地址(RVA)为add2。它们直接相差了一个以0填充的空白区域,假设这个值为H,那么:
Offset =RVA -H
Offset =VA -ImageBase -H
下面开始写代码
首先声明一个函数:
获取文件的缓存区:
原理比较简单:首先判断这个地址是否在PE头中,如果在,文件偏移和内存偏移相等,如果存在于文件的区段中,则利用以下公式: 内存偏移 - 该段起始的RVA(VirtualAddress) = 文件偏移 - 该段的PointerToRawData 内存偏移 = 该段起始的RVA(VirtualAddress) + (文件偏移 - 该段的PointerToRawData) 文件偏移 = 该段的PointerToRawData + (内存偏移 - 该段起始的RVA(VirtualAddress))
代码逻辑如下:
可执行文件使用来自其他DLL的代码或数据的动作称为输入。当PE文件在被载入时,Windows加载器的工作之一就是定位所有被数据的函数和数据,并让正在载入的文件可以使用那些地址。
导入函数就是被程序调用但其执行代码不在程序中的函数,这些函数在DLL文件中,当应用程序调用一个DLL的代码和数据时,它正被隐式地链接到DLL,这个过程由Windows加载器完成。另一种链接是显示链接,它是已经约定目标DLL已经被加载,然后寻找API的地址,一般是通过Loadlibrary 和GetprocAddress完成。
简而言之,导入表主要是PE文件从其他第三方库中导入API,以供本程序调用,结构如下:
OriginalFirstThunk和FirstThunk分别指向两个不同的IMAGE_THUNK_DATA结构的数组。这两个数组都以一个空的IMAGE_THUNK_DATA结构结尾。 一般情况下,导入表只需要关注OriginalFirstThunk和FirstThunk这两个字段。
IMAGE_THUNK_DATA结构:
那么要解析导入表,首先要定位到导入表:
通过PE扩展头里数据目录字段 + 导入表的宏定义,即可定位到导入表,
在Microsoft Virtual Studio中,在IMAGE_DIRECTORY_ENTRY_IMPORT处 ,ctrl +鼠标左键 即可跳转到该宏定义:
然后就是填充结构,前面写了一个找数据表到文件头偏移的函数RvaToOffset,现在就用这个函数来找导入表到文件头的偏移:
然后就是遍历数据:
结果如下,可以看到是没有任何问题的:
导出表是PE文件为其他应用程序提供自身的一些变量、函数以及类,将其导出给第三方程序使用的一张清单,里面包含了可以导出的元素。
结构如下:
从逻辑上来说,导出表由名称表、函数表与序号表组成。函数表和序号表必不可少,名称表则是可选的。序号表与名称表的作用是索引,找到真正需要的函数表,函数表中保存着被导出的函数的地址信息。
这里写一个有导出函数的测试dll:
在loadPE中打开,打开导出表,可以看到有导出函数:
现在就是和导入表一样,找导出表到文件头的偏移,然后手写代码获取相关信息:
逻辑和导入表类似,就不赘述了,直接给出代码:
结果如下,可以看到除了不美观以外,结果是没有任何问题的:
当向程序的虚拟内存加载PE文件时,文件会被加载到ImageBase所指向的地址。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)