前面在学习了关于节的各种操作,但更之前的扩展PE头的DataDirectory中各表项的含义还没具体介绍

这次来学习DataDirectory[0]也就是导出表的具体内容


导出表

导出表作用

一个可执行程序是由多个PE文件组成

依旧拿先前的EverEdit.exe为例,查看运行它所需的所有模块

使用OD载入EverEdit.exe,然后点击上方的e来查看所有模块

image-20210406212544713


image-20210406212652608

可以看到,该程序除了包含EverEdit.exe这个模块外还包含不少其它的dll(动态链接库),这些dll为程序提供一些函数

就比如MessageBoxA这个弹窗的函数就是由user32.dll这个模块提供的

以上这些模块都发挥着其作用,使得程序得以正常运行

一个程序引用哪些模块是由其导入表决定的

与导入表相对的便是导出表,导出表则是决定当前的PE文件能够给其它PE文件提供的函数

拿前面提到的user32.dll为例,其导出表一定是包含MessageBoxA这个函数的


归纳一下导入表和导出表

  • 导入表:该PE文件还使用哪些PE文件
  • 导出表:该PE文件提供了哪些函数给其它PE文件

什么是导出表

导出表就是记录该PE文件提供给其它PE文件的函数的一种结构


定位导出表

定位导出表原理

在前面的笔记:PE文件笔记五 PE文件头之扩展PE头中还剩下一个DataDirectory的结构没有具体说明

DataDirectory是一个数组,每个数组成员对应一个表,如导入表、导出表、重定位表等等

回顾先前的笔记,能得到导出表对应的下标为0

宏定义 含义
IMAGE_DIRECTORY_ENTRY_EXPORT 0 导出表

即DataDirectory[0]表示导出表


接下来来具体研究一下DataDirectory数组成员的结构

先给出C语言中 该成员在扩展PE头里的定义

 复制代码 隐藏代码
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

可以看到数组成员的结构为IMAGE_DATA_DIRECTORY

IMAGE_DATA_DIRECTORY

 复制代码 隐藏代码typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

IMAGE_DATA_DIRECTORY成员 数据宽度 说明
VirtualAddress DWORD(4字节) 表的起始位置(RVA)
Size DWORD(4字节) 表的大小
VirtualAddress

表的起始位置,是一个相对虚拟地址(RVA),不了解RVA的可以回顾先前的:PE文件笔记七 VA与FOA转换


Size

表的大小


根据前面的分析可以得出:

IMAGE_DATA_DIRECTORY这个结构只记录 表的位置和大小,并没有涉及表的具体结构


定位导出表流程

  1. 找到扩展PE头的最后一个成员DataDirectory
  2. 获取DataDirectory[0]
  3. 通过DataDirectory[0].VirtualAddress得到导出表的RVA
  4. 将导出表的RVA转换为FOA,在文件中定位到导出表

按流程定位导出表

要分析的实例

这次分析的程序以MyDll.dll为例(自己编写的dll,只提供了加减乘除的导出函数)

给出导出函数的定义声明

 复制代码 隐藏代码EXPORTS
Add @12
Sub @15 NONAME
Multiply @17
Divide @10

再给出具体的导出函数内容

 复制代码 隐藏代码int _stdcall Add(int x, int y)
{
return x+y;
}

int _stdcall Sub(int x, int y)
{
return x-y;
}

int _stdcall Multiply(int x, int y) {
return x * y;
}

int _stdcall Divide(int x, int y) {
return x / y;
}

完整的DLL源代码和DLL程序在后面的附件中,有需要可以自行取用

找到DataDirectory

使用WinHex打开MyDll.dll,先找到PE文件头的起始地址:0xF8

image-20210407143723020


再数24个字节(PE文件头标志大小+标准PE头大小),到达扩展PE头:0xF8+24=248+24=272=0x110

然后在数224-128=96个字节(扩展PE头大小减去DataDirectory大小)DataDirectory大小= _IMAGE_DATA_DIRECTORY大小×16=8*16

DataDirectory首地址 = 扩展PE头地址+96=0x110+96=272+96=368=0x170

image-20210407145603036

获取DataDirectory[0]

