Windows PE文件结构
Windows PE文件结构
Dos头 | |
NT头 | PE签名 |
PE文件头 | |
PE可选头 | |
数据目录表 | |
节表 | IMAGE_SECTION_HEADER → .text |
IMAGE_SECTION_HEADER → .data | |
IMAGE_SECTION_HEADER → .edata | |
IMAGE_SECTION_HEADER → .reloc | |
…. | |
节 | .text |
.data | |
.edata | |
.reloc | |
…. | |
Dos头
Dos头中目前有意义的值只有两个
typedef struct _IMAGE_DOS_HEADER {
0x00 WORD e_magic; // 魔术数字
0x3c LONG e_lfanew; // NT头地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;直接通过 e_lfanew 寻址到NT头, 其他数据无意义
NT头
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头
包含文件基础信息
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections; // 节数量
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; // PE可选头大小
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;扩展PE头
包含程序运行时详细信息, 例如入口点, 基地址
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic; // 魔术数字
BYTE MajorLinkerVersion; // 链接器主版本号
BYTE MinorLinkerVersion; // 链接器次版本号
DWORD SizeOfCode; // 代码节大小
DWORD SizeOfInitializedData; // 已初始化的数据节大小
DWORD SizeOfUninitializedData; // 未初始化的数据节大小
DWORD AddressOfEntryPoint; // 入口点
DWORD BaseOfCode; // 指向代码部分开头(相对于镜像基)的指针。
ULONGLONG 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;
ULONGLONG SizeOfStackReserve; // 要为堆栈保留的字节数
ULONGLONG SizeOfStackCommit; // 要为堆栈提交的字节数
ULONGLONG SizeOfHeapReserve; // 要为堆保留的字节数
ULONGLONG SizeOfHeapCommit; // 要为堆提交的字节数
DWORD LoaderFlags; // 加载器标志
DWORD NumberOfRvaAndSizes; // 数据目录表的项数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;重要字段:
| 字段 | 描述 | 备注 |
|---|---|---|
Magic | 魔术数字 | 32位为 0x10B, 64位为 0x20B |
SizeOfCode | 代码节大小 | 如果有多个已初始化的数据节,则为所有此类节的总和 |
SizeOfInitializedData | 已初始化的数据节大小 | 如果有多个已初始化的数据节,则为所有此类节的总和 |
SizeOfUninitializedData | 未初始化的数据节大小 | 如果有多个未初始化的数据节,则为所有此类节的总和 |
AddressOfEntryPoint | 入口点 | 对于可执行文件,这是起始地址。 对于设备驱动程序,这是初始化函数的地址。DLL可选, 没有为0, 要与ImageBase相加得到实际地址 |
BaseOfCode | 代码节基址 | 指向代码部分开头(相对于镜像基)的指针。 |
ImageBase | 镜像基址 | DLL 的默认值为 0x10000000。 应用程序的默认值为 0x00400000 |
SectionAlignment | 内存中加载的节的对齐方式 | |
FileAlignment | 镜像文件中各部分的原始数据的对齐方式 | |
SizeOfImage | 镜像的大小 | 包括所有标头。 必须是 SectionAlignment 的倍数。在内存中整个PE文件映射的大小 |
SizeOfHeaders | 头部大小 | 以下项的组合大小,舍入为 FileAlignment 成员中指定的值的倍数。 1. IMAGE_DOS_HEADER 的e_lfanew成员 2. 4 字节签名 3. IMAGE_FILE_HEADER 的大小 4. 可选标头的大小 5. 所有节标题的大小 |
CheckSum | 校验和 | 以下文件在加载时进行验证:所有驱动程序、在启动时加载的任何 DLL,以及加载到关键系统进程中的任何 DLL。 |
NumberOfRvaAndSizes | 数据目录表的项数 | 可选标头的其余部分的目录条目数。 每个条目描述位置和大小。 |
DataDirectory | 数据目录表 | 指向数据目录中第一个 IMAGE_DATA_DIRECTORY 结构的指针。 |
节表
节表的数量为NT头中 NumberOfSections 字段, 每个节表的结构如下
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节名称, 8字节
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // 节在内存中的大小
} Misc;
DWORD VirtualAddress; // 节在内存中的地址
DWORD SizeOfRawData; // 节在硬盘中的大小, 一般经过对齐, 比VirtualSize大
DWORD PointerToRawData; // 节在硬盘中的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;数据目录表里的 VirtualAddress 是按照内存中的偏移来算的, 加载到内存中需要做如下转换:
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++, pSectionHeader++)
{
// 源地址:文件缓冲区 + 节的原始数据偏移
LPVOID pSrc = (BYTE*)pFileBuffer + pSectionHeader->PointerToRawData;
// 目标地址:加载基址 + 节的虚拟地址
LPVOID pDest = (BYTE*)pLoadedBase + pSectionHeader->VirtualAddress;
// 复制节数据(SizeOfRawData是文件中实际的字节数)
memcpy(pDest, pSrc, pSectionHeader->SizeOfRawData);
}如果不映射到内存中, 后续如果要读取文件中的节数据, 则需要做如下转换:
文件偏移 = 节的PointerToRawData + (目标 RVA - 节的VirtualAddress)size_t RvaToFileOffset(IMAGE_NT_HEADERS* ntHeaders, DWORD rva)
{
const IMAGE_SECTION_HEADER* sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);
for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i, ++sectionHeader)
{
DWORD sectionVA = sectionHeader->VirtualAddress;
DWORD sectionSize = sectionHeader->Misc.VirtualSize;
if (rva >= sectionVA && rva < sectionVA + sectionSize)
{
return rva - sectionVA + sectionHeader->PointerToRawData;
}
}
return 0;
}数据目录表
数据目录表共有16中条目, 其中常见的如下:
| 值 | 字段 | 描述 | 备注 |
|---|---|---|---|
0 | IMAGE_DIRECTORY_ENTRY_EXPORT | 导出表 | |
1 | IMAGE_DIRECTORY_ENTRY_IMPORT | 导入表 | |
2 | IMAGE_DIRECTORY_ENTRY_RESOURCE | 资源表 | 图标, 菜单, 图片等信息 |
5 | IMAGE_DIRECTORY_ENTRY_BASERELOC | 基址重定位表 | |
9 | IMAGE_DIRECTORY_ENTRY_TLS | TLS 表 | 线程本地存储表, 比入口点更早执行 |
12 | IMAGE_DIRECTORY_ENTRY_IAT | 导入地址表 | 导入表的地址表 |
13 | IMAGE_DIRECTORY_ENTRY_DELAYIMPORT | 延迟导入表 | 延迟导入表 |
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // RVA: 数据在内存中的地址
DWORD Size; // 数据大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;在解析数据目录表时, 通过 VirtualAddress 寻址到节表的对应范围
DataDirectory [0] VirtualAddress:0x5D60, size:608 // 导出表
DataDirectory [1] VirtualAddress:0x5FC0, size:460 // 导入表
Section 2: Name=.rdata, VirtualAddress=0x5000, VirtualSize=0x1CE6, SizeOfRawData=0x2000, PointerToRawData=0x5000可以看到 .rdata 的地址范围为 0x5000~0x6CE6, 即导入导出表都落到了 .rdata 节中
导出表
导出表位置位于 DataDirectory[0].VirtualAddress 处
导出表的结构体如下
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;其重要字段定义如下
| 字段 | 描述 | 备注 |
|---|---|---|
Base | 基址 | 函数编号的起始id |
NumberOfFunctions | 函数数量 | 包含 命名函数 + 匿名函数 |
NumberOfNames | 名称数量 | 命名函数数量 |
AddressOfFunctions | 函数RVA地址表 | |
AddressOfNames | 函数名称RVA地址表 | |
AddressOfNameOrdinals | 函数名称序号表 |
AddressOfFunctions 里既有命名函数, 也有匿名函数
AddressOfNameOrdinals 里只有命名函数, 其里存的值是 AddressOfFunctions 的索引
所以遍历NumberOfNames的时候要先从AddressOfNameOrdinals里获取到命名函数的实际序号, 然后从AddressOfFunctions里获取到函数地址
for (size_t i = 0; i < exportTable->NumberOfNames; ++i)
{
WORD ordinal = AddressOfNameOrdinals[i]; // 当前命名函数的序号
DWORD funcNameRVA = AddressOfNames[i]; // 当前函数名的RVA
DWORD funcRVA = AddressOfFunctions[ordinal]; // 当前函数地址的RVA
DWORD funcNameFileOffset = RvaToFileOffset(ntHeaders, funcNameRVA);
std::string funcName = dll.at<char>(funcNameFileOffset);
std::println("序号: {}, 函数名: {}, RVA: 0x{:X}", ordinal + exportTable->Base, funcName, funcRVA);
}导入表
导入表位置位于 DataDirectory[1].VirtualAddress 处, 内存为一个结构体数组
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} 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;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;| 字段 | 描述 | 备注 |
|---|---|---|
Characteristics | 导入表结束标志 | 0表示导入表结束 |
OriginalFirstThunk | RVA指向INT表 | |
Name | RVA 指向Dll名称 | |
FirstThunk | RVA指向IAT表 |
IAT表与INT表
IAT表与INT表是两个不同的表, 但是它们的内容是相同的, 都是指向导入函数的地址表
OriginalFirstThunk(INT) 为只读表, 存储原始导入函数名, 可以找到函数的名称或函数的编号
FirstThunk(IAT) 在加载时会被替换为实际函数地址, 可以找到函数指令代码在内存空间中的实际地址
其对应的结构体定义如下:
typedef struct _IMAGE_THUNK_DATA64 {
union {
ULONGLONG ForwarderString; // PBYTE
ULONGLONG Function; // PDWORD
ULONGLONG Ordinal;
ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA64;其重要字段定义如下
| 字段 | 描述 | 备注 |
|---|---|---|
Function | 函数地址 | 为0表示结束, 在IAT表中它将被装在为函数实际内存地址 |
Ordinal | 函数序号 | 按序号导入时的序号值 (最高位为1表示序号, 否则为名称, 可以用IMAGE_ORDINAL宏判断) |
AddressOfData | 函数名称 | IMAGE_IMPORT_BY_NAME RVA, 使用名称导入时使用 |
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;其重要字段定义如下
| 字段 | 描述 | 备注 |
|---|---|---|
Hint | 序号 | 可以使用序号导入, 但是名称导入更稳定 |
Name | 函数名称 | 函数名称 |