2

[原创]由浅入深PE基础学习-菜鸟手动查询导出表、相对虚拟地址(RVA)与文件偏移地址转换(FOA)

lipss 2017-10-12 16:07 960

0 前言

此篇文章想写如何通过工具手查导出表、PE文件代码编程过程中的原理。文笔不是很好,内容也是查阅了很多的资料后整合出来的。希望借此加深对PE文件格式的理解,也希望可以对看雪论坛有所贡献。因为了解PE文件格式知识点对于逆向破解还是病毒分析都是很重要的,且基于对PE文件格式的深入理解还可以延伸出更多非常有意思的攻防思维。另外看雪能支持Markdown真是太好了了!发主题更加方便了

1 导出表查询工具

  • 1 ) dumpbin

VS自带的工具,有很多的功能。但用来查询程序的导出表也非常方便,使用例子如下:

dumpbin.exe /EXPORTS D:\PEDemo.dll

  • 2 ) DLL Export Viewer (DLL导出表查看工具)

一款免费的dll查看工具,可以帮助查看DLL链接库文件中的输出函数,COM类型库及相应的偏移地址。

  • 3)010 Editor

十六进制编辑器010 Editor,有大量格式的解析模板,用EXE模板来解析二进制文件,辅助我们读懂和编辑。

  • 4)LordPE

LordPE是查看PE格式文件信息的工具,并且可以修改相关信息。里面有个位置计算器的功能可以用于计算相对虚拟地址(RVA)转换文件偏移地址(FOA)。

2 Windows导出表相关的结构

2.1 导出表所处位置

在Visual Studio里有一个名为WINNT.H的头文件,里面定义了Windows系统里的PE内部结构。

 

PE结构中有一个NT头(IMAGE_NT_HEADERS NtHeader),NT头里包含了扩展头(IMAGE_OPTIONAL_HEADER),扩展头中包含数据目录表(IMAGE_DATA_DIRECTORY_ARRAY)。

 

扩展头定义:

    typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    ...省略....

    //
    // NT additional fields.
    //

    ....省略...

         IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];  //数据目录表
    } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

IMAGE_NUMBEROF_DIRECTORY_ENTRIES是个宏定义,值是0x16。

 

宏定义:

    #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16

数据目录是一个有16个(WINNT.H中定义为IMAGE_NUMBEROF_DIRECTORY_ENTRIES)元素的结构数组。每个数组元素所指定的内容已经被预先定义好了。

 

WINNT.H文件中的这些IMAGE_DIRECTORY_ENTRY_xxx定义就是数据目录的索引(从0到15)。导出表相对虚拟地址(RVA)就在数据目录表中的第0个数组里。

 

数据目录表有两个结构体成员分别存有数据的相对虚拟地址和数据的大小,定义如下,:

    typedef struct _IMAGE_DATA_DIRECTORY {

        DWORD   VirtualAddress; // 数据的相对虚拟地址(RVA)

        DWORD   Size;   // 数据的大小

    } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

下表描述了每个IMAGE_DIRECTORY_ENTRY_xxx值每个数组的意义。

 

image

 

图1 IMAGE_OPTIONAL_HEADER中数据目录表结构体数组含义

2.2 导出表结构

下面是导出表的数据结构定义说明:

    typedef struct _IMAGE_EXPORT_DIRECTORY {

       DWORD Characteristics;    // 1)  保留,恒为0x00000000

       DWORD TimeDateStamp;      // 2)  时间戳,导出表创建的时间(GMT时间)

       WORD  MajorVersion;       // 3)  主版本号:导出表的主版本号

       WORD  MinorVersion;       // 4)  子版本号:导出表的子版本号

       DWORD Name;               // 5)  指向模块名称的RVA,指向模块名(导出表所在模块的名称)的ASCII字符的RVA

       DWORD Base;               // 6)  导出表用于输出导出函数序号值的基数: 导出函数序号 = 函数入口地址数组下标索引值 + 基数

       DWORD NumberOfFunctions;  // 7)  导出函数入口地址表的成员个数

       DWORD NumberOfNames;      // 8)  导出函数名称表中的成员个数

       DWORD AddressOfFunctions; // 9)  函数入口地址表的相对虚拟地址(RVA),每一个非0的项都对应一个被导出的函数名称或导出序号(序号+基数等于导出函数序号)

       DWORD AddressOfNames;     // 10) 函数名称表的相对虚拟地址(RVA),存储着指向导出函数名称的ASCII字符的RVA

       DWORD AddressOfNameOrdinals; // 11) 存储着函数入口地址表的数组下标索引值(序号表),跟导出函数名称表的成员顺序对应

    } IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
  1. (没用)Characteristics; 保留,恒为0x00000000
  2. (没用)TimeDateStamp; 时间戳,导出表创建的时间(GMT时间)
  3. (没用)MajorVersion; 主版本号:导出表的主版本号
  4. (没用)MinorVersion; 子版本号:导出表的子版本号
  5. (有用)Name; 指向模块名称的RVA,指向模块名(导出表所在模块的名称)的ASCII字符的RVA
  6. (有用)Base; 导出表用于输出导出函数序号值的基数:函数入口地址数组下标索引值 = 导出函数序号-基数
  7. (有用)NumberOfFunctions; 导出函数入口地址表的成员个数
  8. (有用)NumberOfNames; 以函数名称导出的成员个数
  9. (有用)AddressOfFunctions; 函数入口地址表的相对虚拟地址(RVA),每一个非0的项都对应一个被导出的函数名称或导出序号(序号+基数等于导出函数序号)
  10. (有用)AddressOfNames; 函数名称表的相对虚拟地址(RVA),存储着指向导出函数名称的ASCII字符的RVA
  11. (有用)AddressOfNameOrdinals; 存储着函数入口地址表的数组下标索引值(序号表),跟导出函数名称表的成员顺序对应

