PE文件格式学习
PE文件格式
介绍
PE文件是Windows操作系统下使用的可执行文件格式。PE:Portable Executable
PE文件指的是32位的可执行文件,也成为PE32,64位的可执行文件称为PE+或者PE32+,是PE32文件的一种拓展(不是PE64)
观察PE文件的各个结构体成员的值,推荐使用CFF Explorer
PE文件格式
种类 | 主拓展名 |
---|---|
可执行 | EXE, SCR |
库 | DLL, OCX, CRL, DRV |
驱动程序 | SYS, VXD |
对象文件 | OBJ |
以IDA pro(x32)为例
一般判断一个文件是否为PE文件主要看的就是他是否具有 MZ和PE这两个标志
PE头
DOS头
在微软创建PE文件时,DOS文件正在被广泛使用,所以为了兼容DOS文件,DOS头出现了,也就是IMAGE_DOS_HEADER结构体,用于拓展已有的DOS EXE头
IMAGE_DOS_HEADER结构体的大小是64字节
在winnt.h中可以找到该结构体的定义
需要掌握的主要就是e_maigc和e_lfanew两个成员
e_magic:DOS签名,即4D5A —> ASCII值 “MZ”
e_lfanew:指示NT头的偏移(根据不同文件拥有可变值)
在IDA pro(x32)这里,e_magic的值为4D5A,e_lfanew的值为00000100
DOS存根
DOS存根(stub)在DOS头下方,是可选项,主要有代码和数据混合而成,大小不固定(没有DOS存根,文件也可以正常运行)
在32位Windows OS环境下,PE loader识别到DOS头,该文件被识别为PE文件,这段DOS存根不会被执行。
在DOS环境中或者是DOS debug下,可以执行这段代码(不认识PE文件格式,所以识别为DOS文件)。
NT头
在winnt.h文件中可以找到IMAGE_NT_HEADERS结构体的定义
IMAGE_NT_HEADERS主要由三个成员构成,第一个为签名(signature)结构体,值为50450000 (“PE” 00)。
另两外两个成员是文件头(FileHeader)和可选头(OptionalHeader)
IMAGE_NT_HEADERS结构体的大小为F8
FileHeader
IMAGE_FILE_HEADER有下面四个重要成员,如果他们设置不正确,可能导致文件无法正常运行
Machine
每个CPU都拥有唯一的Machine码,兼容32为Intel x86芯片的Machine码为14C
下面是定义在winnt.h里的Machine码
NumberOfSection
PE文件把代码,数据,资源等依据属性分类到各节区中存储
NumberOfSection 用来指出文件中存在的节区数量。
该值一定要大于0,且当定义的节区数量与实际截取不同时,将发生运行错误
SizeOfOptionalHeader
IMAGE_FILE_HEADER的最后一个成员为IMAGE_OPTIONAL_HEADER32结构体,SizeOfOptionalHeader用来指出IMAGE_OPTIONAL_HEADER32结构体的长度
Characteristics
Characteristics用于标识文件的属性,文件是否是可运行的形态,是否为DLL文件等信息,以bit OR形式组合起来
主要记住0x0002和0x2000
其余的成员
1 |
|
IMAGE_FILE_HEADER结构体
machine对应的是8664,对应AMD64处理器。
NumberOfSection对应0060。
TimeDateStamp对应620F1879,转换为2022-02-18 11:54:33。
PointerToSymbolTable:00000000
NumberOfSymbols:00000000
SizeOfOptionalHeader:00F0
Characteristics:0022 –> 0x2 | 0x20
OptionalHeader
IMAGE_OPTIONAL_HEADER结构体
关注以下成员,如果他们设置不正确,可能导致文件无法正常运行
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下可以看到
使用CFF Explorer查看,可以很详细的看到每个成员的值
1 |
|
原创]归纳PE结构基础知识,顺便手撕个PE-编程技术-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)
节区头
节区头定义了个节区的属性
类别 | 访问权限 |
---|---|
code | 执行,读取权限 |
data | 非执行,读写权限 |
resource | 非执行,读取权限 |
节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区
1 |
|
RVA to RAW
Offset
是指数据在文件中相对于文件开始位置的偏移
VA
是指数据在进程虚拟内存的绝对地址
RVA
是指数据在进程虚拟内存中某个基准位置(ImageBase)开始的相对地址
VA和RVA满足的换算关系
1 |
|
文件偏移(RAW)
1 |
|
注:其中VirtualAddress为RVA所在节区的VA(VA - ImageBase)地址,PointerToRawData为RVA所在节区对应在文件中节区的偏移(Offset)
IAT
IMAGE_IMPORT_DESCRIPTOR
该结构体中记录着PE文件要导入哪些库文件
在winnt.h中可以找到关于他的定义
1 |
|
执行一个程序时,导入多少个库就存在多少个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体形成了数组,且以NULL结构体结束。
注意:
INT 和 IAT 是DWORD数组,以NULL结束
INT中各元素的值位IMAGE_IMPORT_BY_NAME结构体指针
INT 与 IAT的大小应该相同
PE装载器把导入函数输入至IAT的顺序
IAT输入顺序:
读取IID的Name成员,获取库名称字符串”kernal32.dll”
装载相应库:LoadLibrary(“kernal32.dll”)
读取IID的OriginalFirstThunk成员,获取INT地址
逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)
使用IMAGE_IMPORT_BY_NAME的Hint或Name成员,获取相应函数的起始地址:GetProcAddress(IMAGE_IMPORT_BY_NAME.NAME)
例如GetProcAddress(“GetCurrentThreadId”)
读取IID的FirstThunk(IAT)成员,获得IAT地址
将上面获得的函数地址输入相应IAT数组值
重复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头中的定义
1 |
|
GetprocAddress()函数引用EAT来获取指定API的地址。
GetProcAddress操作原理
- 利用AddressOfNames成员转到”函数名称数组“
- ”函数名称数组“中存储着字符串地址。通过strcmp,查找指定的函数名称(数组的索引称为name_index)
- 利用AddressOfNameOrdinals成员转到orinal数组
- 在orinal数组中通过name_index查找相应orinal值
- 利用AddressOfFunctions成员转到”函数数组地址“(EAT)
- 在”函数地址数组“中将刚刚求得的oridinal用作数组索引,获得指定函数的起始地址
hint:对于没有函数名称的导出函数,可以通过Ordinal查找到它们的地址。从Ordinal值中减去IMAGE_EXPORT_DIRECTORY.Base成员后得到一个值,使用该值作为”函数地址数组“的索引,即可查找到相应函数的地址