预备知识
一、相关实验
本实验要求您已经认真学习和完成了《IMAGE_DOS_HEADER解析》、《PE头之IMAGE_FILE_HEADER解析》、《PE头之IMAGE_OPTIONAL_HEADER解析》、《节表头解析以及RVA与文件偏移地址的转换》。
本实验相关的基础知识都分散在前面几个实验中,所以如果你对其中有些概念还不是很熟悉的话,建议去回顾一下前面的几个实验。
二、输出表简介
DLL文件在将函数暴露给其他模块调用时,需要将相关的信息存放到输出表中。例如User32.dll的输出表中存在MessageBoxA,表明其他程序可以调用User32.dll的MessageBoxA函数。回顾实验《输入表结构解析》中提到的INT和IAT的区别,提到IAT会被PE装载器填充为函数的实际地址,这个操作就是根据对应DLL的输出表的解析来实现的。
EXE文件一般不存在输出表,而大部分的DLL文件中都存在输出表,当然这也不是绝对的:EXE同样存在有输出表的情况,而一些DLL文件也不存在输出表。
实验目的
1)了解PE文件的输出表结构;
2)手工解析PE文件的输出表;
3)编程实现PE文件输出表的解析。
实验环境

服务器:Windows XP,IP地址:随机分配
辅助工具:C32Asm、LordPE、Python
实验步骤一
输出表结构分析。
本实验将介绍数据目录表的另一个成员——输出表。
输出表中的信息包含了函数的输出名称、输出序号等相关信息。数据目录表的第一个成员指向输出表,输出表是一个类型为IMAGE_EXPORT_DIRECTORY(简称IED)的结构,该结构体在WinNT.h头文件中的定义如下所示:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
关于IED结构体中各个成员的含义介绍如下:
1.Characteristics,表示输出属性,这个值当前没有意义,总是设置为0;
2.TimeDateStamp,输出表创建的时间(格林威治标准时间);
3.MajorVersion,输出表的主版本号,这个值当前没有意义,总是设置为0;
4.MinorVersion,输出表的次版本号,这个值当前没有意义,总是设置为0;
5.Name,RVA,模块的真实名字,指向一个ASCII字符串,这个字符串是与这些输出函数关联的DLL的名字(如User32.dll);
6.Base,输出表的起始序数值;
7.NumberOfFunctions,输出函数的个数;
8.NumberOfNames,以名字输出的函数的个数;
9.AddressOfFunctions,RVA,指向包含输出函数地址的RVA数组;
10.AddressOfNames,RVA,指向包含以名字方式输出的函数的ASCII字符串名字的RVA数组;
11.AddressOfNameOrdinals,RVA,指向每个以名字方式输出的函数对应在AddressOfFunctions指向的数组中的索引。
输出表的结构如下图所示:

实验步骤二
手工解析输出表结构。
因为大部分EXE都是没有输出表的(C:\PE\notepad.exe就没有输出表),因此需要选取一个DLL来进行分析,这里我们选用Kernel32.dll。
使用LordPE的PE编辑器打开C:\PE\ExportTable\Kernel32.dll,在界面右侧点击“目录”查看数据目录表,数据目录表的第一项指向输出表,可以看到输出表的RVA为0x0000262C,如下图所示:

关闭“目录”对话框,点击PE编辑器右侧的“位置计算器”,将RVA值0x0000262C转换为文件偏移值后得到0x00001A2C,如下图所示:

关闭LordPE,使用C32Asm打开C:\PE\ExportTable\Kernel32.dll,按下快捷键Ctrl+G打开跳转对话框,输入文件偏移地址0x00001A2C来到输出表的位置;前面介绍了输出表是一个类型为IMAGE_EXPORT_DIRECTORY的结构,其大小为40字节,这里取40字节的数据进行分析,如下图所示:

将上面分析得到的输出表数据与IMAGE_EXPORT_DIRECTORY结构体的成员进行一一对应,如下所示:
struct IMAGE_EXPORT_DIRECTORY {
Characteristics = 00000000 (00 00 00 00)
TimeDateStamp = 48025BE1 (E1 5B 02 48)
MajorVersion = 0000 (00 00)
MinorVersion = 0000 (00 00)
Name = 00004B8E (8E 4B 00 00)
Base = 00000001 (01 00 00 00)
NumberOfFunctions = 000003B9 (B9 03 00 00)
NumberOfNames = 000003B9 (B9 03 00 00)
AddressOfFunctions = 00002654 (54 26 00 00)
AddressOfNames = 00003538 (38 35 00 00)
AddressOfNameOrdinals = 0000441C (1C 44 00 00)
}
其中Name的RVA值0x00004B8E,使用LordPE将其转换为文件偏移值为0x00003F8E,在C32Asm中按下Ctrl+G快捷键跳转到0x00003F8E,可以看到这里存放着一个字符串KERNEL32.dll,这就是DLL的原始名字(不管如何更改DLL的实际文件名,在输出表中总能够看到其原始的名字),如下图所示:

NumberOfFunctions和NumberOfNames的值都是0x3B9,表示输出了953个函数,且函数全部以名称的方式输出。
AddressOfNames的RVA值0x00003538,使用LordPE的位置计算器将其转换为文件偏移值为0x00002938。根据前面的介绍,知道AddressOfNames指向一个RVA数组,在C32Asm中查看一下这个数组的数据(按下Ctrl+G跳转到地址0x00002938),如下图所示:

