PE文件格式详解

三千风雨三千雪 三千风雪我在写
流了一共三千血 你却始终不了解


简介

PE文件使用的是一个平面地址空间,所有的代码和数据都合并在一起,组成了一个很大的结构;
文件被分为不同的区块(Section,又成为区段或节等),区块中包含代码和数据,各个区块按页边界对齐;
区块没有大小限制,是一个连续结构;每一个块都有其自己的属性,如是否包含代码,是否可读可写等;


PE文件的构成

MS-DOS头部

每个PE文件都是以一个DOS程序开始的,其作用是一旦程序在DOS下执行,DOS就可以识别出这是一个有效的执行体,然后运行紧随的MZ header的DOS stub,即DOS块;
PE文件第一个字节就是MS-DOS头部,称为IMAGE_DOS_HEADER,其结构是这样的:

typedef struct _IMAGE_DOS_HEADER 
{
+00h  WORD e_magic        // DOS可执行文件标记"MZ"
+02h  WORD e_cblp         // 文件最后页的字节数
+04h  WORD e_cp           // 文件页数
+06h  WORD e_crlc         // 重定义元素个数
+08h  WORD e_cparhdr      // 头部尺寸,以段落为单位
+0ah  WORD e_minalloc     // 所需的最小附加段
+0ch  WORD e_maxalloc     // 所需的最大附加段
+0eh  WORD e_ss           // 初始的SS值(相对偏移量)
+10h  WORD e_sp           // 初始的SP值
+12h  WORD e_csum         // 校验和
+14h  WORD e_ip           // 初始的IP值,DOS代码入口ip
+16h  WORD e_cs           // 初始的CS值,DOS代码入口cs
+18h  WORD e_lfarlc       // 重分配表文件地址
+1ah  WORD e_ovno         // 覆盖号
+1ch  WORD e_res[4]       // 保留字
+24h  WORD e_oemid        // OEM标识符(相对e_oeminfo)
+26h  WORD e_oeminfo      // OEM信息
+28h  WORD e_res2[10]     // 保留字
+3ch  LONG e_lfanew         // 指向PE文件头"PE",0,0
} 

其中最为重要的是e_magic和e_lfanew;
e_magic的值为5A4Dh,e_lfanew的值则是指出PE头的文件偏移位置,占用4个字节;
MS-DOS头部

PE文件头

在D0Sstub之后紧跟的就是PE文件头,"PE Header"是PE相关结构NT映射头(IMAGE_NT_HEADERS)的简称,其中包含许多PE装载器可以用到的主要字段;
PE文件头的指针:

PNTHeader = ImageBase + dosHeader->e_lfanew

IMAGE_NT_HEADERS是由3个字段组成的结构:

IMAGE_NT_HEADERS STURST
+00h    Signature           DWORD ;PE文件标识
+04h    FileHeader          IMAGE_FILE_HEADER
+18h    OptionalHeader      IMAGE_OPTIONAL_HEADER32
IMAGE_NT_HEADERS ENDS

字段前数字表示到PE文件头的偏移量,Signature字段被设置为0x00004550,即ASCII的"PE00";

IMAGE_FILE_HEADER

其中IMAGE_FILE_HEADER又是一个结构,其结构如下:

IMAGE_FILE_HEADER STRUCT
+00h    Machine                WORD    ;运行平台
+06h    NumberOfSections       WORD    ;文件区块数
+08h    TimeDateStamp          DWORD   ;文件创建时间
+0Ch    PointerToSymbolTable   DWORD
+10h    NumberOfSymbols        DWORD
+14h    SizeOfOptionalHeader   WORD    ;IMAGE_OPTIONAL_HEADER32结构的大小
+16h    Characteristics        WORD    ;文件属性
IMAGE_FILE_HEADER ENDS

其中字段依次如下图:
IMAGE_FILE_HEADER 结构

IMAGE_OPTIONAL_HEADER32

