PE文件结构学习总结

之前是基于数据来学习PE结构,学习的是PE结构的基础,但为了快速开发,应该使用winnt中提供的结构体来实现基于PE结构的相关功能,如内存载入DLL,内存注入,IAT HOOK等技术。

从文件头开始,即 0x0这个地址开始,遇到的会是一个DOS头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   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;

DOS头的最后一个指针 e_lfanew 指向的是一个PE头,其他数据是为DOS系统服务的,对PE结构毫无影响。(以上数据源于winnt.h)

PE头是个简单的结构体,就3个成员

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature; // PE标准
    IMAGE_FILE_HEADER FileHeader; //PE文件头
    IMAGE_OPTIONAL_HEADER32 OptionalHeader; //PE可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

PE文件头和PE可选头是两个结构体,分别如下:

PE文件头:

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine; //运行平台 0x014C = i386
    WORD    NumberOfSections; // Section的数量
    DWORD   TimeDateStamp; // 时间戳,不是重要数据
    DWORD   PointerToSymbolTable; // 调试相关
    DWORD   NumberOfSymbols; // 调试相关 符号表
    WORD    SizeOfOptionalHeader; // PE可选头的大小
    WORD    Characteristics; // 平台特征,如改文件是不是DLL文件等等特征
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

PE可选头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
typedef struct _IMAGE_OPTIONAL_HEADER {
// Standard fields.
WORD Magic; // 魔术字 0x010BH X32 0x020B X64
BYTE MajorLinkerVersion; // 链接程序的主版本号
BYTE MinorLinkerVersion; // 链接程序的次版本号
DWORD SizeOfCode; // 所有含代码段的节的总大小
DWORD SizeOfInitializedData; // 所有含未初始化数据的节的大小 (其他所有节的大小)
DWORD SizeOfUninitializedData; // // 所有含未初始化数据的节的大小
DWORD AddressOfEntryPoint; // 程序入口点
DWORD BaseOfCode; // 代码块的起始偏移
DWORD BaseOfData; //数据块的起始偏移
// NT additional fields.
DWORD ImageBase; // 模块基地址,一般DLL用
DWORD SectionAlignment; // 内存中的区块的对齐大小 通常 1000h
DWORD FileAlignment; // 文件中的区块的对齐大小 通常 200h
WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
WORD MajorImageVersion; // 可运行于操作系统的主版本号
WORD MinorImageVersion; // 可运行于操作系统的次版本号
WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号
WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号
DWORD Win32VersionValue; // 通常为0
DWORD SizeOfImage; // 映像装入内存后的总尺寸
DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小
DWORD CheckSum; // 映像的校检和 可以用于检测是否被动过 CheckSumMappedFile 函数可以用来计算一次校验和
WORD Subsystem; // 可执行文件期望的子系统
WORD DllCharacteristics; // DllMain()函数何时被调用,默认为 0,已废弃
DWORD SizeOfStackReserve; // 初始化时的栈大小
DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
DWORD LoaderFlags; // 与调试有关,默认为 0
DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录项
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

PE可选头中的数据目录项,16个成员的数组,每个成员结构相对简单,就包含2个成员:数据目录的起始地址和大小:

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 数据起始地址 RVA格式
DWORD Size; // (目录数量 +1) x 目录结构大小 最后一个是000000表示结束
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

现在到了最关键的时候了,16个数据目录项,由于前面PE文件头中指定了PE可选头的大小,所以数据目录项后面紧挨着个区块表。

区块表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 块的名字 如.text 大小固定为8,并不以00结尾,!!小心字符创溢出
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //虚拟内存区块数据或代码的实际大小
} Misc;
DWORD VirtualAddress; //虚拟地址 RVA地址
DWORD SizeOfRawData; //块的大小,通常为文件块基数 200h的倍数
DWORD PointerToRawData; //磁盘文件偏移地址 FOA地址
DWORD PointerToRelocations; //OBJ文件用的重定位
DWORD PointerToLinenumbers; //调试所用
WORD NumberOfRelocations; //OBj文件重定位数目
WORD NumberOfLinenumbers; //行号数目
DWORD Characteristics; //区块属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

