继续PE系列笔记的更新
PE其它笔记索引可前往:
PE文件笔记一 PE介绍
前面在学习了关于节的各种操作,但更之前的扩展PE头的DataDirectory中各表项的含义还没具体介绍
这次来学习DataDirectory[0]也就是导出表的具体内容
导出表
导出表作用
一个可执行程序是由多个PE文件组成的
依旧拿先前的EverEdit.exe为例,查看运行它所需的所有模块
使用OD载入EverEdit.exe,然后点击上方的e
来查看所有模块
可以看到,该程序除了包含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这个结构只记录 表的位置和大小,并没有涉及表的具体结构
定位导出表流程
- 找到扩展PE头的最后一个成员DataDirectory
- 获取DataDirectory[0]
- 通过DataDirectory[0].VirtualAddress得到导出表的RVA
- 将导出表的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
再数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
获取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
运行代码得到:
获得了FOA为0x79B0,也就是导出表的位置了,定位完成
导出表的结构
定位到了导出表后自然要了解导出表的结构才能解读导出表的内容
给出导出表在C语言中的结构体(在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; // 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开始的顺序号加上这个值。大致示意图:
如图所示,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
字段间关系图示
按结构分析导出表
回到先前得到的导出表的FOA,在WinHex中找到FOA:0x79B0
将对应的数据填入结构体成员中得到:
成员 |
值 |
说明 |
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);
运行程序得到结果:
用WinHex找到0x7A0A的位置
得到该导出表的文件名字 字符串为: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);
运行程序得到结果:
用WinHex找到0x79D8的位置
记录下所有导出函数的地址并转化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);
运行程序得到结果:
用WinHex找到0x79F8的位置
记录下所有导出函数名称的地址为
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);
运行程序得到结果:
即得到有名称函数的名称地址为:
顺序索引 |
RVA |
FOA |
1 |
0x19014 |
0x7A14 |
2 |
0x19018 |
0x7A18 |
3 |
0x1901F |
0x7A1F |
用WinHex找到对应的FOA位置
得到了各导出函数的名称为
顺序索引 |
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);
运行程序得到结果:
用WinHex找到0x7A04的位置
得到有名称函数的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 |
导出表分析完毕
由导出表获得导出函数
从前面的分析中可以得知查询导出表有两种方法:
- 根据导出表函数名称获取导出函数地址
- 根据导出表函数序号获取导出函数地址
函数名称获取导出函数
- 根据导出表的函数名称去AddressOfNames指向的每个名称字串查询是否有匹配的字符串
- 找到匹配的字符串后,根据找到的顺序索引去AddressOfNameOrdinals中找到对应的Ordinals
- 根据前面找到的Ordinals到AddressOfFunctions中获得函数地址
图解为:
函数序号获取导出函数
- 根据函数序号-导出表.Base获得导出函数的Ordinal
- 根据前面找到的Ordinals到AddressOfFunctions中获得函数地址
图解为:
代码实现分析导出表
// 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;
}
运行结果
可以看到,得到的结果和先前的手动分析的结果是一致的
使用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
- 导出名称表和导出函数序号表只对有名称的导出函数有效
附件
这次提供的附件为本笔记中用到的例子:
包含1个文件夹和1个dll文件
dll文件为本笔记中分析的dll文件,MyDll文件夹则是dll的源代码
有需要者可以自行取用:点我下载