初识PE文件

文件格式yulabu2026-09-17 30 次阅读

初识PE文件

但凡后缀为.exe的文件,都是PE文件(不包含重命名为.exe文件),同时常见的还有.dll和.sys,都是PE文件

PE指纹:用来识别PE文件,在windows中,你可以改文件的后缀名,但是不会改动内部的结构,也就是说,里面还是会有PE指纹,一般字符串形式展示为MZ或PE

PE文件的结构

按顺序可以划分为:DOS首部、PE头、区块表(或称节/块表)、区块、调试信息

我们可以看到,整个PE文件最开始首先是个MZ,代表这是可执行文件,这就是DOS首部的开头,接着是一个PE文件头

image-20260916101318486

然后后面就是块的部分,可以看到很多data、rdata之类的东西,这就说明了这个PE文件的结构是按照一定顺序展开的

那这些字段都分别有什么用呢?

常见区块作用

  • .text:默认代码区块,内容全是指令代码,基本是个PE文件都会有
  • .data:默认的读/写数据块,全局变量、静态变量一般放在这个区段
  • .rdata:默认只读数据块,程序一般很少用到其中的数据
  • .idata:输入表,包含其他外来DLL的函数及数据信息
  • .edata:输出表,
  • .rsrc:资源
  • .bss:未初始化的数据,很少使用
  • .crt:c runtime,c语言运行时的库
  • .tls:Thread Local Storage,线程局部存储
  • .reloc:Relocation,重定位,用于保存哪些位置需要进行重定位,以及怎么修正这些地址
  • .sdata:Small Data,小型数据区段,不是PE强制规范,通常是编译器自己使用的section名称
  • .pdata:Procedure Data,过程数据,主要保存 函数的异常处理 / 栈展开(unwind)相关信息,逆向挺重要的,这个区段主要用于出现异常情况后的复原和回退
  • .didat:Delay Import Data,延迟导入数据,常见约定名称,但不是PE强制规范,用于延迟加载dll

DOS部分

woc了,得找图,好麻烦,不管了,首先讲一个很有意思的东西:

image-20260916150001504

你们看,红色的部分就是是DOS的MZ文件头,这实际是一个64字节的结构体,你看的红色部分非常整齐,而MZ末尾的两个字节(注意是小端模式)说明了PE文件头的位置,确定在0080的位置(除了开头和结尾,其他好像都可以改)

我们根据两个确定的头,可以的得到中间橙色部分是没啥用的DOS块,里面的内容我们怎么替换都对程序没有影响(可以夹带私货)

PE文件头

PE文件头主要分三部分:

  • signature:PE签名,位置在前四个字节
  • FileHeader:标准PE头,紧接着的20个字节
  • OptionalHeader:扩展PE头,224字节

有啥用呢,不知道,你在01editor上拖来拖去就能看到每个字节属于哪个部分了,第一个签名是个字符串,后面两个部分是一个结构体,所以比较大,具体内容看我后面:

前四个字节就是简单签名,不讲

ok啊,现在我滚去看winnt.h,找一下Windows NT 系列 PE 格式的 NT Headers

https://github.com/microsoft/win32metadata/blob/main/generation/WinSDK/RecompiledIdlHeaders/um/winnt.h

以上是源码链接,翻看一下,这个文件头大概长这样:

c 复制代码
typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

的确是三部分啊,前边的DWORD Signature就是签名嘛,double word,和汇编有点想,就是四个字节,没毛病,接下来让我们看下为啥剩下的两个结构体占了我这么多空间

先转到IMAGE_FILE_HEADER FileHeader看看:

c 复制代码
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

算下来,确实是20个字节吗,那么都有什么用呢?我来随便讲讲:

  • Machine:运行平台,常见有intel(14Ch),基本就是这个,如果为0,说明可以在任何平台运行
  • NumberOfSections:文件区块数,嗯,我来看看👀
image-20260916204809953

这里说是有15个,我到我的StudyPE瞅一眼;

image-20260916204852925

我去还真是15个,牛逼啊

  • TimeDateStamp:这个是时间戳吧,记录文件创建的日期和时间,让我瞅瞅
image-20260916205326055
  • PointerToSymbolTable
  • NumberOfSymbols

这两个家伙都是调试用的,少见,不学

  • SizeOfOptionalHeader:这个,诶,好像是扩展PE头结构的大小,这个大小基本取决于文件是32位还是64位
    64位一般是00F0h,32位一般是00E0h
  • Characteristics:文件属性,他的控制非常神圣,跟状态寄存器做一桌
image-20260916210615057

我们可以看到一个WORD里面塞了一堆东西,这是啥呢?我们知道8个字节有16bit吧,这里是每个bit都作为一个开关,最终确认文件属性

15个开关,我滴妈,让我们一个个看看,先拉一波源码下来,我把注释换成中文:

