PE文件是指windows系统下使用的可执行文件格式,它是微软在unix平台的COFF基础上制作而成的。PE文件一般指32位的可执行文件,也称为PE32。64位的可执行文件称为PE+或者PE32+(不是PE64),是PE32的一种扩展形式。
要了解PE文件,首先要知道PE格式,那么什么是PE格式呢,既然是一个格式,那肯定是需要遵循的一定的定理。其实PE格式就是各种结构体的结合,这些结构体都定义在在这个头文件中。
PE文件整体结构
一个PE文件大致可分为以下几个部分:
- DOS部分
- PE文件头
- 节区头(节表)
- 节数据(块数据)
- 调试信息
从DOS头到节区头是PE头,其下的是PE体。文件中使用偏移(Offset), 内存中使用VA(Virtual Address)来表示位置。
VA指的是进程虚拟内存的绝对地址,RVA(Relative Virtual Address)指从基准位置(ImageBase)开始的相对地.址。两者存在以下换算关系:
PE头内部的信息大多以RVA的形式存在
DOS头结构体大小为40个字节,其中只需要熟悉两个成员变量:与,前者是DOS签名,固定为,取自微软开发人员首字母。后者则是指示NT头的偏移位置(),除了这两个成员,其他成员全部用0填充都不会影响程序正常运行。定义如下
变量固定偏移位置3C处,从此后到所指向的偏移位置为(中文一般翻译为),在win32中未使用。在16位系统中运行便输出一个 就退出了。

由图可知的值是,则到的内容便为。
PE文件头由PE文件头标志,标准PE头,扩展PE头三部分组成
0x02.2.1 IMAGE_NT_HEADERS
由上可知定义了3个成员变量,
- 固定为字符;
- 指向一个为的结构体;
- 在32位下指向一个的结构体。在64位下,指向一个的结构体。
0x02.2.2 IMAGE_FILE_HEADER
我们一个一个分析,先看下的定义:
里面大概有4个重要的成员变量需要掌握,如果这些变量的值设置不正确,程序便不能正常运行。
- :每个CPU都有一个唯一的Machine码,兼容32位Intel x86芯片的machine码是14c。其它Machine码可在winnt.h中查看。
- : 指示文件中存在的节区数量, 此值是一定要大于0,且当定义的节区数与实际的节区数不一样时,将发生运行错误。
- :标识第三个成员变量的大小。
- :标识文件属性,是否是dll,是否可执行等信息。
如下图便是,

根据图片所示,我们可以得出各值:
0x02.2.2 IMAGE_OPTIONAL_HEADER
扩展PE头在32位和64位系统上大小是不同的,在32位系统上有224个字节,16进制就是0xE0,与上面的SizeOfOptionalHeader也能对上。
是PE头结构体最大的一个,先看定义:
部分成员变量说明:
- :指出文件被加载到内存应该被优先加载的内存地址,exe,dll文件被装载到用户内存的中,sys文件被载入内存的中,一般情况下,exe会被装载到,dll文件的值为
- : 用来指定数组的个数,在winnt.h中被明确定义为16,但PE loader一般会通过此值来识别的大小,也就是说的长度不一定都是16。
- 是由结构体数据组成的,数组中的每一项都有定义,详细如下:

但我们一般只需要关心几个常见的即可,导出表、导入表、资源表、TLS表。详细的我们放到后面再讲。

根据图片例子,我们把常见的成员变量值列举出来如下:
还需要知道的是,程序的真正入口点 = ImageBase + AddressOfEntryPoint。
节区头由IMAGE_SECTION_HEADER定义,节区头的结构如下。
- :非必须以NULL结束,也未限制只能使用ASCII,可放入任何值
- : 内存中节区所占大小
- : 内存中节区起始地址(RVA)
- : 磁盘文件中节区所占大小
- :磁盘文件中节区起始位置
- :节区属性。

如图所示,可知有四个节区,外加一个全为0的节区。
先看如何定位导入表起始地址,在头中的最后一个成员变量,我们有提到其是一个包含16个素的数组,其中第2个素就是导入表的起始位置。
先看的定义
即我们可以通过访问到导入表的起始地址。
再来看一下定义导入表的结构体:
一个程序导入了多少个库就有多少个结构体,这些结构体组成一个数组,且结构体数组以一个全为NULL的结构体作为结束。所以被称为.其中比较重要的成员变量如下(以下地址值全为RVA)
- :指向INT(导入名称表 、Improt Name Table)的地址,以全NULL结束。
- :库名称字符串的地址
- : 指向IAT(导入地址表、Import Address Table)的地址,以全NULL结束。
的值为 即RVA=F5A9C, 换算成RAW则为F4A9C(换算方法见附)。

OriginalFirstThunk - INT (Import Name Table)
第一个成员变量为,它是INT的起始地址,换句话说,就是INT是一个包含导入函数信息的结构体指针数组,每个数组的素都指向一个的结构体,并以全为NULL的素结束。根据上图我们知道第一个素的值为F6284(RVA)->换成RAW则为F5284。来到F5284这个地址.如下。由图可知INT数组长度为5。(以就代表着从这个库文件里面导入了5个函数)


要看懂这个结构得先看下的定义
前两个字节是库中函数的固有编号,后面的则是一个字符数组,以00结束。所以这个导入的函数则是.
Name
根据的结构可知,第四个成员则为,其值为F666A,换成RAW为F566A,我们转到这个地址看下,可知其导入的是SHLWAPI.dll


FirstThunk- IAT (Import Address Table)
由结构体可知其最后一个成员为。根据上面的图可知,第一数组素的值为C55F4, RAW为:C45F4。我们来到这个地址处:


既然指向同一个地址,为啥需要两个去索引,这是因为需要区分PE加载前还是加载后。如果是加载前,那个IAT跟INT一样,都可以找到依赖的函数名称,如果是加载后。也就是在内存中的话, 那么IAT表保存的就是函数的地址。
PELoader把导入函数输入至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)
函数查找过程 - GetProAddress工作原理
- 利用AddressOfNames成员转到“函数名称数组”
- “函数名称数组”中存储字符串地址。通过比较字符串,查找指定的函数名称(此时数组的索引称为name_index)
- 利用AddressOfNameOrdinals成员,转到orinal数组
- 在orinal数组中通过name_index查找相应的值
- 利用AddressOfFunction成员转到“函数地址数组”
- 在“函数地址数组”中将刚刚求得的ordinal用作数组索引,获得指定的函数起始地址。
有了导入表的基础,则理解导出表就很简单了,这里就不再举例了。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ri-ji/34173.html