《C++黑客编程解密》05 - PE文件结构

PE 文件结构

前三个部分是数据组织结构部分,节表数据才是PE真正的数据部分

    

  1. DOS 头
  2. PE 头
    保存windows系统加载可执行文件的重要信息。PE头部有 IMAGE_NT_HEADERS 定义,其包含了 IMAGE_NT_SIGNATRUE IMAGE_FILE_HEADER IMAGE_OPTIONAL_HEADER 三部分。PE头的位置固定不变,其有DOS头某个字段给出。
  3. 节表
    程序的组织按照各属性的不同被保存在不同节中,PE头部之后就是一个数组结构的节表。结构体位 IMAGE_SECTION_HEADER 文件有几个节就有几个结构体组成的数组。节表保存了各个节的属性、文件位置、内存位置等相关信息。
  4. 节表数据

DOS 头部 IMAGE_DOS_HEADER

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number  DOS可执行文件的标识符,字符“MZ”: #define IMAGE_DOS_SIGNATURE   0x5A4D // MZ
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header   PE头的起始位置
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

在pe头和dos结构体之间有一个dos存根,这是一个没有用的DOS程序。可以将这一段内容删除然后将pe头前移,也可以在此处填充其他数据

PE头部有 IMAGE_NT_HEADERS

这是一个宏,具体分为32位和64位两个版本:

#ifdef _WIN64
typedef IMAGE_NT_HEADERS64                  IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64                 PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32                  IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32                 PIMAGE_NT_HEADERS;
#endif

32位的具体实现:

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;                         // PE 标识符,用于判断是否是pe文件,就是: #define IMAGE_NT_SIGNATURE    0x00004550  // PE00
    IMAGE_FILE_HEADER FileHeader;            // 文件头
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;  // 可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

IMAGE_FILE_HEADER

是 IMAGE_NT_HEADERS 中的一个结构体,紧跟在PE标识符后面,大小位20字节,起始位置取决于PE头起始位置,PE头位置决定于 IMAGE_DOS_HEADERe_lfanew 的位置。除了 IMAGE_DOS_HEADER 以外,其他头部起始位置都依赖PE头部起始位置。

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;              // 目标CPU
    WORD    NumberOfSections;     // PE文件节区个数
    DWORD   TimeDateStamp;        // 文件创建时间戳
    DWORD   PointerToSymbolTable; // 很少使用
    DWORD   NumberOfSymbols;      // 很少使用
    WORD    SizeOfOptionalHeader; // 指定 IMAGE_OPTIONAL_HEADER 结构的大小,可选结构体的大小会变化不能使用sizeof,获取大小时要使用该字段
    WORD    Characteristics;      // 指定文件类型
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

#define IMAGE_SIZEOF_FILE_HEADER             20

   

 

IMAGE_OPTIONAL_HEADER

这个结构体必须存在,该头的数据目录数组中有的目录项可有可无。“可选头”不可选,内部的数据目录项可选。其紧挨着文件头。其起始位置就是PE标识开始位置加上前一个结构体的长度20字节,结束位置就是节名称“.txt”的前一个字节。

可选头是对文件头的一个补充,文件头描述文件相关信息,可选头管理PE文件被装载时需要的信息。