c 复制代码
#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // 文件中不存在重定位信息
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // 文件是可执行的
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // 不存在行信息
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // 不存在本地符号信息
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // 调整工作集
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // 应用程序可以处理大于2GB的地址
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // 小尾方式
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 只在32位平台上运行
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // 不包含调试信息
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // 不能从可移动盘运行
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // 不能从网络运行
#define IMAGE_FILE_SYSTEM                    0x1000  // 系统文件,不能直接运行
#define IMAGE_FILE_DLL                       0x2000  // 这是一个DLL文件
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // 文件不能在多处理器计算机上运行
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // 大尾方式

不同文件(如exe与dll),文件属性都不一样,根据这些开关来确定

再转到IMAGE_OPTIONAL_HEADER64 OptionalHeader看看:

c 复制代码
typedef struct _IMAGE_OPTIONAL_HEADER64 {

    WORD        Magic;                  // PE32+ 标识,64 位 PE 通常为 0x20B //

    BYTE        MajorLinkerVersion;     // 链接器主版本
    BYTE        MinorLinkerVersion;     // 链接器次版本

    DWORD       SizeOfCode;             // 所有代码段(通常 .text)的总大小
    DWORD       SizeOfInitializedData;  // 已初始化数据的总大小
    DWORD       SizeOfUninitializedData;// 未初始化数据(如 .bss)的总大小

    DWORD       AddressOfEntryPoint;    // 程序入口点 RVA //
    DWORD       BaseOfCode;             // 代码段起始 RVA

    ULONGLONG   ImageBase;              // 程序期望加载的基址

    DWORD       SectionAlignment;       // 内存中 Section 的对齐粒度
    DWORD       FileAlignment;          // 文件中 Section 的对齐粒度

    WORD        MajorOperatingSystemVersion; // 要求的操作系统主版本
    WORD        MinorOperatingSystemVersion; // 要求的操作系统次版本

    WORD        MajorImageVersion;      // 镜像主版本
    WORD        MinorImageVersion;      // 镜像次版本

    WORD        MajorSubsystemVersion;  // 子系统主版本
    WORD        MinorSubsystemVersion;  // 子系统次版本

    DWORD       Win32VersionValue;      // 保留,通常为 0

    DWORD       SizeOfImage;            // PE 映像加载到内存后的总大小
    DWORD       SizeOfHeaders;          // 所有 PE Header 在内存中的大小

    DWORD       CheckSum;               // PE 校验和

    WORD        Subsystem;              // 子系统类型,如 GUI / Console
    WORD        DllCharacteristics;     // DLL 特性/安全相关标志(ASLR、DEP 等)

    ULONGLONG   SizeOfStackReserve;     // 为主线程预留的栈空间大小
    ULONGLONG   SizeOfStackCommit;      // 主线程初始提交的栈空间大小

    ULONGLONG   SizeOfHeapReserve;      // 进程堆预留大小
    ULONGLONG   SizeOfHeapCommit;       // 进程堆初始提交大小

    DWORD       LoaderFlags;            // 保留字段,通常为 0

    DWORD       NumberOfRvaAndSizes;    // DataDirectory 中有效目录项数量

    IMAGE_DATA_DIRECTORY
        DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
                                        // 数据目录:Import、Export、Reloc、
                                        // TLS、Resource 等重要 PE 信息的位置

} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

大胆!怎么敢装这么多东西的!我继续补注释,主要标注常用的,其他的……先不管

区段表

接着就是第三部分了,看看区段表怎么存的,首先看看结构:

c 复制代码
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    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;

基本根据变量名都能知道含义,还记得之前的NumberOfSections,记录的是区块数,说明有多少个这样的结构体,然后我们算一下这个结构体占的字节:8+4+4*6+2+2=40

学c的都知道,union是所有成员共用一个空间,所以整个union大小由其中一个最大的决定,而第一个成员中的宏是8字节,没啥好说的,其他的正常算,最后就40字节

我们先看区块的属性,跟之前的文件属性一样,也是一个按bit设置的开关,让我翻一下:

c 复制代码
// Section 特征(Characteristics)

// 保留/已废弃
// IMAGE_SCN_TYPE_REG                   0x00000000
// IMAGE_SCN_TYPE_DSECT                 0x00000001
// IMAGE_SCN_TYPE_NOLOAD                0x00000002
// IMAGE_SCN_TYPE_GROUP                 0x00000004

#define IMAGE_SCN_TYPE_NO_PAD            0x00000008  // 不进行填充
// IMAGE_SCN_TYPE_COPY                  0x00000010  // 保留

#define IMAGE_SCN_CNT_CODE               0x00000020  // 包含代码
#define IMAGE_SCN_CNT_INITIALIZED_DATA   0x00000040  // 包含已初始化数据
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080  // 包含未初始化数据

#define IMAGE_SCN_LNK_OTHER              0x00000100  // 保留
#define IMAGE_SCN_LNK_INFO               0x00000200  // 包含注释/其他信息
// IMAGE_SCN_TYPE_OVER                  0x00000400  // 保留

#define IMAGE_SCN_LNK_REMOVE             0x00000800  // 不写入最终映像
#define IMAGE_SCN_LNK_COMDAT             0x00001000  // COMDAT 区段