而导出表为DataDirectory[0],也就是从首地址开始的8个字节就是描述导出表的IMAGE_DATA_DIRECTORY

IMAGE_DATA_DIRECTORY成员 说明
VirtualAddress 0x00018FB0 表的起始位置(RVA)
Size 0x00000190 表的大小

得到导出表的RVA

于是得到导出表对应的RVA为:0x18FB0


RVA转换FOA

但是IMAGE_DATA_DIRECTORY中的VirtualAddress是RVA,需要将其转换成FOA

关于RVA转FOA的内容在 PE文件笔记七 VA与FOA转换中已经详细说明了,这里不再赘述

直接使用在笔记七中写的转换代码计算出对应的FOA:

 复制代码 隐藏代码// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664

//VA转FOA 32位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS);
//输出PeEnd
printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize, (UINT)sectionArr[i]->SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment;

if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
printf("SizeInMemory:%X\n", SizeInMemory);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
UINT offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
UINT foa = sectionArr[i]->PointerToRawData + offset;
printf("foa:%X\n", foa);
return foa;
}

}

}

int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Documents and Settings\\Administrator\\桌面\\user32.dll", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);

//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);

//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
//输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
printf("magic:%X\n", *magic);
//根据magic判断为32位程序还是64位程序
switch (*magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
printf("32位程序\n");
//确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS* nt;
//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

VaToFoa32(nt->OptionalHeader.ImageBase +0x18FB0,dos,nt,sectionArr);

break;
}

case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
printf("64位程序\n");
//确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS64* nt;
nt = (_IMAGE_NT_HEADERS64*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);

//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

break;
}

default:
{
printf("error!\n");
break;
}

}
return 0;
}

关键代码:

 复制代码 隐藏代码
VaToFoa32(nt->OptionalHeader.ImageBase +0x18FB0,dos,nt,sectionArr);

因为先前写的函数是VA转FOA,这里得到的是RVA,于是要先用RVA+ImageBase得到VA

运行代码得到:

image-20210407144705123


获得了FOA为0x79B0,也就是导出表的位置了,定位完成


导出表的结构

定位到了导出表后自然要了解导出表的结构才能解读导出表的内容

给出导出表在C语言中的结构体(在winnt.h中可以找到)

image-20210407124035639


即:

 复制代码 隐藏代码typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

结构体分析

成员 数据宽度 说明
Characteristics DWORD(4字节) 标志,未用
TimeDateStamp DWORD(4字节) 时间戳
MajorVersion WORD(2字节) 未用
MinorVersion WORD(2字节) 未用
Name DWORD(4字节) 指向该导出表的文件名字符串
Base DWORD(4字节) 导出函数起始序号
NumberOfFunctions DWORD(4字节) 所有导出函数的个数
NumberOfNames DWORD(4字节) 以函数名字导出的函数个数
AddressOfFunctions DWORD(4字节) 导出函数地址表RVA
AddressOfNames DWORD(4字节) 导出函数名称表RVA
AddressOfNameOrdinals DWORD(4字节) 导出函数序号表RVA

Characteristics

未使用,固定填充0


TimeDateStamp

Image时间戳的低32位。这表示链接器创建Image的日期和时间。根据系统时钟,该值以自1970年1月1日午夜(00:00:00)后经过的秒数表示

与标准PE头中的TimeDateStamp一致


MajorVersion

未使用,固定填充0


MinorVersion

MinorVersion


Name

该字段指示的地址指向了一个以”\0”结尾的字符串,字符串记录了导出表所在的文件的最初文件名


Base

导出函数序号的起始值。DLL中第一个导出函数并不是从0开始的,某导出函数的编号等于从AddressOfFunctions开始的顺序号加上这个值。大致示意图:

image-20210407131635788

如图所示,Fun1的函数编号为nBase+0=200h,Fun2的函数编号为nBase+1=201h,以此类推


NumberOfFunctions

该字段定义了文件中导出函数的总个数


NumberOfNames

在导出表中,有些函数是定义名字的,有些是没有定义名字的。该字段记录了所有定义名字函数的个数。如果这个值是0,则表示所有的函数都没有定义名字。NumbersOfNames一定小于等于NumbersOfFuctions


