PE文件结构1
阅读原文时间:2023年08月15日阅读:4

引言

PE文件格式是Windows操作系统下的可执行文件的格式,包括.exe文件和.dll文件,通过PE文件格式的学习,可以帮助我们更加熟悉有关Windows系统下的逆向分析和PC端病毒的学习,同时PE文件格式也是HOOK,加壳等知识的基础,在这里分享一下自己的有关PE文件格式学习的收获和如何编写一个PE文件解析器。

PE文件格式

PE文件由DOS头部,PE文件头,块表以及数据段,代码段等区块构成。

由于PE文件在磁盘和在内存当中的对齐值的不同,造成偏移量的不同,我们将其分为相对虚拟地址和文件偏移地址。现对这些名词进行一些解释。

1. 基地址(ImageBase):映射文件的起始地址称为模块句柄,通过该句柄可访问其他数据结构。这个起始地址即为基地址。基地址的值由文件本身决定,默认EXE文件的基地址为400000h,DLL文件基地址为10000000h.但是这样显然会产生冲突,所以后续用基地址重定位解决这个问题。

2. 虚拟地址(VA):载入内存后的地址。

3. 相对虚拟地址(RVA):载入内存后相对基地址的偏移量。RVA=VA-ImageBase。

4. 文件偏移地址:在磁盘当中相对基地址的偏移量。

typedef struct  _IMAGE_DOS_HEADER{
uint16_t signature; // Magic number: 0x4D5A ("MZ" in ASCII)
uint16_t last_page_size; // Size of the last page in bytes
uint16_t num_pages; // Total number of pages in the file
uint16_t num_relocations; // Number of relocation entries
uint16_t header_size; // Size of the header in paragraphs
uint16_t min_alloc; // Minimum number of paragraphs to allocate
uint16_t max_alloc; // Maximum number of paragraphs to allocate
uint16_t init_ss; // Initial SS value (relative to the start of the executable)
uint16_t init_sp; // Initial SP value
uint16_t checksum; // Checksum of the executable
uint16_t init_ip; // Initial IP value
uint16_t init_cs; // Initial CS value (relative to the start of the executable)
uint16_t reloc_table_offset; // Relative offset of the relocation table
uint16_t overlay_number; // Overlay number
uint16_t reserved[4]; // Reserved fields
uint16_t oem_id; // OEM identifier (used by OEM-specific programs)
uint16_t oem_info; // OEM information
uint16_t reserved2[10]; // Reserved fields
uint32_t pe_header_offset; // Offset of the PE header from the start of the executable
} DOSHeader;

这里我们需要关注的地方有两个。

1. e_magic即signature:位于起始位置,大小为word,作为标识符,值为5A4Dh,ASCII表示为MZ。

2. e_lfanew:偏移量为3ch,大小为double word,用于表示PE文件头的偏移量。

typedef struct _IMAGE_NT_HEADERS
{
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}


typedef struct _IMAGE_FILE_HEADER {
    WORD  Machine;                   // 目标机器类型
    WORD  NumberOfSections;          // 节表中的节的数量
    DWORD TimeDateStamp;             // 文件创建时间戳
    DWORD PointerToSymbolTable;      // COFF符号表的文件偏移
    DWORD NumberOfSymbols;           // COFF符号表中符号的数量
    WORD  SizeOfOptionalHeader;      // 可选头的大小
    WORD  Characteristics;           // 文件属性标志
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

FILE_HEADERS当中需要关注的是NumnerOfSections表示节的数目,即区块表的数目。

这其中我们重点关注的是OptionalHeader

typedef struct _IMAGE_OPTIONAL_HEADER {
    // 标识PE文件的格式,可以是32位或64位
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;  // 仅在32位PE文件中存在
    // ImageBase是PE文件在内存中的基地址
    ULONGLONG ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    ULONGLONG SizeOfStackReserve;
    ULONGLONG SizeOfStackCommit;
    ULONGLONG SizeOfHeapReserve;
    ULONGLONG SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    // 数据目录表的数组,包含了PE文件中各个数据目录的位置和大小
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

其中,IMAGE_DATA_DIRECTORY是另一个结构体,用于描述PE文件的数据目录。IMAGE_NUMBEROF_DIRECTORY_ENTRIES是一个常量,表示数据目录表的数量。

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;  // 数据目录在内存中的虚拟地址
    DWORD   Size;  // 数据目录的大小(字节)
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

很多我们需要去分析的参数都可以在这个这个里面去找相应的数据,同时数据目录表当中包含了导入表,导出表和资源表等内容,这是我们需要关注的重点。

每一个区块都有一个对应的区块表,区块的数目由FILE_HEADERS当中的NumberOfSections决定。

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];  // 区块名称
    union {
        DWORD   PhysicalAddress;  // 仅在OBJ文件中有效
        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;

区块的对齐值:

FileAlignment表示磁盘区块的对齐值

SectionAlignment表示内存当中区块的对齐值

文件偏移地址和虚拟偏移地址:文件偏移地址是在磁盘当中,虚拟偏移地址是在内存当中,由对齐值影响其大小。

导入表和导出表本质其实是一样的,但是导入表相对而言更加复杂,而且导出表只在.dll文件当中存在,所以在这里我们对导入表进行分析。

1. IMAGE_OPTIONAL_HEADERS当中存储导入表的RVA和SIZE

2. 在对应的RVA即IMPORT Directory Table处是每一个模块对应的一个结构体。

分别为Import Name Table RVA,Name RVA,Import Address Table RVA,Forwarder Chain和Time Date Stamp。

Name RVA可以确定依赖的模块名称

IAT和INT在载入之前都是指向函数名称但是在载入内存后,IAT将指向对应函数的地址。