PE文件

什么是PE文件

总的来说PE文件是Windows操作系统下的可执行文件的总称,是用于存储可执行文件(exe,scr),动态链接库文件(dll,oxc,cpl)和驱动程序(sys,vxd)的标准文件格式,其中exe只是其中一种可执行文件的后缀名,不能简单的把PE和exe划等号,PE文件格式对应Linux的elf文件结构和so文件。

这里插一段有趣的东西,由于最近在研究安卓逆向相关的一些东西,所以就想进行一些知识的关联,其实安卓本质是一个多层嵌套的一个东西,我们把apk文件后缀改成zip发现是可以打开的,因为安卓apk的本质就是一个压缩包,但安卓系统的内核本质就是一个基于Linux或者说类Linux系统,所以在底层运行还是elf文件结构运行程序的,但是安卓程序又是运行在ART虚拟机上的dex文件

PE文件头

PE文件头是由DOS文件头加上NT头组成的

MS-DOS头

首先来看DOS头,DOS头位于整个PE文件的首位部分,是PE文件结构的第一个头,对于现在的系统来说,作用就是用来定位真正的PE头。在peview中可以看到对应IMAGE_DOS_HEADER和MS-DOS Stub Program,IMAGE_DEBUG_TYPE这里可以忽略,是调试类型,对应debug和relese。

c 复制代码
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE 文件头结构体
    WORD   e_magic;                     // 标识符,用于确认这是MZ格式的文件,值为0x5A4D
    WORD   e_cblp;                      // 文件中最后一个扇区的字节数
    WORD   e_cp;                        // 文件中的扇区总数
    WORD   e_crlc;                      // 重定位表中的条目数
    WORD   e_cparhdr;                   // 文件头的大小,以16字节为单位
    WORD   e_minalloc;                  // 程序加载时所需的最小额外内存段落数
    WORD   e_maxalloc;                  // 程序加载时所需的最大额外内存段落数
    WORD   e_ss;                        // 初始堆栈段选择子(段地址)
    WORD   e_sp;                        // 初始堆栈指针值
    WORD   e_csum;                      // 校验和,用于检验文件的完整性
    WORD   e_ip;                        // 初始指令指针(IP值)
    WORD   e_cs;                        // 初始代码段选择子(段地址)
    WORD   e_lfarlc;                    // 文件中重定位表的偏移量
    WORD   e_ovno;                      // 覆盖号,用于实现覆盖功能
    WORD   e_res[4];                    // 保留字段,供未来使用
    WORD   e_oemid;                     // OEM标识符,用于特定于OEM的扩展
    WORD   e_oeminfo;                   // OEM信息,供OEM使用
    WORD   e_res2[10];                  // 保留字段,供未来扩展使用
    LONG   e_lfanew;                    // 指向新EXE(PE)头的偏移量,从文件开始处计算
} I
MAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

从前到后介绍一下有用的字段

首先是e_magic MZ魔数,操作系统在开始执行的时候会先检查这个字段来判断是不是可执行文件(0x5A4D)

其次是e_lfanew 它是指向新的PE头(NT头)的偏移量

从IMAGE_DOS_HEADER到IMAGE_NT_HEADER

c 复制代码
IMAGE_DOS_HEADER dos;
fread(&dos, sizeof(dos), 1, fp);
fseek(fp, dos.e_lfanew, SEEK_SET);
fread(&nt, sizeof(nt), 1, fp); // 读PE头

这里附带一个随便的一个exe文件可以看到0080对应到了PE文件地址

NT头(PE头)

这里到了PE文件的核心部分,里面包含PE文件的重要信息,PE文件头由这两个结构体组成

c 复制代码
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;  // PE签名,0x4字节
    IMAGE_FILE_HEADER FileHeader;  // PE头,0x14字节
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;  // PE可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

这个主要是这个signature在操作系统会检查它是不是这个PE字段,如果不是直接拒绝解析

