PE文件格式学习

PE文件格式

介绍

PE文件是Windows操作系统下使用的可执行文件格式PE:Portable Executable

PE文件指的是32位的可执行文件,也成为PE32,64位的可执行文件称为PE+或者PE32+,是PE32文件的一种拓展(不是PE64

观察PE文件的各个结构体成员的值,推荐使用CFF Explorer

PE文件格式

PE文件种类
种类 主拓展名
可执行 EXE, SCR
DLL, OCX, CRL, DRV
驱动程序 SYS, VXD
对象文件 OBJ

以IDA pro(x32)为例

image-20230306104549571

一般判断一个文件是否为PE文件主要看的就是他是否具有 MZ和PE这两个标志

PE头

DOS头

在微软创建PE文件时,DOS文件正在被广泛使用,所以为了兼容DOS文件,DOS头出现了,也就是IMAGE_DOS_HEADER结构体,用于拓展已有的DOS EXE头

IMAGE_DOS_HEADER结构体的大小是64字节

winnt.h中可以找到该结构体的定义

image-20230306105159377

需要掌握的主要就是e_maigc和e_lfanew两个成员

e_magic:DOS签名,即4D5A —> ASCII值 “MZ”

e_lfanew:指示NT头的偏移(根据不同文件拥有可变值)

image-20230306105646938

在IDA pro(x32)这里,e_magic的值为4D5A,e_lfanew的值为00000100

DOS存根

DOS存根(stub)在DOS头下方,是可选项,主要有代码和数据混合而成,大小不固定(没有DOS存根,文件也可以正常运行)

image-20230306105957244

在32位Windows OS环境下,PE loader识别到DOS头,该文件被识别为PE文件,这段DOS存根不会被执行。

在DOS环境中或者是DOS debug下,可以执行这段代码(不认识PE文件格式,所以识别为DOS文件)。

NT头

在winnt.h文件中可以找到IMAGE_NT_HEADERS结构体的定义

image-20230306110411583

IMAGE_NT_HEADERS主要由三个成员构成,第一个为签名(signature)结构体,值为50450000 (“PE” 00)。

另两外两个成员是文件头(FileHeader)和可选头(OptionalHeader)

image-20230306111040054

IMAGE_NT_HEADERS结构体的大小为F8

FileHeader

image-20230306111107961

IMAGE_FILE_HEADER有下面四个重要成员,如果他们设置不正确,可能导致文件无法正常运行

Machine

每个CPU都拥有唯一的Machine码,兼容32为Intel x86芯片的Machine码为14C

下面是定义在winnt.h里的Machine码

image-20230306111345971

NumberOfSection

PE文件把代码,数据,资源等依据属性分类到各节区中存储

NumberOfSection 用来指出文件中存在的节区数量。

该值一定要大于0,且当定义的节区数量与实际截取不同时,将发生运行错误

SizeOfOptionalHeader

IMAGE_FILE_HEADER的最后一个成员为IMAGE_OPTIONAL_HEADER32结构体,SizeOfOptionalHeader用来指出IMAGE_OPTIONAL_HEADER32结构体的长度

Characteristics

Characteristics用于标识文件的属性,文件是否是可运行的形态,是否为DLL文件等信息,以bit OR形式组合起来

image-20230306112258529

主要记住0x0002和0x2000

其余的成员

1
2
3
TimeDateStamp;        //时间戳:链接器填写的文件生成时间(不影响程序的运行)
PointerToSymbolTable; //指向符号表的地址(主要用于调试)
NumberOfSymbols //符号表中符号个数

IMAGE_FILE_HEADER结构体

image-20230306112811250

machine对应的是8664,对应AMD64处理器。

NumberOfSection对应0060。

TimeDateStamp对应620F1879,转换为2022-02-18 11:54:33。

PointerToSymbolTable:00000000

NumberOfSymbols:00000000

SizeOfOptionalHeader:00F0

Characteristics:0022 –> 0x2 | 0x20

image-20230306223915210

OptionalHeader

IMAGE_OPTIONAL_HEADER结构体

image-20230306220049518

关注以下成员,如果他们设置不正确,可能导致文件无法正常运行

Magic

为IMAGE_OPTIONAL_HEADER32时,Magic为10B

为IMAGE_OPTIONAL_HEADER64时,Magic为20B

AddressOfEntryPoint

AddressOfEntryPoint拥有EP的RVA值,指出程序最先执行的代码起始地址

ImageBase

进程虚拟内存的范围是0~0xFFFFFFFF(32位系统)

ImageBase指出文件的优先装入地址

EXE、DLL文件被装入用户内存的07FFFFFFF中,SYS文件被载入内核内存的80000000FFFFFFFF中

一般使用开发工具创建好的EXE文件,其ImageBase值位00400000,DLL文件的ImageBase值位10000000(可以指定其他值)

执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把EIP设置为ImageBase+AddressOfEntryPoint

SectionAlignment, FileAlignment

FileAlignment指定了节区在磁盘文件中的最小单位

SectionAlignment指定了节区在内存中的最小单位

(一个文件中SectionAlignment, FileAlignment的值可能相同也可能不相同)

SizeOfImage

加载PE文件到内存时,SizeOfImage指定了PE Image在虚拟内存中所占的空间大小

(一般而言,文件的大小与加载到内存中的大小是不同的)

SizeOfHeaders

SizeOfHeaders用来指出整个PE头的大小,第一节区所在位置与SizeOfHeaders距文件开始偏移的量相同

Subsystem

Subsystem的值用来区分系统驱动文件*.sys 和普通的可执行文件 *.exe, *.dll

含义 备注
1 Driver文件 系统驱动(*.sys)
2 GUI文件 窗口应用程序(ida.exe)
3 CUI文件 控制台应用程序(cmd.exe)
NumberOfRvaAndSizes

NumberOfRvaAndSizes用来指定DataDirectory(IMAGE_OPTIONAL_HEADER32最后一个成员)数组的个数

DataDirectory

DataDirectory是由IMAGE_DATA_DIRECTORY结构体组成的数组

在010Editor下可以看到

image-20230306222456975

使用CFF Explorer查看,可以很详细的看到每个成员的值

image-20230306222841182

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 32位选项头结构体:_IMAGE_OPTIONAL_HEADER
typedef struct _IMAGE_OPTIONAL_HEADER
{
WORD Magic; //* PE标志字:32位(0x10B),64位(0x20B)
BYTE MajorLinkerVersion; // 主链接器版本号
BYTE MinorLinkerVersion; // 副链接器版本号
DWORD SizeOfCode; // 代码所占空间大小(代码节大小)
DWORD SizeOfInitializedData; // 已初始化数据所占空间大小
DWORD SizeOfUninitializedData; // 未初始化数据所占空间大小
DWORD AddressOfEntryPoint; //* 程序执行入口RVA,(w)(Win)mainCRTStartup:即0D首次断下来的自进程地址
DWORD BaseOfCode; // 代码段基址
DWORD BaseOfData; // 数据段基址
DWORD ImageBase; //* 内存加载基址,exe默认0x400000,dll默认0x10000000
DWORD SectionAlignment; //* 节区数据在内存中的对齐值,一定是4的倍数,一般是0x1000(4096=4K)
DWORD FileAlignment; //* 节区数据在文件中的对齐值,一般是0x200(磁盘扇区大小512)
WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
WORD MajorImageVersion; // 可运行于操作系统的主版本号
WORD MinorImageVersion; // 可运行于操作系统的次版本号
WORD MajorSubsystemVersion; // 主子系统版本号:不可修改
WORD MinorSubsystemVersion; // 副子系统版本号
DWORD Win32VersionValue; // 版本号:不被病毒利用的话一般为0,XP中不可修改
DWORD SizeOfImage; //* PE文件在进程内存中的总大小,与SectionAlignment对齐
DWORD SizeOfHeaders; //* PE文件头部在文件中的按照文件对齐后的总大小(所有头 + 节表)
DWORD CheckSum; // 对文件做校验,判断文件是否被修改:3环无用,MapFileAndCheckSum获取
WORD Subsystem; // 子系统,与连接选项/system相关:1=驱动程序,2=图形界面,3=控制台/Dll
WORD DllCharacteristics; // 文件特性
DWORD SizeOfStackReserve; // 初始化时保留的栈大小
DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
DWORD LoaderFlags; // 已废弃,与调试有关,默认为 0
DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,此字段自Windows NT发布以来,一直是16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];// 数据目录表
} IMAGE_OPTIONAL_HEADER32, * PIMAGE_OPTIONAL_HEADER32;

