PE文件结构
- 推荐查看
PE
文件格式的工具:010editor
、CFF
、winhex
。 - 其中
010editor
可以在之后逆向作为工具使用(其有PE模版,PE不同头的部分会有不同颜色的高亮),CFF可以具体查看PE文件结构体的具体值,winhex
二进制查看器(用于学习)。 - CFF汉化版:[分享][汉化]CFF Explorer 7.9 官方版(支持中文)-安全工具-看雪-安全社区|安全招聘|kanxue.com
VS2022写汇编
- 由于要用到
vs2022
编写汇编程序所以先来简单介绍一下在vs2022
中如何可以设置编译汇编。先新建一个项目
- 然后选择新建这个类型的项目
- 点击
Windows
桌面引导后就选择如图选项
- 之后右键新创建的项目,选择
生成依赖项--->生成自定义
- 勾选
masm
选项
- 现在就可以新建一个
.asm
后缀的文件,写入如下代码,注意下面这个汇编代码是在i386
架构下的
1 | .386 |
- 既然是
i386
架构下的汇编,我们就需要将这里设置为x86
,建议debug
模式
- 之后查看编译器是
ml.exe
还是ml64.exe
,因为x86
和x64
的汇编语法上会存在不同,使用ml64
编译i386
的程序会编译不通过。
- 如果是
release
模式,在编译的时候就会出现这个问题
- 这个也比较好解决,解决方法就是在下图的
命令行
这一栏添加这么一句ml
的汇编命令/SAFESEH:NO
- 这样就可以编译通过了
PE文件结构简述
- 每个成熟的带有后缀名的文件比如
.exe
、.pdf
、.jpg
等文件都会有属于自己特有的文件头和文件尾。当我们查看或者运行这些文件的时候,操作系统并不是根据后缀名来识别这些文件类型。操作系统是通过识别这些文件的文件头和文件尾来判断这些文件具体是属于哪一类型。 PE
,英文全称为portable executable
是可移植可执行文件。- 对于
PE
可执行文件即.exe
后缀的文件也是如此,PE
文件也有属于自己的文件结果(文件头和文件尾),在这些文件中隐藏着一些程序运行的信息,并且一些反调试技术也与PE文件格式相关,所以在学Windows
用户程序逆向之前,先要具体详细地学习一下PE
文件结构。 PE
文件头大概有如下结构:
1 | IMAGE_DOS_HEADER; //DOS头 |
PE
文件处于内存中的时候通常称为映像
PE文件结构
- 上面我们已经使用
VS2022
汇编出了exe
文件,先使用010Editor
查看一下这个exe
文件PE
结构。
DOS头
MZ
是一个DOS头的识别部分,当操作系统执行这个可执行文件的时候,并不是看拓展名,而是看这个文件的二进制形式,其实DOS
头的标志性标识其实就是0x4D 0x5A
也就是MZ
,MZ
其实是DOS
最早期作者的名字缩写。- 而这个
DOS头
中我们标注CC
的这边实际上是可以任意修改的、可以使用的,修改后并不影响其执行。标注CC
的地方其实是为DOS
文件执行的时候提供环境信息,这个环境信息包括寄存器值、可选重定位表、中断向量表、文件句柄表等 - 最后的
32
位其实标明了新结构的位置(NT头的位置),其中0xC8
其实就是PE
结构标识符中P
字母的位置。
- 在
PE
和DOS
头这边还有一段数据,其实也就是这一段数据。这一部分被称为Stub
数据,通常被叫做残留数据(包含二进制指令和数据)。 - 当该文件在DOS下执行时,其就会真正从
offset=0x40
这个位置开始执行,最先开始执行0x0E
- 在
DOS
执行后就会出现这样的提示:This program cannot be run in DOS mode.
- 使用
IDA
反汇编就能看到这一段的具体指令了
- 接下来为了更熟悉
DOS
文件结构,就将DOS
结构体敲一遍抄下来
1 | typedef stuct _IMAGE_DOS_HEADER{ |
- 其实这一段都是可以
人为修改
和利用
的
NT头
NT
头有三个主要结构体。其中一个是NT标识即IMAGE_NT_HEADERS
,还有俩个子结构IMAGE_FILE_HEADER
、IMAGE_OPTIONAL_HEADER
。
NT头_NT_HEADERS
- 在
VS
中查看NT
头,发现NT
头定义有区分32
位和64
位
- 其结构体就是这样(以32位为例子,先介绍32位的)
1 | typedef struct _IMAGE_NT_HEADERS { |
- 下面是
NT
文件头文件格式的16
进制分布
NT的文件头_FILE_HEADERS
- 依然是这张图片,其中标
CC
的就是可以利用的。
- 直接查看
vs
的IMAGE_FILE_HEADER
的结构体
1 | typedef struct _IMAGE_FILE_HEADER{ |
- 注意如果合理修改了
sizeOfOptionalHeader
的值,其实程序是可以正常运行,但是程序可能不能进行动态调试,动态调试可能就会出现问题。
Machin宏定义
- 上面
FILE_HEADER
中的Machine
宏定义也可以在VS
中能看到,某些宏定义可以经过与
操作组合在一起
- 在上图中又几个比较重要和常见的架构宏定义
1 |
- 介绍一下这些宏定义在的架构和类型
1 |
NT的选项头文件_OPTIONAL_HEADER
- 选项头有三类
32位选项头
、64位选项头
、嵌入式选项头
。这里主要介绍32位选项头
和64
位选项头 - 从
FILE_HEADERS
倒数第3、4
字节就可以得到optional header
的大小,其实就是0xE0
,从而得到optional head
的那一块区域
- 先查看一下
IMAGE_OPTIONAL_HEADER32
的结构体
1 | // 这个是32位的选项头重点介绍 |
- 对于
DWORD FileAlignment;
这个文件对齐,紫色框部分才是代码的部分,但是由于文件需要0x200
字节对齐,所以剩余部分需要补充\x00
SizeOfHeaders
SizeOfHeaders
指的是可执行文件的头部大小,从SizeOfHeaders
中可以看出来PE文件头大小为0x400
,这个0x400
需要与File_aligment
这里面的数据对齐(也就是需要满足0x200的整数倍),由于我们的Headers
大于0x200
所以需要对齐到0x400
也就是Headers
头结束后,填充\x00
当做无效数据
Subsystem
- 主要介绍一下
Subsystem
中一些宏,这个字段可以改,但是需要合理的改。
1 |
DllCharacteristics
- 关于
Dll
的属性描述
1 | 0x0001 保留字段 |
堆栈保留和提交
1 | DWORD SizeOfStackReserve; // 初始保留的栈空间大小,程序能向操作系统申请的最大栈内存 |
IMAGE_DATA_DIRECTORY
- 数据目录是
PE
文件中的一个重要结构
1 | // IMAGE_DATA_DIRECTORY的结构如下 |
- 数据目录数组对应的索引已经规定好了具体的数据表,下面就是具体数据表的宏定义
1 |
|
导入表(IMAGE_DIRECTORY_ENTRY_IMPORT)
-
最复杂的一个数据目录。
-
导入表的作用如下:
- 当程序运行的时候,有时需要调用外部接口,尤其是
操作系统API
。有点类似于ELF
文件中的got表
。 - 当操作系统装载我们的可执行文件时,操作系统首先会分析可执行文件需要哪些库。接着分析需要这些库的哪些函数,将需要调用的函数地址,填入到操作系统和编译器约定好的位置。
- 编译器在编译好代码的时候,运行该程序,该程序在调用操作系统的相关
API
的时候就会到约定好的地址对这个API
进行间接的反问。 - 而操作系统与应用程序约定好填入
API
的位置被称为Import Address Table简称IAT,调用API
的地址是不固定的。
- 当程序运行的时候,有时需要调用外部接口,尤其是
-
导入表需要记录俩个东西
函数对应的动态库
、动态库中的具体函数
(这两者的关系就相当于学生和班级这一数据关系,一个动态库对应多个班级
)
1 | IMAGE_DIRECTORY_ENTRY_IMPORT; //导入表 |
- 在编译时,操作系统装载库函数的过程:
- 首先遍历动态库信息,获得目标的库函数信息
- 然后读取函数信息表,将目标函数填入对应
IAT
结构体的中
- 使用
xdbg
查看内存
- 通过
表的内存偏移
找到真实导入表
- 以下面张图片来将一下具体的过程:
- 首先遍历第一个动态库的信息,先会找到库名称(图中阴影部分第一行最后
3A 22 00 00
就是存储库函数名称的地址) - 接着操作系统就会
载入相应的库
,如果载入失败,就会弹出缺少.dll
依赖
- 首先遍历第一个动态库的信息,先会找到库名称(图中阴影部分第一行最后
- 接着会根据
24 22 00 00
找到对应偏移为24 22 00 00
的这个地方其实是一个数组,存放着被引用函数的信息,但是这里由于我们只使用了第一个库中的第一个函数
,所以这里的数组长度就为1。 - 如果多个的话,就会有很多项直到
00 00 00 00
结尾,之后2c 22 00 00
就会定位到_IMAGE_IMPORT_BY_NAME
结构体
- 之后就是这样的一个数据,该数据其实就是如下结构体
WORD Hint
不是很重要,仅供参考CHAR Name[1]
其实是一个可变长数组,存放的是函数的名称- 这样我们就拿到了函数名称,拿到函数名就可以得到函数对应的地址,得到函数对应地址后
1 | typedef struct _IMAGE_IMPORT_BY_NAME { |
导出表(IMAGE_DIRECTORYT_ENTRY_EXPORT)
导入地址表(Import Address Table)
NT_OPTIONAL_HDR64
ROM_OPTIONAL_HDR
段头
PE
文件头的这个位置其实就是段头的位置,先来看一下每个段头的结构
1 | // 如果要确定段头,需要先找数据目录 |
- 使用
.text
段来分析一下这个结构体,结合着下面内存映射的来解读一下.text
段载入内存- 首先是名为
.text
段的一个段,会从文件偏移0x400
的位置将数据载入到内存相对地址为0x1000
的位置,载入实际有效的数据为0x27
个字节(实际上载入总数是0x200字节即SizeOfRawData
的大小) - 并且内存要满足与
NT可选头中的SectionAlignment对齐
- 而
VirtualSize
这里面的数据是半说明性质的(可以适当修改) - 然后给该内存段
可执行、可读
的权限
- 首先是名为
VirtualAddress
- 存储着是文件中的
各个Section段
载入到内存后的起始偏移地址。所以在内存中的地址应该是这样计算。并且文件载入到内存中每一段的大小需要与NT可选头的SectionAlignment
里面的值对齐
1 | 绝对地址 = VirtualAddress + Image_Base(但是Image_Base不可信这里需要注意) |
Characteristics
1 | R 可读 |
无效数据
- 无效数据是为了对齐,所以在
PE
文件头写完了DOS头
、NT头
和段头
之后如果程序偏移还没有到0x400
,此时就需要使用无效数据\x00
进行偏移操作。 - 之后从
0x400
开始其实就是存放着段数据
段数据
附加数据
VS2022查看PE结构体
- 新建一个项目,编写如下代码,鼠标选中
IMAGE_DOS_HEADER
就可以跳转到对应结构体的定义中。
1 |
|
地址转换
玩转导入表
进阶
- 尝试自己编写一个小程序,试着修改一些无关值,查看程序能不能正常执行。
- 尝试编写一个像
CFF
这样的界面,可以读取查看PE
文件头结构,还可以修改
。 - 尝试修改
DOS
那边的stub
部分,使得某个简单的程序能在DOS
环境下也能实现功能。 - 尝试添加一个节头,该节用于注入操作(添加节头即可,注入的代码后面再说)三步走
- 第一步添加新节描述
- 第二步添加新节数据
- 修改NT可选头中的
SizeOfImage
、修改节表总数即修改NT头中的NumberOfSections
的个数
- 添加一个没有文件映射的节头即未初始化数据,对应高级语言中的未初始化的全局变量,以及汇编中的
.data?
- 添加一个变形的未初始化区,即在文件中的数据
小于等于0x200
,但是映射到内存映射0x2000
字节大小 - 写一个类似
CFF
中能自动计算文件偏移,并且可以自动定位数据的程序,可以作为2.
程序中的功能 - 尝试修改导入表,进行
.dll
hook操作
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 iyheart的博客!