PE(Portable Executable)文件是Windows操作系统中广泛使用的可执行文件格式。
通常所说的PE文件是指32位可执行文件,也称为PE32。
而64位的可执行文件称为PE+或PE32+,是PE(PE32)的一种扩展形式。
PE文件由多个不同的区块组成,每个区块具有特定的功能和结构。
以下是PE文件的主要组成部分:
注意:以上物理结构在磁盘上并非连续存储,因此必须通过一定的计算才能正确解析PE文件。
_IMAGE_DOS_HEADER 是PE文件格式中的一个结构体,用于兼容早期的MS-DOS系统。它存在于PE文件的开头位置,作为DOS头部(DOS Header)的一部分。
名称 | 长度(32/64位) | 含义 |
---|---|---|
e_magic | 2 | 通常情况下,它的值为"MZ",代表MS-DOS的标记。 |
e_cblp | 2 | 文件的最后一个扇区的字节数。通常情况下,这个字段的值会是0或者文件尺寸对512取模的余数。 |
e_cp | 2 | 文件的扇区数。与e_cblp类似,通常情况下,这个字段的值是0或者以512字节为单位的文件大小。 |
e_crlc | 2 | 重定位项的数量。在旧的MS-DOS可执行文件中,这个字段很少被使用。 |
e_cparhdr | 2 | 文件头的大小(以段为单位)。对于标准的MS-DOS可执行文件,这个字段的值通常是4。 |
e_minalloc | 2 | 程序加载时所需的最小附加段的数量。 |
e_maxalloc | 2 | 程序加载时所需的最大附加段的数量。 |
e_ss | 2 | 栈段(stack segment)在程序运行时的初始值。 |
e_sp | 2 | 栈指针(stack pointer)在程序运行时的初始值。 |
e_csum | 2 | 文件校验和。 |
e_ip | 2 | 程序运行时的初始 IP(instruction pointer)。 |
e_cs | 2 | 代码段(code segment)在程序运行时的初始值。 |
e_lfarlc | 2 | 重定位项的地址。 |
e_ovno | 2 | 扩展头部(extended header)的偏移量。 |
e_res | 8 | 预留未使用。 |
e_oemid | 2 | 制造商 ID。 |
e_oeminfo | 2 | 制造商信息。 |
e_res2 | 10 | 预留未使用。 |
e_lfanew | 4 | 新的PE头部(PE Header)的偏移量。 |
_IMAGE_NT_HEADERS 是用于描述Windows可执行文件(PE 文件)格式的结构体。
它包含了PE文件的头部信息、可选头部(Optional Header)以及数据目录(Data Directory)的相关信息。
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
名称 | 长度(32/64位) | 含义 |
---|---|---|
Signature | 4 | PE 文件的标识。通常情况下,它的值为 "PE\0\0"。 |
Machine | 2 | 目标 CPU 架构的类型 |
NumberOfSections | 2 | 文件中的节区(section)数量 |
TimeDateStamp | 4 | 文件的创建时间和日期 |
PointerToSymbolTable | 4 | 符号表的偏移量 |
NumberOfSymbols | 4 | 符号表的条目数量 |
SizeOfOptionalHeader | 2 | 可选头部的大小 |
Characteristics | 2 | 可执行文件的属性 |
Magic | 2 | 可选头部的格式。通常情况下,它的值可以用来区分32位/64位PE文件。 |
MajorLinkerVersion | 1 | 链接器的主要版本号 |
MinorLinkerVersion | 1 | 链接器的次要版本号 |
SizeOfCode | 4 | 代码节区的大小 |
SizeOfInitializedData | 4 | 已初始化的数据节区的大小 |
SizeOfUninitializedData | 4 | 未初始化的数据节区的大小 |
AddressOfEntryPoint | 4 | 程序入口点的 RVA(Relative Virtual Address) |
SizeOfInitializedData | 4 | 已初始化的数据节区的大小 |
BaseOfCode | 4 | 代码节区的起始地址 |
BaseOfData | 4/0 | 数据节区的起始地址 |
ImageBase | 4/8 | 可执行文件的首选装载基址 |
SectionAlignment | 4 | 节区对齐的内存页面大小 |
FileAlignment | 4 | 文件对齐的内存页面大小 |
MajorOperatingSystemVersion | 2 | 操作系统的主要版本号 |
MinorOperatingSystemVersion | 2 | 操作系统的次要版本号 |
MajorImageVersion | 2 | 可执行文件的主要版本号 |
MinorImageVersion | 2 | 可执行文件的次要版本号 |
MajorSubsystemVersion | 2 | 子系统的主要版本号 |
MinorSubsystemVersion | 2 | 子系统的次要版本号 |
Win32VersionValue | 4 | 预留字段 |
SizeOfImage | 4 | 可执行文件在内存中的映像大小 |
SizeOfHeaders | 4 | 可执行文件头部的总大小 |
CheckSum | 4 | 文件的校验和 |
Subsystem | 2 | 应用程序所需的子系统类型 |
DllCharacteristics | 2 | DLL 文件的特性 |
SizeOfStackReserve | 4/8 | 堆栈的保留大小 |
SizeOfStackCommit | 4/8 | 堆栈的提交大小 |
SizeOfHeapReserve | 4/8 | 堆的保留大小 |
SizeOfHeapCommit | 4/8 | 堆的提交大小 |
LoaderFlags | 4 | 加载器的标志 |
NumberOfRvaAndSizes | 4 | 数据目录的数量 |
DataDirectory | 128 | 数据目录数组 |
项 | 含义 |
---|---|
IMAGE_DIRECTORY_ENTRY_EXPORT | 导出表,包含函数和符号的导出信息 |
IMAGE_DIRECTORY_ENTRY_IMPORT | 导入表,包含从其他模块导入的函数和符号 |
IMAGE_DIRECTORY_ENTRY_RESOURCE | 资源表,包含从其他模块导入的函数和符号 |
IMAGE_DIRECTORY_ENTRY_RESOURCE | 异常处理表,包含用于异常处理的函数指针 |
IMAGE_DIRECTORY_ENTRY_SECURITY | 安全表,包含数字签名或其他安全相关信息 |
IMAGE_DIRECTORY_ENTRY_SECURITY | 基址重定位表,包含用于重定位可执行文件的地址信息 |
IMAGE_DIRECTORY_ENTRY_DEBUG | 调试信息表,包含调试器使用的调试信息 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 体系结构相关数据,适用于特定的体系结构 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 全局指针,用于在实模式下访问全局变量 |
IMAGE_DIRECTORY_ENTRY_TLS | TLS(Thread Local Storage) 数据,包含线程本地存储信息 |
IMAGE_DIRECTORY_ENTRY_TLS | 加载配置表,包含可执行文件的加载配置信息 |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 绑定导入表,描述绑定到特定模块的延迟加载的函数 |
IMAGE_DIRECTORY_ENTRY_IAT | 导入地址表,包含从其他模块导入的函数和符号的实际地址 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 延迟导入表,包含从其他模块延迟导入的函数和符号 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | Com描述符表,包含与COM组件相关的信息 |
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 表的相对虚拟地址(**RVA**)
DWORD Size; // 表的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
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;
引申:
设VA为PE文件的绝对地址,RVA地址是相对于模块基址的偏移地址,
那么从RVA计算绝对地址FOA的方式,
- 遍历节表,定位当前RVA地址所在的节
- 计算RVA相对节的偏移量 1 = RVA - 目标节的起始虚拟地址(Virtual Address)
- 计算RVA相对文件的偏移量 = 偏移量 1 + 目标节的文件偏移地址(Pointer to Raw Data)
int RVATOFOA(PIMAGE_FILE_HEADER fileHeader, DWORD rva) {
PIMAGE_SECTION_HEADER sectionHeader =
(PIMAGE_SECTION_HEADER)((DWORD_PTR)fileHeader + IMAGE_SIZEOF_FILE_HEADER +
fileHeader->SizeOfOptionalHeader);
if (rva < sectionHeader->VirtualAddress) {
return rva;
}
for (int i = 0; i < fileHeader->NumberOfSections; i++) {
// 判断是否处在当前节表
if (rva >= sectionHeader[i].VirtualAddress &&
rva <=
sectionHeader[i].VirtualAddress + sectionHeader[i].SizeOfRawData) {
return sectionHeader[i].PointerToRawData + rva -
sectionHeader[i].VirtualAddress;
}
}
return rva;
}
int LoadDependdncy(LPCWSTR filepath) {
HANDLE hFile = CreateFile(filepath, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return 1;
}
DWORD dwFileSize = GetFileSize(hFile, NULL);
HANDLE hMapping =
CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, dwFileSize, NULL);
if (hMapping == INVALID_HANDLE_VALUE) {
return 1;
}
LPVOID lpBaseAddress =
MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, dwFileSize);
if (dwFileSize == NULL) {
return 1;
}
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress;
// 检查DOS头
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
CloseHandle(hFile);
return 1;
}
PIMAGE_NT_HEADERS ntHeader =
(PIMAGE_NT_HEADERS)((DWORD_PTR)lpBaseAddress + dosHeader->e_lfanew);
// 检查NT头
if (ntHeader->Signature != IMAGE_NT_SIGNATURE) {
CloseHandle(hFile);
return 1;
}
DWORD importTableRva = 0;
// 根据PE类型获取导入表RVA
if (ntHeader->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
importTableRva =
((PIMAGE_NT_HEADERS32)(ntHeader))
->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
.VirtualAddress;
} else {
importTableRva =
((PIMAGE_NT_HEADERS64)(ntHeader))
->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
.VirtualAddress;
}
// 获取导入表描述符
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor =
(PIMAGE_IMPORT_DESCRIPTOR)((DWORD_PTR)lpBaseAddress +
RVATOFOA(&ntHeader->FileHeader,
importTableRva));
// 遍历导入表
while(pImportDescriptor->Name) {
// 获取模块名
char* moduleName = (char*)lpBaseAddress +
RVATOFOA(&ntHeader->FileHeader, pImportDescriptor->Name);
printf("module name = %s\n", moduleName);
// 获取导入函数
IMAGE_THUNK_DATA* pImportThunk =
(IMAGE_THUNK_DATA*)((char*)lpBaseAddress +
RVATOFOA(&ntHeader->FileHeader,
pImportDescriptor->FirstThunk));
// 遍历导入函数
while (pImportThunk->u1.AddressOfData != 0) {
// 判断是否为序号导入
if (IMAGE_SNAP_BY_ORDINAL(pImportThunk->u1.Ordinal)) {
// 对于序号导入,低16位存储了序号值
WORD ordinal = static_cast<WORD>(pImportThunk->u1.Ordinal & 0xFFFF);
printf("ordinal = %d\n", ordinal);
} else {
// 对于名称导入,低31位存储了名称表RVA
IMAGE_IMPORT_BY_NAME* pImportByName =
(IMAGE_IMPORT_BY_NAME*)((char*)lpBaseAddress +
RVATOFOA(&ntHeader->FileHeader,
pImportThunk->u1.AddressOfData));
printf("function %d = %s\n", pImportByName->Hint, pImportByName->Name);
}
// 移动到下一个导入函数
pImportThunk++;
}
// 移动到下一个导入描述符
pImportDescriptor++;
};
UnmapViewOfFile(lpBaseAddress);
CloseHandle(hMapping);
CloseHandle(hFile);
return 0;
}