IMAGE_OPTIONAL_HEADER32也是一个结构而且比较大,但是这个结构只是一个可选结构,其不足以定义PE文件属性,所以里面包含的字段也比较多;
其结构如下,字段前数字仍然是到PE文件头的偏移量:

IMAGE_OPTIONAL_HEADER32 STRUCT
+18H    Magic                           WORD
+1Ah    MajorLinkerVersion              BYTE
+1Bh    MinorLinkerVersion              BYTE
+1Ch    SizeOfCode                      DWORD
+20h    SizeOfInitializedData           DWORD
+24h    SizeOfUninitializedData         DWORD
+28h    AddressOfEntryPoint             DWORD
+2Ch    BaseOfCode                      DWORD   ;代码区块起始RVA
+30h    BaseOfData                      DWORD   ;数据区块起始RVA
+34h    ImageBase                       DWORD   ;程序默认载入基址
+38h    SectionAlignment                DWORD   ;内存中区块对齐值
+3Ch    FileAlignment                   DWORD   ;文件中区块对齐值
+40h    MajorOperatingSystemVersion     WORD
+42h    MinorOperatingSystemVersion     WORD
+44h    MajorImageVersion               WORD
+46h    MinorImageVersion               WORD
+48h    MajorSubsystemVersion           WORD
+4Ah    MinorSubsystemVersion           WORD
+4Ch    Win32versionValue               DWORD
+50h    SizeOfImage                     DWORD
+54h    SizeOfHeaders                   DWORD
+58h    CheckSum                        DWORD
+5Ch    Subsystem                       WORD
+5Eh    DllCharacteristics              WORD
+60h    SizeOfStackReserve              DWORD
+64h    SizeOfStackCommit               DWORD
+68h    SizeOfHeapReserve               DWORD
+6Ch    SizeOfHeapCommit                DWORD
+70h    LoaderFlags                     DWORD
+74h    NumberOfRvaAndSizes             DWORD
+78h    DataDirectory    IMAGE_DATA_DIRECTORY 16 DUP ;数据目录表
IMAGE_OPTIONAL_HEADER32 ENDS

其中比较重要的就是数据目录表IMAGE_DATA_DIRECTORY,其结构如下:

IMAGE_DATA_DIRECTORY    STRUCT
VirtualAddress  DWORD   ;数据块的起始RVA
Size            DWORD   ;数据块长度
IMAGE_DATA_DIRECTORY ENDS

数据目录表中总共有16个成员:
IMAGE_DATA_DIRECTORY 结构
PE文件在定位输出表,输入表和资源等重要的数据时,就是从IMAGE_DATA_DIRECTORY结构开始的
如图:
IMAGE_DATA_DIRECTORY 结构
158h位置是数据目录表的第一项,值为0,所以这个程序的输出表地址和大小都为0,即这个程序没有输出表;160h位置是数据表的第二项,表示输入表的RVA为2A000h,大小为3ch;

区块表

紧跟IMAGE_NT_HEADERS 的就是区块表,它是一个IMAGE_SECTION_HEADER数据数组;每个数组包含了所关联的区块的信息,比如位置、长度、属性等;
IMAGE_SECTION_HEADER结构定义如下:

IAMGE_SECTION_HEADER STURCT
Name                    BYTE8 DUP   ;8字节的块名
union Misc
  PhysicalAddress       DWORD       
  VirtualSize           DWORD
ENDS
VistualAddress          DWORD       ;区块的RVA地址
SizeOfRawData           DWORD       ;在文件中对齐后的尺寸
PointerToRelocations    DWORD       ;在文件中的偏移
PointerToLinenumbers    DWORD       
NumberOfRelocations     WORD
NumberOfLinenumbers     WORD
Characteristics         DWORD       ;区块的属性
IMAGE_SECTION_HEADER ENDS

