目录

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文件通常包含以下部分:

windows PE文件都包含哪些信息【详细汇总介绍】-LMLPHP

 

1. DOS头

DOS头的作用

        DOS头的主要作用是使PE文件向后兼容,即使在早期的操作系统(如MS-DOS)上也能被识别为可执行文件。在Windows环境中,DOS头主要是作为PE文件的一个标准组成部分,提供指向实际PE头部的指针(即e_lfanew字段)。

DOS头的结构

  1. e_magic:Magic number,通常为“MZ”,标识文件是一个可执行文件。这是MS-DOS可执行文件的标准签名。

  2. e_cblp:文件最后一页的字节数。

  3. e_cp:文件页数。

  4. e_crlc:重定位项的数量。

  5. e_cparhdr:头部大小,单位为段落(每段落16字节)。

  6. e_minalloc:所需的最小额外段落。

  7. e_maxalloc:所需的最大额外段落。

  8. e_ss:初始的SS(栈段)值。

  9. e_sp:初始的SP(栈指针)值。

  10. e_csum:校验和。

  11. e_ip:初始的IP(指令指针)值。

  12. e_cs:初始的CS(代码段)值。

  13. e_lfarlc:重定位表的文件地址。

  14. e_ovno:覆盖号。

  15. e_res:保留字。

  16. e_oemide_oeminfo:OEM标识符和OEM信息。

  17. e_res2:保留字。

  18. 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头的开始位置。 

C代码判断PE文件

        在Windows系统上,DOS头的关键作用是包含一个指向PE头的偏移量(e_lfanew字段),这使得操作系统能够正确地找到并解析PE头,从而加载和执行程序。 

        以下是一个简单的C语言示例,演示了如何读取一个PE文件的DOS头并找到PE头的位置:

#include <stdio.h>
#include <stdlib.h>

// 定义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 <PE file>\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;
}

2. PE文件签名

PE文件签名的位置和作用

  1. 位置:PE文件签名位于文件的开始部分,紧随DOS头之后。在DOS头中,e_lfanew字段指明了PE文件签名的偏移量。这意味着,通过读取DOS头中的e_lfanew字段,可以找到PE文件签名的确切位置。

  2. 作用:PE文件签名用于标识文件为PE格式。它是一个固定的、标准化的数据结构,使得操作系统和其他软件能够确认文件是一个有效的Windows可执行文件。如果这个签名不存在或不正确,文件将不被视为有效的PE文件,因此无法在Windows环境中正常加载或执行。

PE文件签名的结构

PE文件签名通常由以下部分组成:

  • 签名本身:固定为"PE\0\0"(在十六进制中表示为50 45 00 00)。这个签名紧随DOS头之后,位于由DOS头中的e_lfanew字段指出的位置。
#include <stdio.h>
#include <stdint.h>

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 <PE file>\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(Common Object File Format)头

COFF头的结构

COFF头包含以下主要字段:

  1. Machine:指定目标机器的类型。例如,0x14c代表Intel 386或更高版本兼容机器。

  2. NumberOfSections:文件中的节(Section)数量。

  3. TimeDateStamp:文件被创建的时间戳。

  4. PointerToSymbolTable:符号表的起始位置。在许多PE文件中,这个值为零,因为符号表通常不包含在最终分发的可执行文件中。

  5. NumberOfSymbols:符号表中的符号数量。

  6. SizeOfOptionalHeader:可选头部的大小。这个值指示紧随COFF头部之后的PE可选头部的大小。

  7. Characteristics:表示文件的特性,例如是否为可执行文件,是否可以在32位机器上运行等。

COFF头的作用

COFF头在PE文件中扮演着核心角色:

  • 兼容性:通过Machine字段,操作系统可以确定该文件是否与当前系统兼容。

  • 管理和链接:通过对节的数量和特性的描述,操作系统和链接器可以正确地管理和链接不同部分。

  • 时间戳:有助于系统或开发工具确定文件的版本。

  • 可选头部信息:提供了关于接下来的可选头部大小的信息,这对于解析PE文件结构至关重要。

代码

#include <stdio.h>
#include <stdint.h>

// 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 <PE file>\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;
}