3 手动操作

本文旨在用于科普,大牛们可能要见笑了。手动操作部分借助工具010 Editor、LordPE先搞清楚PE结构的内容。涉及到相对虚拟地址(RVA)转换文件偏移地址(FOA)的地方都用LordPE自带功能转换。原理与代码后续贴出

3.1 010 Editor + 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模板查看的顺序如下:

    1)struct IMAGE_NT_HEADERS nt_headers[NT头]

    2)struct IMAGE_OPTIONAL_HEADER32 OptionalHeader[可选头]

    3)struct IMAGE_DATA_DIRECTORIES DataDirectory[目录表]

    4)struct IMAGE_DATA_DIRECTORY Export[导出表]

下图中内容里对应的是导出表结构体中的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指向位置为导出表数据,导出表结构体标注

 

其中比较有用的是

    1) Name;  指向模块名称的RVA,指向模块名(导出表所在模块的名称)的ASCII字符的RVA

    2) Base;  导出表用于输出导出函数序号值的基数:函数入口地址数组下标索引值 = 导出函数序号-基数

    3) NumberOfFunctions; 导出函数入口地址表的成员个数

    4) NumberOfNames; 以函数名称导出的函数个数

    5) AddressOfFunctions;函数入口地址表的相对虚拟地址(RVA),每一个非0的项都对应一个被导出的函数名称或导出序号(序号+基数等于导出函数序号)

    6) AddressOfNames;函数名称表的相对虚拟地址(RVA),存储着指向导出函数名称的ASCII字符的RVA

    7) AddressOfNameOrdinals; 存储着函数入口地址表的数组下标索引值(序号表),跟导出函数名称表的成员顺序对应

导出表数据结构(IMAGE_EXPORT_DIRECTORY)的相对虚拟地址为:

    IMAGE_EXPORT_DIRECTORY.Name  == 0x00007F20

    IMAGE_EXPORT_DIRECTORY.Base  == 0x00000001

    IMAGE_EXPORT_DIRECTORY.NumberOfFunctions == 0x00000004

    IMAGE_EXPORT_DIRECTORY.NumberOfNames == 0x00000004

    IMAGE_EXPORT_DIRECTORY.AddressOfFunctions== 0x00007EF8

    IMAGE_EXPORT_DIRECTORY.AddressOfNames== 0x00007F08

    IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals == 0x00007F18

通过以上数据可以知道导出表用于输出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 //导出函数名称表位置对应,存储着指向函数入口地址序号表的索引

3.1.1 IMAGE_EXPORT_DIRECTORY.Name

0x00007F20 转换为 00006F20,指向导出表Name字段,内容存储的是模块函数名称的ASCII字符,测试用的DLL名称如下:

PEDemo.dll

 

 

图7 指向导出文件名的字符串

3.1.2 IMAGE_EXPORT_DIRECTORY.AddressOfFunctions

0x00007EF8 转换成文件偏移地址为 00006EF8,这个地址对应的结构体成员是AddressOfFunctions,AddressOfFunctions指向的是所有导出函数地址表的RVA地址,有多少个地址根据NumberOfFunctions的值得出。

 

再使用LordPE【位置计算器】功能通过RVA转换为FOA,查看所有导出函数地址表,得到以下的导出函数地址:

000010E6  --> 序号为[0]

00001087  --> 序号为[1]

0000108C  --> 序号为[2]

00009138  --> 序号为[3]

 

图8 IMAGE_EXPORT_DIRECTORY.AddressOfFunctions

3.1.3 IMAGE_EXPORT_DIRECTORY.AddressOfNames

0x00007F08 转换成文件偏移地址为 00006F08,这个地址对应的结构体成员是AddressOfNames,这个成员表存储的是导出函数名字RVA。

 