IAMGE_SECTION_HEADER 中的字段依次对应如下图:
IAMGE_SECTION_HEADER 结构
每个IMAGE_SECTION_HEADER的大小是40字节,区块的个数通过IMAGE_FILE_HEADER->NumberOfSections 来确定
其中比较重要的是VistualAddress 和 PointerToRelocations,上面图中显示的.text段的VistualAddress地址,即RVA为1000h,PointerToRelocations的值也是1000h,即在文件中的偏移为1000h;
而这两个数相减就是△k,即

File_Offset = RVA - △k;

File_Offsetd的值就是PointerToRelocations的值,在上图中.text区块的△k = RVA - File_Offset = 1000h - 1000h = 0h;
需要注意的:不是在整个文件中△k都不变的,因为页边界的不一样,不同区块在磁盘中与内存中的差值不同,即△k不同;
如果我们设初始内存的地址,即基地址为ImageBase,内存中实际地址为VA,则有:

File_Offset = VA - ImageBase - △k;

这里给一张图作参考:
应用程序加载映射示意图

输入表

输入表以一个IMAGE_IMPORT_DESCRIPTOR(IID)数组开始,每个被PE文件隐式链接的DLL有一个IID;
IMAGE_IMPORT_DESCRIPTOR结构如下:

IMAGE_IMPORT_DESCRIPTOR STRUCT
union                                ;00h
    Charateristics          DWORD    ;
    OriginalFirstThunk      DWORD    ;包含指向输入表名称表(INT)的RVA
ends
TimeDataStamp               DWORD
ForwarderChain              DWORD
Name                        DWORD    ;DLL的名称指针,也是一个RVA
FirstThunk                  DWORD    ;包含指向输入表(IAT)的RVA
IMAGE_IMPORT_DESCRIPTOR ENDS

寻找输入表的基本方法:PE文件头偏移80h的位置找到指向输入表的地址,假设是Addr,不过这个地址是RVA,需要用这个值减去△k,才是输入表的在文件中的偏移地址,需要注意的是找△k时,要先确定Addr在哪个区块中,然后再用该区块的RVA减去该区块的PointerToRawData才是△k;至于区块的RVA和PointerToRawData就在IAMGE_SECTION_HEADER结构中看了;
至于找到输入表后,找输入表中的字段时,就可以直接用字段指向的RVA减去上面计算出的△k,然后得到文件偏移地址了;
PE文件加载后的IAT:
应用程序加载映射示意图

输出表

输出表一般不存在于EXE文件中,大部分在DLL文件中的;当一个DLL函数被EXE或另外一个DLL文件使用时,它就被"输出了"(Exported),其中输出信息被保存在输出表中,DLL文件通过输出表向系统提供输出函数名、序号、入口地址等信息;
输出表是数据目录表中的第一个成员,指向IMAGE_EXPORT_DIRECTORY结构,简称IED;
IED结构如下:


IMAGE_EXPORT_DIRECTORY STRUCT
    DWORD   Characteristics         ;//未使用,总是定义为0
    DWORD   TimeDateStamp           ;//文件生成时间
    WORD    MajorVersion            ;//未使用,总是定义为0
    WORD    MinorVersion            ;//未使用,总是定义为0
    DWORD   Name                    ;//模块的真实名称的RVA
    DWORD   Base                    ;//基数,加上序数就是函数地址数组的索引值
    DWORD   NumberOfFunctions       ;//导出函数的总数
    DWORD   NumberOfNames           ;//输出函数名称表(ENT)里的条目总数
    DWORD   AddressOfFunctions      ;// RVA from base of image指向输出函数地址的RVA
    DWORD   AddressOfNames          ;// RVA from base of image指向输出函数名字的RVA
    DWORD   AddressOfNameOrdinals   ;// RVA from base of image向输出函数序号的RVA
IMAGE_EXPORT_DIRECTORY ENDS 

输出表的查询和输入表查询的方法是一样的;
下图是一个输出表的格式及其中的3个阵列:
一个典型的输出表

资源

