前言
本博客系统讲解了PE文件结构。
PE文件结构其实不复杂,但内容较多,希望朋友们能“沉心静气做学问”。
目录
- 前言
- 1 PE文件及其表现形式<span id="1"/>
- 2 PE文件格式与恶意软件的关系<span id="2"/>
- 3 PE文件格式的总体结构<span id="3"/>
- 3.1 PE文件原始数据
- 3.2 相关恶意代码分析工具
- 3.3 DOS-MZ文件头(0x40)+DOS Stub(IMAGE_DOS_HEADER/MS-DOS Stub Program)
- 3.4 PE文件头(IMAGE_NT_HEADERS)
- 3.5 节表(区块表)(IMAGE_SECTION_HEADER)
- 3.6 区块(节)
- 3.7 代码节 .text<span id="4"/>
- 3.8 已初始化的数据节 .data
- 3.9 未初始化的数据节 .bbs
- 3.10 引入函数节 .rdata:PE文件的引入函数机制<span id="5"/>
- 3.11 引出函数节 .edata:DLL文件的函数引出机制<span id="6"/>
- 3.12 资源节 .rsrc:文件资源索引、定位与修改<span id="7"/>
- 3.13 重定位节 .reloc :镜像地址改变后的地址自动修正<span id="8"/>
- 4 思考题
- 5 实验题
1 PE文件及其表现形式
-
可移植的可执行文件(PE,Portable Executable File),
Win32平台可执行文件使用的一种格式
。 -
其他EXE文件格式:
DOS
:MZ格式Windows 3.0/3.1
:- NE,New Executable
- 16位Windows可执行文件格式
-
可执行程序的不同形态(以QQ为例)
-
用户眼中的QQ.
-
本质上:
-
2 PE文件格式与恶意软件的关系
-
何为文件感染?[或控制权获取]
- 使目标PE文件
具备[或启动]病毒功能[或目标程序]
- 但
不破坏
目标PE文件原有
功能和外在形态(如图标)等
- 使目标PE文件
-
病毒代码如何与目标PE文件融为一体?
- 代码植入
需要了解PE文件中哪里可以植入代码。 - 控制权获取
- 图标更改
- …
- 代码植入
3 PE文件格式的总体结构

PE文件至少包含两个段,即数据段和代码段
。Windows NT 的应用程序有9个预定义的段,分别为 .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,这些段并不是都是必须的
,当然,也可以根据需要定义更多的段(比如一些加壳程序)
。
在应用程序中最常出现的段有以下6种:
中文名 | 英文名 |
---|---|
.执行代码段 | 通常 .text (Microsoft)或 CODE(Borland)命名; |
.数据段 | 通常以 .data 、.rdata 或 .bss(Microsoft) |
.资源段 | 通常以 .rsrc命名 |
.导出表 | 通常以 .edata命名 |
.导入表 | 通常以 .idata命名 |
.调试信息段 | 通常以 .debug命名 |
3.1 PE文件原始数据
使用UltraEdit打开之后看到的是PE文件的十六进制代码。
以text.exe为例:
3.2 相关恶意代码分析工具
看雪学院论坛可以下载。

3.2.1 PE文件格式查看工具1-PEView
可按照PE文件格式对目标文件的各字段
进行详细解析。
3.2.2 PE文件格式查看工具2-Stud_PE
可按照PE文件格式对目标文件的各字段
进行详细解析。与 PEView
功能类似。

使用前的设置
设置之后可以使用右键用Stud_PE打开PE文件。
3.2.3 PE程序调试工具-Ollydbg
可跟踪
目标程序的执行过程,属于用户态调试工具。
无法调试内核程序。

使用前的设置
设置之后可以使用右键用Ollydbg打开PE文件。


3.2.4 16进制文件编辑工具-UltraEdit
可对目标文件进行16进制查看和修改
。