区块表中保存的是区块的信息,即文件块到内存块的地址映射关系,前面有一个PE可选头的选项叫SizeOfHeaders

除了从0X00到 SizeOfHeaders 的内存区域是PE头,其他的都是以区块表划分的内存块

所以就有了RVA 和 FOA 的区别 ,FOA 指的是 在PE文件中的地址,RVA 指的是在内存中的地址。

为了方便PE解析,所以我们EXE程序中存储的都是RVA,但如果我们想对一个文件直接进行修改,那么RVA转FOA的计算就不可避免了。

首先现将区块表给大家介绍清楚了再往下。

至此,PE结构基本结束,但是FOV 和 RVA的转换是干嘛的呢,想到这里呢,我们似乎把数据目录表给忘了。

数据目录表中存的数据起始地址是RVA形式存在的,我们若想写一个DLL加载器,在装入内存后,再直接根据RVA去修复IAT表和重定位表就可以了

但如果我们想写一个PE解析工具,想在载入内存之前从文件中读取这些数据就需要计算FOV和RVA了

(其实也不用这么费劲,直接写工具的时候,根据块表,把数据装入内存就可以读取处导入导出表等信息了,这里是为了学习,就折腾一下)

RVA -> FOA 计算步骤:

第一步: 判断该RVA地址在哪个数据块当中。
第二步: RVA地址 - 该块内存起始地址 + 该块文件起始地址。

FOA -> RVA 计算步骤:

第一步: 判断FOA地址在哪个数据块当中。
第二部: FOA地址 - 该块FOA起始地址 + 该块内存起始地址

现在开始真正学习数据目录表了,一共16个数据目录表,索引如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor

1.导出表

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 未使用,总为0
DWORD TimeDateStamp; // 文件创建时间戳
WORD MajorVersion; // 主版本,未使用,总为0
WORD MinorVersion; // 次版本,未使用,总为0
DWORD Name; // 指向一个代表此 DLL名字的 ASCII字符串的 RVA
DWORD Base; // 函数的起始序号
DWORD NumberOfFunctions; // 导出函数的总数
DWORD NumberOfNames; // 以名称方式导出的函数的总数 和上面的数量必须一致
DWORD AddressOfFunctions; // 指向输出函数地址的RVA
DWORD AddressOfNames; // 指向输出函数名字的RVA
DWORD AddressOfNameOrdinals; // 指向输出函数序号的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

DLL函数导出有2种方式,1是通过序号导出,2是通过名字导出(都要学一下,玩意别人故意用了序号导出,不是又得重新学一遍了吗)

通过序号导出方式,查到DLL函数入口:

1.获取到序号,将序号 - base ,得到真正的索引值
2.判断索引值是否 < NumberOfFunctions
3.条件成立则通过 AddressOfNameOrdinals[index]定位到函数地址

使用方法: GetProcAddress(hlib, (LPCTSTR)1)

通过函数名导出方式,查找函数入口:

1.在 AddressOfNames 数组中遍历函数名,若匹配,得到索引值x1
2.利用索引值x1,在AddressOfNameOrdinals[x1]处得到索引值x2
3.利用索引值x2,在AddressOfFunctions[x2]处获得函数地址

2.导入表

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; //指向输入名称表的表(INT)的RVA(他不是INT的RVA,他是个指向指针的指针)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; // 导入的DLL名称
DWORD FirstThunk; // 指向输入地址表的表(IAT)的RVA
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
1
2
3
4
5
6
7
8
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;//ordinal
BYTE Name[1];//function name string
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

OriginalFirstThunk指向的 INT表是一个指针数组,每个成员指向一个函数名字符创,以00000000作为结束(暂时先不要用64位程序做实验,差点没反应过来)

