天天看点

逆向-PE文件结构MS-DOS头PE文件头区块

首先复习一下PE文件的结构:

逆向-PE文件结构MS-DOS头PE文件头区块

MS-DOS头

DOS部首有MZ Header和Dos stub两个部分组成,这两部分通常合成为Dos 文件头,其中Dos stub多由编译器自动生成,用户较少关注,PE文件的第一个字节为IMAGE_DOS_HEADER,其结构如下:

逆向-PE文件结构MS-DOS头PE文件头区块

其中e_magic和e_lfanew两个字段比较重要,e_magic被称为魔术字,所有能与MS-DOS兼容的可执行文件内,该值都被设置为5A4DH,对应ASCII码为“MZ”,是DOS系统创建者之一 Mark Zbikowski 的缩写。e_lfanew指向PE文件头部,是真正的PE文件头的相对偏移(RAV),占用4字节。

使用lordPE查看文件

逆向-PE文件结构MS-DOS头PE文件头区块

PE文件头

PE Header(PE 头部)是 PE 相关结构 IMAGE_NT_HEADERS 的简称,其中包含了许多 PE 装载器用到的重要域。执行体在执行时,装载器从IMAGE_DOS_HEADER结构中的e_lfanew字段找到PE Header的起始偏移量,用其加上基址得到PE文件头的指针。

pNTHeader = dosHeader + dosHeader->e_lfanew

IMAGE_NT_HEADERS由三个字段组成(左边数字为到PE文件头的偏移量),结构如下:

逆向-PE文件结构MS-DOS头PE文件头区块

Singture字段:有效PE文件中,该值被设置为0x00004550,ASCII为PE00,标志着PE文件头的开始,e_lfanew就是指向这个字段。

**IMAGE_FILE_HEADER:**包含了PE文件的一些基本信息,其中一个域指出了IMAGE_OPTIONAL_HEADER的大小,其结构如下:

逆向-PE文件结构MS-DOS头PE文件头区块
逆向-PE文件结构MS-DOS头PE文件头区块

当文件为exe时characteristics为34,当文件为DLL时其值为8450。

OptionalHeader ( 可 选 头 部 ) 是 PE Header 的 第 三 个 成 员 , 是IMAGE_OPTIONAL_HEADER 结构体,它由 224 个字节组成,虽然名为“可选头部”但它是“必需”的,因为IMAGE_FILE_HEADER不足以完全定义PE文件属性,因此该可选文件中定义了更多数据,两者联合构成了完整的PE文件头结构,其成员比较复杂,它包含了 PE 文件的逻辑分布信息,该结构共有 31 个域,主要成员表如下。

逆向-PE文件结构MS-DOS头PE文件头区块

IMAGE_OPTIONAL_HEADER 中最重要的一个域是 DataDirectory,它是一个包含 16 个元素的结构数组,每一个结构对应着一个 Section(注意这里的 Section是按照按作用进行划分的 Section,不是最终生成的 PE 文件中包含的块),结构中的两个域分别描述了该 Section 的 RVA 和 SIZE; 这样一来加载器就能够通过这个数组迅速在 image 中找到特定的 Section,后面讲到的输入表、输出表都要用到这个数组中相应的元素。

区块

区块表在PE文件头和原始数据之间,其中包含每个块在映像中的信息,分别指向不同区块实体。

Section Table ( 区 块 表 ) 在 PE 文 件 中 是 紧 挨 着 PE Header 的 , 是Image_Section_Table 的结构数组的简称,其成员个数由 IMAGE_FILE_HEADER结构中的 NumberOfSections 域值决定。

其结构定义如下:

逆向-PE文件结构MS-DOS头PE文件头区块
逆向-PE文件结构MS-DOS头PE文件头区块

区块是PE文件的真正内容划分,每一块是拥有共同属性的数据的集合,块的种类和含义如下图:

逆向-PE文件结构MS-DOS头PE文件头区块

块在映像中按起始地址(RAV)排列,而非块名,所以块名在使用中无关紧要。

逆向-PE文件结构MS-DOS头PE文件头区块
逆向-PE文件结构MS-DOS头PE文件头区块

区块的合并和对齐:编译器会自动生成一些了标准区块,但程序员也可以通过

#pragma data_seg("xx")

来将所有C++处理的数据放到xx数据块内,而不是默认的data数据块。可以通过链接器把两个属性相同的区块合并,合并区块的优点是节约磁盘和内存空间,如下将text和rdata合并为text区块。

