0x1 前言

本篇文章会详细讲述Data Directory(数据目录)、Section Headers(节头)、Section(节),从概念、作用到具体的结构体定义,话不多说开始正文

0x2 Data Directory

Data Directory是PE Optional Header中的最后一个成员,本质是IMAGE_DATA_DIRECTORY结构体数组,定义如下

1
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

其中IMAGE_NUMBEROF_DIRECTORY_ENTRIES是一个常量,值是16,结构体定义如下

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

这是一个简单的结构体,只有2个成员,第一个成员存储地址,第二个成员存储大小

那Data Directory是什么?简单说,它存储的是一段特定功能数据的索引和大小,注意这段特定功能数据不等于节,不要和节搞混,它是节中的一段数据,一共有16个索引,比如说导出表这段特定功能的数据,用于对外暴露可被外部调用的函数,它位于.rdata节(只读初始化数据节),全部索引及特定功能如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 导出表
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 导入表
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // 资源表
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // 异常表
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // 安全目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // 重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // 调试目录
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // 架构相关
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // 全局指针
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // 加载配置
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // 绑定导入表
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // 导入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // 延迟导入表
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM描述符
保留 15 // 保留

上述16段数据,经常用到的有导出表、导入表、重定位表、其中导入表位于.rdata节中,里面装的是程序需要用到的外部函数的名字,重定位表位于.reloc节中,里面装的是需要修复的地址,我们看一个实际PE文件的Data Directory部分
image
如果Address和Size都为0的话,表示没有这部分数据

0x3 Section

简单说,节就是PE文件中存储数据的容器,它们占据了除DOS Header、NT Headers、Section Headers外,PE文件中剩余的全部内容,常见的节如下

  • .text:包含程序的可执行代码
  • .data:包含初始化数据
  • .bss:包含未初始化数据
  • .rdata:包含只读初始化数据
  • .edata:包含导出表(有时没有这个节,导出表位于.rdata中)
  • .idata:包含导入表(有时没有这个节,导入表位于.rdata中)
  • .reloc:包含重定位信息
  • .rsrc:包含程序使用的资源信息
  • .tls:为程序的每个线程提供存储

我们看一个实际PE文件的各个节
image

0x4 Section Headers

PE Optional Header之后,Section之前,中间这部分,就是Section Headers,它包含着PE中各个节的信息

Section Headers本质是一个IMAGE_SECTION_HEADER结构体数组,定义位于winnt.h中,其中结构体IMAGE_SECTION_HEADER定义如下

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];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Name:这个字段装的是节的名字,其中IMAGE_SIZEOF_SHORT_NAME是一个常量,值为8,表示名字最长为8个字符
PhysicalAddress、VirtualSize:这个字段是一个联合体,定义2个名字,其实指向同一个东西,表示PE文件载入到内存后,节在内存中的大小
VirtualAddress:PE文件被载入内存后,节的首地址相对于PE文件内存基址的偏移,对于Object文件,这个字段表示重定位被应用前,相对于基址的偏移
SizeOfRawData:这个字段表示节在磁盘上的大小,它必须是IMAGE_OPTIONAL_HEADER.FileAlignment的整数倍
PointerToRawData:磁盘上PE文件中的节,相对于文件起始地址的偏移
PointerToRelocations:节中重定位项开始地址的指针,对于PE文件来说通常不涉及,字段值为0
PointerToLineNumbers:节中COFF行号项开始地址的指针,由于不鼓励COFF调试信息,字段值为0
NumberOfRelocations:节中重定位项的数量,对于PE文件来说,字段值为0
NumberOfLinenumbers:节中COFF行号项的数量,由于不鼓励COFF调试信息,字段值为0
Characteristics:用于描述节属性,比如节是否包含可执行代码、是否初始化数据、映射到内存后是否可被共享,完整的属性列表可以查看:https://learn.microsoft.com/en-us/windows/win32/debug/pe-format

需要注意VirtualSize和SizeOfRawData是有区别的

  • VirtualSize是PE文件在内存中的大小,SizeOfRawData是PE文件在磁盘中大小,在磁盘中需要遵循FileAlignment对齐,如果文件大小不足FileAlignment的整数倍,需要填充0,但是PE文件映射到内存后,VirtualSize的大小是PE文件FileAlignment对齐前的大小,这个时候VirtualSize是小于SizeOfRawData
  • 还有相反的情况,如果PE文件被载入到内存的同时,未初始化数据被初始化了数据,这个时候VirtualSize是大于SizeOfRawData

如下是实际PE文件中的Section Headers
image

如图所示,原始地址就是PointerToRawData,原始大小就是SizeOfRawData,虚拟地址就是VirtualAddress,虚拟大小就是VirtualSize,如果你尝试加一下原始地址和原始大小,虚拟地址和虚拟大小,会发现正好等于下一个节的原始地址和虚拟地址

特征字段表示节是只读的,还是读写的,还是读写执行的

参考链接

https://0xrick.github.io/win-internals/pe5/