3.3 DOS-MZ文件头(0x40)+DOS Stub(IMAGE_DOS_HEADER/MS-DOS Stub Program)
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // ChecksumWORD e_ip; // Initial IP valueWORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
所有的PE文件都是以一个64字节的DOS头(MZ文件头)开始。这个DOS头只是为了兼容早期的DOS操作系统。
- 该结构体中需要掌握的字段只有 2 个,分别是第一个字段
e_magic
和最后一个字段e_lfanew
字段e_magic
字段:
DOS 可执行文件的标识符,占用 2 字节。该位置保存着的字符是“MZ”。
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
e_lfanew
字段:
保存着PE头的起始位置。
- 作用:
定位PE文件头开始位置
,也可用于PE文件合法性检测
- DOS系统下运行PE文件时,将提示用户:“This program cannot be run in DOS mode”!

更正:上图中的“PE文件”表述应为“PE文件头"。特此说明。”
- 使用PEView上图内容:
3.4 PE文件头(IMAGE_NT_HEADERS)
IMAGE_NT_HEADERS是一个宏,其定义如下:
#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#define IMAGE_FIRST_SECTION(ntheader) IMAGE_FIRST_SECTION64(ntheader)
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#define IMAGE_FIRST_SECTION(ntheader) IMAGE_FIRST_SECTION32(ntheader)
#endif
该头分为32位和64位两个版本,其定义依赖于是否定义了_WIN64。这里只讨论32位的PE文件格式,来看一下IMAGE_NT_HEADERS32的定义,如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //该结构体中的Signature就是PE标识符,标识该文件是否是PE文件。该部分占4字节,即“50 45 0000”。
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
如何简单判断一个文件是否为PE文件?
首先要判断DOS头部的开始字节是否是“MZ”。如果是“MZ”头部,则通过DOS头部找到PE头部,接着判断PE头部的前四个字节是否为“PE\0\0”。如果是的话,则说明该文件是一个有效的PE文件。

可以看到,开始位置正是 00B0h 。
- PE header 由三部分组成【开始于000000B0】
字串“PE\0\0”(Signature)
:
以此识别给定文件是否为有效PE文件。映像文件头(FileHeader)
:
结构域包含了关于PE文件物理分布的信息。可选映像头(OptionalHeader)
3.4.1 字串“PE\0\0”
以此识别给定文件是否为有效PE文件。
- Signature 一 dword类型,值为
50h, 45h, 00h, 00h
(PE\0\0)。
- 本域为PE标记,
可以此识别给定文件是否为有效PE文件
。
但有时仅仅凭字串是不够的。
- 本域为PE标记,
3.4.2 文件头-IMAGE_FILE_HEADER(映像文件头)
文件头结构体IMAGE_FILE_HEADER
是IMAGE_NT_HEADERS
结构体中的一个结构体,紧接在PE标识符的后面。IMAGE_FILE_HEADER结构体的大小为20字节,起始位置为0x000000CC,结束位置在0x000000DF。结构域包含了关于PE文件物理分布的信息。


- 该结构域包含了关于PE文件物理分布的信息
- 比如
节数目
、后续可选文件头大小
、机器类型
等。
- 比如
- 映像文件头的结构
包含了PE文件的一些基础信息。
//
//File header format.
//
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
#define IMAGE_SIZEOF_FILE_HEADER 20


- Machine: 该字段是WORD类型,占用2字节。该字段表示可执行文件的目标CPU类型。
- NumberOfSections:该字段是WORD类型,占用两个字节。该字段表示PE文件的节区的个数。
- TimeDataStamp:该字段表明文件是何时被创建的,这个值是自1970年1月1日以来用格林威治时间计算的秒数。
- PointerToSymbolTable:该字段很少被使用,这里不做介绍。
- NumberOfSymbols:该字段很少被使用,这里不做介绍。
- SizeOfOptionalHeader:该字段为WORD类型,占用两个字节。该字段指定IMAGE_OPTION AL_HEADER结构的大小。注意,在计算IMAGE_OPTIONAL_HEADER的大小时,应该从IMAGE_FILE_HEADER结构中的SizeOfOptionalHeader字段指定的值来获取,而不应该直接使用 sizeof ( IMAGE_OPTIONAL_HEADER )来计算。
- Characteristics:该字段为WORD类型,占用2字节。该字段指定文件的类型。
3.4.3 可选头-IMAGE_OPTIONAL_HEADER(可选映像头)(可选文件头)
IMAGE_OPTINAL_HEADER在几乎所有的参考书中都被称作“可选头”。虽然被称作可选头,但是该头部不是一个可选的,而是一个必须存在的头,不可以没有。该头被称作“可选头”的原因是在该头的数据目录数组中,有的数据目录项是可有可无的,数据目录项部分是可选的,因此称为“可选头”。它定义了PE文件的很多关键信息。大小可从 文件头-IMAGE_FILE_HEADER 中得知。可选头紧挨着文件头,文件头的结束位置在 0x000000DF,那么可选头的起始位置为0x000000E0。