//* 字段6:AddressOfEntryPoint 表 程序入口RVA,即OEP:
EOP:程序入口点,壳相关概念
OEP:原本的程序入口点(实际为偏移,+模块基址=实际入口点)
EP: 被加工后的入口点
//* 字段9:ImageBase 表 模块加载基地址,exe默认0x400000,dll默认0x10000000
建议装载地址:exe映射加载到内存中的首地址= PE 0处,即实例句柄hInstance
一般而言,exe文件可遵从装载地址建议,但dll文件无法满足
//* 尾字段:DataDirectory 表 数据目录表,用来定义多种不通用处的数据块。
存储了PE中各个表的位置,详情参考IMAGE_DIRECTORY_ENTRY...系列宏

原创]归纳PE结构基础知识,顺便手撕个PE-编程技术-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

节区头

节区头定义了个节区的属性

类别 访问权限
code 执行,读取权限
data 非执行,读写权限
resource 非执行,读取权限

节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区

image-20230306223555772

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// IMAGE_SECTION_HEADER 节表结构体,大小40B
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称:描述性字段
// 下方4个字段:从文件S1处开始,拷贝S2大小的数据,到内存S3处,有效数据占用内存S4大小
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // S4:内存大小
} Misc;
DWORD VirtualAddress; // S3:内存地址:基于模块基址
DWORD SizeOfRawData; // S2:文件大小
DWORD PointerToRawData; // S1:文件偏移
DWORD PointerToRelocations; // 无用
DWORD PointerToLinenumbers; // 无用
WORD NumberOfRelocations; // 无用
WORD NumberOfLinenumbers; // 无用
DWORD Characteristics; // 节属性,取值IMAGE_SCN_...系列宏(bit OR)
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

