目录
1. DOS头
DOS头的作用
DOS头的结构
C代码判断PE文件
2. PE文件签名
PE文件签名的位置和作用
PE文件签名的结构
COFF(Common Object File Format)头
COFF头的结构
COFF头的作用
代码
3. 标准PE头&可选PE头
标准PE头
可选PE头
4. 数据目录
数据目录的结构
5. 节
6. 重定位给表
重定位表的作用
重定位表的结构
重定位过程
重要性
代码
7. 资源表
资源表的作用
资源表的结构
导入表(Import Table)
导出表(Export Table)
重要性
8. 调试信息
调试信息的内容
9. TLS
TLS表的作用
TLS表的结构
10. 数字签名
数字签名的作用
数字签名的实现
读取数字签名
总结
PE(Portable Executable)文件是Windows操作系统上常用的可执行文件格式。它包含了运行应用程序或库所需的多种数据和代码。一个PE文件通常包含以下部分:
DOS头:包含用于保持向后兼容性的MS-DOS可执行文件头。
PE文件签名:标识该文件为PE格式。
COFF(Common Object File Format)头:提供了关于PE文件结构的基本信息,如机器类型和节的数量。
标准PE头:也称为“文件头”,包含重要的信息,如代码的入口点、基址和图像大小。
可选PE头:不是所有PE文件都有。它包括更多的信息,如堆和栈的大小、操作系统版本、图像的大小和特征。
数据目录:指向重要数据结构,如导入表、导出表、资源表、异常表等。
节:PE文件的主体部分,包含实际的代码和数据。常见的节有:
- .text节:包含程序的可执行代码。
- .data节:包含初始化的全局和静态变量。
- .rdata节:包含只读数据,如常量字符串和导入表。
- .bss节:包含未初始化的全局和静态变量。
- .idata节:包含导入函数和变量的信息。
- .edata节:包含导出函数和变量的信息。
- .rsrc节:包含资源数据,如图标、菜单和对话框。
- .reloc节:包含重定位信息,用于动态链接。
重定位表:当程序不能被加载到它的首选地址时,重定位表被用来调整硬编码的指针。
资源表:存储程序图标、菜单和对话框等资源。
导入/导出表:列出程序所需的其他模块(DLL等)及其函数和变量。
调试信息:包含调试符号等信息,通常仅在调试版本的PE文件中存在。
TLS(Thread Local Storage)表:用于多线程应用中存储每个线程的数据。
异常处理和安全特性:如堆栈展开信息和安全cookie。
数字签名:对于某些PE文件,可能包含数字签名以验证文件的完整性和来源。
DOS头的主要作用是使PE文件向后兼容,即使在早期的操作系统(如MS-DOS)上也能被识别为可执行文件。在Windows环境中,DOS头主要是作为PE文件的一个标准组成部分,提供指向实际PE头部的指针(即e_lfanew
字段)。
e_magic:Magic number,通常为“MZ”,标识文件是一个可执行文件。这是MS-DOS可执行文件的标准签名。
e_cblp:文件最后一页的字节数。
e_cp:文件页数。
e_crlc:重定位项的数量。
e_cparhdr:头部大小,单位为段落(每段落16字节)。
e_minalloc:所需的最小额外段落。
e_maxalloc:所需的最大额外段落。
e_ss:初始的SS(栈段)值。
e_sp:初始的SP(栈指针)值。
e_csum:校验和。
e_ip:初始的IP(指令指针)值。
e_cs:初始的CS(代码段)值。
e_lfarlc:重定位表的文件地址。
e_ovno:覆盖号。
e_res:保留字。
e_oemid 和 e_oeminfo:OEM标识符和OEM信息。
e_res2:保留字。
e_lfanew:新头部(PE头部)的文件地址,这是连接DOS头和PE头的关键字段。这个字段非常重要,因为它提供了从文件开头到PE头(即实际的Windows格式头)的偏移量。这是DOS头和PE头之间的桥梁。
4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00
B8 00 00 00 00 00 00 00 40 01 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 80 00 00 00 0E 1F BA 0E 00 B4 09 CD
21 B8 01 4C CD 21 54 68 69 73 20 70 72 6F 67 72
61 6D 20 63 61 6E 6E 6F 74 20 62 65 20 72 75 6E
20 69 6E 20 44 4F 53 20 6D 6F 64 65 2E 0D 0D 0A
24 00 00 00 00 00 00 00
在此示例中,“4D 5A”是“MZ”签名,“e_lfanew”字段通常位于文件的第0x3C字节处,指向PE头的开始位置。
在Windows系统上,DOS头的关键作用是包含一个指向PE头的偏移量(e_lfanew
字段),这使得操作系统能够正确地找到并解析PE头,从而加载和执行程序。
以下是一个简单的C语言示例,演示了如何读取一个PE文件的DOS头并找到PE头的位置:
#include
#include
// 定义DOS头结构
typedef struct _IMAGE_DOS_HEADER {
unsigned short e_magic; // Magic number (必须是'MZ')
unsigned short e_cblp; // Bytes on last page of file
unsigned short e_cp; // Pages in file
unsigned short e_crlc; // Relocations
unsigned short e_cparhdr; // Size of header in paragraphs
unsigned short e_minalloc; // Minimum extra paragraphs needed
unsigned short e_maxalloc; // Maximum extra paragraphs needed
unsigned short e_ss; // Initial (relative) SS value
unsigned short e_sp; // Initial SP value
unsigned short e_csum; // Checksum
unsigned short e_ip; // Initial IP value
unsigned short e_cs; // Initial (relative) CS value
unsigned short e_lfarlc; // File address of relocation table
unsigned short e_ovno; // Overlay number
unsigned short e_res[4]; // Reserved words
unsigned short e_oemid; // OEM identifier
unsigned short e_oeminfo; // OEM information
unsigned short e_res2[10]; // Reserved words
unsigned long e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER;
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s \n", argv[0]);
return 1;
}
// 打开文件
FILE *file = fopen(argv[1], "rb");
if (!file) {
perror("Error opening file");
return 1;
}
// 读取DOS头
IMAGE_DOS_HEADER dosHeader;
if (fread(&dosHeader, sizeof(IMAGE_DOS_HEADER), 1, file) != 1) {
perror("Error reading DOS header");
fclose(file);
return 1;
}
// 检查MZ签名
if (dosHeader.e_magic != 0x5A4D) { // 'MZ' in little endian
fprintf(stderr, "Not a valid PE file (missing MZ header)\n");
fclose(file);
return 1;
}
printf("PE header found at offset: 0x%X\n", dosHeader.e_lfanew);
// 清理并退出
fclose(file);
return 0;
}
位置:PE文件签名位于文件的开始部分,紧随DOS头之后。在DOS头中,e_lfanew
字段指明了PE文件签名的偏移量。这意味着,通过读取DOS头中的e_lfanew
字段,可以找到PE文件签名的确切位置。
作用:PE文件签名用于标识文件为PE格式。它是一个固定的、标准化的数据结构,使得操作系统和其他软件能够确认文件是一个有效的Windows可执行文件。如果这个签名不存在或不正确,文件将不被视为有效的PE文件,因此无法在Windows环境中正常加载或执行。
PE文件签名通常由以下部分组成:
"PE\0\0"
(在十六进制中表示为50 45 00 00
)。这个签名紧随DOS头之后,位于由DOS头中的e_lfanew
字段指出的位置。#include
#include
typedef struct _IMAGE_DOS_HEADER {
uint16_t e_magic; // Magic number (应为'MZ')
uint16_t e_cblp; // Bytes on last page of file
// ... 其他字段 ...
uint32_t e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER;
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s \n", argv[0]);
return 1;
}
FILE *file = fopen(argv[1], "rb");
if (!file) {
perror("Error opening file");
return 1;
}
IMAGE_DOS_HEADER dosHeader;
if (fread(&dosHeader, sizeof(IMAGE_DOS_HEADER), 1, file) != 1) {
perror("Error reading DOS header");
fclose(file);
return 1;
}
if (dosHeader.e_magic != 0x5A4D) { // 'MZ' in little endian
fprintf(stderr, "Not a valid DOS header\n");
fclose(file);
return 1;
}
fseek(file, dosHeader.e_lfanew, SEEK_SET);
uint32_t peSignature;
if (fread(&peSignature, sizeof(uint32_t), 1, file) != 1) {
perror("Error reading PE signature");
fclose(file);
return 1;
}
if (peSignature != 0x00004550) { // 'PE\0\0' in little endian
fprintf(stderr, "Not a valid PE signature\n");
fclose(file);
return 1;
}
printf("Valid PE signature found\n");
fclose(file);
return 0;
}
COFF头包含以下主要字段:
Machine:指定目标机器的类型。例如,0x14c
代表Intel 386或更高版本兼容机器。
NumberOfSections:文件中的节(Section)数量。
TimeDateStamp:文件被创建的时间戳。
PointerToSymbolTable:符号表的起始位置。在许多PE文件中,这个值为零,因为符号表通常不包含在最终分发的可执行文件中。
NumberOfSymbols:符号表中的符号数量。
SizeOfOptionalHeader:可选头部的大小。这个值指示紧随COFF头部之后的PE可选头部的大小。
Characteristics:表示文件的特性,例如是否为可执行文件,是否可以在32位机器上运行等。
节(Section)的作用
在PE文件中,节是文件的主要组成部分之一,用于存储代码、数据、资源等。不同类型的节有不同的作用:
.text:包含程序的可执行代码。
.data:包含初始化的全局和静态变量。
.rdata:包含只读数据,如常量字符串和导入表。
.bss:包含未初始化的全局和静态变量。
.idata:包含导入函数和变量的信息。
.edata:包含导出函数和变量的信息。
.rsrc:包含资源数据,如图标、菜单和对话框。
.reloc:包含重定位信息,用于动态链接。
其他自定义节:开发者可以根据需要定义其他节。
COFF头在PE文件中扮演着核心角色:
兼容性:通过Machine字段,操作系统可以确定该文件是否与当前系统兼容。
管理和链接:通过对节的数量和特性的描述,操作系统和链接器可以正确地管理和链接不同部分。
时间戳:有助于系统或开发工具确定文件的版本。
可选头部信息:提供了关于接下来的可选头部大小的信息,这对于解析PE文件结构至关重要。
#include
#include
// COFF头部结构
typedef struct _IMAGE_COFF_HEADER {
uint16_t Machine;
uint16_t NumberOfSections;
uint32_t TimeDateStamp;
uint32_t PointerToSymbolTable;
uint32_t NumberOfSymbols;
uint16_t SizeOfOptionalHeader;
uint16_t Characteristics;
} IMAGE_COFF_HEADER;
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s \n", argv[0]);
return 1;
}
FILE *file = fopen(argv[1], "rb");
if (!file) {
perror("Error opening file");
return 1;
}
// 读取DOS头以找到PE签名的位置
fseek(file, 0x3C, SEEK_SET);
uint32_t peOffset;
fread(&peOffset, sizeof(uint32_t), 1, file);
// 跳转到COFF头的开始位置
fseek(file, peOffset + 4, SEEK_SET);
// 读取COFF头
IMAGE_COFF_HEADER coffHeader;
if (fread(&coffHeader, sizeof(IMAGE_COFF_HEADER), 1, file) != 1) {
perror("Error reading COFF header");
fclose(file);
return 1;
}
// 显示COFF头信息
printf("Machine: 0x%X\n", coffHeader.Machine);
printf("NumberOfSections: %d\n", coffHeader.NumberOfSections);
printf("TimeDateStamp: 0x%X\n", coffHeader.TimeDateStamp);
printf("PointerToSymbolTable: 0x%X\n", coffHeader.PointerToSymbolTable);
printf("NumberOfSymbols: %d\n", coffHeader.NumberOfSymbols);
printf("SizeOfOptionalHeader: %d\n", coffHeader.SizeOfOptionalHeader);
printf("Characteristics: 0x%X\n", coffHeader.Characteristics);
fclose(file);
return 0;
}
在PE(Portable Executable)文件格式中,标准PE头,也常被称为“文件头”(File Header),是紧随COFF(Common Object File Format)头之后的一个关键部分。这个头部包含了一些对于可执行文件至关重要的信息,这些信息是操作系统用来正确加载和执行文件所必需的。
标准PE头包括以下主要字段:
Machine:指定目标机器的类型。这个字段定义了文件是为哪种类型的CPU构建的,例如x86、Itanium、AMD64等。
NumberOfSections:如前所述,这个字段指定文件中节的数量。
TimeDateStamp:文件创建的时间戳。
PointerToSymbolTable 和 NumberOfSymbols:这两个字段用于指向和定义符号表的位置和大小。在许多情况下,尤其是在最终用户的可执行文件中,这些字段通常不被使用。
SizeOfOptionalHeader:指定紧随其后的可选头部(Optional Header)的大小。这个信息告诉解析器如何定位和解析可选头部。
Characteristics:文件的属性,例如是不是DLL文件,是不是可以在32位或64位系统上运行等。
紧随标准PE头之后的是可选PE头(Optional Header),虽然它被称为“可选”,但对于大多数PE文件来说是必需的,因为它包含了一些重要的执行和加载信息。可选PE头包括:
Magic:一个标识,指示文件是32位(PE32)还是64位(PE32+)格式。
EntryPoint:程序的入口点,当操作系统加载文件时,执行将从这里开始。
ImageBase:建议加载此可执行文件的内存地址。操作系统可以使用这个地址,或者选择一个不同的地址。
SectionAlignment 和 FileAlignment:内存和文件中各个节的对齐方式。
SizeOfImage:加载到内存后的总尺寸。
SizeOfHeaders:PE头部的总大小。
Subsystem:指定文件运行的环境,例如Windows GUI或命令行。
DLLCharacteristics:DLL特定的属性。
StackReserveSize, StackCommitSize, HeapReserveSize, HeapCommitSize:分别定义了进程的堆栈和堆的预留大小和提交大小。
DataDirectories:指向各种重要数据表的指针,如导入表、导出表、资源表等。
这些头部字段对于PE文件的正确解析和执行至关重要。操作系统依赖于这些信息来决定如何加载文件到内存,以及如何映射文件中的不同节到进程的地址空间。特别是,入口点、基址和图像大小等信息对于确保文件正确执行非常关键。在安全分析和逆向工程中,这些信息也被用来理解文件的行为和结构。
#include
#include
// 标准PE头结构
typedef struct _IMAGE_FILE_HEADER {
uint16_t Machine;
uint16_t NumberOfSections;
uint32_t TimeDateStamp;
uint32_t PointerToSymbolTable;
uint32_t NumberOfSymbols;
uint16_t SizeOfOptionalHeader;
uint16_t Characteristics;
} IMAGE_FILE_HEADER;
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s \n", argv[0]);
return 1;
}
FILE *file = fopen(argv[1], "rb");
if (!file) {
perror("Error opening file");
return 1;
}
// 定位到PE签名
fseek(file, 0x3C, SEEK_SET);
uint32_t peOffset;
fread(&peOffset, sizeof(uint32_t), 1, file);
fseek(file, peOffset, SEEK_SET);
// 跳过PE签名(4字节)
fseek(file, 4, SEEK_CUR);
// 读取并显示标准PE头信息
IMAGE_FILE_HEADER fileHeader;
if (fread(&fileHeader, sizeof(IMAGE_FILE_HEADER), 1, file) != 1) {
perror("Error reading file header");
fclose(file);
return 1;
}
printf("Machine: 0x%X\n", fileHeader.Machine);
printf("NumberOfSections: %d\n", fileHeader.NumberOfSections);
printf("TimeDateStamp: 0x%X\n", fileHeader.TimeDateStamp);
printf("PointerToSymbolTable: 0x%X\n", fileHeader.PointerToSymbolTable);
printf("NumberOfSymbols: %d\n", fileHeader.NumberOfSymbols);
printf("SizeOfOptionalHeader: %d\n", fileHeader.SizeOfOptionalHeader);
printf("Characteristics: 0x%X\n", fileHeader.Characteristics);
fclose(file);
return 0;
}
数据目录的主要作用是为PE文件中的关键数据提供快速访问。这些数据结构包括但不限于:
导入表(Import Table):列出了文件依赖的其他模块(如DLL)及其导入的函数。
导出表(Export Table):包含了文件提供给其他模块使用的函数和变量。
资源表(Resource Table):存储程序的资源,如图标、字符串、菜单等。
异常表(Exception Table):用于处理异常和错误。
证书表(Certificate Table):存储数字签名等安全相关的信息。
基址重定位表(Base Relocation Table):如果文件不能加载到预期的内存地址,这个表提供了必要的地址调整信息。
调试信息(Debug Directory):包含程序调试时使用的信息。
线程局部存储(Thread Local Storage Directory):用于多线程应用中存储每个线程的局部数据。
加载配置表(Load Configuration Table):包含了加载和运行程序所需的配置信息。
绑定导入表(Bound Import Table):包含了提前绑定的导入信息,以加快加载速度。
导入地址表(Import Address Table):用于修正导入函数的地址。
延迟导入描述符(Delay Import Descriptor):列出了延迟加载的模块。
CLR运行时头(CLR Runtime Header):对于.NET程序,包含了指向公共语言运行时信息的指针。
每个数据目录项通常由两个部分组成:
VirtualAddress:指向相关数据结构的RVA(相对虚拟地址)。
Size:数据结构的大小。
#include
#include
// 定义DOS头结构
typedef struct _IMAGE_DOS_HEADER {
uint16_t e_magic; // Magic number (必须是'MZ')
uint16_t e_cblp; // Bytes on last page of file
uint16_t e_cp; // Pages in file
uint16_t e_crlc; // Relocations
uint16_t e_cparhdr; // Size of header in paragraphs
uint16_t e_minalloc; // Minimum extra paragraphs needed
uint16_t e_maxalloc; // Maximum extra paragraphs needed
uint16_t e_ss; // Initial (relative) SS value
uint16_t e_sp; // Initial SP value
uint16_t e_csum; // Checksum
uint16_t e_ip; // Initial IP value
uint16_t e_cs; // Initial (relative) CS value
uint16_t e_lfarlc; // File address of relocation table
uint16_t e_ovno; // Overlay number
uint16_t e_res[4]; // Reserved words
uint16_t e_oemid; // OEM identifier
uint16_t e_oeminfo; // OEM information
uint16_t e_res2[10]; // Reserved words
uint32_t e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER;
// 定义标准PE头(COFF头)结构
typedef struct _IMAGE_FILE_HEADER {
uint16_t Machine;
uint16_t NumberOfSections;
uint32_t TimeDateStamp;
uint32_t PointerToSymbolTable;
uint32_t NumberOfSymbols;
uint16_t SizeOfOptionalHeader;
uint16_t Characteristics;
} IMAGE_FILE_HEADER;
// 定义数据目录结构
typedef struct _IMAGE_DATA_DIRECTORY {
uint32_t VirtualAddress;
uint32_t Size;
} IMAGE_DATA_DIRECTORY;
typedef struct _IMAGE_OPTIONAL_HEADER {
// 标准字段
uint16_t Magic; // 魔数,标识PE32(0x10b)或PE32+(0x20b)
uint8_t MajorLinkerVersion; // 连接器主版本号
uint8_t MinorLinkerVersion; // 连接器次版本号
uint32_t SizeOfCode; // 所有包含代码的节的总大小
uint32_t SizeOfInitializedData; // 所有包含已初始化数据的节的总大小
uint32_t SizeOfUninitializedData; // 所有包含未初始化数据的节的总大小
uint32_t AddressOfEntryPoint; // 程序执行入口的相对虚拟地址
uint32_t BaseOfCode; // 代码节的起始相对虚拟地址
uint32_t BaseOfData; // 数据节的起始相对虚拟地址
// NT额外字段
uint32_t ImageBase; // 图像的首选加载地址
uint32_t SectionAlignment; // 内存中节的对齐粒度
uint32_t FileAlignment; // 文件中节的对齐粒度
uint16_t MajorOperatingSystemVersion; // 所需操作系统的主版本号
uint16_t MinorOperatingSystemVersion; // 所需操作系统的次版本号
uint16_t MajorImageVersion; // 图像的主版本号
uint16_t MinorImageVersion; // 图像的次版本号
uint16_t MajorSubsystemVersion; // 子系统的主版本号
uint16_t MinorSubsystemVersion; // 子系统的次版本号
uint32_t Win32VersionValue; // 保留字段,必须为0
uint32_t SizeOfImage; // 图像的总大小,包括所有头部和节
uint32_t SizeOfHeaders; // 所有头部的总大小
uint32_t CheckSum; // 图像的校验和
uint16_t Subsystem; // 子系统(如Windows GUI或控制台)
uint16_t DllCharacteristics; // DLL特性标志
uint32_t SizeOfStackReserve; // 线程的初始堆栈保留大小
uint32_t SizeOfStackCommit; // 线程的初始堆栈提交大小
uint32_t SizeOfHeapReserve; // 进程的初始堆保留大小
uint32_t SizeOfHeapCommit; // 进程的初始堆提交大小
uint32_t LoaderFlags; // 保留字段,必须为0
uint32_t NumberOfRvaAndSizes; // 数据目录的数量
// 数据目录
IMAGE_DATA_DIRECTORY DataDirectory[16]; // 数据目录数组
} IMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_SIGNATURE 0x00004550 // "PE\0\0"
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s \n", argv[0]);
return 1;
}
FILE* file = fopen(argv[1], "rb");
if (!file) {
perror("Error opening file");
return 1;
}
// 读取DOS头
IMAGE_DOS_HEADER dosHeader;
if (fread(&dosHeader, sizeof(IMAGE_DOS_HEADER), 1, file) != 1) {
perror("Error reading DOS header");
fclose(file);
return 1;
}
if (dosHeader.e_magic != 0x5A4D) { // 检查'MZ'标记
fprintf(stderr, "Not a valid DOS header\n");
fclose(file);
return 1;
}
// 定位到PE头
fseek(file, dosHeader.e_lfanew, SEEK_SET);
uint32_t peSignature;
fread(&peSignature, sizeof(uint32_t), 1, file);
if (peSignature != IMAGE_NT_SIGNATURE) { // 检查'PE\0\0'签名
fprintf(stderr, "Not a valid PE signature\n");
fclose(file);
return 1;
}
// 读取标准PE头
IMAGE_FILE_HEADER fileHeader;
fread(&fileHeader, sizeof(IMAGE_FILE_HEADER), 1, file);
// 读取可选头部
IMAGE_OPTIONAL_HEADER optionalHeader;
fread(&optionalHeader, sizeof(IMAGE_OPTIONAL_HEADER), 1, file);
// 打印数据目录信息
for (int i = 0; i < 16; i++) {
printf("Data Directory %d: VirtualAddress = 0x%X, Size = 0x%X\n",
i, optionalHeader.DataDirectory[i].VirtualAddress,
optionalHeader.DataDirectory[i].Size);
}
fclose(file);
return 0;
}
.text节:
.data节:
.rdata节:
.bss节:
.idata节:
.edata节:
.rsrc节:
.reloc节:
#include
#include
#include
// ...(之前定义的IMAGE_DOS_HEADER, IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER结构体)...
// 定义节(Section)头结构
typedef struct _IMAGE_SECTION_HEADER {
uint8_t Name[8]; // 节名称
uint32_t VirtualSize; // 虚拟大小
uint32_t VirtualAddress; // 虚拟地址
uint32_t SizeOfRawData; // 节的大小
uint32_t PointerToRawData; // 文件中的偏移
uint32_t PointerToRelocations;
uint32_t PointerToLinenumbers;
uint16_t NumberOfRelocations;
uint16_t NumberOfLinenumbers;
uint32_t Characteristics; // 节特性
} IMAGE_SECTION_HEADER;
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s \n", argv[0]);
return 1;
}
FILE *file = fopen(argv[1], "rb");
if (!file) {
perror("Error opening file");
return 1;
}
// ...(读取DOS头,定位到PE头的代码)...
// 读取标准PE头(COFF头)
IMAGE_FILE_HEADER fileHeader;
fread(&fileHeader, sizeof(IMAGE_FILE_HEADER), 1, file);
// 跳过可选头部
fseek(file, fileHeader.SizeOfOptionalHeader, SEEK_CUR);
// 读取节头
for (int i = 0; i < fileHeader.NumberOfSections; i++) {
IMAGE_SECTION_HEADER sectionHeader;
fread(§ionHeader, sizeof(IMAGE_SECTION_HEADER), 1, file);
// 打印节信息
printf("Section %d: %.*s\n", i + 1, 8, sectionHeader.Name);
printf(" Virtual Size: 0x%X\n", sectionHeader.VirtualSize);
printf(" Virtual Address: 0x%X\n", sectionHeader.VirtualAddress);
printf(" Size of Raw Data: 0x%X\n", sectionHeader.SizeOfRawData);
printf(" Characteristics: 0x%X\n", sectionHeader.Characteristics);
// 你也可以根据Characteristics字段的值来解析并打印节的具体属性,例如是否可读、可写等
}
fclose(file);
return 0;
}
在Windows PE(Portable Executable)文件格式中,重定位表(Base Relocation Table)是一种机制,用于在程序不能加载到其预先定义的首选地址时调整硬编码的指针。这在多种情况下是必需的,特别是在支持地址空间布局随机化(ASLR)的系统上。
地址空间布局随机化(ASLR):
动态加载:
硬编码地址的调整:
加载时处理:
地址调整:
读取并利用PE文件的重定位表是一个相对复杂的过程,涉及到解析PE文件结构,定位重定位表,然后根据表中的条目调整内存中的地址。以下是一个C++代码示例,演示了如何读取PE文件的重定位表。但请注意,实际上“利用”这些重定位信息——比如用于动态修改程序行为——通常不是一个简单的任务,且可能涉及到深入的系统编程和对底层内存管理的理解。
#include
#include
#include
#include
// ...(之前定义的IMAGE_DOS_HEADER, IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER, IMAGE_SECTION_HEADER结构体)...
// 定义重定位块的结构
struct IMAGE_BASE_RELOCATION {
uint32_t VirtualAddress;
uint32_t SizeOfBlock;
// 紧随其后的是重定位项(uint16_t TypeOffset[1];)
};
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " " << std::endl;
return 1;
}
std::ifstream file(argv[1], std::ios::binary);
if (!file) {
std::cerr << "Error opening file" << std::endl;
return 1;
}
// ...(读取DOS头,定位到PE头的代码)...
// ...(读取标准PE头和可选头部的代码)...
// 定位到.rdata节(或包含重定位表的节)
// 注意:这假设重定位表位于.rdata节中。你可能需要遍历节表来找到正确的节。
// 读取重定位表
IMAGE_DATA_DIRECTORY relocDir = optionalHeader.DataDirectory[5]; // 基址重定位表索引为5
file.seekg(relocDir.VirtualAddress, std::ios::beg);
while (true) {
IMAGE_BASE_RELOCATION relocBlock;
file.read(reinterpret_cast(&relocBlock), sizeof(IMAGE_BASE_RELOCATION));
if (relocBlock.SizeOfBlock == 0) {
break;
}
// 计算此块的重定位项数量
size_t numberOfEntries = (relocBlock.SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(uint16_t);
std::vector entries(numberOfEntries);
file.read(reinterpret_cast(entries.data()), numberOfEntries * sizeof(uint16_t));
for (uint16_t entry : entries) {
// 分析重定位项
uint16_t type = entry >> 12; // 高4位是类型
uint16_t offset = entry & 0xfff; // 低12位是偏移
// 在这里处理重定位项
// 注意:这里仅打印信息,实际上需要根据类型和偏移进行适当的内存修改
std::cout << "Relocation - Type: " << type << ", Offset: " << offset << std::endl;
}
}
return 0;
}
在Windows PE(Portable Executable)文件格式中,资源表(Resource Table)是一种特殊的数据结构,用于存储程序的资源。这些资源包括图标、菜单、对话框、字符串表、位图、字体等。资源表是PE文件的一个关键组成部分,使得程序可以包含各种非代码数据。
存储多媒体和UI元素:资源表通常包含图标、光标、位图等多媒体资源,以及菜单、对话框等用户界面元素。
本地化和国际化:资源表可以包含为不同语言或地区定制的字符串和其他资源,支持软件的本地化。
动态加载:程序运行时可以按需加载资源,而不是在启动时加载所有资源,这有助于提高效率和响应速度。
资源目录:资源表以树状结构组织,顶级是资源目录,其中包含指向不同类型资源的指针。
资源类型:资源按类型组织,常见的类型包括图标、光标、位图、菜单、对话框等。
资源名称和ID:资源可以通过名称或数字ID进行标识。
资源语言:每种资源可以有针对不同语言的多个版本。
资源数据:实际的资源数据,如图像文件的内容、菜单的描述等。
#include
#include
#include
#include
// ...(之前定义的IMAGE_DOS_HEADER, IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER, IMAGE_SECTION_HEADER结构体)...
// 定义资源目录结构
struct IMAGE_RESOURCE_DIRECTORY {
uint32_t Characteristics;
uint32_t TimeDateStamp;
uint16_t MajorVersion;
uint16_t MinorVersion;
uint16_t NumberOfNamedEntries;
uint16_t NumberOfIdEntries;
// 紧随其后的是目录项(IMAGE_RESOURCE_DIRECTORY_ENTRY)
};
struct IMAGE_RESOURCE_DIRECTORY_ENTRY {
uint32_t Name;
uint32_t OffsetToData;
};
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " " << std::endl;
return 1;
}
std::ifstream file(argv[1], std::ios::binary);
if (!file) {
std::cerr << "Error opening file" << std::endl;
return 1;
}
// ...(读取DOS头,定位到PE头的代码)...
// ...(读取标准PE头和可选头部的代码)...
// 定位到资源表
IMAGE_DATA_DIRECTORY resourceTable = optionalHeader.DataDirectory[2]; // 资源表索引为2
file.seekg(resourceTable.VirtualAddress, std::ios::beg);
// 读取资源目录
IMAGE_RESOURCE_DIRECTORY resourceDir;
file.read(reinterpret_cast(&resourceDir), sizeof(IMAGE_RESOURCE_DIRECTORY));
// 解析资源目录项
for (int i = 0; i < resourceDir.NumberOfNamedEntries + resourceDir.NumberOfIdEntries; ++i) {
IMAGE_RESOURCE_DIRECTORY_ENTRY dirEntry;
file.read(reinterpret_cast(&dirEntry), sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY));
// 这里可以进一步解析每个目录项
// 注意:实际资源数据可能位于不同的节中,可能需要进行额外的定位和读取操作
std::cout << "Resource Directory Entry: " << i << std::endl;
std::cout << " Name: " << dirEntry.Name << std::endl;
std::cout << " Offset to Data: " << dirEntry.OffsetToData << std::endl;
}
return 0;
}
8. 导入导出表
在Windows PE(Portable Executable)文件格式中,导入表和导出表是关键的部分,它们分别描述了程序依赖的外部模块(如DLL文件)以及程序向外提供的函数和变量。
导入表是PE文件中的一个数据结构,用于列出程序运行时需要从其他模块(通常是DLL文件)导入的函数和变量。这个表使得程序可以使用动态链接库(DLL)中的代码和数据,而不需要将这些代码和数据静态地编译到执行文件中。
导入表的主要组成部分:
导入目录:包含一系列条目,每个条目对应一个程序依赖的DLL。
导入查找表:每个导入目录条目都指向一个导入查找表,它包含了程序从相应DLL导入的函数和变量的名称或序号。
导入地址表:在程序运行时,这个表被填充为实际的函数或变量的地址。它的初始内容与导入查找表相同。
导出表是PE文件中的一个数据结构,用于列出程序向外提供的函数和变量,使得其他程序或DLL可以使用这些功能。导出表对于DLL文件尤为重要,因为它们通常用于提供多个程序共享的函数和变量。
导出表的主要组成部分:
导出目录:提供关于导出的一般信息,如相关DLL的名称和导出函数的总数。
导出地址表:包含指向实际导出函数或变量的指针。
名称指针表:包含指向导出函数名称的指针。
序号表:为每个导出的函数或变量分配一个唯一的序号。
模块化和代码重用:通过导入表和导出表,PE文件可以实现高度的模块化和代码重用。它们使得程序能够在不增加体积的情况下访问丰富的功能。
动态链接:这两个表支持动态链接,即在程序运行时按需加载代码和数据,而非在程序启动时静态加载所有内容。这提高了内存利用效率,并减少了程序的初始加载时间。
版本控制和更新:使用DLL可以方便地更新和维护共享代码,无需重新编译使用这些DLL的所有程序。
#include
#include
#include
#include
#include
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " " << std::endl;
return 1;
}
std::ifstream file(argv[1], std::ios::binary);
if (!file) {
std::cerr << "Error opening file" << std::endl;
return 1;
}
IMAGE_DOS_HEADER dosHeader;
file.read(reinterpret_cast(&dosHeader), sizeof(IMAGE_DOS_HEADER));
if (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) {
std::cerr << "Not a valid PE file" << std::endl;
return 1;
}
file.seekg(dosHeader.e_lfanew, std::ios::beg);
IMAGE_NT_HEADERS ntHeaders;
file.read(reinterpret_cast(&ntHeaders), sizeof(IMAGE_NT_HEADERS));
if (ntHeaders.Signature != IMAGE_NT_SIGNATURE) {
std::cerr << "Not a valid PE file" << std::endl;
return 1;
}
// Print Import Table
if (ntHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size != 0) {
std::cout << "Import Table:" << std::endl;
// Add logic to read and print the import table
}
else {
std::cout << "No Import Table found." << std::endl;
}
// Print Export Table
if (ntHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size != 0) {
std::cout << "Export Table:" << std::endl;
// Add logic to read and print the export table
}
else {
std::cout << "No Export Table found." << std::endl;
}
return 0;
}
在Windows PE(Portable Executable)文件格式中,调试信息是指嵌入在可执行文件中的用于调试的元数据。这些信息通常仅存在于调试版本的PE文件中,为开发者在软件开发和维护阶段提供了额外的调试支持。
调试信息可以包含以下类型的数据:
调试符号(Debug Symbols):
源文件路径:
类型信息:
调用堆栈信息:
在Windows PE(Portable Executable)文件格式中,TLS(Thread Local Storage)表是一种用于支持多线程应用程序的机制。TLS表允许每个线程存储其自己的唯一数据副本。这在并发编程中非常有用,因为它提供了一种在不同线程之间隔离数据的方法。
线程专有数据:
避免数据冲突:
性能优化:
在C++中,使用Thread Local Storage(TLS)可以通过thread_local
关键字实现。这个关键字指示编译器为每个线程创建该变量的唯一副本。下面是一个使用thread_local
的C++示例,它创建了一个线程局部变量,并在多个线程中分别修改这个变量的值。
#include
#include
#include
// 使用thread_local关键字声明一个线程局部存储变量
thread_local int threadSpecificCounter = 0;
void incrementCounter() {
++threadSpecificCounter;
std::cout << "Counter for thread " << std::this_thread::get_id() << ": " << threadSpecificCounter << std::endl;
}
int main() {
// 创建一些线程并在每个线程中增加计数器
std::vector threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(incrementCounter);
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
return 0;
}
验证完整性:
确认来源:
防止恶意软件:
符合安全政策:
数字签名通常使用公钥基础设施(PKI)实现:
创建签名:
验证签名:
证书:
读取并验证PE文件的数字签名是一个涉及到Windows安全API的高级任务。在Windows系统中,可以使用如WinVerifyTrust
函数等相关API来验证文件的数字签名。以下是一个C++示例代码,演示如何使用Windows API来读取并验证PE文件的数字签名。
请注意,此代码需要在Windows环境下编译并运行,且需要链接到Wintrust.lib
和Crypt32.lib
。
#include
#include
#include
#include
#pragma comment(lib, "wintrust")
#pragma comment(lib, "crypt32")
bool VerifySignature(const std::wstring& filename) {
GUID WVTPolicyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_FILE_INFO fileInfo;
WINTRUST_DATA winTrustData;
memset(&fileInfo, 0, sizeof(fileInfo));
fileInfo.cbStruct = sizeof(fileInfo);
fileInfo.pcwszFilePath = filename.c_str();
memset(&winTrustData, 0, sizeof(winTrustData));
winTrustData.cbStruct = sizeof(winTrustData);
winTrustData.pPolicyCallbackData = NULL;
winTrustData.pSIPClientData = NULL;
winTrustData.dwUIChoice = WTD_UI_NONE;
winTrustData.fdwRevocationChecks = WTD_REVOKE_NONE;
winTrustData.dwUnionChoice = WTD_CHOICE_FILE;
winTrustData.dwStateAction = WTD_STATEACTION_VERIFY;
winTrustData.hWVTStateData = NULL;
winTrustData.pwszURLReference = NULL;
winTrustData.dwUIContext = 0;
winTrustData.pFile = &fileInfo;
LONG lStatus = WinVerifyTrust(NULL, &WVTPolicyGUID, &winTrustData);
winTrustData.dwStateAction = WTD_STATEACTION_CLOSE;
return lStatus == ERROR_SUCCESS;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " " << std::endl;
return 1;
}
std::wstring filename = std::wstring_convert>().from_bytes(argv[1]);
if (VerifySignature(filename)) {
std::cout << "The file is digitally signed and the signature was verified." << std::endl;
} else {
std::cout << "The file is not digitally signed or the signature could not be verified." << std::endl;
}
return 0;
}
这段代码首先定义了VerifySignature
函数,它使用Windows的WinVerifyTrust
函数来验证指定文件的数字签名。然后在main
函数中调用此函数并传入文件路径。
WinVerifyTrust
函数是 Windows API 的一部分,用于验证文件(通常是可执行文件或驱动程序)的数字签名。WinVerifyTrust
的验证过程全面且严格,涵盖了多个方面来确保文件的可信度和安全性。以下是WinVerifyTrust
函数验证过程中涉及的关键检查项:1. 签名存在性检查
首先,
WinVerifyTrust
检查文件是否有数字签名。如果文件没有签名,函数将返回错误,指示文件未签名。2. 签名有效性检查
如果文件有签名,
WinVerifyTrust
检查签名本身是否在技术上有效。这包括验证签名所用的数字证书是否正确,以及签名是否与文件内容匹配。任何不一致都会导致验证失败。3. 证书信任链验证
WinVerifyTrust
检查签名所用的证书是否由可信任的证书颁发机构(CA)颁发,并验证其信任链。证书需要由系统信任的根证书颁发机构签发,且整个证书链都需要是有效的。4. 证书撤销列表(CRL)检查
函数检查用于签名的证书是否已被撤销。如果证书或其任何父证书在证书撤销列表(CRL)上,验证将失败。
5. 证书有效期检查
WinVerifyTrust
检查签名所用证书的有效期。如果证书在签名时有效,但现在已过期,根据WinVerifyTrust
的配置,这可能会导致验证失败。6. 时间戳验证
大多数数字签名包含一个时间戳,证明文件是在证书有效期内签名的。
WinVerifyTrust
会验证这个时间戳的有效性。即使签名证书现在已经过期,只要文件是在证书有效期内签名的,签名仍然被视为有效。7. 额外的安全检查
根据
WinVerifyTrust
的配置,它还可以执行其他安全检查,如检查文件是否符合特定的策略或标准。8. 用户界面和交互
WinVerifyTrust
可以配置为在需要时显示用户界面(UI),例如当需要用户确认时。但在许多情况下,它在无UI模式下运行,以适应自动化的安全检查。
设置WINTRUST_DATA
结构中的dwProvFlags
字段来自定义校验选项。
dwProvFlags
是 Windows API 中 WINTRUST_DATA
结构的一个字段,用于设置 WinVerifyTrust
函数的行为。下面是 dwProvFlags
的一些标志及其中文详细注释:
WTD_PROV_FLAGS_MASK
- 用作掩码,用于限制可用的标志位。
WTD_USE_IE4_TRUST_FLAG
- 使用Internet Explorer 4.0的信任逻辑。
WTD_NO_IE4_CHAIN_FLAG
- 不使用Internet Explorer 4.0样式的链构建逻辑。
WTD_NO_POLICY_USAGE_FLAG
- 在验证过程中不检查证书中的任何策略。
WTD_REVOCATION_CHECK_NONE
- 不执行证书的撤销检查。
WTD_REVOCATION_CHECK_END_CERT
- 仅检查末端证书的撤销状态。
WTD_REVOCATION_CHECK_CHAIN
- 检查证书链中每个证书的撤销状态。
WTD_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT
- 检查除根证书外的整个证书链的撤销状态。
WTD_SAFER_FLAG
- 使用更安全的标准来评估证书链。
WTD_HASH_ONLY_FLAG
- 仅对文件的哈希值进行验证,而不是整
例子:不查签名有效期 代码怎么实现:
#include
#include
#include
#include
#pragma comment (lib, "wintrust")
bool VerifySignature(const wchar_t* filename, bool checkRevocation) {
GUID WVTPolicyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_FILE_INFO fileInfo;
WINTRUST_DATA winTrustData;
memset(&fileInfo, 0, sizeof(fileInfo));
fileInfo.cbStruct = sizeof(fileInfo);
fileInfo.pcwszFilePath = filename;
memset(&winTrustData, 0, sizeof(winTrustData));
winTrustData.cbStruct = sizeof(winTrustData);
winTrustData.dwUIChoice = WTD_UI_NONE;
winTrustData.fdwRevocationChecks = checkRevocation ? WTD_REVOKE_WHOLECHAIN : WTD_REVOKE_NONE;
winTrustData.dwUnionChoice = WTD_CHOICE_FILE;
winTrustData.dwStateAction = WTD_STATEACTION_VERIFY;
winTrustData.pFile = &fileInfo;
// 设置不检查证书有效期
winTrustData.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL | WTD_REVOCATION_CHECK_NONE;
LONG status = WinVerifyTrust(NULL, &WVTPolicyGUID, &winTrustData);
winTrustData.dwStateAction = WTD_STATEACTION_CLOSE;
return status == ERROR_SUCCESS;
}
int wmain(int argc, wchar_t* argv[]) {
if (argc != 2) {
std::wcerr << L"Usage: " << argv[0] << L" " << std::endl;
return 1;
}
if (VerifySignature(argv[1], false)) {
std::wcout << L"The file is digitally signed and the signature was verified." << std::endl;
} else {
std::wcout << L"The file is not digitally signed or the signature could not be verified." << std::endl;
}
return 0;
}
PE(Portable Executable)文件是Windows操作系统中使用的可执行文件格式,用于.exe、.dll、.sys等文件。PE文件结构是由一系列紧密相关的头部和数据节组成的,每个部分都承担着特定的功能。