Windows程序的各种界面称为资源,包括加速键、位图、光标、对话框、图标等,在PE文件的所有结构中,资源部分是最为复杂的;
资源用类似于磁盘目录结构的方式来保存,目录通常包含3层;
第1层目录类似一个文件系统的根目录,每个根目录下的条目总是它自己权限下的一个目录;第2层目录中的每一个都对应于一个资源类型(字符串表、菜单、对话框等);第2层资源类型目录下是第3层目录;
目录结构如图:
资源的树形结构

资源表位于数据目录表的第3项,共动态分配字节,其中结构体中的成员指出的RVA偏移量都是对于此结构体的地址作为基地址;
IMAGE_RESOURCE_DIRECTORY结构长度为16字节,共6个字段,定义如下:

IMAGE_RESOURCE_DIRECTORY STRUCT 
{
+00h    DWORD   Characteristics     ; 理论上为资源的属性,不过事实上总是0
+04h    DWORD   TimeDateStamp       ; 资源的产生时刻
+08h    WORD    MajorVersion         ; 理论上为资源的版本,不过事实上总是0
+0Ah    WORD    MinorVersion
+0Ch    WORD    NumberOfNamedEntries ; 以名称(字符串)命名的入口数量(重要)
+0Eh    WORD    NumberOfIdEntries    ; 以ID(整型数字)命名的入口数量(重要)
}IMAGE_RESOURCE_DIRECTORY ENDS

资源目录入口结构:

IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCT
{
+10h   DWORD    Name            ; 目录项的名称字符串指针或ID,高位为1时指向子结构体一
+14h   DWORD    OffsetToData    ; 目录项指针,高位为1时指向子结构体二
};IMAGE_RESOURCE_DIRECTORY_ENTRY ENDS

资源数据入口:

IMAGE_RESOURCE_DATA_ENTRY STRUCT
{
+00h    DWORD   OffsetToData    ; 资源数据的RVA(重要)
+04h    DWORD   Size            ; 资源数据的长度(重要)
+08h    DWORD   CodePage        ; 代码页, 一般为0
+0Ch    DWORD   Reserved        ; 保留字段
};IMAGE_RESOURCE_DATA_ENTRY ENDS

总结