// 0x00002000                                      // 保留
// IMAGE_SCN_MEM_PROTECTED - 已废弃     0x00004000

#define IMAGE_SCN_NO_DEFER_SPEC_EXC      0x00004000  // 禁用延迟异常
#define IMAGE_SCN_GPREL                  0x00008000  // 可通过 GP 相对访问
#define IMAGE_SCN_MEM_FARDATA            0x00008000  // 远数据(旧定义)

// IMAGE_SCN_MEM_SYSHEAP - 已废弃        0x00010000

#define IMAGE_SCN_MEM_PURGEABLE          0x00020000  // 可清除
#define IMAGE_SCN_MEM_16BIT              0x00020000  // 16 位数据(旧定义)
#define IMAGE_SCN_MEM_LOCKED             0x00040000  // 锁定内存
#define IMAGE_SCN_MEM_PRELOAD            0x00080000  // 预加载

// Section 对齐方式
#define IMAGE_SCN_ALIGN_1BYTES            0x00100000  // 1 字节对齐
#define IMAGE_SCN_ALIGN_2BYTES            0x00200000  // 2 字节对齐
#define IMAGE_SCN_ALIGN_4BYTES            0x00300000  // 4 字节对齐
#define IMAGE_SCN_ALIGN_8BYTES            0x00400000  // 8 字节对齐
#define IMAGE_SCN_ALIGN_16BYTES           0x00500000  // 16 字节对齐(默认)
#define IMAGE_SCN_ALIGN_32BYTES           0x00600000  // 32 字节对齐
#define IMAGE_SCN_ALIGN_64BYTES           0x00700000  // 64 字节对齐
#define IMAGE_SCN_ALIGN_128BYTES          0x00800000  // 128 字节对齐
#define IMAGE_SCN_ALIGN_256BYTES          0x00900000  // 256 字节对齐
#define IMAGE_SCN_ALIGN_512BYTES          0x00A00000  // 512 字节对齐
#define IMAGE_SCN_ALIGN_1024BYTES         0x00B00000  // 1024 字节对齐
#define IMAGE_SCN_ALIGN_2048BYTES         0x00C00000  // 2048 字节对齐
#define IMAGE_SCN_ALIGN_4096BYTES         0x00D00000  // 4096 字节对齐
#define IMAGE_SCN_ALIGN_8192BYTES         0x00E00000  // 8192 字节对齐

// 0x00F00000                                      // 未使用
#define IMAGE_SCN_ALIGN_MASK              0x00F00000  // 对齐字段掩码

#define IMAGE_SCN_LNK_NRELOC_OVFL         0x01000000  // 包含扩展重定位
#define IMAGE_SCN_MEM_DISCARDABLE         0x02000000  // 可丢弃
#define IMAGE_SCN_MEM_NOT_CACHED          0x04000000  // 不缓存
#define IMAGE_SCN_MEM_NOT_PAGED           0x08000000  // 不分页
#define IMAGE_SCN_MEM_SHARED              0x10000000  // 可共享
#define IMAGE_SCN_MEM_EXECUTE             0x20000000  // 可执行
#define IMAGE_SCN_MEM_READ                0x40000000  // 可读
#define IMAGE_SCN_MEM_WRITE               0x80000000  // 可写

我去好多,这好多,难怪要四个字节,直接来个32位的开关!不过重要的是最后八个

这个区段表就先这样吧,然后我们开始区段数据

区段数据

记住两个名词:

  • raw offset:物理地址
  • file offset:文件偏移地址

offset改成address也行

然后我们看看区段数据,这个数据的位置信息是存储在PointerToRawData里面的物理地址,然后直接根据地址就可以跳转到对应的数据位置

image-20260917001223944

找到了,然后根据这个地址跳转:

image-20260917001252953

这样就找到了区段数据,不过我的01editor已经帮我找好了(

同时根据SizeOfRawData确认尺寸,我们就可以知道这个区段的范围

接下来要用上之前的两个名词了

这两个名词一般是一个意思:区段相对于文件头部的位置

加载到内存中运行起来后,就变成虚拟地址了,就是我们之前把文件拖到动态调试器后,在侧边显示的地址,这两个是有区别的

文件偏移地址之所以叫文件偏移地址,因为整个地址是在文件内部的偏移,头部的位置就是0

而在虚拟地址中,头部会有一个模块基地址,然后后面的部分,都是根据这个文件偏移地址和模块基地址算出来的相对虚拟地址,相当于就是把原本静态的文件丢到内存里面去了

我们到工具里看一眼:

image-20260917103520893

首先这里我们知道这个程序的模块基地址在00400000的位置,然后程序入口在000014E0,这是一个相对基地址的偏移地址,我们到dbg里面看:

image-20260917103703517

我们跳转到用户模块后,断点自动设置在了这里,这就是在内存中,我们这个程序入口的位置

模块基地址exe一般是0x00400000,这一点我们之前读反汇编的时候其实注意到了

还有就是模块基地址dll常见为0x10000000

模块:PE文件运行后,在内存中的版本称为模块,此时的模块基地址也称为模块句柄

评论

正在加载评论区…