输出表结构解析

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

预备知识

一、相关实验

本实验要求您已经认真学习和完成了《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的解析结果进行对比,如下图所示:
在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值