AddressOfFunctions

该指针指向了全部导出函数的入口地址的起始。从入口地址开始为DWORD数组,数组的个数由NumbersOfFuctions决定

导出函数的每一个地址按函数的编号顺序依次往后排开。在内存中,可以通过函数编号来定位某个函数的地址


AddressOfNames

该值为一个指针。该指针指向的位置是一连串的DWORD值,这些值均指向了对应的定义了函数名的函数的字符串地址。这一连串的DWORD值的个数为NumberOfNames


AddressOfNameOrdinals

该值也是一个指针,与AddressOfNames是一一对应关系

不同的是,AddressOfNames指向的是字符串的指针数组,而AddressOfNameOrdinals则指向了该函数在AddressOfFunctions中的索引值


注意:索引值数据类型为WORD,而非DWORD。该值与函数编号是两个不同的概念,两者的关系为:

索引值 = 编号 - Base


字段间关系图示

image-20210407132858847


按结构分析导出表

回到先前得到的导出表的FOA,在WinHex中找到FOA:0x79B0

image-20210407145702135


将对应的数据填入结构体成员中得到:

成员 说明
Characteristics 0X00000000 标志,未用,固定为0
TimeDateStamp 0xFFFFFFFF 时间戳
MajorVersion 0X0000 未用,固定为0
MinorVersion 0X0000 未用,固定为0
Name 0x0001900A 指向该导出表的文件名字符串
Base 0x0000000A 导出函数起始序号
NumberOfFunctions 0x00000008 所有导出函数的个数
NumberOfNames 0x00000003 以函数名字导出的函数个数
AddressOfFunctions 0x00018FD8 导出函数地址表RVA
AddressOfNames 0x00018FF8 导出函数名称表RVA
AddressOfNameOrdinals 0x00019004 导出函数序号表RVA

Name

存储的值为指针,该指针为RVA,同样需要转成FOA

 复制代码 隐藏代码
VaToFoa32(nt->OptionalHeader.ImageBase+0x1900A,dos,nt,sectionArr);

运行程序得到结果:

image-20210407145928618


用WinHex找到0x7A0A的位置

image-20210407150048888

得到该导出表的文件名字 字符串为:MyDll.dll


Base

导出函数起始序号为0xA,对应十进制10

回顾一下前面导出函数的定义声明

 复制代码 隐藏代码EXPORTS
Add @12
Sub @15 NONAME
Multiply @17
Divide @10

不难发现,这里的base=最小的序号=min{12,15,17,10}=10


NumberOfFunctions

所有导出函数的个数为8

明明前面声明的导出函数只有4个,为什么这里显示的导出函数个数为8?

这里的NumberOfFunctions = 最大的序号减去最小的序号+1=17-10+1=8


NumberOfNames

以函数名字导出的函数个数为3,和定义声明中有名称的导出函数 数量一致


AddressOfFunctions

存储的值为指针,该指针为RVA,同样需要转成FOA

 复制代码 隐藏代码
VaToFoa32(nt->OptionalHeader.ImageBase+0x18FD8,dos,nt,sectionArr);

运行程序得到结果:

image-20210407150355678


用WinHex找到0x79D8的位置

image-20210407150517575

记录下所有导出函数的地址并转化RVA为FOA得到:

Oridinals 序号(Oridinals+Base) 导出函数地址(RVA) 导出函数地址(FOA)
0 10 0x00011320 0x720
1 11 0x00000000
2 12 0x00011302 0x702
3 13 0x00000000
4 14 0x00000000
5 15 0x000111EF 0x5EF
6 16 0x00000000
7 17 0x000111A4 0x5A4

可以看到只有4个导出函数是有效的,和前面DLL导出声明定义一致


AddressOfNames

存储的值为指针,该指针为RVA,同样需要转成FOA

 复制代码 隐藏代码
VaToFoa32(nt->OptionalHeader.ImageBase+0x18FF8,dos,nt,sectionArr);

运行程序得到结果:

image-20210407150839193


用WinHex找到0x79F8的位置

image-20210407150925784

记录下所有导出函数名称的地址为

0x00019014

0x00019018

0x0001901F