c 复制代码
typedef struct _IMAGE_FILE_HEADER {
    WORD Machine;  // 目标机器的类型码,如x86或ARM
    WORD NumberOfSections;  // 文件中节(section)的数量
    DWORD TimeDateStamp;  // 文件创建或最后修改的时间戳
    DWORD PointerToSymbolTable;  // 指向文件中符号表的偏移地址
    DWORD NumberOfSymbols;  // 符号表中符号条目的数量
    WORD SizeOfOptionalHeader;  // 可选头的大小,用于存储扩展的文件信息
    WORD Characteristics;  // 文件特性标志,指DLL、应用程序、可执行文件等
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

machine可以匹配文件的平台

节的数量可以在后面循环读取节

时间戳可以做简单取证

然后比较有用的就是characteristics文件的标识位,常见值有:

  • 0x0001 IMAGE_FILE_RELOCS_STRIPPED --- 已移除重定位信息

  • 0x0002 IMAGE_FILE_EXECUTABLE_IMAGE --- 可执行镜像

  • 0x0004 IMAGE_FILE_LINE_NUMS_STRIPPED --- 行号被移除

  • 0x2000 IMAGE_FILE_DLL --- 这是一个 DLL

  • 0x0100 IMAGE_FILE_32BIT_MACHINE --- 用于 32-bit 机器(legacy)

  • 0x0200 IMAGE_FILE_DEBUG_STRIPPED --- 调试信息被剥离

    主要作用是判断是否为 DLL、是否去掉重定位、是否有调试信息等。可用于静态检测或快速分类(e.g., 阻止可疑无重定位可执行体直接认定为"可能已被加壳/修改")。

c 复制代码
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16  // 数据目录项数,固定为16

typedef struct _IMAGE_OPTIONAL_HEADER {
    // 标准域
    WORD Magic;  // 可选头类型,0x10表示32位,0x20表示64位
    BYTE MajorLinkerVersion;  // 主链接器的版本号,以字节为单位
    BYTE MinorLinkerVersion;  // 副链接器的版本号,以字节为单位
    DWORD SizeOfCode;  // 代码段大小,以字节为单位
    DWORD SizeOfInitializedData;  // 初始化数据段大小,以字节为单位
    DWORD SizeOfUninitializedData;  // 未初始化数据段大小,以字节为单位
    DWORD AddressOfEntryPoint;  // 程序入口点地址,相对于ImageBase
    DWORD BaseOfCode;  // 代码段起始基址RVA
    DWORD BaseOfData;  // 数据段起始基址RVA

    // NT附加域
    DWORD ImageBase;  // 镜像基址,即加载到内存的起始地址
    DWORD SectionAlignment;  // 节在内存中的对齐大小,以字节为单位
    DWORD FileAlignment;  // 节在文件中的对齐大小,以字节为单位
    WORD MajorOperatingSystemVersion;  // 主操作系统版本号
    WORD MinorOperatingSystemVersion;  // 副操作系统版本号
    WORD MajorImageVersion;  // 主镜像版本号
    WORD MinorImageVersion;  // 副镜像版本号
    WORD MajorSubsystemVersion;  // 主子系统版本号
    WORD MinorSubsystemVersion;  // 副子系统版本号
    DWORD Win32VersionValue;  // Win32版本值,通常为0
    DWORD SizeOfImage;  // 镜像在内存中的大小,以字节为单位
    DWORD SizeOfHeaders;  // PE头物理大小,以字节为单位
    DWORD CheckSum;  // 校验和,用于验证镜像的完整性
    WORD Subsystem;   // 子系统类型
    WORD DllCharacteristics;  // DLL特性标志,指示文件是DLL、应用程序等
    DWORD SizeOfStackReserve;  // 运行时为每个线程栈保留的内存大小
    DWORD SizeOfStackCommit;  // 运行时每个线程栈初始占用的内存大小
    DWORD SizeOfHeapReserve;  // 运行时为进程堆保留的内存大小
    DWORD SizeOfHeapCommit;  // 运行时进程堆初始占用的内存大小
    DWORD LoaderFlags;  // 载入器标志,通常为0
    DWORD NumberOfRvaAndSizes;  // 数据目录的项数,固定为IMAGE_NUMBEROF_DIRECTORY_ENTRIES的值
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];  // 数据目录数组
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

接下来是可选文件头

Magic作用决定可选字段是否存在,主要是区分32位还是64位

节表区

这也是NT头后PE头的最后一个部分

节区表记录了 PE 文件中所有节区的相关属性,节区表由一系列的 IMAGE_SECTION_HEADER 结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的 IMAGE_SECTION_HEADER 结构作为结束,所以节表中 IMAGE_SECTION_HEADER 结构数量等于节的数量加一。

至此,关于PE文件头的内容大概就是这些,还是挺多的,后面还会更新一些在此基础上的东西,就再说吧。!-*-*/-!