初识PE文件
初识PE文件
但凡后缀为.exe的文件,都是PE文件(不包含重命名为.exe文件),同时常见的还有.dll和.sys,都是PE文件
PE指纹:用来识别PE文件,在windows中,你可以改文件的后缀名,但是不会改动内部的结构,也就是说,里面还是会有PE指纹,一般字符串形式展示为MZ或PE
PE文件的结构
按顺序可以划分为:DOS首部、PE头、区块表(或称节/块表)、区块、调试信息

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

然后后面就是块的部分,可以看到很多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了,得找图,好麻烦,不管了,首先讲一个很有意思的东西:

你们看,红色的部分就是是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
以上是源码链接,翻看一下,这个文件头大概长这样:
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:文件区块数,嗯,我来看看👀

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

我去还真是15个,牛逼啊
- TimeDateStamp:这个是时间戳吧,记录文件创建的日期和时间,让我瞅瞅

- PointerToSymbolTable
- NumberOfSymbols
这两个家伙都是调试用的,少见,不学
- SizeOfOptionalHeader:这个,诶,好像是扩展PE头结构的大小,这个大小基本取决于文件是32位还是64位
64位一般是00F0h,32位一般是00E0h - Characteristics:文件属性,他的控制非常神圣,跟状态寄存器做一桌

我们可以看到一个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里面的物理地址,然后直接根据地址就可以跳转到对应的数据位置

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

这样就找到了区段数据,不过我的01editor已经帮我找好了(
同时根据SizeOfRawData确认尺寸,我们就可以知道这个区段的范围
接下来要用上之前的两个名词了
这两个名词一般是一个意思:区段相对于文件头部的位置
加载到内存中运行起来后,就变成虚拟地址了,就是我们之前把文件拖到动态调试器后,在侧边显示的地址,这两个是有区别的
文件偏移地址之所以叫文件偏移地址,因为整个地址是在文件内部的偏移,头部的位置就是0
而在虚拟地址中,头部会有一个模块基地址,然后后面的部分,都是根据这个文件偏移地址和模块基地址算出来的相对虚拟地址,相当于就是把原本静态的文件丢到内存里面去了
我们到工具里看一眼:

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

我们跳转到用户模块后,断点自动设置在了这里,这就是在内存中,我们这个程序入口的位置
模块基地址exe一般是0x00400000,这一点我们之前读反汇编的时候其实注意到了
还有就是模块基地址dll常见为0x10000000
模块:PE文件运行后,在内存中的版本称为模块,此时的模块基地址也称为模块句柄
评论