PE体系
PE结构&整体叙述
PE结构&导入表
PE结构&导出表
PE结构&基址重定位表
PE结构&绑定导入实现
PE结构&延迟加载导入表
导入表简介
当我们源文件里面需要去如何画窗口,如何显示指定字符串这样功能的代码,只需要简单调用Windows API函数。这些调用的函数在源文件中并不存在。这些代码存储在DLL文件中,即动态链接库中。在动态链接库里存放的不是函数的源代码,而是编译链接后生成的字节码。
当源程序调用了动态链接库的相关函数,在进行编译和链接的时候,编译程序和链接程序就会把调用的相关信息写入最终生成的PE文件中,从而来告诉操作系统这些函数的执行字节码能从哪里获取,这些信息就是导入表所要描述的内容。
DLL加载方式有两种:显式链接(Explicit Linking) 和 隐式链接(Implicit Linking)
- 显示链接:程序在使用DLL时进行加载,使用完毕后释放内存
- 隐式链接:程序在开始时即一同加载DLL,程序终止时再释放占用的内存
IAT提供的机制与DLL的隐式链接有关。
优点:
- 不把函数库包含进应用程序中,单独组成DLL文件,在需要使用时再进行调用。
- 使用内存映射技术将加载后的DLL代码、资源在多个进程中实现共享。
- 在对函数库进行更新时,只更新DLL文件即可。
导入函数
当我们调用一个API时,这个API我们不可能自己手写,基础源码这些东西都不知道,所以直接导入头文件然后使用即可。
#include<iostream>
#include<Windows.h>
using namespace std;
int main() {
LPCSTR text = "hello world";
LPCSTR title = "第一个MessageBoxA";
MessageBoxA(NULL, text, title, MB_OK);
}
关于API调用的汇编代码如下:
.text:00401516 mov [ebp+text], offset aHelloWorld ; "hello world"
.text:0040151D mov [ebp+title], offset unk_48800C
.text:00401524 mov dword ptr [esp+0Ch], 0 ; uType
.text:0040152C mov eax, [ebp+title]
.text:0040152F mov [esp+8], eax ; lpCaption
.text:00401533 mov eax, [ebp+text]
.text:00401536 mov [esp+4], eax ; lpText
.text:0040153A mov dword ptr [esp], 0 ; hWnd
.text:00401541 mov eax, ds:__imp__MessageBoxA@16 ; MessageBoxA(x,x,x,x)
.text:00401546 call eax ; MessageBoxA(x,x,x,x) ; MessageBoxA(x,x,
关于导入函数就两行:
.text:00401541 mov eax, ds:__imp__MessageBoxA@16 ; MessageBoxA(x,x,x,x)
.text:00401546 call eax ; MessageBoxA(x,x,x,x) ; MessageBoxA(x,x,
也就是把一个内存单元里面存的地址放在eax里面去,然后call它。即MessageBoxA
的实际地址被放在一个偏移处,查看这个偏移
地址0x004924C0
存放着0x77211930
(MessageBoxA
的实际地址)
从0x004924C0
内存单元开始取dd长度的内存数据放在eax里面,
然后直接 call 0x77211930
查看一下文件中0x004924C0
中存放的数据:
它存放的是0x61562100
查看一下内存中0x004924C0
中存放的数据:
它存放的是0x77211930
这意味着,文件被装载到内存后,这里的值发生了变化,真正的API函数地址的应该是内存中此处的值。那文件中的值0x61562100
和内存映像中的值0x77211930
它俩有什么关系呢?
注意
dump程序时,
错误示范:
这样操作后,你得到的文件是一个
dmp文件,然后我找了三个小时。。啥也没找到。。。
正确示范:
这样dump后你得到的才是一个exe程序
文件偏移和虚拟地址转换才用得上。。。以前dump的时候都是直接用的是PEtools,今晚不知道脑子哪里抽筋,偏偏乱搞了三小时。。。
观察一下文件数据,导入表中数据
RVA数值为0x92000
,大小为0xEE8
核心内容
导入表数据所在地址RVA=0x92000
导入表数据大小=0xEE8
导入函数数据地址表所在地址RVA=0x9228C
导入地址表表数据大小=0x23C
转换成文件偏移后是:
导入表数据所在地址0x8F200
,导入表数据大小=0xEE8
导入地址表数据所在地址0x8F48C
,导入地址表表数据大小=0x23C
导入表描述符IMAGE_IMPORT_DESCRIPTOR
导入表数据的起始是一组导入表描述符结构。每组为20个字节,该结构定义如下:
IMAGE_IMPORT_DESCRIPTOR STRUCT
union
Characteristics dd
OriginalFirstThunk dd ;000h - 桥1
ends
TimeDateStamp dd ;0004h - 时间表
ForwarderChain dd ;0008h - 链表的前一个结构
Name1 dd ;000ch - 指向链接库名字的指针
FirstThunk dd ;0010 - 桥2
以下作出解释:
IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk:
+0000h,双字。因为它是指向另外数据结构的通路,因此简称桥1。该字段指向一个包含了一系列结构的数据。
指向的数组中每个结构定义了一个导入函数的信息,最后一个内容为全0的结构作为结束。指向的数据中每一项为一个结构,此结构的名称是IMAGE_THUNK_DATA。该结构实际上只是一个双字,但在不同时刻却拥有不同的解释。该字段有两种解释:
- 双字最高位为0,表示导入符号是一个数值,该数值是一个RVA。
- 双字最高位为1,表示导入符号是一个名称
IMAGE_IMPORT_DESCRIPTOR.TimeDateStamp:
+0004h,双字。时间戳,一般不用。如果该导入表被绑定,那么绑定后的这个时间戳就被设置为对应DLL文件的时间戳。操作系统在加载时,可以通过这个时间戳来判断绑定的信息是否过时
IMAGE_IMPORT_DESCRIPTOR.ForwarderChain:
+0008h,双字。链表的前一个结构
IMAGE_IMPORT_DESCRIPTOR.Name1:
+000ch,双字。这个字段的含义和名称并不一致,这里的Name1是一个RVA,它指向该结构对应的DLL文件的名称,而这个名称是以“\0”结尾的Ansi字符串。
IMAGE_IMPORT_DESCRIPTOR.FirstThunk :
+0010h,双字。与OriginalFirstThunk相同,它指向的链表定义了针对Name1这个动态链接库引入的所有导入函数,简称桥2
导入表双桥结构
重新找个程序举例,上面那个数据太大了,头疼。。。
导入表数据所在地址RVA=0x1B1C4
导入表数据大小=0x50
导入函数数据地址表所在地址RVA=0x1B000
导入地址表表数据大小=0x1C4
转换成文件偏移后是:
导入表数据所在地址0x83C4
,导入表数据大小=0x50
导入地址表数据所在地址0x8200
,导入地址表表数据大小=0x1C4
桥1和桥2最终都通向了一个目的地,都指向了引入函数的“编号-名称”(Hint/Name)描述部分。而从桥2到目的地的过程中,还经过了另外一个很重要的结构IAT(Import Address Table)
AC B2 01 00
桥1,最高位为0,这是一个RVA,表明函数是以字符串类型的函数名导入的。RVA转换为FOA,值为
0x84AC
,从文件的该位置开始取双字,直到取出的双字为"0"结束,每一个双字都是结构IMAGE_THUNK_DATA。该结构的详细定义如下
IMAGE_THUNK_DATA STRUCT
union u1
ForwarderString dd?
Function dd?
Ordinal dd?
AddressOfData dd?
ends
IMAGE_THUNK_DATA ENDS
0x0001B450
转换成文件偏移为0x8650
2E 00 5F 5F 76 63 72 74 5F 47 65 74 4D 6F 64 75 6C 65 46 69 6C 65 4E 61 6D 65 57 00
这些值组成的数据结构就是IMAGE_IMPORT_BY_NAME
IMAGE_IMPORT_BY_NAME STRUCT
Hint dw ?;0000h -函数编号
Name1 db ?;0004h -表示函数名的字符串
IMAGE_IMPORT_BY_NAME ENDS
以下是对每个字段的具体解释:
IMAGE_IMPORT_BY_NAME.Hint:
+0000h,双字。函数的编号,在DLL中对每个函数进行了编号,访问函数时可以通过名称访问,也可以通过编号访问
IMAGE_IMPORT_BY_NAME.Name1:
+0004h,大小不确定。函数名字字符串的具体内容,以“\0”作为字符串结束标志。
其中002E
标识该函数在动态链接库的编号,后面紧跟着函数名“GetModuleFileNameW”。
00 00 00 00
时间戳,这里为0
00 00 00 00
链表的前一个结构,这里为0
9E B4 01 00
RVA,指向动态链接库RUNTIME140D.dll
的名字字符串,转换为文件偏移是0x869E
98 B0 01 00
桥二,转换成文件偏移是0x8298
,
根据上面可知IAT
是0x8200
~0x83C4
,桥2指向的地方在IAT范围内,所以指向IAT
和桥1指向的数据值相同。。但是存储的位置是不同的。桥1指向的INT与桥2指向的IAT
内容完全一样,但INT和IAT却存储在文件的不同位置。
每一个结构IMAGE_IMPORT_DESCRIPORT
都对应一个唯一的动态链接库,以及引用了该动态链接库的多个函数,每个函数的最终“值-名称”描述均可以沿桥1或者桥2找到。这种导入表结构被称为双外结构。
双桥结构的导入表在文件中存在两份内容完全相同的地址列表。一般情况下,桥2指向的地址列表被定义为IAT,而桥1指向的地址列表则被定义为INT(import name table)。
注意:
有的链接程序只只为导入表存储一个桥,如Borland公司的Tlink只保留桥2,这样的导入表我们称之为单桥结构的导入表(单桥结构的导入表是无法指向绑定导入操作的。)
文件中的导入表:
在文件中,桥1指向INT,桥2指向IAT,内容一致。
内存中的导入表:
在内存中,桥1可以找到调用的函数名称或函数的索引编号,桥2却可以帮你找到该函数指令代码在内存空间中的地址。
当PE被加载进虚拟地址空间以后,IAT 的内容会被操作系统更改为函数的VA。这个修改最终会导致通向“值-名称”描述的桥2发生断裂,如上图。
当桥2发送断裂以后,如果没有桥1作为参照(因为桥1和桥2维护了两个一一对应的函数RVA),我们就无法重新找到该地址到底是调用了那个函数。这就是为什么会存在两个桥的原因。也是为什么单桥导入表无法实施绑定的原因。
实际操作
内存中的导入表:
桥1的值为01B2AC
桥2的IAT 值为0x01B098
两个函数实际地址值和函数名(函数编号)可以一一对应。。。
PE装载器把导入函数输入至IAT的顺序
- 读取IID的Name成员,获取库名称字符串(eg:kernel32.dll)
- 装载相应库:
LoadLibrary(“kernel32.dll”) - 读取IID的OriginalFirstThunk成员,获取INT地址
- 逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)
- 使用IMAGE_IMPORT_BY_NAME的Hint(ordinal)或Name项,获取相应函数的起始地址:GetProcAddress(“GetCurrentThreadld”)
- 读取IID的FirstThunk(IAT)成员,获得IAT地址
- 将上面获得的函数地址输入相应IAT数组值
- 重复以上步骤4~7,知道INT结束(遇到NULL)
同一个DLL文件的多个函数的导入表
这是文件中的导入表,上图中FirstThunk和OriganalFirstThunk一样也有一份函数指针表,即桥2.只是没画而已下面是装载入内存后的导入表:
定位导入函数地址表的方法有两种:
- 从导入表的而最后一个导入表项##
IMAGE_IMPORT_DESCRIPTOR
结构中的字段IMAGE_IMPORT_DESCRIPTOR.FirstThunk
定位IAT - 通过数据目录第13个数据项的描述直接定位IAT
每个动态链接库都维护了自己的IAT内容,不同的 链接库维护的这些内容可以是不连续的。
导入表的平面解析
解释如下: