前面提到头OPTIONAL_HEADER 的最后一项,是一个16个元素的结构体数组,为数据目录。
而数据目录项的第一个结构,就是动态链接库中经常提到的 导出表.
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
如何找到这个结构前面的文章已经说过。而且这个 Size 字段不一定是真实的。
上面 VirtualAddress 字段 作为 RVA, 需转换成 FOA 然后加上 我们以文件方式打开的 FileBuffer ,才能定位到真正的 导出表。(这种 RVA 寻址前面也用到不少了,应该很熟悉了)
下文及以后在数据目录表中出现的所有地址,全都为 RVA
真正的导出表结构如下:(来自三期课件)
其中 最左的 AddressOfFunctions 就是记录了前面动态库用到的 每个导出函数 的 所在地址,但仍然是RVA,找到这个绝对地址就能调用到函数。因为是地址,所以表宽为4字节,表项数为 NumberOfFunctions
最右的 AddressOfNames 表 记录了 导出函数名字的 RVA ,需要将该 RVA 转换成绝对地址(先转FOA在加上当前FileBuffer基址)才能找到真正的函数名的字符串。即前面动态库被名称粉碎或没有粉碎的名字,就存在这个 RVA 指向的地方。具体如下图:
要注意:函数的真正的名字在文件中如上图所示,位置是不确定的。但函数名称表中是按名字排序的,即A开头的函数在 AddressOfNames 排在最前面。但AXX这个名字字符串的位置,可能排在BXX后面.
注意表项数为 NumberOfNames 和 前面的函数地址表 不一样了。
动态库中提到 可以导出无名字函数,如果无名字那就不会存在名称表中,即不包括在 NumberOfNames 里,名称表不会存它的数据也不会有位置给它。
中间的序号表 AddressOfNameOrdinals ,表项宽度为2字节
该表中存储的内容 + Base = 函数的导出序号
Base 即是 导出表_IMAGE_EXPORT_DIRECTORY 结构体中的一个字段。记录了导出函数起始序号。
如何根据函数名获取到函数地址?(这些事都是操作系统做的事,当然我们懂了原理即可以绕开操作系统自己做)
见下图,遍历名称表内的字符串,逐一匹配,比如匹配到下标为 2 的名称,则同样取下标为 2 的序号表元素。该元素是4,则直接以4为下标,取出函数地址表中的函数地址。(注意这全部的操作都和 base 字段无关!全部都只是以这三个表记录内容来操作)
那么如何根据函数的导出序号获取一个函数的地址?
为了再巩固一下,这里再举一个真实的例子,根据上面的规则自己走一遍。
dll 导出函数由以下def 文件导出,可见序号为 2 3 5 6
EXPORTS
Plus @2
Sub @5 NONAME
Mul @3
Div @6
由上述 def 生成.dll ,查看导出表的结果是:
1、为什么要分成3张表?
函数导出的个数与函数名的个数未必一样.所以要将函数地址表和函数名称表分开.
2、函数地址表是不是一定大于函数名称表?
未必,一个相同的函数地址,可能有多个不同的名字.
练习:
1、编写程序打印所有的导出表信息
2、编写函数 GetFunctionAddrByName ( FileBuffer指针,函数名指针)
3、编写函数 GetFunctionAddrByOrdinals ( FileBuffer指针,函数名导出序号)
#include "Currency.h"
#include "windows.h"
#include "stdio.h"
VOID h324() //输出导出表
{
char FilePath[] = "Dll1.dll"; //CRACKME.EXE CrackHead.exe
LPVOID pFileBuffer = NULL; //会被函数改变的 函数输出之一
LPVOID* ppFileBuffer = &pFileBuffer; //传进函数的形参
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL;
DWORD nameFOA = NULL;
DWORD AddressOfNamesFOA = NULL;
DWORD AddressOfNameOrdinalsFOA = NULL;
DWORD AddressOfFunctionsFOA = NULL;
DWORD AddressOfFunctions = NULL;
WORD Ordinal = NULL;
char * name = NULL;
DWORD result = NULL;
typedef int(*lpPlus)(int, int); //测试查找出的函数用
lpPlus myPlus;
if (!ReadPEFile(FilePath, ppFileBuffer))
{
printf("文件读取失败\n");
return;
}
//Dos头
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; // 强转 DOS_HEADER 结构体指针
//可选PE头 简化后的处理
pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
//导出表
pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));
printf("DIRECTORY_ENTRY_EXPORT VirtualAddress:%x\n", pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
printf("FOA:%x\n", RVA2FOA(pFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));
printf("导出表文件名字符串Name:%x\n", pExportDirectory->Name);
printf("导出函数起始序号Base:%d\n", pExportDirectory->Base);
printf("导出函数的个数:%d\n", pExportDirectory->NumberOfFunctions);
printf("以函数名字导出的函数个数NumberOfNames:%d\n", pExportDirectory->NumberOfNames);
printf("*******函数地址表*******\n");
AddressOfFunctionsFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfFunctions);
for (int i = 0; i < pExportDirectory->NumberOfFunctions; i++)
{ //因Address表元素为4字节,绝对地址加上i*4直接取第i个元素
AddressOfFunctions = *(PDWORD)((DWORD)pFileBuffer + AddressOfFunctionsFOA + i * 4);
printf("下标:%d,函数地址:%x\n", i, AddressOfFunctions);
}
printf("*******函数名称表*******\n");
//导出表中的AddressOfNames为Rva,将其转换为FOA得到AddressOfNamesFOA
AddressOfNamesFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfNames);
for (int i = 0; i < pExportDirectory->NumberOfNames; i++)
{ //AddressOfNamesFOA只是Names表的FOA地址,需加上pFileBuffer构成的绝对地址才能取出其中的值。
//取出的值即Names地址表第i个name的Rva地址,转成FOA得到name的FOA地址
nameFOA = RVA2FOA(pFileBuffer, *(PDWORD)((DWORD)pFileBuffer + AddressOfNamesFOA));
name = (char *)(nameFOA + (DWORD)pFileBuffer);//name的FOA加上pFileBuffer构成绝对地址,该地址才真正指向字符串
printf("下标:%d,函数名:%s\n", i, name);
AddressOfNamesFOA += 4; //往前走4字节,指向Names地址表下一个元素,即下一个name地址
}
printf("*******函数序号表*******\n");
AddressOfNameOrdinalsFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfNameOrdinals); //同Names表找法
for (int i = 0; i < pExportDirectory->NumberOfNames; i++)
{
Ordinal = *(PWORD)((DWORD)pFileBuffer + AddressOfNameOrdinalsFOA + i * 2); //因为Ordinal表元素为2字节,绝对地址加上i*2直接取第i个元素
printf("下标:%d,Ordinal序号:%d\n", i, Ordinal);
}
result = (DWORD)GetFunctionAddrByName(pFileBuffer, "Plus"); //得到的是函数Rva地址
printf("result:%x\n", result);
result = (DWORD)GetFunctionAddrByOrdinals(pFileBuffer, 2);
printf("result:%x\n", result);
}
其中 GetFunctionAddrByName 和 GetFunctionAddrByOrdinals 函数的实现都在 Currency.cpp 中,详见 滴水逆向三期实践附:前后使用到的PE相关函数合集 Currency.h_Rivival_S 的博客-CSDN博客
结果如下:(就是上面画的图表)