FirstThunk 指向的IAT表 内容刚开始与INT表内容相同,需要我们去修复他,根据INT表,获取导入函数的地址,后将地址写入IAT表中

3.资源表

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; //理论上为资源的属性,不过事实上总是0
DWORD TimeDateStamp;
WORD MajorVersion; //理论上为资源的版本,不过事实上总是0
WORD MinorVersion;
WORD NumberOfNamedEntries; //以名称(字符串)命名的入口数量
WORD NumberOfIdEntries; //以ID(整型数字)命名的入口数量
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

包括窗口信息,图片资源等 都存放在资源表当中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31; //资源名偏移
DWORD NameIsString:1; //资源名为字符串
} DUMMYSTRUCTNAME;
DWORD Name; //资源/语言类型
WORD Id; //资源数字ID
} DUMMYUNIONNAME;
union {
DWORD OffsetToData; //数据偏移地址
struct {
DWORD OffsetToDirectory:31; //子目录偏移地址
DWORD DataIsDirectory:1; //数据为目录
} DUMMYSTRUCTNAME2;
} DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

资源表 有2个部分:
_IMAGE_RESOURCE_DIRECTORY 主要是资源数量
_IMAGE_RESOURCE_DIRECTORY_ENTRY 是上述资源数量个资源表

如果 NameIsString == 1 ,字符串生效,否则ID生效。

根据 OffsetToData 最高位,判断是子目录还是根目录,若最高位为1,则为根目录。

如果是数据目录,则从头再来一遍,得到的偏移,指向的会是 _IMAGE_RESOURCE_DIRECTORY这个结构,后面还是跟着 资源数量 x _IMAGE_RESOURCE_DIRECTORY_ENTRY

如果还是目录,接着往下找,若出现最高位为0,则会得到一个 _IMAGE_RESOURCE_DATA_ENTRY 结构

1
2
3
4
5
6
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; //资源数据的RVA
DWORD Size; //资源数据的长度
DWORD CodePage; //代码页
DWORD Reserved; //保留字段
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

第一层资源目录的资源ID决定着资源的类型。

ID 类型
0x01 光标
0x02 位图
0x03 图标
0x05 对话框
0x06 字符串
0x0c 光标组
0x0e 图标组
0x10 版本
0x18 其他ID

4.异常处理表

5.安全证书表

6.重定位表

typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // 重定位地址
DWORD SizeOfBlock; // 结构体大小
// WORD TypeOffset[1]; // 重定位地址数组 高4位固定为 0011 低12位位重定位偏移
} IMAGE_BASE_RELOCATION;

重定位表结构简单,计算方法:

重定向地址值 = 重定向地址值 - imagebase +当前模块实际地址

7.调试信息表

8.IMAGE_DIRECTORY_ENTRY_COPYRIGHT

9.IMAGE_DIRECTORY_ENTRY_GLOBALPTR

10.线程级局部存储目录(TLS)

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData; // TSL初始化数据在内存中的起始地址
DWORD EndAddressOfRawData; // TSL初始化数据在内存中的结束地址
DWORD AddressOfIndex; // 存储TLS索引的位置
DWORD AddressOfCallBacks; // 指向TLS注册的回调函数的函数指针数组
DWORD SizeOfZeroFill; // 用于指定非零初始化数据后面的空白空间的大小
DWORD Characteristics; // 保留
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;

11.IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG

12.绑定导入表

1
2
3
4
5
6
7
//最后一个结构全0表示绑定导入表结束
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp; //表示绑定的时间戳,如果和PE头中的TimeDateStamp不同则可能被修改过
WORD OffsetModuleName; //dll名称地址
WORD NumberOfModuleForwarderRefs; //依赖dll个数
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;

13.导入表

其实就是导入表的IAT,只是描述的是所有的IAT

14.延迟导入表

15.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR

至此,PE结构基本解析完毕,荒废了好久的PE结构,终于又复习了起来。

下一步应该是常见的反调试技巧以及Windows相关知识了。