因为前面根据NumberOfFunctions的值已经知道这个DLL里有4个导出函数,然后利用010Editor定位到文件偏移处00006F08的位置。里面存储的是4个导出函数名称对应的RVA地址。根据各个RVA转换成FOA。得到地址如下:

 

这里需要注意结构体里存储的是存放导出函数名称的RVA,要得到存储的导出函数名称的ASCII还要多转换一层RVA。

 

 

图9 存储着导出函数名称的RVA

 

RVA转换FOA如下:

    0x00007F2B 转换FOA为 00006F2B 

    0x00007F37 转换FOA为 00006F37 

    0x00007F44 转换FOA为 00006F44

    0x00007F51 转换FOA为 00006F51

使用010Editor查看00006F2B 、00006F37 、00006F44、00006F51可以看到其实每个函数的名称存放位置是连续的,使用00进行了隔断。

 

指向导出函数名称表RVA,AddressOfNames字段的值如下:

    fnPEDemoFun   

    fnPEDemoFunA 

    fnPEDemoFunB 

    nPEDemo

 

图10 指向导出表文件名的字符串

 

然后导出函数名称表存储的其实是指向真实函数字符串的RVA地址,转换前的对应关系如下图。

 

 

图11 导出函数名称地址表

3.1.4 IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals

0x00007F18 转换成文件偏移地址为 00006F18,这个地址对应的结构体成员是IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals。

 

 

图12 序号表

 

这张表与导出函数名称地址表的顺序对应,存储着指向函数入口地址表的索引值(序号表,起始值从0开始)。编程的时候为DWORD类型,但却是WORD类型,因为只有2字节。对应的导出函数名称关系如下:

    fnPEDemoFun   ---> 00 00

    fnPEDemoFunA  ---> 01 00

    fnPEDemoFunB  ---> 02 00

    nPEDemo       ---> 03 00

AddressOfFunctions为函数入口地址,AddressOfNames为导出函数名,AddressOfNameOrdinals存储着函数入口地址表的数组下标索引值(序号表),用三张表的数据进行对比,结构就更清晰了,关系表如下:

 

 

图13 序号对应关系

 

根据导出函数序号 = 函数入口地址序号 + 基数,已知Base基数数为1,计算如下:

000010E6  序号[0] + 基数[1] = 导出序号为[1]

00001087  序号[1] + 基数[1] = 导出序号为[2]

0000108C  序号[2] + 基数[1] = 导出序号为[3]

00009138  序号[3] + 基数[1] = 导出序号为[4]

函数名称与函数入口地址表数组下标索引序号对应,计算如下:

fnPEDemoFun   ---> 00 00 --->对应函数入口地址[000010E6]

fnPEDemoFunA  ---> 01 00 --->对应函数入口地址[00001087]

fnPEDemoFunB  ---> 02 00 --->对应函数入口地址[0000108C]

nPEDemo       ---> 03 00 --->对应函数入口地址[00009138]

3.1.5 小结

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关系图

3.2 PE工具查看

印证自己查询的是不是正确,可以通过VS自带的dumpbin工具或是 DLL Export Viewer这类工具查看。

dumpbin.exe查看如下:

C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin>dumpbin.exe /EXPORTS d:\PEDemo.dll
Microsoft (R) COFF/PE Dumper Version 14.00.24210.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file d:\PEDemo.dll

File Type: DLL

  Section contains the following exports for PEDemo.dll

    00000000 characteristics
    59948A34 time date stamp Thu Aug 17 02:08:52 2017
        0.00 version
           1 ordinal base
           4 number of functions
           4 number of names

    ordinal hint RVA      name

          1    0 000010E6 fnPEDemoFun = @ILT+225(_fnPEDemoFun)
          2    1 00001087 fnPEDemoFunA = @ILT+130(_fnPEDemoFunA)
          3    2 0000108C fnPEDemoFunB = @ILT+135(_fnPEDemoFunB)
          4    3 00009138 nPEDemo = _nPEDemo

  Summary

        1000 .00cfg
        1000 .data
        1000 .gfids
        1000 .idata
        3000 .rdata
        1000 .reloc
        1000 .rsrc
        5000 .text

DLL Export Viewer (DLL导出表查看工具)


图15 DLL导出表查看工具结果

4 相对虚拟地址(RVA)转文件偏移(FOA)

前面的章节使用工具进行手动查看导出表,而这一节则是查阅了大量网上的文章然后汇总而成的笔记。希望可以给大家带来一些帮助!

 

术语:

  • RVA:RVA 是相对虚拟地址(Relative Virtual Address)的缩写,它是文件映射到内存中的“相对地址”。

  • FOA:FOA是文件偏移地址(File Offest Address)的缩写,它是文件在磁盘上存放时相对文件开头的偏移地址。

4.1 转换公式

