手写 PE 文件

此内容是逆向工程实验内容,对应完整 PE 文件结构更加复杂

此处是借助 C 语言完成的,因为真正手写没有意义

  • 真正手写查错困难
  • 核心目的是了解 PE 文件的大概结构
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <winnt.h>

/**
 * @brief 生成一个简单 32 位 Windows 的 PE 文件 || 
 *      功能: 创建一个 pe.exe 文件,其功能是弹出提示对话框 ||
 *      主要使用 user32.dll 的 MessageBoxA 函数和 kernel32.dll 的 ExitProcess 函数  ||
 * 
 * @return int 
*/
int main(void)
{
    /* 1. 生成 DOS 头 */
    
    IMAGE_DOS_HEADER dosHeader = {
        .e_magic = IMAGE_DOS_SIGNATURE,
        .e_cblp = 0,
        .e_cp = 0,
        .e_crlc = 0,
        .e_cparhdr = 0,
        .e_minalloc = 0,
        .e_maxalloc = 0,
        .e_ss = 0,
        .e_sp = 0,
        .e_csum = 0,
        .e_ip = 0,
        .e_cs = 0,
        .e_lfarlc = 0,
        .e_ovno = 0,
        .e_res = {0, 0, 0, 0},
        .e_oemid = 0,
        .e_oeminfo = 0,
        .e_res2 = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        .e_lfanew = 0x00000040,                         // PE 头的偏移
    };


    /* 2. 生成 PE 头 */

    IMAGE_NT_HEADERS32 peHeader = {

        // PE 标识符 0x00004550
        .Signature = IMAGE_NT_SIGNATURE,

        // 生成文件头 IMAGE_FILE_HEADER     
        .FileHeader = {
            .Machine = IMAGE_FILE_MACHINE_I386,         // 可执行文件的目标 CPU 类型,此处为 32 位
            .NumberOfSections = 0x0003,                 // 包含的节表的数量,此处为 3 个
            .TimeDateStamp = 0x00000000,                // 文件创建时间【值以格林威治时间(GMT)1970 年 1 月 1 日午夜开始所经过的秒数表示】
            .PointerToSymbolTable = 0x00000000,         // 符号表的偏移,此处为 0
            .NumberOfSymbols = 0x00000000,              // 符号表的数量,此处为 0
            .SizeOfOptionalHeader = 0x00E0,             // 可选头的大小,此处为 0xE0
            .Characteristics = 0x0103,                  // 文件类型,此处为 可执行文件且不存在重定向信息
        },

        // 生成可选头 IMAGE_OPTIONAL_HEADER
        .OptionalHeader = {
            .Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC,     // 可选头的标识符,此处为 32 位
            .MajorLinkerVersion = 0x00,                 // 链接器的主版本号
            .MinorLinkerVersion = 0x00,                 // 链接器的次版本号
            .SizeOfCode = 0x00001000,                   // 代码区的大小,此处为 0x1000
            .SizeOfInitializedData = 0x00000000,        // 初始化数据区的大小
            .SizeOfUninitializedData = 0x00000000,      // 未初始化数据区的大小
            .AddressOfEntryPoint = 0x00001000,          // 入口点的地址,此处为 0x1000
            .BaseOfCode = 0x00001000,                   // 代码区的基地址,此处为 0x1000
            .BaseOfData = 0x00002000,                   // 数据区的基地址,此处为 0x2000
            .ImageBase = 0x00400000,                    // PE 文件的基地址
            .SectionAlignment = 0x00001000,             // 节表对齐【节表的大小必须是此值的整数倍,Win32 通过为 0x1000 表示 4KB】,此处为 0x1000
            .FileAlignment = 0x00001000,                // 文件对齐【文件的大小必须是此值的整数倍,Win32 通过为 0x1000 或 0x200 表示 4KB 或 512B】,此处为 0x200
            .MajorOperatingSystemVersion = 0x0004,      // 操作系统的主版本号
            .MinorOperatingSystemVersion = 0x0000,      // 操作系统的次版本号
            .MajorImageVersion = 0x0000,                // PE 文件的主版本号
            .MinorImageVersion = 0x0000,                // PE 文件的次版本号
            .MajorSubsystemVersion = 0x0004,            // 子系统的主版本号
            .MinorSubsystemVersion = 0x0000,            // 子系统的次版本号
            .Win32VersionValue = 0x00000000,            // 保留
            .SizeOfImage = 0x00004000,                  // PE 文件的大小,此处为 0x4000
            .SizeOfHeaders = 0x00001000,                // 整个 PE 文件头的大小【包括 DOS 头、PE 头、节表】,此处为 0x1000
            .CheckSum = 0x00000000,                     // 校验和【通常为 0】
            .Subsystem = 0x0002,                        // 子系统类型,此处为 Windows 图形界面子系统
            .DllCharacteristics = 0x0000,               // 指定 DLL 的特性
            .SizeOfStackReserve = 0x00100000,           // 保留的栈空间大小
            .SizeOfStackCommit = 0x00001000,            // 提交的栈空间大小
            .SizeOfHeapReserve = 0x00100000,            // 保留的堆空间大小
            .SizeOfHeapCommit = 0x00001000,             // 提交的堆空间大小
            .LoaderFlags = 0x00000000,                  // 保留
            .NumberOfRvaAndSizes = 0x00000010,          // 数据目录的数量,此处为 16 个
            .DataDirectory = {
                {0x00000000, 0x00000000},               // 0. 导出表
                {0x00003010, 0x00000028},               // 1. 导入表,此处为 0x3010,大小为 0x28
                {0x00000000, 0x00000000},               // 2. 资源表
                {0x00000000, 0x00000000},               // 3. 异常表
                {0x00000000, 0x00000000},               // 4. 安全表
                {0x00000000, 0x00000000},               // 5. 重定位表
                {0x00000000, 0x00000000},               // 6. 调试表
                {0x00000000, 0x00000000},               // 7. 特征表
                {0x00000000, 0x00000000},               // 8. 全局指针
                {0x00000000, 0x00000000},               // 9. TLS 表
                {0x00000000, 0x00000000},               // 10. 加载配置表
                {0x00000000, 0x00000000},               // 11. 绑定表
                {0x00003000, 0x00000010},               // 12. 导入地址表,此处为 0x3000,大小为 0x10
                {0x00000000, 0x00000000},               // 13. 延迟导入表
                {0x00000000, 0x00000000},               // 14. COM+ 运行时头
                {0x00000000, 0x00000000},               // 15. 保留
            },
        },
    };


    /* 3. 节表 */

    // 生成节表 IMAGE_SECTION_HEADER,此处为 3 个节表:.text, .data, .idata
    IMAGE_SECTION_HEADER sectionHeader[3] = {
        {
            .Name = ".text",                            // 节表的名称
            .Misc = {0x00001000},                       // 保留
            .VirtualAddress = 0x00001000,               // 节表的虚拟地址,此处为 0x1000
            .SizeOfRawData = 0x00001000,                // 节表的大小,此处为 0x1000
            .PointerToRawData = 0x00001000,             // 节表在文件中的偏移,此处为 0x1000
            .PointerToRelocations = 0x00000000,         // 重定位表的偏移,此处为 0
            .PointerToLinenumbers = 0x00000000,         // 行号表的偏移,此处为 0
            .NumberOfRelocations = 0x0000,              // 重定位表的数量,此处为 0
            .NumberOfLinenumbers = 0x0000,              // 行号表的数量,此处为 0
            .Characteristics = 0x60000020,              // 节表的特性,此处为 可执行代码且可读
        },
        {
            .Name = ".data",                            // 节表的名称
            .Misc = {0x00001000},                       // 保留
            .VirtualAddress = 0x00002000,               // 节表的虚拟地址,此处为 0x2000
            .SizeOfRawData = 0x00001000,                // 节表的大小,此处为 0x1000
            .PointerToRawData = 0x00002000,             // 节表在文件中的偏移,此处为 0x2000
            .PointerToRelocations = 0x00000000,         // 重定位表的偏移,此处为 0
            .PointerToLinenumbers = 0x00000000,         // 行号表的偏移,此处为 0
            .NumberOfRelocations = 0x0000,              // 重定位表的数量,此处为 0
            .NumberOfLinenumbers = 0x0000,              // 行号表的数量,此处为 0
            .Characteristics = 0xC0000040,              // 节表的特性,此处为 可读写数据且可读
        },
        {
            .Name = ".idata",                           // 节表的名称
            .Misc = {0x00001000},                       // 保留
            .VirtualAddress = 0x00003000,               // 节表的虚拟地址,此处为 0x3000
            .SizeOfRawData = 0x00001000,                // 节表的大小,此处为 0x1000
            .PointerToRawData = 0x00003000,             // 节表在文件中的偏移,此处为 0x3000
            .PointerToRelocations = 0x00000000,         // 重定位表的偏移,此处为 0
            .PointerToLinenumbers = 0x00000000,         // 行号表的偏移,此处为 0
            .NumberOfRelocations = 0x0000,              // 重定位表的数量,此处为 0
            .NumberOfLinenumbers = 0x0000,              // 行号表的数量,此处为 0
            .Characteristics = 0xC0000040,              // 节表的特性,此处为 可读写数据且可读
        }
    };


    /* 4. 填充整个 PE 文件头【DOS 头 + NT 头 + 节表】的大小为 0x1000 剩余的空间 */

    char fillPeHeader[0x1000 - sizeof(IMAGE_DOS_HEADER) - sizeof(IMAGE_NT_HEADERS32) - sizeof(IMAGE_SECTION_HEADER) * 3] = {0};


    /* 节表信息 */

    // .text 节表区域
    char TextSection[0x1000] = {0};
    // .text 汇编代码
    char textCode[] = {
        0x6A, 0x00,                                 // PUSH 0x00000000
        0x68, 0x20, 0x20, 0x40, 0x00,               // PUSH 0x00402020      ; "Binary Diy"
        0x68, 0x00, 0x20, 0x40, 0x00,               // PUSH 0x00402000      ; "Hello, Pe Binary Diy!!"
        0x6A, 0x00,                                 // PUSH 0x00000000
        0xE8, 0x07, 0x00, 0x00, 0x00,               // CALL 0x0040101A      ; JMP. &user32.MessageBoxA
        0x6A, 0x00,                                 // PUSH 0x00000000
        0xE8, 0x06, 0x00, 0x00, 0x00,               // CALL 0x00401020      ; JMP. &kernel32.ExitProcess
        0xFF, 0x25, 0x00, 0x30, 0x40, 0x00,         // JMP 0x00403000       ; user32.MessageBoxA
        0xFF, 0x25, 0x08, 0x30, 0x40, 0x00          // JMP 0x00403008       ; kernel32.ExitProcess
    };
    // 写入 .text 汇编代码
    memcpy(TextSection, textCode, sizeof(textCode));

    // .data 节表区域
    char DataSection[0x1000] = {0};
    // .data 数据
    char data1[] = "Hello, Pe Binary Diy!!";
    char data2[] = "Binary Diy";
    // 写入 .data 数据
    memcpy(DataSection, data1, sizeof(data1));
    memcpy(DataSection + 0x20, data2, sizeof(data2));

    // .idata 节表区域
    char RDataSection[0x1000] = {0};
    // .idata 数据
    IMAGE_THUNK_DATA32 importAddressTable[4] = {
        {
            .u1.AddressOfData = 0x00003070,         // 导入函数名称表
        },
        {
            .u1.AddressOfData = 0x00000000,         // 导入函数名称表
        },
        {
            .u1.AddressOfData = 0x000030A0,         // 导入函数名称表
        },
        {
            .u1.AddressOfData = 0x00000000,         // 导入函数名称表
        }
    };
    IMAGE_IMPORT_DESCRIPTOR importDescriptor[3] = {
        {
            .OriginalFirstThunk = 0x00003060,       // 原始导入地址表
            .TimeDateStamp = 0x00000000,            // 时间戳
            .ForwarderChain = 0x00000000,           // 转发链
            .Name = 0x00003050,                     // DLL 名称
            .FirstThunk = 0x00003000,               // 导入地址表
        },
        {
            .OriginalFirstThunk = 0x00003090,       // 原始导入地址表
            .TimeDateStamp = 0x00000000,            // 时间戳
            .ForwarderChain = 0x00000000,           // 转发链
            .Name = 0x00003080,                     // DLL 名称
            .FirstThunk = 0x00003008,               // 导入地址表
        },
        {
            .OriginalFirstThunk = 0x00000000,       // 原始导入地址表
            .TimeDateStamp = 0x00000000,            // 时间戳
            .ForwarderChain = 0x00000000,           // 转发链
            .Name = 0x00000000,                     // DLL 名称
            .FirstThunk = 0x00000000                // 导入地址表
        }
    };
    char user32[] = "user32.dll";
    char messageboxa[] = "MessageBoxA";
    char kernel32[] = "kernel32.dll";
    char exitprocess[] = "ExitProcess";
    // 写入 .idata 数据
    memcpy(RDataSection, importAddressTable, sizeof(importAddressTable));
    memcpy(RDataSection + 0x10, importDescriptor, sizeof(importDescriptor));
    memcpy(RDataSection + importDescriptor[0].Name - 0x00003000, user32, sizeof(user32));
    memcpy(
        RDataSection + importDescriptor[0].OriginalFirstThunk - 0x00003000, 
        RDataSection + importDescriptor[0].FirstThunk - 0x00003000,
        sizeof(importAddressTable[0].u1.AddressOfData)
    );
    memcpy(
        RDataSection + importAddressTable[0].u1.AddressOfData - 0x00003000 + 0x2, 
        messageboxa,
        sizeof(messageboxa)
    );
    memcpy(RDataSection + importDescriptor[1].Name - 0x00003000, kernel32, sizeof(kernel32));
    memcpy(
        RDataSection + importDescriptor[1].OriginalFirstThunk - 0x00003000, 
        RDataSection + importDescriptor[1].FirstThunk - 0x00003000,
        sizeof(importAddressTable[2].u1.AddressOfData)
    );
    memcpy(
        RDataSection + importAddressTable[2].u1.AddressOfData - 0x00003000 + 0x2, 
        exitprocess,
        sizeof(exitprocess)
    );

    /* 上面 PE 数据写入文件 pe.exe,注意文件操纵时的异常处理 */
    FILE *fp = fopen("pe.exe", "wb");
    if (fp == NULL) {
        printf("open file error!\n");
        return -1;
    }

    // 写入 DOS 头、PE 头、节表、PE 头填充数据
    fwrite(&dosHeader, sizeof(dosHeader), 1, fp);                   
    fwrite(&peHeader, sizeof(peHeader), 1, fp);                     
    fwrite(&sectionHeader, sizeof(sectionHeader), 1, fp);           
    fwrite(fillPeHeader, sizeof(fillPeHeader), 1, fp);              

    // 写入 .text .data .idata 节表区域
    fwrite(TextSection, sizeof(TextSection), 1, fp);            
    fwrite(DataSection, sizeof(DataSection), 1, fp);
    fwrite(RDataSection, sizeof(RDataSection), 1, fp);

    /* 输出上述结构体的大小 */
    printf("SIZE: %X, TO: %X\n", sizeof(IMAGE_DOS_HEADER), sizeof(IMAGE_DOS_HEADER));
    printf("SIZE: %X, TO: %X\n", sizeof(IMAGE_NT_HEADERS32), sizeof(IMAGE_DOS_HEADER) + sizeof(IMAGE_NT_HEADERS32));
    printf(
        "SIZE: %X, TO: %X\n", sizeof(IMAGE_SECTION_HEADER) * 3, 
        sizeof(IMAGE_DOS_HEADER) + sizeof(IMAGE_NT_HEADERS32) + sizeof(IMAGE_SECTION_HEADER) * 3
    );
    printf(
        "SIZE: %X, TO: %X\n", sizeof(fillPeHeader), 
        sizeof(IMAGE_DOS_HEADER) + sizeof(IMAGE_NT_HEADERS32) + sizeof(IMAGE_SECTION_HEADER) * 3 + sizeof(fillPeHeader)
    );

    return 0;
}