image-20230306223719784

RVA to RAW

Offset

是指数据在文件中相对于文件开始位置的偏移

VA

是指数据在进程虚拟内存的绝对地址

RVA

是指数据在进程虚拟内存中某个基准位置(ImageBase)开始的相对地址

VA和RVA满足的换算关系

1
RVA = VA - ImageBase

文件偏移(RAW)

1
RAW = RVA - VirtualAddress + PointerToRawData

注:其中VirtualAddress为RVA所在节区的VA(VA - ImageBase)地址,PointerToRawData为RVA所在节区对应在文件中节区的偏移(Offset)

IAT

IMAGE_IMPORT_DESCRIPTOR

该结构体中记录着PE文件要导入哪些库文件

在winnt.h中可以找到关于他的定义

image-20230313110516820

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;//INT(Import Name Table) address (RVA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; //library name string address(RVA)
DWORD FirstThunk; //IAT(Import Address Table) address(RVA)
} IMAGE_IMPORT_DESCRIPTOR;

typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //ordinal
CHAR Name[1]; //function name string
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

执行一个程序时,导入多少个库就存在多少个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体形成了数组,且以NULL结构体结束。

注意:

  • INT 和 IAT 是DWORD数组,以NULL结束

  • INT中各元素的值位IMAGE_IMPORT_BY_NAME结构体指针

  • INT 与 IAT的大小应该相同

image-20230313111429483

PE装载器把导入函数输入至IAT的顺序

IAT输入顺序:

  1. 读取IID的Name成员,获取库名称字符串”kernal32.dll”

  2. 装载相应库:LoadLibrary(“kernal32.dll”)

  3. 读取IID的OriginalFirstThunk成员,获取INT地址

  4. 逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)

  5. 使用IMAGE_IMPORT_BY_NAME的Hint或Name成员,获取相应函数的起始地址:GetProcAddress(IMAGE_IMPORT_BY_NAME.NAME)

    例如GetProcAddress(“GetCurrentThreadId”)

  6. 读取IID的FirstThunk(IAT)成员,获得IAT地址

  7. 将上面获得的函数地址输入相应IAT数组值

  8. 重复4~7,直到INT结束(遇到NULL)

稍微总结一下,整个过程就是找到IID,获得库的INT地址,获得对应函数的起始地址,获得IAT地址,将函数地址写入IAT

EAT

EAT是一种核心机制,它使不同的应用程序可以调用库文件中提供的函数

IMAGE_EXPORT_DIRECTORY

IMAGE_EXPORT_DIRECTORY结构体保存着导出信息

IMAGE_OPTIONAL_HEADER32.DateDirectory[0].VirtualAddress的值即是IMAGE_EXPORT_DIRECTORY结构体数组的起始地址

IMAGE_EXPORT_DIRECTORY

winnt.h头中的定义

image-20230313121213457

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp; //creation time date stamp
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; //address of library file name
DWORD Base; //ordinal base
DWORD NumberOfFunctions; //number of functions
DWORD NumberOfNames; //number of names
DWORD AddressOfFunctions; //address of function start address array
DWORD AddressOfNames; //addresss of function name string array
DWORD AddressOfNameOrdinals; //addresss of ordinal array
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

image-20230313121347228

GetprocAddress()函数引用EAT来获取指定API的地址。

GetProcAddress操作原理

  1. 利用AddressOfNames成员转到”函数名称数组“
  2. ”函数名称数组“中存储着字符串地址。通过strcmp,查找指定的函数名称(数组的索引称为name_index)
  3. 利用AddressOfNameOrdinals成员转到orinal数组
  4. 在orinal数组中通过name_index查找相应orinal值
  5. 利用AddressOfFunctions成员转到”函数数组地址“(EAT)
  6. 在”函数地址数组“中将刚刚求得的oridinal用作数组索引,获得指定函数的起始地址

hint:对于没有函数名称的导出函数,可以通过Ordinal查找到它们的地址。从Ordinal值中减去IMAGE_EXPORT_DIRECTORY.Base成员后得到一个值,使用该值作为”函数地址数组“的索引,即可查找到相应函数的地址


PE文件格式学习
http://example.com/2023/03/06/PE文件格式学习/
Author
Eutop1a
Posted on
March 6, 2023
Licensed under