3. 标准PE头&可选PE头

        在PE(Portable Executable)文件格式中,标准PE头,也常被称为“文件头”(File Header),是紧随COFF(Common Object File Format)头之后的一个关键部分。这个头部包含了一些对于可执行文件至关重要的信息,这些信息是操作系统用来正确加载和执行文件所必需的。

标准PE头

标准PE头包括以下主要字段:

  1. Machine:指定目标机器的类型。这个字段定义了文件是为哪种类型的CPU构建的,例如x86、Itanium、AMD64等。

  2. NumberOfSections:如前所述,这个字段指定文件中节的数量。

  3. TimeDateStamp:文件创建的时间戳。

  4. PointerToSymbolTableNumberOfSymbols:这两个字段用于指向和定义符号表的位置和大小。在许多情况下,尤其是在最终用户的可执行文件中,这些字段通常不被使用。

  5. SizeOfOptionalHeader:指定紧随其后的可选头部(Optional Header)的大小。这个信息告诉解析器如何定位和解析可选头部。

  6. Characteristics:文件的属性,例如是不是DLL文件,是不是可以在32位或64位系统上运行等。

可选PE头

紧随标准PE头之后的是可选PE头(Optional Header),虽然它被称为“可选”,但对于大多数PE文件来说是必需的,因为它包含了一些重要的执行和加载信息。可选PE头包括:

  1. Magic:一个标识,指示文件是32位(PE32)还是64位(PE32+)格式。

  2. EntryPoint:程序的入口点,当操作系统加载文件时,执行将从这里开始。

  3. ImageBase:建议加载此可执行文件的内存地址。操作系统可以使用这个地址,或者选择一个不同的地址。

  4. SectionAlignmentFileAlignment:内存和文件中各个节的对齐方式。

  5. SizeOfImage:加载到内存后的总尺寸。

  6. SizeOfHeaders:PE头部的总大小。

  7. Subsystem:指定文件运行的环境,例如Windows GUI或命令行。

  8. DLLCharacteristics:DLL特定的属性。

  9. StackReserveSize, StackCommitSize, HeapReserveSize, HeapCommitSize:分别定义了进程的堆栈和堆的预留大小和提交大小。

  10. DataDirectories:指向各种重要数据表的指针,如导入表、导出表、资源表等。

#include <stdio.h>
#include <stdint.h>

// 标准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 <PE file>\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;
}

4. 数据目录

数据目录的主要作用是为PE文件中的关键数据提供快速访问。这些数据结构包括但不限于:

  1. 导入表(Import Table):列出了文件依赖的其他模块(如DLL)及其导入的函数。

  2. 导出表(Export Table):包含了文件提供给其他模块使用的函数和变量。

  3. 资源表(Resource Table):存储程序的资源,如图标、字符串、菜单等。

  4. 异常表(Exception Table):用于处理异常和错误。

  5. 证书表(Certificate Table):存储数字签名等安全相关的信息。

  6. 基址重定位表(Base Relocation Table):如果文件不能加载到预期的内存地址,这个表提供了必要的地址调整信息。

  7. 调试信息(Debug Directory):包含程序调试时使用的信息。

  8. 线程局部存储(Thread Local Storage Directory):用于多线程应用中存储每个线程的局部数据。

  9. 加载配置表(Load Configuration Table):包含了加载和运行程序所需的配置信息。

  10. 绑定导入表(Bound Import Table):包含了提前绑定的导入信息,以加快加载速度。

  11. 导入地址表(Import Address Table):用于修正导入函数的地址。

  12. 延迟导入描述符(Delay Import Descriptor):列出了延迟加载的模块。

  13. CLR运行时头(CLR Runtime Header):对于.NET程序,包含了指向公共语言运行时信息的指针。

数据目录的结构

每个数据目录项通常由两个部分组成:

  • VirtualAddress:指向相关数据结构的RVA(相对虚拟地址)。

  • Size:数据结构的大小。

#include <stdio.h>
#include <stdint.h>

// 定义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 <PE file>\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;
}