将RVA转化为FOA:

 复制代码 隐藏代码VaToFoa32(nt->OptionalHeader.ImageBase+0x19014,dos,nt,sectionArr);
VaToFoa32(nt->OptionalHeader.ImageBase+0x19018,dos,nt,sectionArr);
VaToFoa32(nt->OptionalHeader.ImageBase+0x1901F,dos,nt,sectionArr);

运行程序得到结果:

image-20210407151149110


即得到有名称函数的名称地址为:

顺序索引 RVA FOA
1 0x19014 0x7A14
2 0x19018 0x7A18
3 0x1901F 0x7A1F

用WinHex找到对应的FOA位置

image-20210407151409695


得到了各导出函数的名称为

顺序索引 RVA FOA 导出函数名称
1 0x19014 0x7A14 Add
2 0x19018 0x7A18 Divide
3 0x1901F 0x7A1F Multiply

AddressOfNameOrdinals

存储的值为指针,该指针为RVA,同样需要转成FOA

 复制代码 隐藏代码
VaToFoa32(nt->OptionalHeader.ImageBase+0x19004,dos,nt,sectionArr);

运行程序得到结果:

image-20210407151553911


用WinHex找到0x7A04的位置

image-20210407152925004


得到有名称函数的Ordinals

注意Oridinals的数据宽度为2个字节(WORD)

顺序索引 Oridinals 序号(Oridinals+Base)
1 0x0002 12
2 0x0000 10
3 0x0007 17

根据有名称函数的Oridinals结合前面得到的AddressOfFunctions和AdressOfNames,就可以得到函数的名称、函数的地址的关系

顺序索引 Oridinals 导出函数地址(RVA) 导出函数地址(FOA) 函数名称
1 0x0002 0x00011302 0x702 Add
2 0x0000 0x00011320 0x720 Divide
3 0x0007 0x000111A4 0x5A4 Multiply

导出表分析完毕

由导出表获得导出函数

从前面的分析中可以得知查询导出表有两种方法:

  1. 根据导出表函数名称获取导出函数地址
  2. 根据导出表函数序号获取导出函数地址

函数名称获取导出函数

  1. 根据导出表的函数名称去AddressOfNames指向的每个名称字串查询是否有匹配的字符串
  2. 找到匹配的字符串后,根据找到的顺序索引去AddressOfNameOrdinals中找到对应的Ordinals
  3. 根据前面找到的Ordinals到AddressOfFunctions中获得函数地址

图解为:

image-20210407225659644


函数序号获取导出函数

  1. 根据函数序号-导出表.Base获得导出函数的Ordinal
  2. 根据前面找到的Ordinals到AddressOfFunctions中获得函数地址

图解为:

image-20210407225716859


代码实现分析导出表

 复制代码 隐藏代码// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664

//VA转FOA 32位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
//printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS);
//输出PeEnd
//printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize, (UINT)sectionArr[i]->SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment;