/MERGE:.rdata=.text

区块的大小需要对齐,一种是磁盘内的,一种是内存中的,两个值在PE文件头中指出,FlieAlignment定义了磁盘的对齐值,SectionAlignment定义了内存的对齐值。

#输入表

可执行文件使用其他DLL的代码或数据称为输入。

载入PE文件时,定位所有被输入的函数与数据,并让被载入文件使用那些地址,这个过程需要输入表(import table)来完成,表中保存函数名和驻留DLL等动态链接信息。当程序调用 DLL 中的函数时,编译器编译后的 CALL 指令并不会把控制权直接传给 DLL 中的函数,而是传给一个 JMP DWORD PTR [XXXXXXXX] 指令,后者位于.text 中,JMP 指令跳转到一个地址,此地址储存在.idata 的一个 DWORD之中。

如果加载器要调用 DLL 中的函数,首先必须知道所加载的程序调用了哪些DLL 以及 DLL 中的哪些函数,然后找出这些函数的地址,并把这些地址添入到.idata 的 DWORD 中。输入表(Import Table)的作用就是告诉加载器,程序调用了哪些 DLL 的哪些函数,并且通过 Export Table 找出这些函数的地址。

输入表在PE文件中的位置如图:

逆向-PE文件结构MS-DOS头PE文件头区块

输入表是一个 IMAGE_IMPORT_DESCRIPTOR (IID)结构数组,每个结构包含 PE文件引入函数的一个相关 DLL 的信息,最后有一个内容全为0的IID作为结尾,IID的结构如下:

逆向-PE文件结构MS-DOS头PE文件头区块

MAGE_IMPORT_DESCRIPTOR 结构体的第一项是一个 union。该成员是指向 输 入 名 称 表 ( Import Name Table , 简 称 INT ) 数 组 的 RVA 。 INT 是IMAGE_THUNK_DATA 结构的数组,每个 IMAGE_THUNK_DATA 对应于一个输入函数,这个 DWORD(即 IMAGE_THUNK_DATA union)的内容由文件是否被加载(加载前后内容不同)、函数是以名称输入还是以序号输入而定(输入方式不 同 其 内 容 也 不 同 ) 。 当 一 个 函 数 以 序 号 输 入 , EXE 文 件 的IMAGE_THUNK_DATA DWORD 中的最高位(0x80000000)设立。如果函数以名 称 输 入 , IMAGE_THUNK_DATA DWORD 就 内 含 一 个 RVA , 指 向IMAGE_IMPORT_BY_NAME 结 构 , 由 于 一 个 输 入 函 数 对 应 一 个IMAGE_THUNK_DATA union , 而 当 函 数 以 名 称 输 入 的 时 候 ,IMAGE_THUNK_DATA 又指向一个 IMAGE_IMPORT_BY_NAME 结构,所以此时 一 个 输 入 函 数 对 应 一 个 IMAGE_IMPORT_BY_NAME 结 构 。

#输入地址表

逆向-PE文件结构MS-DOS头PE文件头区块

载入器一一检阅每一个 IMAGE_THUNK_DATA ,然后进一步通过其他结构找出引入函数的地址。然后用引入函数真实地址来替代由 FirstThunk 指向的IMAGE_THUNK_DATA 数组里的元素值。因此当 PE 文件准备执行时,上图已转换成下图,这也证实了前面所说的 IMAGE_THUNK_DATA 结构的内容不仅和函数是按序号或者名字引入有关,而且和文件是否加载有关。由于此时 FirstThunk指 向 的IMAGE_THUNK_DATA 数 组 已 经 被 修 改 , 所 以 它 和IMAGE_IMPORT_BY_NAME 数组的关系也被切断。

逆向-PE文件结构MS-DOS头PE文件头区块

#输出表

创建DLL时实际上创建了能被其他DLL或EXE调用的函数,装载文件根据该输出修改被执行文件的导入地址表(IAT),DLL文件通过输出表向系统提供函数名,序号和入口地址等信息。

#基址重定位

PE 文件中的每个模块都有一个优先加载地址(ImageBase,也可以称作基地址),其值由连接器给出,连接器生成指令中的地址是相对于优先加载地址而言的,如果模块加载的起始地址不是优先加载地址,那么程序中的指令地址就需要修改,重定位表往往单独作为一块,用.reloc表示。(Q:那不初始化基地址的值即可?)

其原理图如下:

逆向-PE文件结构MS-DOS头PE文件头区块

继续阅读