使用UE查看PE文件格式,下载链接:
链接:https://pan.baidu.com/s/18dETrHiic0zoK1Mfu_6M8g
提取码:tyms
不错的视频:https://www.bilibili.com/video/BV18r4y1K7sa?p=4&spm_id_from=pageDriver
1、什么是可执行文件?
可执行文件 (executable file) 指的是可以由操作系统进行加载执行的文件。
可执行文件的格式:
- Windows平台:PE(Portable Executable)文件结构
- Linux平台:ELF(Executable and Linking Format)文件结构
PE和ELF非常相似,它们都是源于同一种可执行文件格式 COFF
- COFF 是由Unix System V Release 3首先提出并且使用的格式规范,
- 微软基于COFF格式,制定了PE格式标准,并将其用于当时的Windows NT系统
- System V Release 4在COFF的基础上引入了ELF格式。
事实上,在Windows平台,VISUAL C++编译器产生的目标文件仍然使用COFF格式,而可执行文件为PE格式
微软对64位Windows平台上的PE文件结构叫做PE32+,就是把那些原来32位的字段变成了64位。
2、PE文件的特征
识别一个文件是不是PE文件不应该只看文件后缀名,还应该通过PE指纹
使用UE打开一个exe文件,发现文件的头两个字节都是MZ,0x3C位置保存着一个地址,查该地址处发现保存着“PE”,这样基本可以认定该文件是一个PE文件
通过这些重要的信息(“MZ”和“PE”)验证文件是否为PE文件,这些信息即PE指纹。
3、PE文件的整体结构
这里将一个PE文件的主要部分列为4部分,这里可以先有模糊概念,后面会详细解释
“节”或“块”或”区块“都是一个意思,后文会穿插使用
下面从二进制层面整体把握其结构,看看一个PE文件的组成
4、PE文件到内存的映射
PE文件存储在磁盘时的结构和加载到内存后的结构有所不同。
当PE文件通过Windows加载器载入内存后,内存中的版本称为模块(Module)。
映射文件的起始地址称为模块句柄(hModule),也称为基地址(ImageBase)。
(模块句柄是不是和其他句柄不太一样呢?)
文件数据一般512字节(1扇区)对齐(现也多4k),32位内存一般4k(1页)对齐,512D = 200H,4096D = 1000H
文件中块的大小为200H的整数倍,内存中块的大小为1000H的整数倍,映射后实际数据的大小不变,多余部分可用0填充
PE文件头部(DOS头+PE头)到块表之间没有间隙,然而他们却和块之间有间隙,大小取决于对齐参数
VC编译器默认编译时,exe文件基地址是0x400000,DLL文件基地址是0x10000000
VA:虚拟内存地址
RVA:相对虚拟地址即相对于基地址的偏移地址
FOA: 文件偏移地址
5、DOS部分
DOS MZ文件头实际是一个结构体(IMAGE_DOS_HEADER),占64字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
DOS头用于16位系统中,在32位系统中DOS头成为冗余数据,但还存在两个重要成员e_magic字段(偏移 0x0)和 e_lfanew字段(偏移 0x3C)
e_magic保存“MZ”字符,e_lfanew保存PE文件头地址,通过这个地址找到PE文件头,得到PE文件标识“PE”。
e_magic和e_lfanew是验证PE指纹的重要字段,其他字段现基本不使用(可填充任意数据)
“DOS Stub”区域的数据由链接器填充(可自己填充如意数据),是一段可以在DOS下运行的一小段代码,
这段代码的唯一作用是向终端输出一行字:“This program cannot be run in DOS”(“e_cs”和“e_ip”指向)
然后退出程序,表示该程序不能在DOS下运行。
6、PE文件头(PE Header)
PE文件头是一个结构体(IMAGE_NT_HEADERS32),里面还包含两个其它结构体,占用4B + 20B + 224B
1 2 3 4 5 |
|
Signature字段设置为0x00004550,ANCII码字符是“PE00”,标识PE文件头的开始,PE标识不能破坏。
1、IMAGE_FILE_HEADER结构体
IMAGE_FILE_HEADER(映像文件头或标准PE头)结构包含PE文件的一些基本信息,该结构在微软的官方文档中被称为标准通用对象文件格式(Common Object File Format,COFF)头
1 2 3 4 5 6 7 8 9 |
|
重要字段:NumberOfSections,SizeOfOptionalHeader
对应结构为下图紫线部分
0x014C说明运行于x86 CPU;0x0007说明当前exe有7个节;
0x00E0说明IMAGE_OPTIONAL_HEADER32为224字节;
0x030F(0000 0011 0000 1111)代表文件属性 ,由下列对应位为1的组合
2、IMAGE_OPTIONAL_HEADER结构体
IMAGE_OPTIONAL_HEADER(可选映像头或扩展PE头)是一个可选的结构,是IMAGE_FILE_HEADER结构的扩展
大小由IMAGE_FILE_HEADER结构的SizeOfOptionalHeader字段记录(可能不准确)
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 |
|
重要字段:
AddressOfEntryPoint:程序入口地址(RVA),下图为32C40H
ImageBase:内存镜像基地址,下图为400000H
FileAlignment:文件对齐,下图为200H
SectionAlignment:内存对齐,下图为1000H
DataDirectory[16]:数据目录表,由数个相同的IMAGE_DATA_DIRECTORY结构组成,
指向输出表、输入表、资源块,重定位表等(后面详解这里先跳过)
1 2 3 4 |
|
ImageBase + AddressOfEntryPoint = 程序实际运行入口地址(实际加载地址等于ImageBase)
0x400000 + 0x32C40 = 0x432C40 (使用OD运行程序发现就是从这个地址开始运行)
应用:在PE文件空白区添加代码,让程序执行先执行添加的代码再跳转程序入口
思路:
① 在PE的空白区构造一段代码(call -> E8)
② 修改入口地址为新增代码(IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint)
③ 新增代码执行后,跳回入口地址(jmp -> E9)
7、块表
块表是一个IMAGE_SECTION_HEADER的结构数组,每个IMAGE_SECTION_HEADER结构40字节。
每个IMAGE_SECTION_HEADER结构包含了它所关联的区块的信息,例如位置、长度、属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
重要字段:Name[8],VirtualSize,VirtualAddress,SizeOfRawData,PointerToRawData,Characteristics
IMAGE_FILE_HEADER的NumberOfSections字段是不是记录着当前文件的节数呢?
31C80H代表载入内存代码块对齐前大小;1000H代表代码块装载到内存RVA1000H;
31E00H代表文件对齐后代码块大小;400H代表代码块在文件中的偏移
60000020H代表代码块属性(0110 0000 0000 0000 0000 0000 0010 0000)查下表得到属性为可读可执行的代码
更多属性参考:https://docs.microsoft.com/zh-cn/windows/win32/api/winnt/ns-winnt-image_section_header
8、RVA与FOA的转换
RVA:相对虚拟地址,FOA:文件偏移地址。
计算步骤:
① 计算RVA = 虚拟内存地址 - ImageBase
② 若RVA是否位于PE头:FOA == RVA
③ 判断RVA位于哪个节:
RVA >= 节.VirtualAddress (节在内存对齐后RVA )
RVA <= 节.VirtualAddress + 当前节内存对齐后的大小
偏移量 = RVA - 节.VirtualAddress;
④ FOA = 节.PointerToRawData + 偏移量;
应用举例:
有初始值的全局变量初始值会存储在PE文件中,想要修改文件中全局变量的数据值即
需要找到文件中存储全局变量值的地方,然后修改即可
2、输出表和输入表
可选PE头(扩展PE头)的最后一个字段DataDirectory[16]代表数据目录表,由16个相同的IMAGE_DATA_DIRECTORY结构组成,成员分别指向输出表、输入表、资源块等
1 2 3 4 |
|
1、输出表(导出表)
创建一个DLL时,实际上创建了一组能让EXE或其他DLL调用的函数
DLL文件通过输出表(Export Table)向系统提供输出函数名、序号和入口地址等信息。
数据目录表的第1个成员指向输出表。
找到文件中的输出表(以DllDemo.dll为例,看图就行)
成功找到输出表在文件偏移0C00H处,如下:
特别说明:① 如果文件对齐与内存对齐都是4k则不需要地址转换 ② 输出表大小是指输出表大小与其子表大小和
输出表实际是一个40字节的结构体(IMAGE_EXPORT_DIRECTORY),输出表的结构如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
重要字段: Name,Base,NumberOfNames,AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals
过程分析:
//功能:加载动态链接库到内存
HMODULE WINAPI LoadLibrary(
LPCTSTR lpFileName //模块的文件名
);
/*功能:检索指定的动态链接库(DLL)中的输出库函数地址*/
FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄 (模块基地址)
LPCSTR lpProcName // 函数名 或者 指定函数的序数值
);
PE装载器调用GetProcAddress来查找DlIDemo.DLL里的API函数MsgBox,
系统通过定位DlIDemo.DLL的输出表(IMAGE_EXPORT_DIRECTORY)结构获得输出函数名称表(ENT)的起始地址,
对名字进行二进制查找,直到发现字符串“MsgBox”为止,PE装载器发现MsgBox是数组的第1个条目后,加载器从输出序数表
中读取相应的第1个值,这个值是MsgBox的在函数地址表(EAT)的索引。使用索引在EAT取值得到MsgBox的RVA1008h。
用1008h加DllDemo.DLL的载入地址,得到MsgBox的实际地址。
特别说明:如果lpProcName 是序号,则需要通过字段Base确定起始序号,序号 - Base的差值作为索引得到函数RVA地址(注意这里的序号和索引)
注意:输出序号表存放的是索引值而不是序号,真正的序号是Base+索引值
例如:写一个简单加法函数(int add(int a, int b)),创建一个A.dll
//def文件
EXPORTS
add @12
分析A.dll的导出表
当时用序号(12)获得函数地址时会拿12-Base = 0作为输出函数地址表的索引值
使用A.dll
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 |
|
2、输入表(导入表)
PE 文件映射到内存后,Windows 将相应的 DLL文件装入,EXE 文件通过“输入表”找到相应的 DLL 中的导入函数,从而完成程序的正常运行
数据目录表的第2个成员指向输入表。当前文件依赖几个模块就会有几张输入表且是连续排放的。
如何找到输入表?
上图看出当前文件只依赖一个模块,只有一张导入表,如果有多个会连续存放直到连续出现20个0说明结束。
输入表实际是个20字节的结构体 IMAGE_IMPORT_DESCRIPTOR
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
重要字段:
Name:DLL(依赖模块)名字的指针。是一个以“00”结尾的ASCII字符的RVA地址。
OriginalFirstThunk:包含指向输入名称表(INT)的RVA。
INT是一个IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构都指向
IMAGE_IMPORT_BY_NAME结构,数组以一个内容为0的IMAGE_THUNK_DATA结构结束。
FirstThunk:包含指向输入地址表(IAT)的RVA。IAT是一个IMAGE_THUNK_DATA结构的数组。
IMAGE_THUNK_DATA结构实际只占4字节
1 2 3 4 5 6 7 8 |
|
如果IMAGE_THUNK_DATA32的最高位为1,则低31位代表函数的导出序号,
否则4个字节是一个RVA,指向IMAGE_IMPORT_BY_NAME结构
IMAGE_IMPORT_BY_NAME结构字面仅有4个字节,存储了一个输入函数的相关信息
1 2 3 4 |
|
由上图,我们是不是通过导入表能够很轻松获得当前文件依赖模块的名字和函数名?
这里INT和IAT完全内容一致,为什么呢?稍后解释
INT和IAT内容一致其实是PE文件未加载时的状态,
PE加载器将文件载入内存后会向IAT填入真正的函数地址(GetProcAddress)
例如:
3、重定位表
如果PE文件不在首选的地址(ImageBase)载入,那么文件中的每一个绝对地址都需要被修正。
需要修正的地址有很多,可以在文件中使用重定位表记录这些绝对地址的位置,在载入内存后若载入基地址与ImageBase不同再进行修正,若相同就不需要修正这些地址。
数据目录项的第6个结构,指向重定位表(Relocation Table)
重定位表由一个个的重定位块组成,每个块记录了4KB(一页)的内存中需要重定位的地址。
每个重定位数据块的大小必须以DWORD(4字节)对齐。它们以一个IMAGE_BASE_RELOCATION结构开始,格式如下
1 2 3 4 5 6 7 8 9 10 11 |
|
这些字段可能直接不好理解在后面会看一个实例一切就彻底明白了
虽然有多种重定位类型,但对x86可执行文件来说,所有的基址重定位类型都是IMAGE_REL_BASED_HIGHLOW。
在一组重定位结束的地方会出现一个类型IMAGE_REL_BASED_ABSOLUTE的重定位,这些重定位什么都不做,只用于填充,以便下一个MAGE_BASE_RELOCATION按4字节分界线对齐。
对于IA-64可执行文件,重定位类型似乎总是IMAGE_REL_BASED_DIR64。
有趣的是,尽管IA-64的EXE页大小是8KB,但基址重定位仍是4KB的块
所有重定位块以一个VitualAddress字段为0的MAGE_BASE_RELOCATION结构结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
示例分析:
继续以DllDemo.dll为例
先用工具定位重定位表在文件的位置如下
查看重定位表信息如下
1 2 3 4 5 6 7 8 9 10 11 12 |
|
下面实际分析
根据下面判断出当前RVA在CODE节
所以
100Fh(RVA) → 60Fh(FOA)
1023h(RVA) → 623h(FOA)
60Fh和623h分别指向00402000h和00403030h处,即为所需要重定位的数据
执行PE文件前,加载程序在进行重定位的时候,会用PE文件在内存中的实际映像地址减PE文件所要求的映像地址,根据重定位类型的不同将差值添加到相应的地址数据中。
可以看到重定位表扮演的角色:文件加载到内存后,通过重定位表记录的RVA找到需要重定位的数据
重定位表通过页基址RVA+页内偏移地址方式得到一个完整RVA大大缩小了表大小。
4、资源
Windows程序的各种界面称为资源,包括加速键(Accelerator)、位图(Bitmap)、光标(Cursor)、对话框(Dialog Box)、图标(Icon)、菜单(Menu)、串表(String Table)、工具栏(Toolbar)和版本信息(Version Information)等。
定义资源时,既可以使用字符串作为名称来标识一个资源,也可以通过ID号来标识资源
资源分类
- 标准资源类型
- 非标准资源类型
若资源类型的高位如果为1,说明对应的资源类别是一个非标准的新类型
数据目录项的第3个结构,指向资源表,不直接指向资源数据,而是以磁盘目录形式定位资源数据
资源表是一个四层的二叉排序树结构。
每一个节点都是由资源目录结构和紧随其后的数个资源目录项结构组成的,
两种结构组成了一个资源目录结构单元(目录块)
资源目录结构(IMAGE_RESOURCE_DIRECTORY)占16字节,其定义如下
1 2 3 4 5 6 7 8 9 10 11 |
|
资源目录项结构(IMAGE_RESOURCE_DIRECTORY_ENTRY),占8字节,包含2个字段,结构定义如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
重要字段:
Name字段:定义目录项的名称或ID。
- 当结构用于第1层目录时,定义的是资源类型;
- 当结构用于第2层目录时,定义的是资源的名称;
- 当结构用于第3层目录时,定义的是代码页编号。
- 当最高位为0时,表示字段的值作为ID使用;由该字段的低16位组成整数标识符ID
- 当最高位为1时,表示字段的低位作为指针使用,资源名称字符串使用Unicode编码,
这个指针不直接指向字符串,而指向一个IMAGE_RESOURCE_DIR_STRING_U结构。
1 2 3 4 |
|
OffsetToData字段:是一个指针。
- 当最高位(位31)为1时,低位数据指向下一层目录块的起始地址;
- 当最高位为0时,指针指向IMAGE_RESOURCE_DATA_ENTRY结构。
第3层目录结构中的OffsetToData将指向IMAGE_RESOURCE_DATA_ENTRY结构。
该结构描述了资源数据的位置和大小,其定义如下。
1 2 3 4 5 6 |
|
重要字段:
OffsetToData:指向资源数据的指针(RVA)
Size:资源数据的长度
实例分析:
定位资源在文件中的位置
由于当前exe文件对齐与内存对齐都是4k,RVA不需要转FOA
所以:
图标的真正资源数据RVA为4100h,大小为2E8h。
菜单的真正资源数据RVA为4400h,大小为5Ah。
图标组的真正资源数据RVA为43E8h,大小为14h。
使用工具验证
'
可以清晰看到根目录有3个资源目录项(Icon,Menu,Icon Group)
第二层为资源ID或资源名称
第三层为代码页ID为2052表简体中文,1033表美国英语
右下角图标为真正资源数据