if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
//printf("SizeInMemory:%X\n", SizeInMemory);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
UINT offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
UINT foa = sectionArr[i]->PointerToRawData + offset;
//printf("foa:%X\n", foa);
return foa;
}

}

}
//获取导出表
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
void getExportTable(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
_IMAGE_DATA_DIRECTORY exportDataDirectory = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
UINT exportAddress = VaToFoa32(exportDataDirectory.VirtualAddress+nt->OptionalHeader.ImageBase, dos, nt, sectionArr);
_IMAGE_EXPORT_DIRECTORY* exportDirectory = (_IMAGE_EXPORT_DIRECTORY*) ((UINT)dos+ exportAddress);
printf("导出函数总数:%X\n", exportDirectory->NumberOfFunctions);
printf("导出有名称的函数总数:%X\n", exportDirectory->NumberOfNames);
int i;
for (i = 0; i < exportDirectory->NumberOfNames; i++) {
printf("顺序序号:%d\t", i);
//获取指向导出函数文件名称的地址
UINT namePointerAddress = VaToFoa32(exportDirectory->AddressOfNames + nt->OptionalHeader.ImageBase + 4 * i, dos, nt, sectionArr);
if (namePointerAddress == -1)return;
printf("namePointerAddress:%X\t", namePointerAddress);
//获取指向名字的指针
UINT* nameAddr =(UINT*) ((UINT)dos + namePointerAddress);
printf("nameAddr(RVA):%X\t", *nameAddr);
//获取存储名字的地址
UINT nameOffset = VaToFoa32(*nameAddr + nt->OptionalHeader.ImageBase, dos, nt, sectionArr);
if (nameOffset == -1)return;
printf("nameOffset:%X\t", nameOffset);
//根据名字指针输出名字
CHAR* name = (CHAR*) ((UINT)dos+ nameOffset);
printf("name:%s\t",name);
//因为AddressOfNames与AddressOfNameOrdinals一一对应,于是可以获得对应的NameOrdinals

//获取存储Ordinals的地址
UINT OrdinalsOffset = VaToFoa32(exportDirectory->AddressOfNameOrdinals + nt->OptionalHeader.ImageBase + 2 * i, dos, nt, sectionArr);
printf("OrdinalsOffset:%X\t", OrdinalsOffset);
if (OrdinalsOffset == -1)return;
WORD* Ordinals =(WORD*)((UINT)dos + OrdinalsOffset);
printf("Ordinals:%d\t", *Ordinals);

//获得Ordinals后可以根据Ordinals到AddressOfFunctions中找到对应的导出函数的地址
UINT* functionAddress=(UINT*)((UINT)dos + VaToFoa32(exportDirectory->AddressOfFunctions + nt->OptionalHeader.ImageBase + 4* *Ordinals, dos, nt, sectionArr));
printf("functionAddress(RVA):%X\n", *functionAddress);
}

}
int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Users\\lyl610abc\\Desktop\\MyDll.dll", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);

//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);

//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
//输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
printf("magic:%X\n", *magic);
//根据magic判断为32位程序还是64位程序
switch (*magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
printf("32位程序\n");
//确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS* nt;
//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

getExportTable(dos, nt, sectionArr);

break;
}

case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
printf("64位程序\n");
//确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS64* nt;
nt = (_IMAGE_NT_HEADERS64*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);

//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

break;
}

default:
{
printf("error!\n");
break;
}

}
return 0;
}

运行结果

image-20210407221926491

可以看到,得到的结果和先前的手动分析的结果是一致的


image-20210407222732346

使用PE工具:DIE查看导出表,可以看到结果也是一致的


代码说明

这次的代码部分主要是getExportTable这个函数

该函数并不长,代码中用到了较多的类型转换 和 指针相关的内容

要注意的地方是

 复制代码 隐藏代码//获取指向导出函数文件名称的地址
UINT namePointerAddress = VaToFoa32(exportDirectory->AddressOfNames + nt->OptionalHeader.ImageBase + 4 * i, dos, nt, sectionArr);

 复制代码 隐藏代码//获取存储Ordinals的地址
UINT OrdinalsOffset = VaToFoa32(exportDirectory->AddressOfNameOrdinals + nt->OptionalHeader.ImageBase + 2 * i, dos, nt, sectionArr);

这里一个是加上4×i;一个是加上2×i

4和2都是偏移量,偏移量取决于要获取的数据的数据宽度

Names的数据宽度为4字节(DWORD),所以每次要加4

而Ordinals的数据宽度为2字节(WORD),所以每次要加2


总结

  • 导出表中还包含了三张小表:导出函数地址表、导出函数名称表、导出函数序号表
  • 导出表中存储了指向这三张表地址的指针,而不是直接存储表的内容
  • 无论是根据函数名称还是根据函数序号获取导出函数都需要用到Ordinals,用Ordinals到导出函数地址表中获取地址
  • 导出表的Base取决于编写DLL时导出定义的最小序号
  • 导出表的NumberOfFuctions取决于编写DLL时导出定义的序号最大差值+1
  • 导出名称表和导出函数序号表只对有名称的导出函数有效

附件

这次提供的附件为本笔记中用到的例子:

image-20210407225538454


包含1个文件夹和1个dll文件

dll文件为本笔记中分析的dll文件,MyDll文件夹则是dll的源代码

有需要者可以自行取用:点我下载

转载自吾爱破解上的一位大佬