LordPE的位置偏移功能的确很方便,但始终还是要熟悉原理,才能写出操作PE的代码。

 

有一条公式可以帮助我们很方便的计算出文件偏移的位置。

    文件偏移(磁盘文件的位置 FOA)=相对虚拟地址(任意RVA)-该区段相对虚拟地址(RVA)+该区段的文件偏移(offset)

要转换的相对虚拟地址会落在一个区段中是因为每个偏移,不管是在文件中,还是在内存中,它们距离区段开始位置的距离总是相等的。

 

该区段相对虚拟地址(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)结构,如下所示:

    typedef struct _IMAGE_SECTION_HEADER {

        BYTEName[IMAGE_SIZEOF_SHORT_NAME];  // 1)区段名

        union {

            DWORD   PhysicalAddress;

            DWORD   VirtualSize;

        } Misc;                            // 2)区段大小

        DWORD   VirtualAddress;            // 3)区段的RVA地址

        DWORD   SizeOfRawData;             // 4)文件中的区段对齐大小

        DWORD   PointerToRawData;          // 5)区段在文件中的偏移

        DWORD   PointerToRelocations;      // 6)重定位的偏移(用于OBJ文件)

        DWORD   PointerToLinenumbers;      // 7)行号表的偏移(用于调试)

        WORDNumberOfRelocations;           // 8)重定位表项数量(用于OBJ文件)

        WORDNumberOfLinenumbers;           // 9)行号表项数量

        DWORD   Characteristics;           // 10)区段的属性

    } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

LordPE中的显示关系

 

 

图17 LordPE中的显示关系

    Voffset   == IMAGE_SECTION_HEADER.VirtualAddress   //区段的RVA地址

    VSize     == IMAGE_SECTION_HEADER.Misc             //区段的物理地址大小

    Roffset   == IMAGE_SECTION_HEADER.PointerToRawData //区段在文件中的偏移

    RSize     == IMAGE_SECTION_HEADER.SizeOfRawData    //文件中的区段对齐大小

代码例子

 

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 值,就得到了真正的文件偏移地址。

    /////////////////////////////////////////
    //          RVA转FOA函数               //
    /////////////////////////////////////////
    DWORD   RvaToOffset(const void* pFileData, DWORD dwRva)
    {

        // 获取DOS头
        IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER*)pFileData;

        // 获取NT头
        IMAGE_NT_HEADERS *pNtHeader = (IMAGE_NT_HEADERS*)((DWORD)pDosHeader + pDosHeader->e_lfanew);

        // 得到区段个数
        DWORD   dwSectionNumber = pNtHeader->FileHeader.NumberOfSections;

        // 得到区段
        IMAGE_SECTION_HEADER* pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);

        // 遍历区段表,找到RVA所在的区段
        /*
        * 每个偏移,不管是在文件中,还是在内存中,它们距离区段开始位置的距离
        * 总是相等的。
        * 而且,区段表中,保存着两个开始偏移:
        *  1. 文件中的开始偏移
        *  2. 内存中的开始偏移
        * 具体过程:
        *  找到RVA所在区段, 然后计算出这个RVA到区段在内存中的开始位置的距离。
        *  用这个距离加上区段在文件中的开始位置就得到文件偏移了
        */

        for (int i = 0; i < dwSectionNumber; ++i) {

            // 区段的起始相对虚拟地址RVA
            DWORD dwSectionBeginRva = pSectionHeader[i].VirtualAddress

            // 区块的结束相对虚拟地址RVA = 区段的RVA地址 + 文件中的区段对齐大小
            DWORD dwSectionEndRva = pSectionHeader[i].VirtualAddress + pSectionHeader[i].SizeOfRawData;


            // 判断RVA是否在当前的区段中
            if (dwRva >= dwSectionBeginRva
                && dwRva <= dwSectionEndRva) {

                // 计算出RVA对应的文件偏移
                // 公式:文件偏移  =  RVA - 区段的起始相对虚拟地址RVA + 区段的起始文件偏移FOA
                // 1. 要转换的RVA - 区段的起始相对虚拟地址RVA
                DWORD dwTemp = dwRva - pSectionHeader[i].VirtualAddress;
                // 2. 加上区段的起始文件偏移FOA,dwOffset为FOA
                DWORD dwOffset = dwTemp + pSectionHeader[i].PointerToRawData;
                // 3. 得到文件偏移FOA
                return dwOffset;
            }
        }

        return -1;
    }

4.2 读取PE导出表函数

查询代码用了C++和Python两种语言实现,由于我习惯写很多注释,就不去分块进行代码讲解了,代码如下:

 