#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64            PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC         IMAGE_NT_OPTIONAL_HDR64_MAGIC
#else
typedef IMAGE_OPTIONAL_HEADER32             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32            PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC         IMAGE_NT_OPTIONAL_HDR32_MAGIC
#endif

 32位结构体如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    /*
     * IMAGE_NT_OPTIONAL_HDR_MAGIC  0x10b  可执行文件
     * IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107  ROM 文件
     */
    WORD    Magic;                     // 文件状态
    BYTE    MajorLinkerVersion;        // 主连结版本号
    BYTE    MinorLinkerVersion;        // 次连接版本号
    DWORD   SizeOfCode;                // 代码节大小,偶尔有多个代码节则为所有代码节大小的和
    DWORD   SizeOfInitializedData;     // 已初始化数据块的大小
    DWORD   SizeOfUninitializedData;   // 未初始化数据块的大小
    DWORD   AddressOfEntryPoint;       // 程序入口地址,相对虚拟地址 EP(EntryPoint),指向第一条要执行的代码。加壳后改变该字段的值。该字段指向的不是main函数地址,而是运行库的启动代码地址
    DWORD   BaseOfCode;                // 代码段起始相对虚拟地址
    DWORD   BaseOfData;                // 数据段起始相对地址

    //
    // NT additional fields.
    //

    DWORD   ImageBase;                 // 文件被装入内存后首选建议装载地址
    DWORD   SectionAlignment;          // 节表装入内存后的对齐值,Win32下通常为0x1000 就是4KB
    DWORD   FileAlignment;             // 页表在文件中对齐值,为0x1000与内存一致方便加载,为0x200就是512字节节省空间
    WORD    MajorOperatingSystemVersion;  // 最低os主版本号
    WORD    MinorOperatingSystemVersion;  // 最低os次版本号
    WORD    MajorImageVersion;         // 可执行文件主版本号
    WORD    MinorImageVersion;         // 可执行文件次版本号
    WORD    MajorSubsystemVersion;      
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;         // 保留
    DWORD   SizeOfImage;               // 装入内存后总大小,内存对齐
    DWORD   SizeOfHeaders;             // DOS头、PE头、节表的总大小
    DWORD   CheckSum;                  // 校验和值,exe通常为0,sys文件必须有
    WORD    Subsystem;                 // 可执行文件的子系统类型
    WORD    DllCharacteristics;        // DLL文件属性,一般为 0
    DWORD   SizeOfStackReserve;        // 为线程保留的栈大小
    DWORD   SizeOfStackCommit;         // 为线程提交的栈大小
    DWORD   SizeOfHeapReserve;         // 为线程保留的堆大小
    DWORD   SizeOfHeapCommit;          // 为线程提交的堆大小
    DWORD   LoaderFlags;               // 过时
    DWORD   NumberOfRvaAndSizes;       // 数据目录项个数   #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];    // 数据目录表,由NumberOfRvaAndSizes决定个数。包含输入表、输出表、资源、重定位等数据目录项的相对虚拟地址
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;


typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;            // 目录项的相对虚拟地址起始值
    DWORD   Size;                      // 该目录项的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

 

在数据目录项中并不是所有目录项都有值,很多目录项值为0,所以称为可选。

 

节表IMAGE_SECTION_HEADER

节表位置在 IMAGE_OPTIONAL_HEADER 后面,每个结构体都存放在可执行文件被映射到内存后所在的位置信息,节的个数由 IMAGE_FILE_HEADER 中的 NumberOfSections 给出。一个节表项占40个字节。

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];  // ASCII码格式保存的节表项名称,超过8字节自动截断。如“.txet”
    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;

#define IMAGE_SIZEOF_SECTION_HEADER          40

 

PE 结构的3种地址

  1. VA (虚拟地址):PE文件映射到内存后的地址
  2. RAV (相对虚拟地址):内存地址相对于映射基地址的偏移地址
  3. FileOffset (文件偏移地址):相对于PE文件在磁盘上的文件开头的偏移地址

PE 文件在磁盘和在内存结构是一样的。不同的是,在磁盘上,文件是按照 IMAGE_OPTIONAL_HEADER 的 FileAlignment 对齐的;内存中,按照 IMAGE_OPTIONAL_HEADER 的 SectionAlignment 对齐的。FileAlignment 以磁盘上扇区为单位,最小512字节;SectionAlignment 是以内存分页为单位对齐,通常4K。一般情况二者相同,这样磁盘文件和内存映像结构完全一样。当二者值不同时存在一定差异。主要区别是为了对齐填充了许多0。

EXE文件在内存中的起始地址为 IMAGE_OPTIONAL_HEADER 的 ImageBase 字段,DLL文件的映射地址不固定。

3种地址的转换

 

posted @ 2022-08-10 08:40  某某人8265  阅读(257)  评论(0编辑  收藏  举报