IMAGE_OPTIONAL_HEADER是一个宏,其定义如下:
#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64 PIMAGE_OPTIONAL_HEADER;
#define IMAGE_SIZEOF_NT_OPTIONAL_HEADER IMAGE_SIZEOF_NT_OPTIONAL64_HEADER
#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR64_MAGIC
#else
typedef IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32 PIMAGE_OPTIONAL_HEADER;
#define IMAGE_SIZEOF_NT_OPTIONAL_HEADER IMAGE_SIZEOF_NT_OPTIONAL32_HEADER
#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR32_MAGIC
#endif
32位版本和64位版本的选择是根据是否定义了_WIN64而决定的,这里只讨论其32位的版本。IMAGE_OPTIONAL_HEADER32的定义如下:
//
//Optional header format.
//
typedef struct _IMAGE_OPTIONAL_HEADER {
//
//Standard fields.
//
WORD Magic;BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
//NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
-
可选头的定位:
- 起始位置:
可选头的定位有一定的技巧性,起始位置的定位相对比较容易找到,按照 PE 标识开始寻找是非常简单的。 - 结束位置:
可选头的结尾后面跟的是第一项节表的名称,所以可选头的结束位置就在.text节的前一个位置。
- 起始位置:
-
定义了PE文件的很多关键信息
可选头是对文件头的一个补充。文件头主要描述文件的相关信息,而可选头主要用来管理 PE 文件被操作系统装载时所需要的信息。该头同样有 32 位版本与 64 位版本之分。
- 程序入口点(代码从哪里开始执行?)
即ImageBase+RVA
。Relative Virtual Address
(RVA
),相对虚拟地址,它是相对内存中ImageBase的偏移位置
,可由此得知PE文件真正的装载地址。
一般为
ImageBase+ RVA
,即00400000h+RVA
。- 内存镜像加载地址(ImageBase)
PE文件在内存中的优先装载地址
。对于大多数程序都是 00400000h和77F40000h。 - 节在文件和内存中的对齐粒度
比喻:桶的容量为100升,现有367升水,请问需要使用多少个桶?4个。- 问题:代码节的代码实际长度为0x46字节,在内存和文件中占多少字节?
- 内存中节对齐粒度为
0x1000
字节,故占4096字节。
(勘误:抱歉,上图中的“1000”应该改为“4096”,即占了4096字节。) - 文件中节对齐粒度为
0x200
字节,故占512字节。
- 内存中节对齐粒度为
- 问题:代码节的代码实际长度为0x46字节,在内存和文件中占多少字节?
正因为
对齐粒度
的存在,PE文件中才有有很多“00”
字节(或是“CC”
字节)。- 本程序在
内存中的镜像大小
、文件头大小
- 程序入口点(代码从哪里开始执行?)
如何修改程序的入口地址?
本小节为实际操作,讲述如何修改程序的入口地址
。
-
AddressOfEntryPoint
的作用
-
实际操作:
问题:是否可以修改AddressOfEntryPoint指向任意代码?
回答:可以的。下面进行演示:
在D8H处的4个字节处修改1000H为1016H:
更正:上图文字应为“将地址D8h处的 00 10修改成16 10之后...”。
- 修改:
使用UltraEdit修改
- 结果:
此时程序入口地址已变成 00401016h !
DataDirectory - 数据目录表
DataDirectory是可选映像头的最后128个字节(16项 * 8 bytes)
,也是IMAGE_NT_HEADERS(PE文件头)的最后一部分数据。
它由16个IMAGE_DATA_DIRECTORY结构组成的数组构成,指向输出表、输入表、资源块、重定位
等数据目录项的RVA(相对虚拟地址)和大小。
IMAGE_DATA_DIRECTORY的结构如下:
//
//Directory format.
//
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据块的起始RVA
DWORD Size; //数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
- 使用 PEView 查看数据目录表:
读图可知Import table(输入表)
的地址为 00402014h ,大小为 003Ch 。
- 使用 LordPE 查看数据目录表:
注意。下图所查看的exe文件与上图查看的exe文件是不同的。
在数据目录中,并不是所有的目录项都会有值,很多目录项的值都为 0。因为很多目录项的值为0,所以说数据目录项是可选的。
可选头的结构体介绍完了,各位可以按照该结构体中各成员变量的含义自行学习可选头中的十六进制值的含义。只有参考结构体的说明去对照分析PE文件格式中的十六进制值,才能更好、更快地掌握PE结构。
3.5 节表(区块表)(IMAGE_SECTION_HEADER)

- 在PE文件头与原始数据之间存在一个区块表(Section Table),它是一个IMAGE_SECTION_HEADER结构数组,
区块表包含每个块在映像中的信息
(如位置、长度、属性),分别指向不同的区块实体。
举例:test.exe的代码节表(.text)
- 全部有效结构
最后以一个空的IMAGE_SECTION_HEADER结构作为结束
,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一
。 - 另外,节表中 IMAGE_SECTION_HEADER 结构的总数总是由PE文件头
IMAGE_NT_HEADERS->FileHeader.NumberOfSections
字段来指定的。(因为节表的个数是节的个数+1)
3.5.1 IMAGE_SECTION_HEADER 的结构定义
typedef struct _IMAGE_SECTION_HEADER {
Name //8个字节的块名
union
{
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; //区块尺寸</span>
DWORD VirtualAddress; //区块的RVA地址
DWORD SizeOfRawData; //在文件中对齐后的尺寸
DWORD PointerToRawData; //在文件中偏移
DWORD PointerToRelocations; //在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers; //行号表的偏移(供调试使用地)
WORD NumberOfRelocations; //在OBJ文件中使用,重定位项数目
WORD NumberOfLinenumbers; //行号表中行号的数目
DWORD Characteristics; //区块属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
IMAGE_SECTION_HEADER 的成员名称 | IMAGE_SECTION_HEADER 的成员意义 |
---|---|
Name | 这是一个8位的ASCII(不是Unicode内码),用来定义块名,多数块名以,开始(如.Text),这个实际上不是必需的,注意如果块名超过了8个字节,则没有最后面的终止标志NULL字节,带有 的区块的名字会从编译器里将带有 的区块的名字会从编译器里将带有 的区块的名字会从编译器里将带有的相同名字的区块被按字母顺序合并。 |
VirtualSize | 指出实际的,被使用的区块大小,是区块在没有对齐处理前的实际大小.如果VirtualSize < SizeOfRawData,那么SizeOfRawData是可执行文件初始化数据的大小(SizeOfRawData – VirtualSize)的字节用0来填充。这个字段在OBJ文件中被设为0。 |
VirtualAddress | 该块时装载到内存中的RVA,注意这个地址是按内存页对齐的,她总是SectionAlignment的整数倍,在工具中第一个块默认RVA为1000,在OBJ中为0。 |
SizeofRawData | 该块在磁盘中所占的大小,在可执行文件中,该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。 |
PointerToRawData | 该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。 |
PointerToRelocations | 在PE中无意义 |
PointerToLinenumbers | 行号表在文件中的偏移值,文件调试的信息 |
NumberOfRelocations | 在PE中无意义 |
NumberOfLinenumbers | 该块在行号表中的行号数目 |
Characteristics | 块属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志: |
- Characteristics-节属性
- 使用Stud_PE查看:
- 使用Stud_PE查看:
3.6 区块(节)
每个区块的名称都是唯一的,不能有同名的两个区块
。
但事实上节的名称不表示任何含义
,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” (一般为.text)或者说将包含数据的区块命名为“.Code”(一般为.rdata等) 都是合法的。
当我们要从PE 文件中读取需要的区块的时候,不能以区块的名称作为定位的标准和依据
,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位
。
疑问: 可是 IMAGE_OPTIONAL_HEADER32 中并没有各区块的地址啊,咋定位?
3.6.1 区块名称及其意义

- 常见的区块(节)
- 代码节
- 数据节
- 引入函数节
- 资源节等(如图标)
- 引出函数节(DLL文件中常见)
- 重定位节(DLL文件中常见)
3.6.2 文件偏移和RVA
由于一些PE文件为减少体积,磁盘对齐值不是一个内存页 1000h,而是 200h,当这类文件被映射到内存后,同一数据相对于文件头的偏移量在内存中和磁盘文件中是不同的,这样就存在着文件偏移地址与虚拟地址的转换
问题。

从这张图可以看到:
- DOS头部的起始地址(基地址)变成了00400000h
- 块表与.text块之间用0填充了,因为.text块的起始地址变成了00401000h
- .text块.rdata块、.data块三者之间用0填充了,因为要保证每一块的大小都是1000h
3.7 代码节 .text

- 代码节一般名为
.text
或CODE,该节含有程序的可执行代码
每个PE文件都有代码节
3.8 已初始化的数据节 .data

- 这个节一般取名为
.data
或DATA - 已初始化的数据节中放的是在编译时刻就已确定的数据。
如test.exe中的字符串“PE入口点测试1:进入第 一入口位置00401000H!”

3.9 未初始化的数据节 .bbs
- 节名称一般叫
.bbs
- 这个节里放有未初始化的全局变量和静态变量。
- 例如“static int k;”
3.10 引入函数节 .rdata:PE文件的引入函数机制
函数引入机制。
3.10.1 引入目录表 (输入表)(IMPORT Directory Table)


-
如何从PE文件定位到引入目录表(IDT)的起始位置?
PE文件可选文件头的DataDirectory
。
-
引入目录表由一系列的
IMAGE_IMPORT_DESCRIPTOR结构
组成- 结构的数量取决于程序要使用的DLL文件的数量,
每一个 IMAGE_IMPORT_DESCRIPTOR 结构对应一个DLL文件
- 在所有这些结构的最后,由一个
内容全为0
的IMAGE_IMPORT_DESCRIPTOR
结构作为结束
- 结构的数量取决于程序要使用的DLL文件的数量,
-
引入目录表的组成部分-
IMAGE_IMPORT_DESCRIPTOR结构
是咋样的?- IMAGE_IMPORT_DESCRIPTOR 的数据结构
其中 OriginalFirstThunk 和 FirstThunk 得特别说明一下:
- OriginalFirstThunk 和 FirstThunk
- 其中
FirstThunk
指向引入函数节(.data)中的引入名字表
(Import Address Table,与引入目录表平级) - 其中
OriginalFirstThunk
指向引入函数节(.data)中的引入地址表
(Import Name Table,与引入目录表平级) - 都指向一个包含一系列
IMAGE_THUNK_DATA
结构的数组
这个所谓的“数组”其实就是引入名字表
和引入地址表
。- 每个
IMAGE_THUNK_DATA
结构定义了一个
导入函数(注意不是一个.dll)的信息,实际为一个双字
,不同时刻可能代表不同含义。 - 以一个内容为
0
的IMAGE_THUNK_DATA
结构作为结束。
- 每个
- 其中
OriginalFirstThunk和FirstThunk :
在文件中:它们所指向IMAGE_THUNK_DATA结构的数组所有值都是相同
的;
在内存中:FirstThunk
所指向IMAGE_THUNK_DATA结构的数组的值会改变
。
(下图是PE文件中的数据,不是内存中的,看得到 OriginalFirstThunk 和 FirstThunk 指向的数据都是相同的。)
注意:
一个OriginalFirstThunk或FirstThunk只指向一个.dll。上图每一个红框中都含有两个.dll,所以分别由两个OriginalFirstThunk或FirstThunk指向其对应红框。- 一个IMAGE_THUNK_DATA结构数组(上图一个红框中有两个数组)为一个.dll中的所有导入函数数据:
如:Data 0002064 + Data 00000000(Value 0080 ExitProcess + Kenel32.dll) - 一个IMAGE_THUNK_DATA结构是一个导入函数数据(双字):
如:Data 00002064 (Value 0080 ExitProcess)
补充:IMAGE_THUNK_DATA结构
一个IMAGE_THUNK_DATA结构实际上就是一个双字,它在不同时刻有不同的含义。
- IMAGE_IMPORT_DESCRIPTOR 的数据结构
从上面内容可以总结出引入目录表中的从属关系(由高到低排序)
.rdata
↑
IMPORT Directory Table
↑
IMAGE_IMPORT_DESCRIPTOR
↑
OriginalFirstThunk和FirstThunk
(O…和 F…均指向一个包含一系列 IMAGE_THUNK_DATA
结构的数组(引入名字表和引入地址表))
3.10.2 引入名字表(IMPORT Name Table)
是上一节中的IMAGE_THUNK_DATA结构数组
。

实际操作:将通过函数名
引入函数改成通过序号
引入函数
1、
如图,将其修改为80002064
。(00002064 -> 80002064)
2、
通过序号引入函数
时无法在kenel32.dll中找到序号为8292(2064h)
的函数,故程序无法执行。
3、
经过查找,函数ExitProcess序号为183(B7h)
。
4、
5、
现在程序正常运行!
3.10.3 IMPORT Hints/Names &DLL names
当IMAGE_THUNK_DATA结构(双字)的最高位为0
时,表示函数以字符串类型的函数名
方式输入,这时双字的值是一个RVA
,指向一个(IAMGE_IMPORT_BY_NAME)结构,该结构定义如下:
STRUCT IAMGE_IMPORT_BY_NAME
{
DWORD Hint; //本函数在其所驻留DLL的输出表中的序号
BYTE name //输入函数的函数名,函数名是一个ASCII码字符串,以NULL结尾
};

- IMAGE_IMPORT_BY_NAME结构
- 80 00为Hints,ExitProcess为引入函数名
- 62 02 为Hints,wsprintfA为引入函数名
- 9D 01 为Hints,MessageBoxA为引入函数名
- DLL names字符串
- kernel32.dll 为dll文件名
- user32.dll 为dll文件名
3.10.4 引入地址表 IAT ( IMPORT Address Table)

- DWORD数组[
可通过可选文件头中的DataDirectory的第13项定位
]- 在文件中时,其内容与
Import Name Table
完全一样。 - 在内存中时,每个双字中存放着对应引入函数的地址。
此时这些值与其装入内存前(在文件中)的值一般会有所不同
。
- 在文件中时,其内容与
3.11 引出函数节 .edata:DLL文件的函数引出机制
引出函数节一般名为.edata
,这是本文件向其他程序提供调用函数的列表,函数所在的地址及具体代码实现的区块。有时合并入.text节
(如下图)。
- 关键结构:引出目录表(导出表、输出表)
3.11.1 引出目录表(导出表、输出表)IMAGE_EXPORT_DIRECTORY

- 上图的解析:
Base
一般为1
,函数的序号等于Base+引出序号表里的值
。
如何定位 Export Directory Table-引出目录表
DataDirectory第一项。
3.11.2 导出地址表 -EXPORT ADDRESS Table

导出地址表的data两种含义
-
dwExportRVA
- 指向导出地址
-
dwForwarderRVA
- 指向另外一个DLL中的某个API函数名。
- 举例:Kernel32.AddVectoredExceptionHandler → NTDLL.RtlAddVectoredExceptionHandler
3.11.3 导出名字表 -EXPORT Name Table

3.11.4 导出序号表 -EXPORT Oridinal Table
该表保存的是各导出函数的函数地址在导出地址表的序号
,但序号并不是data中的值,而是待寻找函数在导出序号表排第几个,例如AddAtomA()的排第二
个所以序号是2
。

为何需要导出序号表?
导出函数名字和导出地址表中的地址不是一一对应关系
。
- 一个函数实现可能有多个名字
- 某些函数没有名字,仅通过序号导出
3.11.5 如何通过函数名定位函数导出地址?

先通过AddressOfNames
查到到函数名,然后通过AddressOfNameOrdinals
查找到函数序号,再通过AddressOfFunctions
找到函数的RVA。
通过函数名定位函数的地址,下面给出实际操作步骤:
任务:查找HashData函数。
操作:按照下图的顺序操作,一共分为4
步
-
查找
导出表
起始地址:
-
通过
AddressOfNames
查找函数对应的序号
:
-
通过
AddressOfNameOrdinals
和序号
查找函数的索引值
。

- 通过
AddressOfFunctions
和索引值
找到函数的RVA
:
3.12 资源节 .rsrc:文件资源索引、定位与修改
-
资源节一般名为
.rsrc
-
这个节放有如图标、对话框等程序要用到的资源
-
资源节是树形结构的,它有一个主目录,主目录下又有子目录,子目录下可以是子目录或数据。
通常有3层目录(资源类型、资源标识符、资源语言ID),第4层是具体的资源
-
3个重要结构
目录
是IMAGE_RESOURCE_DIRECTORY
结构
-
目录项
是IMAGE_RESOURCE_DIRECTORY_ENTRY
结构
-
数据项
是IMAGE_RESOURCE_DATA_ENTRY
结构
以PEView.exe为例(我自己分析我自己)

3个重要结构
-
如何定位资源目录位置?
可选文件头(也称可选映像头)的DataDirectory项。 -
如何定位资源?
-
资源节的部分应用
- 图标修改
- 汉化
- …
3.13 重定位节 .reloc :镜像地址改变后的地址自动修正

在exe文件中一般没有,但在dll文件中基本都会有。
- 重定位节存放了一个重定位表,若装载器不是把程序装到程序编译时默认的基地址时,就需要这个重定位表来做一些调整
- 重定位节以
IMAGE_BASE_RELOCATION
结构开始
从上图我们能可以看到,定位项的数量是
不定
的。
VirtualAddress
:是一个4KB(一页)的边界。该值加上后面的TypeOffset数组的成员便得到了需要重定位数据的地址。SizeBlock
:为这一结构块的大小。该大小减去前两项(VirtualAddress和SizeBlock本身)的字节数8便得到了第3项(下一项,也就是重定位项数组TypeOffset[]
)的大小,再除以2(因为重定位项
大小为2个字节)即得到了重定位项
的个数。重定位项
:每项都是16位的,其中的最高4
位代表了所需要的重定位类型
,剩下的12
位代表了页面中重定位地址的偏移量(delta)
。重定位的类型
MAGE_REL_BASED_HIGHLOW(3)
将偏移量(delta)
添加到原来的偏移位置(RVA)
的32位字段上,它是32位地址重定位的首选
类型。

更正:上图中的“定位项”表述应为“重定位项数组”。
观察上图,可以发现重定位项数组
中最高4
位的值是3
(代表MAGE_REL_BASED_HIGHLOW(3)
),剩下的12
位就是偏移量(delta)
。
- 为什么需要重定位?
PE文件中部分数据是以VA地址存储的,当PE文件无法加载到预期ImageBase
时,这些地址就需要修正。
参考资料:
云课堂武大慕课
PE文件格式分析 https://blog.csdn.net/shitdbg/article/details/49734495
4 思考题
用于验收PE文件结构学习情况。

如何判断目标程序是否为合法PE文件?
使用PEView找到PE文件头(IMAGE_NT_HEADERS)的字串(Signature)
若其开头双字内容为 “50 45 00 00
”,则说明给定文件是有效PE文件。
如果不使用引入函数节,如何使用外部DLL中的API函数?
暂时还不会。下次一定补上。
Kenel32.dll提供了GetProcAddress函数,用于获取指定函数的地址。该函数的具体是怎样实现的?
暂时还不会。下次一定补上。
熊猫烧香病毒感染其他PE文件后,目标文件图标会变成容易被用户差距的熊猫图案,为什么?如何解决问题?
5 实验题