5. 节

  1. .text节

    • 作用:包含程序的可执行代码。
    • 特点:这是代码的主要存储地点,通常是只读的,以防止程序运行时对其自身代码的意外修改。
  2. .data节

    • 作用:包含初始化的全局和静态变量。
    • 特点:这些变量在程序开始执行之前已经被初始化,它们的初始值存储在文件中。
  3. .rdata节

    • 作用:包含只读数据,如常量字符串和导入表。
    • 特点:这些数据在整个程序运行期间不会被修改,例如文字字符串和其他常量。
  4. .bss节

    • 作用:包含未初始化的全局和静态变量。
    • 特点:这些变量在程序开始执行之前未被初始化。为了节省文件大小,这些变量在文件中不占用实际空间,而是在程序加载时由操作系统分配并初始化为零。
  5. .idata节

    • 作用:包含导入函数和变量的信息。
    • 特点:此节包含了程序使用的所有外部函数和变量(通常来自DLL)的信息,这些信息用于在运行时动态链接。
  6. .edata节

    • 作用:包含导出函数和变量的信息。
    • 特点:如果PE文件是一个DLL,该节包含其他程序或DLL可以使用的函数和变量的地址。
  7. .rsrc节

    • 作用:包含资源数据,如图标、菜单和对话框。
    • 特点:这些资源是程序的非代码部分,可以包括图像、音频、UI布局等。
  8. .reloc节

    • 作用:包含重定位信息,用于动态链接。
    • 特点:当程序不能被加载到其预定的基址时,这些信息用于调整硬编码的地址。这对于支持地址空间布局随机化(ASLR)的系统尤为重要。
#include <stdio.h>
#include <stdint.h>
#include <string.h>

// ...(之前定义的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 <PE file>\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(&sectionHeader, 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;
}

6. 重定位给表

        在Windows PE(Portable Executable)文件格式中,重定位表(Base Relocation Table)是一种机制,用于在程序不能加载到其预先定义的首选地址时调整硬编码的指针。这在多种情况下是必需的,特别是在支持地址空间布局随机化(ASLR)的系统上。

重定位表的作用

  1. 地址空间布局随机化(ASLR)

    • ASLR是一种安全技术,用于随机化进程在内存中的地址,从而使攻击者难以预测地址。
    • 当系统启用ASLR时,每次程序加载时都可能位于不同的地址。
  2. 动态加载

    • 在动态链接库(DLL)或可执行文件被加载时,如果它们不能被加载到预定的基址,操作系统需要调整内部的地址引用。
  3. 硬编码地址的调整

    • 程序中的某些指针可能是硬编码的,指向特定的内存地址。
    • 如果程序未加载到其预定基址,这些硬编码地址需要根据实际加载位置进行调整。

重定位表的结构

  • 重定位表由一系列重定位项(Relocation Entries)组成。
  • 每个重定位项指示需要进行调整的地址,以及如何进行调整。
  • 通常,这些项是相对于节(Section)的偏移量,并指明在该偏移处如何修改地址。

重定位过程

  1. 加载时处理

    • 当操作系统加载PE文件时,它检查是否能够将文件加载到其首选地址。
    • 如果不能,操作系统遍历重定位表中的每一项。
  2. 地址调整

    • 对于表中的每一个条目,操作系统读取指定偏移处的地址,并根据文件的实际加载位置进行调整。
    • 调整通常涉及将原始地址加上实际加载地址与预期加载地址之间的差值。

重要性

  • 可执行文件的灵活性:重定位表使得PE文件可以在内存中的任何位置运行,提高了其灵活性和兼容性。
  • 安全性:对于现代操作系统,重定位表对于支持ASLR是必要的,这有助于提高应用程序和系统的安全性。
  • 动态链接支持:重定位表对于支持动态链接的系统至关重要,尤其是在处理共享库和模块时。

代码 

        读取并利用PE文件的重定位表是一个相对复杂的过程,涉及到解析PE文件结构,定位重定位表,然后根据表中的条目调整内存中的地址。以下是一个C++代码示例,演示了如何读取PE文件的重定位表。但请注意,实际上“利用”这些重定位信息——比如用于动态修改程序行为——通常不是一个简单的任务,且可能涉及到深入的系统编程和对底层内存管理的理解。        

#include <iostream>
#include <fstream>
#include <vector>
#include <string>