PE结构大部分结构是这些,当然还有一些结构没有指出,如果要深入了解PE结构,那么最好的方法就是编写一个PEload工具,在编写的过程中,会更深入的理解PE结构;
最后在一个放一个PE文件结构全局图,基本包含了PE结构中的所有结构:
PE结构

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PE文件格式逆向解析成汇编语言是一个非常复杂的任务,需要对PE文件格式有深入的了解,并且需要具备较强的反汇编技能。以下是一个简单的示例,演示如何将PE文件头部信息逆向解析成汇编语言: ``` ; 定义IMAGE_DOS_HEADER结构体 IMAGE_DOS_HEADER struct e_magic dw ? e_cblp dw ? e_cp dw ? e_crlc dw ? e_cparhdr dw ? e_minalloc dw ? e_maxalloc dw ? e_ss dw ? e_sp dw ? e_csum dw ? e_ip dw ? e_cs dw ? e_lfarlc dw ? e_ovno dw ? e_res dw 4 dup(?) e_oemid dw ? e_oeminfo dw ? e_res2 dw 10 dup(?) e_lfanew dd ? IMAGE_DOS_HEADER ends ; 定义IMAGE_NT_HEADERS结构体 IMAGE_NT_HEADERS struct Signature dd ? FileHeader IMAGE_FILE_HEADER <> OptionalHeader IMAGE_OPTIONAL_HEADER32 <> IMAGE_NT_HEADERS ends ; 定义IMAGE_FILE_HEADER结构体 IMAGE_FILE_HEADER struct Machine dw ? NumberOfSections dw ? TimeDateStamp dd ? PointerToSymbolTable dd ? NumberOfSymbols dd ? SizeOfOptionalHeader dw ? Characteristics dw ? IMAGE_FILE_HEADER ends ; 定义IMAGE_OPTIONAL_HEADER32结构体 IMAGE_OPTIONAL_HEADER32 struct Magic dw ? MajorLinkerVersion db ? MinorLinkerVersion db ? SizeOfCode dd ? SizeOfInitializedData dd ? SizeOfUninitializedData dd ? AddressOfEntryPoint dd ? BaseOfCode dd ? BaseOfData dd ? ImageBase dd ? SectionAlignment dd ? FileAlignment dd ? MajorOperatingSystemVersion dw ? MinorOperatingSystemVersion dw ? MajorImageVersion dw ? MinorImageVersion dw ? MajorSubsystemVersion dw ? MinorSubsystemVersion dw ? Win32VersionValue dd ? SizeOfImage dd ? SizeOfHeaders dd ? CheckSum dd ? Subsystem dw ? DllCharacteristics dw ? SizeOfStackReserve dd ? SizeOfStackCommit dd ? SizeOfHeapReserve dd ? SizeOfHeapCommit dd ? LoaderFlags dd ? NumberOfRvaAndSizes dd ? DataDirectory dd 16 dup(?) IMAGE_OPTIONAL_HEADER32 ends ; 定义节表结构体 IMAGE_SECTION_HEADER struct Name db 8 dup(?) VirtualSize dd ? VirtualAddress dd ? SizeOfRawData dd ? PointerToRawData dd ? PointerToRelocations dd ? PointerToLinenumbers dd ? NumberOfRelocations dw ? NumberOfLinenumbers dw ? Characteristics dd ? IMAGE_SECTION_HEADER ends ; 定义变量 dos_header IMAGE_DOS_HEADER <> nt_headers IMAGE_NT_HEADERS <> section_headers IMAGE_SECTION_HEADER 16 dup(?) ; 读取PE文件 filename db 'test.exe', 0 handle dw ? buffer db 512 dup(?) bytes_read dw ? section_table_offset dd ? size_of_section_table dd ? ; 打开文件 mov ah, 3dh mov al, 0 ; 只读模式 mov dx, offset filename int 21h mov handle, ax ; 读取DOS头部信息 mov ah, 3fh mov bx, handle mov cx, sizeof IMAGE_DOS_HEADER mov dx, offset dos_header int 21h ; 获取PE头部偏移地址 mov ax, word ptr [dos_header+0x3c] mov bx, handle mov cx, sizeof IMAGE_NT_HEADERS mov dx, offset nt_headers add dx, ax int 21h ; 解析PE头部信息 mov ax, word ptr [nt_headers.Signature] cmp ax, 'PE' jne exit_program ; 解析文件头部信息 mov ax, word ptr [nt_headers.FileHeader.NumberOfSections] mov section_table_offset, dx mov size_of_section_table, ax * sizeof IMAGE_SECTION_HEADER add dx, sizeof IMAGE_FILE_HEADER mov cx, sizeof IMAGE_FILE_HEADER mov si, offset nt_headers.FileHeader mov di, dx rep movsb ; 解析可选头部信息 mov ax, word ptr [nt_headers.OptionalHeader.Magic] cmp ax, IMAGE_NT_OPTIONAL_HDR32_MAGIC jne exit_program mov cx, sizeof IMAGE_OPTIONAL_HEADER32 mov si, offset nt_headers.OptionalHeader mov di, dx rep movsb ; 解析节表信息 mov ah, 3fh mov bx, handle mov cx, size_of_section_table mov dx, offset section_headers add dx, section_table_offset int 21h exit_program: ; 关闭文件 mov ah, 3eh mov bx, handle int 21h ``` 以上代码仅为示例,实际上解析PE文件格式的过程要比这个更加复杂,需要对不同的结构体进行不同的解析方式,并且需要处理一些特殊情况。同时,反汇编的过程中还需要考虑一些优化问题,例如去除无用代码、还原函数调用等,以确保反汇编的代码正确、可读性强。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值