C++实现代码:

    // 2017_GetPE_IMAGE_EXPORT_DIRECTORY.cpp : 定义控制台应用程序的入口点。
    //

    #include "stdafx.h"
    #include <windows.h>


    //获取PE文件导出表
    //编程思路:
    //1、打开文件,获取文件句柄
    //2、读取文件到内存缓冲区
    //3、获取DOS头结构
    //4、获取NT头结构
    //5、获取扩展头结构
    //6、获取数据目录表
    //7、获取导出表
    //8、封装相对虚拟地址转换文件偏移地址函数



    /////////////////////////////////////////
    //          RVA转FOA函数               //
    /////////////////////////////////////////
    DWORD   RvaToOffset(const void* pFileData, DWORD dwRva)
    {

        // 获取DOS头
        IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER*)pFileData;

        // 获取NT头
        IMAGE_NT_HEADERS *pNtHeader = (IMAGE_NT_HEADERS*)((DWORD)pDosHeader + pDosHeader->e_lfanew);

        // 得到区段个数
        DWORD   dwSectionNumber = pNtHeader->FileHeader.NumberOfSections;

        // 得到区段
        IMAGE_SECTION_HEADER* pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);

        // 遍历区段表,找到RVA所在的区段
        /*
        * 每个偏移,不管是在文件中,还是在内存中,它们距离区段开始位置的距离
        * 总是相等的。
        * 而且,区段表中,保存着两个开始偏移:
        *  1. 文件中的开始偏移
        *  2. 内存中的开始偏移
        * 具体过程:
        *  找到RVA所在区段, 然后计算出这个RVA到区段在内存中的开始位置的距离。
        *  用这个距离加上区段在文件中的开始位置就得到文件偏移了
        */

        for (int i = 0; i < dwSectionNumber; ++i) {

            // 区段的起始相对虚拟地址RVA
            DWORD dwSectionBeginRva = pSectionHeader[i].VirtualAddress;

            // 区块的结束相对虚拟地址RVA = 区段的RVA地址 + 文件中的区段对齐大小
            DWORD dwSectionEndRva = pSectionHeader[i].VirtualAddress + pSectionHeader[i].SizeOfRawData;


            // 判断RVA是否在当前的区段中
            if (dwRva >= dwSectionBeginRva
                && dwRva <= dwSectionEndRva) {

                // 计算出RVA对应的文件偏移
                // 公式:文件偏移  =  RVA - 区段的起始相对虚拟地址RVA + 区段的起始文件偏移FOA
                // 1. 要转换的RVA - 区段的起始相对虚拟地址RVA
                DWORD dwTemp = dwRva - pSectionHeader[i].VirtualAddress;
                // 2. 加上区段的起始文件偏移FOA,dwOffset为FOA
                DWORD dwOffset = dwTemp + pSectionHeader[i].PointerToRawData;
                // 3. 得到文件偏移FOA
                return dwOffset;
            }
        }

        return -1;
    }


    int main()
    {
        // 要解析的PE文件
        //char dllPath[MAX_PATH] = "D:\\PEDemo.dll";
        //char dllPath[MAX_PATH] = "D:\\PEDemofnPEDemoFunB@2fnPEDemoFunA@3.dll";
        char dllPath[MAX_PATH] = "D:\\PEDemofnPEDemoFunB@2NONAME.dll";
        //char dllPath[MAX_PATH] = "D:\\PEDemofnPEDemoFunB@2private.dll";


        // 读取PE文件
        HANDLE hFile = INVALID_HANDLE_VALUE;
        hFile = CreateFileA(dllPath,   //PE文件路径
            GENERIC_READ,              //文件访问的权限,通常用GENERIC_READ, GENERIC_WRITE
            FILE_SHARE_READ,           //打开文件的操作方式
            NULL,                      //安全描述符
            OPEN_EXISTING,             //对存在的文件采用的操作
            FILE_ATTRIBUTE_NORMAL,     //文件或设备属性和标志
            NULL);                     //为创建的文件提供文件属性和扩展属性

        if (hFile == INVALID_HANDLE_VALUE)
        {
            printf("文件不存在,或者被占用\n");
            return 0;
        }

        // 获取文件大小
        DWORD dwFileSize = GetFileSize(hFile, NULL);
        // 申请缓冲区保存文件内容
        BYTE* pFileData = new BYTE[dwFileSize];
        DWORD dwRead = 0;
        // 将文件读取到缓冲区
        ReadFile(hFile, pFileData, dwFileSize, &dwRead, NULL);

        // 使用DOS头结构指向缓冲区
        IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)pFileData;

        // 获取IMAGE_NT_HEADERS,NT头
        IMAGE_NT_HEADERS* pNtHeader = (IMAGE_NT_HEADERS*)(pDosHeader->e_lfanew + (DWORD)pFileData);

        // 获取IMAGE_OPTIONAL_HEADER32,扩展头
        IMAGE_OPTIONAL_HEADER* pOptionHeader = (IMAGE_OPTIONAL_HEADER*)&pNtHeader->OptionalHeader;

        // 获取IMAGE_DATA_DIRECTORIES,数据目录表 
        IMAGE_DATA_DIRECTORY* pDataDirectory = pOptionHeader->DataDirectory;

        // 获取IMAGE_DATA_DIRECTORY.Export,数据目录表.导出表
        DWORD dwExportTableRva = pDataDirectory[0].VirtualAddress;

        // RVA转FOA,IMAGE_DATA_DIRECTORY.Export转文件偏移
        DWORD dwExportTableOffset = RvaToOffset(pFileData, dwExportTableRva);

        ////////////////////////////////////////////////////////////
        // 获取导出表IMAGE_EXPORT_DIRECTORY结构体文件偏移位置,以下是结构体含义:
        //IMAGE_EXPORT_DIRECTORY{
        //    DWORD   Characteristics;        // 1)  保留,恒为0x00000000
        //    DWORD   TimeDateStamp;          // 2)  时间戳,导出表创建的时间(GMT时间)
        //    WORD MajorVersion;              // 3)  主版本号:导出表的主版本号
        //    WORDMinorVersion;               // 4)  子版本号:导出表的子版本号
        //    DWORD   Name;                   // 5)  (有用)指向模块名称的RVA,指向模块名(导出表所在模块的名称)的ASCII字符的RVA
        //    DWORD   Base;                   // 6)  (有用)导出表用于输出API函数索引值的基数(函数索引值=导出函数索引值-基数)
        //    DWORD   NumberOfFunctions;      // 7)  (有用)EAT 导出地址表中的成员个数
        //    DWORD   NumberOfNames;          // 8)  (有用)ENT 导出名称表中的成员个数
        //    DWORD   AddressOfFunctions;     // 9)  (有用)EAT 函数地址表的相对虚拟地址(RVA),每一个非0的项都对应一个被导出的函数名称或序号
        //    DWORD   AddressOfNames;         // 10) (有用)ENT 函数名称表的相对虚拟地址(RVA),每一个非0的项都对应一个被导出的函数地址或序号
        //    DWORD  AddressOfNameOrdinals;   // 11) (有用)指向导出函数序列号的数组,导出序号表的相对虚拟地址(RVA)
        //} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
        ////////////////////////////////////////////////////////////
        IMAGE_EXPORT_DIRECTORY* pExportTable = /*换行*/
            (IMAGE_EXPORT_DIRECTORY*)(dwExportTableOffset + (DWORD)pFileData);

        //输出会用到的值
        printf("IMAGE_EXPORT_DIRECTORY.Name:0x%08X\n", pExportTable->Name);                            // 模块名称
        printf("IMAGE_EXPORT_DIRECTORY.Base:0x%08X\n", pExportTable->Base);                            // 函数索引值的基数
        printf("IMAGE_EXPORT_DIRECTORY.NumberOfFunctions:0x%08X\n", pExportTable->NumberOfFunctions);  // 导出地址表中的成员个数
        printf("IMAGE_EXPORT_DIRECTORY.NumberOfNames:0x%08X\n", pExportTable->NumberOfNames);          // 导出名称表中的成员个数
        printf("IMAGE_EXPORT_DIRECTORY.AddressOfFunctions:0x%08X\n", pExportTable->AddressOfFunctions);// 函数地址表的相对虚拟地址(RVA)
        printf("IMAGE_EXPORT_DIRECTORY.AddressOfNames:0x%08X\n", pExportTable->AddressOfNames);        // 函数名称表的相对虚拟地址(RVA)
        printf("IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals:0x%08X\n", pExportTable->AddressOfNameOrdinals); // 指向导出函数序列号的数组


        // IMAGE_EXPORT_DIRECTORY.Name转换为FOA,获取指向导出表文件名的字符串
        DWORD dwNameOffset = RvaToOffset(pFileData, pExportTable->Name);

        // IMAGE_EXPORT_DIRECTORY.Base转换为FOA,获取基数
        DWORD dwBaseOffset = RvaToOffset(pFileData, pExportTable->Base);

        // IMAGE_EXPORT_DIRECTORY.AddressOfFunctions转换为FOA,获取导出函数地址表
        DWORD dwAddressOfFunctionsOffset = RvaToOffset(pFileData, pExportTable->AddressOfFunctions);

        // IMAGE_EXPORT_DIRECTORY.AddressOfNames转换为FOA,获取导出函数名称地址表
        DWORD dwAddressOfNamesOffset = RvaToOffset(pFileData, pExportTable->AddressOfNames);

        // IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals转换为FOA,获取导出函数序号表
        DWORD dwAddressOfNameOrdinalsOffset = RvaToOffset(pFileData, pExportTable->AddressOfNameOrdinals);

        //  IMAGE_EXPORT_DIRECTORY.Name 指向导出表Name字段,内容存储的是模块函数名称的ASCII字符
        char* pDllName = (char*)(dwNameOffset + (DWORD)pFileData);
        printf("\nDll_Name: %s\n", pDllName);

        /////////////////////////////////////////
        // 把所有的导出的函数地址打印出来。
        // 并且,如果是以名称导出,则输出该名称
        // 如果是以序号导出,则输出该序号。
        /////////////////////////////////////////

        // IMAGE_EXPORT_DIRECTORY.AddressOfFunctions指向导出函数地址表
        DWORD* pAddressTable = /*换行*/
            (DWORD*)((DWORD)pFileData + dwAddressOfFunctionsOffset);

        // IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals指向导出函数序号表
        // WORD* pOrdinalTable =(WORD*)((DWORD)pFileData + RvaToOffset(pFileData, pExportTable->AddressOfNameOrdinals));
        WORD* pOrdinalTable = (WORD*)((DWORD)pFileData + dwAddressOfNameOrdinalsOffset);

        // 导出函数名称,需要再转换一层RVA,才能得到函数名称所在的位置
        DWORD* pNameTable =(DWORD*)((DWORD)pFileData + dwAddressOfNamesOffset);

        // 判断是以序号导出,还是以函数名导出
        BOOL bIndexIsExist = FALSE;

        //////////////////////////////////////////////////////////////////////////////////////
        /// pExportTable->NumberOfFunctions 对应了IMAGE_EXPORT_DIRECTORY.NumberOfFunctions ///
        /// 导出地址表中的成员个数                                                         ///
        //////////////////////////////////////////////////////////////////////////////////////
        for (int i = 0; i < pExportTable->NumberOfFunctions; ++i) {

            // 打印虚序号、导出函数地址表(RVA)
            printf("虚序号[%d] ", i);
            printf("地址(RVA): %08X", pAddressTable[i]);

            // 判断当前的这个地址是否是以名称方式导出的
            // 判断依据:
            //   序号表保存的是地址表的一个下标,这个下标记录着
            //   地址表中哪个地址是以名称方式导出的。
            //   如果当前的这个下标保存在序号表中,则说明这个地址
            //   是一个名称方式导出,如果这个下标在序号表中不存在,
            //   则说明,这个地址不是一个名称方式导出,而是以序号进行导出
            bIndexIsExist = FALSE;


            // 以导出名称导出的函数个数的数量循环
            int nNameIndex = 0;
            for (; nNameIndex < pExportTable->NumberOfNames; ++nNameIndex) {

                // 判断地址表的下标是否存在于序号表中
                if (i == pOrdinalTable[nNameIndex]) {
                    bIndexIsExist = TRUE;
                    break;
                }
            }

            // 判断如果bIndexIsExist为真就是函数名导出,否则以函数序号导出。
            // 函数名要多转换一层RVA
            if (bIndexIsExist == TRUE) {

                // 得到名称表中的RVA
                DWORD dwNameRva = pNameTable[nNameIndex];

                // 将名称Rva转换成存有真实函数名称的文件偏移
                char* pFunName =
                    (char*)((DWORD)pFileData + RvaToOffset(pFileData, dwNameRva));

                printf(" 函数名:【%s】\t", pFunName);
                // i : 是地址表中的索引号,也就是一个虚序号
                // 真正的序号 = 虚序号 + 序号基数
                printf(" 序号:【%d】 ", i + pExportTable->Base);
            }
            // 当没有导出函数名称,则是以序号进行导出使用
            if (bIndexIsExist == FALSE)
            {

                // 判断地址表当前索引到的袁术是否保存着地址
                if (pAddressTable[i] != 0) {

                    printf(" 函数名:【-】\t");
                    // i : 是地址表中的索引号,也就是一个虚序号
                    // 真正的序号 = 虚序号 + 序号基数
                    printf(" 序号:【%d】", i + pExportTable->Base);
                }
            }

            printf("\n");
        }

        system("pause");
        return 0;
    }