// ...(之前定义的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] << " <PE file>" << 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<char*>(&relocBlock), sizeof(IMAGE_BASE_RELOCATION));
        if (relocBlock.SizeOfBlock == 0) {
            break;
        }

        // 计算此块的重定位项数量
        size_t numberOfEntries = (relocBlock.SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(uint16_t);

        std::vector<uint16_t> entries(numberOfEntries);
        file.read(reinterpret_cast<char*>(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;
}


7. 资源表 

        在Windows PE(Portable Executable)文件格式中,资源表(Resource Table)是一种特殊的数据结构,用于存储程序的资源。这些资源包括图标、菜单、对话框、字符串表、位图、字体等。资源表是PE文件的一个关键组成部分,使得程序可以包含各种非代码数据。

资源表的作用

  1. 存储多媒体和UI元素:资源表通常包含图标、光标、位图等多媒体资源,以及菜单、对话框等用户界面元素。

  2. 本地化和国际化:资源表可以包含为不同语言或地区定制的字符串和其他资源,支持软件的本地化。

  3. 动态加载:程序运行时可以按需加载资源,而不是在启动时加载所有资源,这有助于提高效率和响应速度。

资源表的结构

  • 资源目录:资源表以树状结构组织,顶级是资源目录,其中包含指向不同类型资源的指针。

  • 资源类型:资源按类型组织,常见的类型包括图标、光标、位图、菜单、对话框等。

  • 资源名称和ID:资源可以通过名称或数字ID进行标识。

  • 资源语言:每种资源可以有针对不同语言的多个版本。

  • 资源数据:实际的资源数据,如图像文件的内容、菜单的描述等。

#include <iostream>
#include <fstream>
#include <vector>
#include <string>

// ...(之前定义的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] << " <PE file>" << 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<char*>(&resourceDir), sizeof(IMAGE_RESOURCE_DIRECTORY));

    // 解析资源目录项
    for (int i = 0; i < resourceDir.NumberOfNamedEntries + resourceDir.NumberOfIdEntries; ++i) {
        IMAGE_RESOURCE_DIRECTORY_ENTRY dirEntry;
        file.read(reinterpret_cast<char*>(&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文件)以及程序向外提供的函数和变量。

导入表(Import Table)

        导入表是PE文件中的一个数据结构,用于列出程序运行时需要从其他模块(通常是DLL文件)导入的函数和变量。这个表使得程序可以使用动态链接库(DLL)中的代码和数据,而不需要将这些代码和数据静态地编译到执行文件中。

导入表的主要组成部分:

  1. 导入目录:包含一系列条目,每个条目对应一个程序依赖的DLL。

  2. 导入查找表:每个导入目录条目都指向一个导入查找表,它包含了程序从相应DLL导入的函数和变量的名称或序号。

  3. 导入地址表:在程序运行时,这个表被填充为实际的函数或变量的地址。它的初始内容与导入查找表相同。

导出表(Export Table)

        导出表是PE文件中的一个数据结构,用于列出程序向外提供的函数和变量,使得其他程序或DLL可以使用这些功能。导出表对于DLL文件尤为重要,因为它们通常用于提供多个程序共享的函数和变量。

导出表的主要组成部分:

  1. 导出目录:提供关于导出的一般信息,如相关DLL的名称和导出函数的总数。

  2. 导出地址表:包含指向实际导出函数或变量的指针。

  3. 名称指针表:包含指向导出函数名称的指针。

  4. 序号表:为每个导出的函数或变量分配一个唯一的序号。

重要性

  • 模块化和代码重用:通过导入表和导出表,PE文件可以实现高度的模块化和代码重用。它们使得程序能够在不增加体积的情况下访问丰富的功能。

  • 动态链接:这两个表支持动态链接,即在程序运行时按需加载代码和数据,而非在程序启动时静态加载所有内容。这提高了内存利用效率,并减少了程序的初始加载时间。

  • 版本控制和更新:使用DLL可以方便地更新和维护共享代码,无需重新编译使用这些DLL的所有程序。

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <windows.h>

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <PE file>" << 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<char*>(&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<char*>(&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;
}

8. 调试信息

在Windows PE(Portable Executable)文件格式中,调试信息是指嵌入在可执行文件中的用于调试的元数据。这些信息通常仅存在于调试版本的PE文件中,为开发者在软件开发和维护阶段提供了额外的调试支持。

调试信息的内容

调试信息可以包含以下类型的数据:

  1. 调试符号(Debug Symbols)

    • 包括函数名、变量名、行号等。
    • 使得开发者可以在源代码级别进行调试,而不仅仅是汇编级别。
  2. 源文件路径

    • 指向源代码文件的路径,有助于在调试器中定位源代码。
  3. 类型信息

    • 包含程序中使用的数据类型的信息,有助于更好地理解程序的数据结构。
  4. 调用堆栈信息

    • 提供函数调用堆栈的信息,有助于跟踪函数调用的顺序和层次。

9. TLS

        在Windows PE(Portable Executable)文件格式中,TLS(Thread Local Storage)表是一种用于支持多线程应用程序的机制。TLS表允许每个线程存储其自己的唯一数据副本。这在并发编程中非常有用,因为它提供了一种在不同线程之间隔离数据的方法。

TLS表的作用

  1. 线程专有数据

    • 在多线程环境中,每个线程可以拥有自己的TLS副本,用于存储线程特有的数据。
    • 例如,线程的状态、特定于线程的配置或临时数据。
  2. 避免数据冲突

    • 使用TLS,不同的线程可以同时操作相同类型的数据而不会互相干扰。
    • 这避免了线程间的数据竞争和同步问题。
  3. 性能优化

    • 在某些情况下,使用TLS可以减少锁的需求,从而提高应用程序的性能。

TLS表的结构

  • TLS表通常包含一系列指针,每个指针指向一个TLS索引。
  • 每个索引对应于线程特有的数据块。
  • TLS表还包含了初始化和销毁这些数据块的函数指针。

        在C++中,使用Thread Local Storage(TLS)可以通过thread_local关键字实现。这个关键字指示编译器为每个线程创建该变量的唯一副本。下面是一个使用thread_local的C++示例,它创建了一个线程局部变量,并在多个线程中分别修改这个变量的值。

#include <iostream>
#include <thread>
#include <vector>

// 使用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<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(incrementCounter);
    }

    // 等待所有线程完成
    for (auto& thread : threads) {
        thread.join();
    }

    return 0;
}

10. 数字签名

数字签名的作用

  1. 验证完整性

    • 数字签名确保文件自签名以来未被修改。任何对文件的更改都会导致签名验证失败。
  2. 确认来源

    • 签名提供了关于文件来源的信息,确认文件是由签名证书中指定的实体创建的。
  3. 防止恶意软件

    • 通过验证签名,操作系统可以阻止未经签名或签名无效的潜在恶意软件执行。
  4. 符合安全政策

    • 许多组织的IT政策要求只运行已签名的软件,以增强安全性。

数字签名的实现

数字签名通常使用公钥基础设施(PKI)实现:

  1. 创建签名

    • 文件的创建者使用私钥对文件的哈希(摘要)进行加密,产生数字签名。
    • 签名一起打包在PE文件中,通常附加在文件的末尾。
  2. 验证签名

    • 验证方使用相应的公钥解密签名,并产生文件的哈希值。
    • 然后将这个哈希值与文件当前的哈希值比较。如果两个哈希值匹配,则签名有效。
  3. 证书

    • 签名通常包含一个证书,证明公钥属于声明的实体。
    • 证书由受信任的证书颁发机构(CA)签发。

读取数字签名

        读取并验证PE文件的数字签名是一个涉及到Windows安全API的高级任务。在Windows系统中,可以使用如WinVerifyTrust函数等相关API来验证文件的数字签名。以下是一个C++示例代码,演示如何使用Windows API来读取并验证PE文件的数字签名。

        请注意,此代码需要在Windows环境下编译并运行,且需要链接到Wintrust.libCrypt32.lib

#include <iostream>
#include <windows.h>
#include <wintrust.h>
#include <softpub.h>

#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] << " <file path>" << std::endl;
        return 1;
    }

    std::wstring filename = std::wstring_convert<std::codecvt_utf8<wchar_t>>().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函数中调用此函数并传入文件路径。

        设置WINTRUST_DATA结构中的dwProvFlags字段来自定义校验选项。

  dwProvFlags 是 Windows API 中 WINTRUST_DATA 结构的一个字段,用于设置 WinVerifyTrust 函数的行为。下面是 dwProvFlags 的一些标志及其中文详细注释:

        例子:不查签名有效期 代码怎么实现:

#include <windows.h>
#include <wintrust.h>
#include <softpub.h>
#include <iostream>

#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" <PE file>" << 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文件结构是由一系列紧密相关的头部和数据节组成的,每个部分都承担着特定的功能。

12-31 18:53