此篇文章想写如何通过工具手查导出表、PE文件代码编程过程中的原理。文笔不是很好,内容也是查阅了很多的资料后整合出来的。希望借此加深对PE文件格式的理解,也希望可以对看雪论坛有所贡献。因为了解PE文件格式知识点对于逆向破解还是病毒分析都是很重要的,且基于对PE文件格式的深入理解还可以延伸出更多非常有意思的攻防思维。另外看雪能支持Markdown真是太好了了!发主题更加方便了
VS自带的工具,有很多的功能。但用来查询程序的导出表也非常方便,使用例子如下:
dumpbin.exe /EXPORTS D:\PEDemo.dll
一款免费的dll查看工具,可以帮助查看DLL链接库文件中的输出函数,COM类型库及相应的偏移地址。
十六进制编辑器010 Editor,有大量格式的解析模板,用EXE模板来解析二进制文件,辅助我们读懂和编辑。
LordPE是查看PE格式文件信息的工具,并且可以修改相关信息。里面有个位置计算器的功能可以用于计算相对虚拟地址(RVA)转换文件偏移地址(FOA)。
在Visual Studio里有一个名为WINNT.H的头文件,里面定义了Windows系统里的PE内部结构。
PE结构中有一个NT头(IMAGE_NT_HEADERS NtHeader),NT头里包含了扩展头(IMAGE_OPTIONAL_HEADER),扩展头中包含数据目录表(IMAGE_DATA_DIRECTORY_ARRAY)。
扩展头定义:
IMAGE_NUMBEROF_DIRECTORY_ENTRIES是个宏定义,值是0x16。
宏定义:
数据目录是一个有16个(WINNT.H中定义为IMAGE_NUMBEROF_DIRECTORY_ENTRIES)元素的结构数组。每个数组元素所指定的内容已经被预先定义好了。
WINNT.H文件中的这些IMAGE_DIRECTORY_ENTRY_xxx定义就是数据目录的索引(从0到15)。导出表相对虚拟地址(RVA)就在数据目录表中的第0个数组里。
数据目录表有两个结构体成员分别存有数据的相对虚拟地址和数据的大小,定义如下,:
下表描述了每个IMAGE_DIRECTORY_ENTRY_xxx值每个数组的意义。
图1 IMAGE_OPTIONAL_HEADER中数据目录表结构体数组含义
下面是导出表的数据结构定义说明:
本文旨在用于科普,大牛们可能要见笑了。手动操作部分借助工具010 Editor、LordPE先搞清楚PE结构的内容。涉及到相对虚拟地址(RVA)转换文件偏移地址(FOA)的地方都用LordPE自带功能转换。原理与代码后续贴出
010 Edito通过EXE模板解析PE结构方法如下:
菜单 --> Templates --> Edit Template List
图2 模板使用
在线模板文件地址:
http://www.sweetscape.com/010editor/repository/templates/
保存-模板-打开模板-F5运行,010 Editor会出来一个小窗口。
图3 010Editor多出来的小窗口
010 Editor EXE模板查看的顺序如下:
下图中内容里对应的是导出表结构体中的VirtualAddress(导出表相对虚拟地址)和Size(导出表数据大小)两个数据结构体成员,010 Editor面板里蓝色高亮出来的数据就是导出表的相对虚拟地址和数据大小,对应IMAGE_DATA_DIRECTORY export结构体。
图4 利用010 Editor的EXE模板查看导出表两个数据结构体成员VirtualAddress和Size
在内存中数据是以“小尾方式”存放,“小尾方式”存放是以字节为单位,按照数据类型长度,低数据位排放在内存的低端,高数据排放在内存的高端。如0x00007ED0在内存中会被存储为D07E0000。通过上面的操作,我们已经知道导出表
对应IMAGE_DATA_DIRECTORY.VirtualAddress的相对虚拟地址是0x00007ED0
对应IMAGE_DATA_DIRECTORY.Size的大小是0x000001A4
这里的0x00007ED0是相对虚拟地址(RVA),用LordPE工具转换文件偏移地址(FOA)方法如下:
使用LordPE【位置计算器】功能模块-【PE编辑器】-【位置计算器】,将0x00007ED0(RVA)转换文件偏移得到0x00006ED0
图5 使用LordPE计算出文件偏移得到0x00006ED0
0x00006ED0指向导出表数据结构(IMAGE_EXPORT_DIRECTORY)的位置,010Editor解析如图6所示,导出表数据结构具体的字段含义已经在《2.2 导出表结构》中注明。参照图标注如下:
图6 0x00006ED0指向位置为导出表数据,导出表结构体标注
其中比较有用的是
导出表数据结构(IMAGE_EXPORT_DIRECTORY)的相对虚拟地址为:
通过以上数据可以知道导出表用于输出API函数索引值的基数为0x00000001。所有导出函数中的成员个数有4个,以导出名称导出的函数个数有4个。
接下来使用LordPE的【位置计算器】功能逐个计算出指向模块函数名称、函数地址表、函数名称地址表、导出序列号的相对虚拟地址(RVA)得出文件偏移(FOA):
IMAGE_EXPORT_DIRECTORY.Name == 0x00007F20 转换为 00006F20 //指向导出表文件名的字符串
IMAGE_EXPORT_DIRECTORY.AddressOfFunctions == 0x00007EF8 转换为 00006EF8 //导出函数入口地址表
IMAGE_EXPORT_DIRECTORY.AddressOfNames == 0x00007F08 转换为 00006F08 //导出函数名称地址表
IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals == 0x00007F18 转换为 00006F18 //导出函数名称表位置对应,存储着指向函数入口地址序号表的索引
0x00007F20 转换为 00006F20,指向导出表Name字段,内容存储的是模块函数名称的ASCII字符,测试用的DLL名称如下:
PEDemo.dll
图7 指向导出文件名的字符串
0x00007EF8 转换成文件偏移地址为 00006EF8,这个地址对应的结构体成员是AddressOfFunctions,AddressOfFunctions指向的是所有导出函数地址表的RVA地址,有多少个地址根据NumberOfFunctions的值得出。
再使用LordPE【位置计算器】功能通过RVA转换为FOA,查看所有导出函数地址表,得到以下的导出函数地址:
图8 IMAGE_EXPORT_DIRECTORY.AddressOfFunctions
0x00007F08 转换成文件偏移地址为 00006F08,这个地址对应的结构体成员是AddressOfNames,这个成员表存储的是导出函数名字RVA。
因为前面根据NumberOfFunctions的值已经知道这个DLL里有4个导出函数,然后利用010Editor定位到文件偏移处00006F08的位置。里面存储的是4个导出函数名称对应的RVA地址。根据各个RVA转换成FOA。得到地址如下:
这里需要注意结构体里存储的是存放导出函数名称的RVA,要得到存储的导出函数名称的ASCII还要多转换一层RVA。
图9 存储着导出函数名称的RVA
RVA转换FOA如下:
使用010Editor查看00006F2B 、00006F37 、00006F44、00006F51可以看到其实每个函数的名称存放位置是连续的,使用00进行了隔断。
指向导出函数名称表RVA,AddressOfNames字段的值如下:
图10 指向导出表文件名的字符串
然后导出函数名称表存储的其实是指向真实函数字符串的RVA地址,转换前的对应关系如下图。
图11 导出函数名称地址表
0x00007F18 转换成文件偏移地址为 00006F18,这个地址对应的结构体成员是IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals。
图12 序号表
这张表与导出函数名称地址表的顺序对应,存储着指向函数入口地址表的索引值(序号表,起始值从0开始)。编程的时候为DWORD类型,但却是WORD类型,因为只有2字节。对应的导出函数名称关系如下:
AddressOfFunctions为函数入口地址,AddressOfNames为导出函数名,AddressOfNameOrdinals存储着函数入口地址表的数组下标索引值(序号表),用三张表的数据进行对比,结构就更清晰了,关系表如下:
图13 序号对应关系
根据导出函数序号 = 函数入口地址序号 + 基数,已知Base基数数为1,计算如下:
函数名称与函数入口地址表数组下标索引序号对应,计算如下:
AddressOfNames中保存着一组RVA,每个RVA指向一个字符串,即导出的函数名。与导出的函数名对应的是AddressOfNameOrdinals中对应的项。AddressOfNameOrdinals是一张设计得非常巧妙的一张表,让我们很方便的利用这张表按导出函数名称查找对应函数入口地址和按函数入口地址查找对应导出函数名称。
1、获取已函数地址表的个数,NumberOfFunctions为4
2、获取以名称导出的函数个数,NumberOfNames为4
3、获取函数名称的地址
4、获取函数序号表的值
5、导出函数名与AddressOfNameOrdinals(函数序号表)的顺序对应
6、序号表存储的序号值是函数入口地址表的数组下标索引值
例子:我们已经知道3个函数名称Func1、Func2、Func3对应的序号值为0、2、3。那么函数地址表的第1、3、4的RVA(相对虚拟地址)就是有函数名称导出的函数入口地址,因为数组下标索引值由0起始计算。
1、获取已函数地址表的个数,NumberOfFunctions为4
2、获取以名称导出的函数个数,NumberOfNames为4
3、获取函数名称的地址
4、获取函数序号表的值
5、导出函数名与AddressOfNameOrdinals(函数序号表)的顺序相互对应
6、遍历函数入口地址的数组索引值与AddressOfNameOrdinals(函数序号表)的实际内容值(非数组下标索引值)对比
7、如果函数入口地址的数组索引值与序号表内的存储内容相同,那么与函数序号表数组索引值顺序对应的函数名称就是函数入口地址对应的导出函数名称。
当函数是以序号方式导出的,查找的时候直接用函数入口地址序号加上基数(Base)就等于导出函数序号了。
逆向推回来,就是导出函数序号减去基数就得到函数入口地址(AddressOfFunctions)的顺序了,也就是数组下标索引值。
【导出函数序号】 = 【函数入口地址序号】+【基数】
【函数入口地址序号】 = 【导出函数序号】-【基数】
参考图:
图14 AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals关系图
印证自己查询的是不是正确,可以通过VS自带的dumpbin工具或是 DLL Export Viewer这类工具查看。
图15 DLL导出表查看工具结果
前面的章节使用工具进行手动查看导出表,而这一节则是查阅了大量网上的文章然后汇总而成的笔记。希望可以给大家带来一些帮助!
术语:
RVA:RVA 是相对虚拟地址(Relative Virtual Address)的缩写,它是文件映射到内存中的“相对地址”。
FOA:FOA是文件偏移地址(File Offest Address)的缩写,它是文件在磁盘上存放时相对文件开头的偏移地址。
LordPE的位置偏移功能的确很方便,但始终还是要熟悉原理,才能写出操作PE的代码。
有一条公式可以帮助我们很方便的计算出文件偏移的位置。
要转换的相对虚拟地址会落在一个区段中是因为每个偏移,不管是在文件中,还是在内存中,它们距离区段开始位置的距离总是相等的。
该区段相对虚拟地址(RVA) <--对应--> IMAGE_SECTION_HEADER.VirtualAddress
该区段的文件偏移(offset) <--对应--> IMAGE_SECTION_HEADER.PointerToRawData
在写代码前我们首先弄清楚基本概念。
图16 PE文件映射到虚拟内存
根据上图看出,区段装入内存之后的偏移与文件偏移是存在差异的。所以当我们进行文件偏移与虚拟内存地址之间换算时,首先要得出所转换的地址在第几区段内。每个区段的含义如下。
4.1.1 区段名称约定
.text代码段,此区段内的数据全部为代码
.data可读写的数据段,此区段内存放全局变量或静态变量
.rdata只读数据区段
.idara导入数据区段,此区段内存放导入表信息
.edata导出数据区段,次区段内存放导出表信息
.rsrc 资源区段,此区段内存放应用程序会用到的所有资源,如图标、菜单等
.bss未初始化数据
.crt此区段包含用于支持C++运行时库(CRT)所添加的数据
.tls此区段包含用于支持通过_declspec(thread)声明的线程局部存储变量的数据
.reloc此区段包含重定位信息
.sdata此区段包含相对于可被全局指针定位的可读写数据
.srdata此区段包含相对于可被全局指针定位的只读数据
.pdata此区段包含异常表
....等
4.1.2 区段表结构
重新温习区段表(IMAGE_SECTION_HEADER)结构,如下所示:
LordPE中的显示关系
图17 LordPE中的显示关系
代码例子
RVA是不变的,对比RVA在哪个区段内。找到RVA所在区段,然后计算出这个RVA到区段在内存中的开始位置的距离。
编程思路:
步骤1:循环扫描区块表得出每个区块在内存中的起始 RVA(根据IMAGE_SECTION_HEADER 中的VirtualAddress 字段),并根据区块的大小(根据IMAGE_SECTION_HEADER 中的SizeOfRawData 字段)算出区块的结束 RVA(两者相加即可),最后判断目标 RVA 是否落在该区块内。
步骤2:通过步骤1定位目标 RVA 处于具体的某个区块中后,那么用目标 RVA 减去该区块的起始 RVA ,这样就能得到目标 RVA 相对于起始地址的偏移量 RVA2.
步骤3:在区块表中获取该区块在文件中所处的偏移地址(根据IMAGE_SECTION_HEADER 中的PointerToRawData 字段), 将这个偏移值加上步骤2得到的 RVA2 值,就得到了真正的文件偏移地址。
查询代码用了C++和Python两种语言实现,由于我习惯写很多注释,就不去分块进行代码讲解了,代码如下:
C++实现代码:
Python利用pefile模块查询导出函数,是不是很懵逼?就两行代码?
因为示例DLL序号表与函数入口地址表本身就是0、1、2、3对应,附件多给几个DLL进行对比。以下是附件DLL的说明与dumpbin.exe结果对比
请留意DLL序号表对应关系:
1、《黑客免杀攻防》-7.4
2、[原创]手查PE导出表
http://bbs.pediy.com/thread-205989.htm
3、Portable Executable
https://en.wikipedia.org/wiki/Portable_Executable
4、PE文件的相对虚拟地址(RVA)和文件偏移地址(FOA)的转换
http://blog.csdn.net/zhao0811112157/article/details/42192881
5、导出表一课
http://edu.csdn.net/course/detail/3002/49415?auto_start=1
6、[原创]解析PE结构之-----导出表
http://bbs.pediy.com/thread-122632.htm
7、[原创]PE学习之手工解析导出表
http://bbs.pediy.com/thread-217229.htm
8、IMAGE_DATA_DIRECTORY 数据目录详解
http://www.nohacks.cn/post-58.html
9、Windows Pe 第三章 PE头文件-EX-相关编程-2(RVA_FOA转换)
http://blog.csdn.net/u013761036/article/details/52751721
10、the-export-directory
http://resources.infosecinstitute.com/the-export-directory/
11、PE文件结构详解(三)PE导出表
http://blog.csdn.net/evileagle/article/details/12176797
12、WindowsPE 第五章 导出表
http://blog.csdn.net/u013761036/article/details/53241515
13、小甲鱼PE详解之区块描述、对齐值以及RVA详解
http://blog.csdn.net/hk_5788/article/details/48225007
14、15PB学习参考资料
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)