从上图中可以看出,该数组的第一个DWORD值为0x00004B9B,这也是一个RVA值。使用LordPE的位置计算器将0x00004B9B转换为文件偏移值为0x00003F9B。在C32Asm跳转到0x00003F9B,得到字符串为ActivateActCtx,表示第一个以名称方式输出的函数的名字为ActivateActCtx,如下图所示:

AddressOfNameOrdinals的RVA值为0x0000441C,使用LordPE的位置计算器将其转换为文件偏移值为0x0000381C,其指向一个WORD类型的数组,表示AddressOfNames中的函数在AddressOfFunctions所对应的下标,在C32Asm中可以看到0x0000381C处第一个WORD值为0000,即下标索引为0,如下图所示:

AddressOfFunctions的RVA值为0x00002654,使用LordPE的位置计算器将其转换为文件偏移地址值为0x00001A54,其指向一个RVA数组。通过之前的分析,知道ActivateActCtx在该数组中的索引为0,而0x00001A54处第一个DWORD值为0000A6D4,表示ActivateActCtx函数对应的函数地址的RVA为A6D4,如下图所示:

使用LordPE的PE编辑器打开C:\PE\ExportTable\Kernel32.dll并查看输出表,可以看到手工解析输出表的结果是正确的,如下图所示(与手工分析结果一致):

实验步骤三
编程实现输出表结构的解析。
在实验步骤二中已经实现了手工解析输出表,同时在实验《输入表结构解析》已经实现了许多基础的代码,可以在之前的代码上,增加输出表的解析代码。
本实验目标是解析给定PE文件的输出表结构,包括:
1.输出表所在的RVA地址;
2.输出表的大小;
3.DLL文件的原始名称;
4.输出表Base的值;
5.总的输出函数的个数;
6.以名字方式输出的函数的个数;
7.输出函数的名字以及函数对应的RVA值。
修改后的代码位于C:\PE\ExportTable\PeParser.py,新增的用于解析输出表的代码如下:
def parse_export_table(self):
"""解析PE文件的输出表"""
print "\n[*] Parsing Export Table..."
# 获取输出表在数据目录表中对应的RVA和SIZE值
rva = self.get_dword(self.data_dirs[0:4])
size = self.get_dword(self.data_dirs[4:8])
print "[*] RVA: %08X\tSIZE: %08X" % (rva, size)
# 如果RVA为0或者SIZE为0,表明PE文件没有输出表
if rva == 0 or size == 0:
print "No export table data exists."
return
# 将输出表的RVA转换为文件偏移地址
ied_ptr = self.rva_to_offset(rva)
# IMAGE_EXPORT_DIRECTORY的大小为40字节
ied_size = 40
# 读取IMAGE_EXPORT_DIRECTORY结构的内容
ied = self.data[ied_ptr:ied_ptr+ied_size]
# 获取DLL名字的RVA值
name = self.get_dword(ied[12:16])
# 将DLL名字的RVA值转换为Offset并读取出这个字符串
name = self.get_string(self.rva_to_offset(name))
# 打印DLL的名字
print "\tDll Name: %s" % name
# 解析并打印Base
base = self.get_dword(ied[16:20])
print "\tBase: %08X" % base
# 解析并打印NumberOfFunctions(函数的总的个数)
func_num = self.get_dword(ied[20:24])
print "\tNumber of Functions: %08X" % func_num
# 解析并打印NumberOfNames(以名字方式输出的函数的个数)
name_num = self.get_dword(ied[24:28])
print "\tNumber of Names: %08X" % name_num
# 解析AddressOfFunctions
fun_rva = self.get_dword(ied[28:32])
fun_ptr = self.rva_to_offset(fun_rva)
# 解析AddressOfNames
name_rva = self.get_dword(ied[32:36])
name_ptr = self.rva_to_offset(name_rva)
# 解析AddressOfNameOrdinals
ord_rva = self.get_dword(ied[36:40])
ord_ptr = self.rva_to_offset(ord_rva)
# 循环解析PE文件输出表中输出的函数
for i in xrange(0, name_num):
# 解析输出函数的名字
rva = self.get_dword(self.data[name_ptr:name_ptr+4])
ptr = self.rva_to_offset(rva)
name = self.get_string(ptr)
# 解析输出函数的RVA
index = self.get_word(self.data[ord_ptr:ord_ptr+2])
ptr = fun_ptr + index*4
rva = self.get_dword(self.data[ptr:ptr+4])
# 打印解析结果
print "\t%08X %s" % (rva, name)
# 迭代到下一个函数
name_ptr += 4
ord_ptr += 2
注意上面的代码只解析了以名字输出的函数。可以对比LordPE的解析结果进行验证:打开CMD,切换到目录C:\PE\ExportTable,输入PeParser.py Kernel32.dll即可输出DLL的输出表信息(包括之前的输入表信息),因为Kernel32.dll的输出信息过多,可以将其重定向到txt文本文件中,如PeParser.py Kernel32.dll > tmp.txt,如下图所示:

将Python脚本的解析结果与LordPE的解析结果进行对比,如下图所示:


本文详细阐述了如何通过手动和编程方式理解PE文件中的输出表结构,包括输出表的组成、分析步骤以及如何使用C32Asm和Python工具进行实践。实验覆盖了输出表的查找、数据解读与功能实现,有助于PE开发者和安全研究员掌握关键信息提取技术。
1496

被折叠的 条评论
为什么被折叠?