Python利用pefile模块查询导出函数,是不是很懵逼?就两行代码?

    import pefile

    # 通过pefile模块读取PE文件
    pe = pefile.PE('notepad.exe')  

    for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        print hex(pe.OPTIONAL_HEADER.ImageBase + exp.address), exp.name, exp.ordinal

附件说明

因为示例DLL序号表与函数入口地址表本身就是0、1、2、3对应,附件多给几个DLL进行对比。以下是附件DLL的说明与dumpbin.exe结果对比

 

请留意DLL序号表对应关系:

   【PEdemo.dll】

       NumberOfFunctions    0x00000004    unsigned long

       NumberOfNames    0x00000004    unsigned long

       Base    0x00000001    unsigned long


    AddressOfFunctions   AddressOfNames  ->    name             AddressOfNameOrdinal 实际上为(WORD)
       E6 10 00 00        2B 7F 00 00       fnPEDemoFun             00 00
       87 10 00 00        37 7F 00 00       fnPEDemoFunA            01 00
       8C 10 00 00        44 7F 00 00       fnPEDemoFunB            02 00
       38 91 00 00        51 7F 00 00       nPEDemo                 03 00

       dumpbin result:
            0.00 version
              1 ordinal base
              4 number of functions
              4 number of names

       ordinal hint RVA      name

             1    0 000010E6 fnPEDemoFun
             2    1 00001087 fnPEDemoFunA
             3    2 0000108C fnPEDemoFunB
             4    3 00009138 nPEDemo


   【PEDemofnPEDemoFunB@2fnPEDemoFunA@3.dll(指定序号)】

       NumberOfFunctions    0x00000004    unsigned long

       NumberOfNames    0x00000004    unsigned long

       Base    0x00000002    unsigned long


    AddressOfFunctions   AddressOfNames  ->    name             AddressOfNameOrdinal 实际上为(WORD)
       8C 10 00 00       2B 7F 00 00        fnPEDemoFun             02 00 
       87 10 00 00       37 7F 00 00        fnPEDemoFunA            01 00 
       E6 10 00 00       44 7F 00 00        fnPEDemoFunB            00 00 
       38 91 00 00       51 7F 00 00        nPEDemo                 03 00


     dumpbin result:
          0.00 version
              2 ordinal base
              4 number of functions
              4 number of names

       ordinal hint RVA      name

             4    0 000010E6 fnPEDemoFun = @ILT+225(_fnPEDemoFun)
             3    1 00001087 fnPEDemoFunA = @ILT+130(_fnPEDemoFunA)
             2    2 0000108C fnPEDemoFunB = @ILT+135(_fnPEDemoFunB)
             5    3 00009138 nPEDemo = _nPEDemo  

   结论:先找函数名称,查看序号,得到函数入口地址


   【D:\\PEDemofnPEDemoFunB@2NONAME.dll(隐藏函数名以序号导出)】

        NumberOfFunctions    0x00000004    unsigned long

        NumberOfNames    0x00000003    unsigned long

        Base    0x00000002    unsigned long

     AddressOfFunctions   AddressOfNames  ->    name             AddressOfNameOrdinal 实际上为(WORD)
       8C 10 00 00         25 7F 00 00       fnPEDemoFun             01 00  
       E6 10 00 00         31 7F 00 00       fnPEDemoFunA            02 00 
       87 10 00 00         3E 7F 00 00       nPEDemo                 03 00 
       38 91 00 00                                                   

     dumpbin result:
           0.00 version
              2 ordinal base
              4 number of functions
              3 number of names

       ordinal hint RVA      name

             3    0 000010E6 fnPEDemoFun
             4    1 00001087 fnPEDemoFunA
             5    2 00009138 nPEDemo
             2      0000108C [NONAME]


   结论:查询函数入口地址的数组序号,序号加基数等于导出函数序号(ordinal)



   【D:\\PEDemofnPEDemoFunB@2private.dll(private关键字)】

           NumberOfFunctions    0x00000004    unsigned long
           NumberOfNames    0x00000004    unsigned long
           Base    0x00000002    unsigned long

     AddressOfFunctions   AddressOfNames  ->    name             AddressOfNameOrdinal 实际上为(WORD)
        8C 10 00 00         2B 7F 00 00       fnPEDemoFun              00 01 
       E6 10 00 00         37 7F 00 00       fnPEDemoFunA             00 02
       87 10 00 00         44 7F 00 00       fnPEDemoFunB             00 00
       38 91 00 00         51 7F 00 00       nPEDemo                  00 03

     dumpbin result:
           0.00 version
              2 ordinal base
              4 number of functions
              4 number of names

       ordinal hint RVA      name

             3    0 000010E6 fnPEDemoFun
             4    1 00001087 fnPEDemoFunA
             2    2 0000108C fnPEDemoFunB
             5    3 00009138 nPEDemo         

   结论:遍历函数入口地址得到序号,遍历名称个数获取对应的序号,得出函数入口地址对应的函数名称

5 参考

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学习参考资料

上传的附件:
最新回复 (7)
11
FishSeeWater 2017-10-12 16:58
2
楼主,看看这个工具  https://bbs.pediy.com/thread-127408.htm
yangya 2017-10-12 21:06
3
这个编辑器很好。
2
lipss 2017-10-15 21:56
4
FishSeeWater 楼主,看看这个工具 https://bbs.pediy.com/thread-127408.htm
编辑器写得很好!  我会看看的
newu 2017-10-29 09:28
5
此文很适合入门
newu 2017-10-29 09:28
6
谢谢楼主
niuzuoquan 2017-10-29 09:38
7
mark
独一无② 5天前
8
学习了,很适合入门
返回