前言
这块是真的麻烦,真的会有人写代码少了一个*,真的会调试一下午吗???
课堂
简介(引出)
前面学过一个 Win32 下的.exe 文件,是由多个 PE 文件组成。比如通过 OD 打开一个 Ipmsg.exe,查看模块M
,会发现一个 ipmsg.exe 和多个.dll(即模块)构成
这种以动态链接库.dll 的方式导出函数时,如果此.exe 需要用到某个.dll 中的函数,那么.exe 怎么知道:
引入的.dll 中有哪些函数可以供.exe 使用
.dll 中的各种函数都存在哪里
所以.dll 中应当有一个目录或者结构提供给.exe 文件,来记录.dll 中的函数有哪些,函数的起始地址等信息。所以如果.dll 或者.exe 文件想要给别的程序提供函数,就必须同时给别的程序一个“清单”,这个“清单”就是导出表
故当我们分析上面例子中的 ole32.dll,或者 LPK.dll 等.dll 文件结构时,就可以看到有导出表
误区
不是只有.dll 才提供函数给别的程序使用,有些.exe 也可以提供函数
有些.exe 的导出表数据目录不是全 0,表示有此.exe 程序有导出表,即可对外提供函数;有些没有导出表,就不对外提供函数
所以一个 PE 文件(如.dll/.exe/.sys)如果对外提供函数,那么就需要有导出表,且需要遵守一定的格式
一个安全的 PE 文件,是不会显示导出表的,但可以对外提供函数:正常情况下如果一个 PE 文件不提供导出表,别人是无法使用其函数的;但是逆向一个游戏时,如果知道了别人的导出表,就不用找 call 了(不现实)。所以游戏会做安全加固,我们不知道哪些函数是导出的,就算给我函数地址,我也没办法拿这个地址去调用,只能分析反汇编代码,分析程序入口是什么,分析函数的参数是什么
导出表的定位
注意事项:学到现在解析 PE 结构时就不用拉伸了,用 FileBuffer 分析即可。因为拉伸只是有助于理解,如果直接在 FileBuffer 中做,只是需要多使用一个 RVA–>FOA 的地址转换函数,每次遇到 RVA 地址时转换一下即可,就省去了将文件整个拉伸的过程,会减少很多代码量和多余操作,而且市面上关于 PE 的书籍几乎都是直接用 FileBuffer 讲解操作的,所以现在开始,我们就要学会不拉伸的情况下去分析
导出表的结构
struct _IMAGE_EXPORT_DIRECTORY{ //40字节
DWORD Characteristics; //未使用
DWORD TimeDateStamp; //时间戳
WORD MajorVersion; //未使用
WORD MinorVersion; //未使用
DWORD Name; //指向该导出表文件名字符串 *
DWORD Base; //导出函数起始序号 *
DWORD NumberOfFunctions; //所有导出函数的个数 *
DWORD NumberOfNames; //以函数名字导出的函数个数 *
DWORD AddressOfFunctions; //导出函数地址表RVA *
DWORD AddressOfNames; //导出函数名称表RVA *
DWORD AddressOfNameOrdinals; //导出函数序号表RVA *
};
- Name
指向该导出表文件名字符串的 RVA,比如一个 messagebox.dll 的 PE 文件提供函数,那么这个 PE 文件的 Name 指向的字符串为为 messagebox.dll
- Base
导出函数起始序号(最小的序号),比如有序号为 14、6、10、8 的导出函数,那么 Base 的值为 6
- NumberOfFunctions
所有导出函数的个数,注意:上篇文章讲过,这个值是通过导出函数的最大序号 - 最小序号 + 1 算出来的;正常来说这个值是多少,那么此 PE 文件中导出函数的个数就是多少。但是如果使用自定义序号,序号定义时不是连续的,而是中间有空缺的序号,那么此时 NumberOfFunctions 的值会比实际的定义的导出函数个数多,所以这个值是不准确的
- NumberOfNames
以函数名字导出的函数个数:比如以动态链接库的方式导出,如果导出时加了 NONAME 关键字,那么就不计数
- AddressOfFunctions
- AddressOfNames
导出函数名称表 RVA(拉伸后的内存地址偏移,所以要先转成 FOA),即这个地址指向一个表,这个表中记录的是导出函数的名称字符串地址!!不是直接存储名称(且此字符串地址也是 RVA),这个就和之前有一次的指针数组的练习一样
就像 C 语言中的字符串char* name = "abc";
,name 变量值为 abc 这个字符串的首地址。所以导出函数名称表中存储的是 name 值(即名称的地址),而不是直接存储的 abc 这个名称
- AddressOfNameOrdinals
表中元素个数:由 NumberOfNames 决定,且和 AddressOfName 表一一对应的
如果导出时,定义的无名字函数,即Div @13 NONAME
,那么函数名称表中就不会有指向 Div 函数名的元素,同样函数序号表中也不会有 Div 的相对序号,但是在函数地址表中会留出来一个元素位置存储 Div 函数地址
由导出表获取函数地址
那个序号表是给名字表查函数值做一个中转,现在比如一共四个函数,三个函数是按名字表导出的,而一个函数是按照序号表导出的,名字表里面肯定只有 3 个,而序号表里面也是只有 3 个,那个函数既然都没有名字,那它肯定也就没有对应的序号表去做中转了,序号查询的话就是完全和这个序号表没有任何关系的
根据函数的名字获取
-
找到导出表后,先根据 AddressOfNames,将RVA 转成 FOA,即可定位到函数名称表,遍历函数名称表,用名字依次与函数名称表每个元素指向的字符串做比较,直到名字匹配,记录下此时元素在函数名称表的下标
i
-
再根据 AddressOfNameOrdinals,将 RVA 转成 FOA,即可定位到函数序号表,得到此表下标为
i
的元素值n
-
最后根据 AddressOfFunctions,将 RVA 转成 FOA,即可定位到函数地址表,则此表下标为
n
的元素值就是该名字对应函数的 RVA 地址,将 RVA 转成 FOA 就得到了该导出函数在 FileBuffer 中的地址
按序号找函数地址
-
找到导出表后,根据 AddressOfFunctions,将 RVA 转成 FOA,定位到函数地址表
-
再用给定的序号 - Base = 相对序号
i
,得到相对序号i
-
最后找函数地址表中下标为
i
的元素值即为该序号对应函数 RVA 地址,将 RVA 转成 FOA 就得到了该函数在 FileBuffer 中的地址
对比
作业
这三个功能我都放在了一个程序里面
项目的总体结构
头文件如下
#pragma once
int PeFileSize(char* FilePath);
char* ReadPeFile(char* FilePath);
int RvaToFoa(char* FileBufferPoint, int RVA);
void PrintExportTables(char* FileBufferPoint);
int SearchByName(char* FileBufferPoint, char* FucName);
int SearchByNum(char* FileBufferPoint, int Num);
main 函数
#include <iostream>
#include <windows.h>
#include "Fuction.h"
int main()
{
char* FilePath = (char*)"D:/my_c++project/Test_Dll/move_storehouse.dll"; //打开的PE文件绝对路径
char* FileBufferPoint = ReadPeFile(FilePath);
/*
* 测试RvaToFoa函数是否好使时候写的
int FOA = RvaToFoa(FileBufferPoint,0x10019024);
printf("0x%08X\n",FOA);
*/
PrintExportTables(FileBufferPoint);
char* funcName = (char*)"Div";
printf("%08X\n", SearchByName(FileBufferPoint,funcName));
printf("%08X\n",SearchByNum(FileBufferPoint,1));
return 0;
}
功能函数如下:
#include <stdlib.h>
#include <cstdio>
#include <atomic>
#include "Fuction.h"
typedef unsigned short WORD;
typedef unsigned int DWORD;
typedef unsigned char BYTE;
#define MZ 0x5A4D
#define PE 0x4550
#define IMAGE_SIZEOF_SHORT_NAME 8
//DOS头
struct _IMAGE_DOS_HEADER {
WORD e_magic; //MZ标记
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
DWORD e_lfanew; //PE文件真正开始的偏移地址
};
//标准PE头
struct _IMAGE_FILE_HEADER {
WORD Machine; //文件运行平台
WORD NumberOfSections; //节数量
DWORD TimeDateStamp; //时间戳
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; //可选PE头大小
WORD Characteristics; //特征值
};
//数据目录
struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
};
//可选PE头
struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; //文件类型
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode; //代码节文件对齐后的大小
DWORD SizeOfInitializedData; //初始化数据文件对齐后的大小
DWORD SizeOfUninitializedData; //未初始化数据文件对齐后大小
DWORD AddressOfEntryPoint; //程序入口点(偏移量)
DWORD BaseOfCode; //代码基址
DWORD BaseOfData; //数据基址
DWORD ImageBase; //内存镜像基址
DWORD SectionAlignment; //内存对齐粒度
DWORD FileAlignment; //文件对齐粒度
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; //文件装入虚拟内存后大小
DWORD SizeOfHeaders; //DOS、NT头和节表大小
DWORD CheckSum; //校验和
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve; //预留堆栈大小
DWORD SizeOfStackCommit; //实际分配堆栈大小
DWORD SizeOfHeapReserve; //预留堆大小
DWORD SizeOfHeapCommit; //实际分配堆大小
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; //目录项数目
_IMAGE_DATA_DIRECTORY DataDirectory[16]; //数据目录
};
//NT头
struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE签名
_IMAGE_FILE_HEADER FileHeader;
_IMAGE_OPTIONAL_HEADER OptionalHeader;
};
//节表
struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //节表名
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //内存中未对齐大小
}Misc;
DWORD VirtualAddress; //该节在内存中偏移地址
DWORD SizeOfRawData; //该节在硬盘上文件对齐后大小
DWORD PointerToRawData; //该节在硬盘上文件对齐后偏移地址
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //该节特征属性
};
//导出表
struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //未使用
DWORD TimeDateStamp; //时间戳
WORD MajorVersion; //未使用
WORD MinorVersion; //未使用
DWORD Name; //指向该导出表文件名字符串 *
DWORD Base; //导出函数起始序号 *
DWORD NumberOfFunctions; //所有导出函数的个数 *
DWORD NumberOfNames; //以函数名字导出的函数个数 *
DWORD AddressOfFunctions; //导出函数地址表RVA *
DWORD AddressOfNames; //导出函数名称表RVA *
DWORD AddressOfNameOrdinals; //导出函数序号表RVA *
};
int PeFileSize(char* FilePath) {
//PeFileSize:计算文件在硬盘上的大小
//参数说明:
//FilePath:指向文件的绝对路径
//返回值说明:
//读取成功返回文件在硬盘上的大小,读取失败则返回0
FILE* pf = fopen(FilePath, "rb");
if (pf == NULL) {
perror("打开文件错误");
fclose(pf);
return 0;
}
fseek(pf, 0, 2);
int length = ftell(pf);
fseek(pf, 0, 0);
fclose(pf);
printf("已经成功读取该文件的大小\n");
return length;
}
char* ReadPeFile(char* FilePath) {
//ReadPeFile:将可执行文件从硬盘读取到FileBuffer
//参数说明:
//FilePath:指向文件的绝对路径
//返回值说明:
//读取成功返回FileBuffer的首地址,读取失败则返回0
FILE* pf = fopen(FilePath, "rb");
if (pf == NULL) {
perror("打开文件错误");
fclose(pf);
return 0;
}
int length = PeFileSize(FilePath);
char* ptr_1 = (char*)malloc(sizeof(char) * length);
if (ptr_1 == NULL) {
perror("File堆内存分配失败");
fclose(pf);
return 0;
}
memset(ptr_1, 0, sizeof(char) * length);
int flag = fread(ptr_1, length, 1, pf);
if (flag == NULL) {
perror("读取数据失败,请检查文件路径");
fclose(pf);
free(ptr_1);
return 0;
}
fclose(pf);
//这里之所以没有free(ptr),原因是咱们下面还要用到这块堆的内存,所以可以在main函数结束之前释放掉就行
printf("已成功将可执行文件从硬盘读取到FileBuffer\n");
return ptr_1;
}
int RvaToFoa(char* FileBufferPoint,int RVA) {
//RvaToFoa:将可执行文件在内存中的地址转换为在FileBuffer中的地址
//参数说明:
//FileBufferPoint:指向可执行文件在FileBuffer的地址
//返回值说明:
//读取成功返回FOA,读取失败则返回0
_IMAGE_DOS_HEADER* _image_dos_header = NULL;
_IMAGE_FILE_HEADER* _image_file_header = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
_IMAGE_SECTION_HEADER* _image_section_header = NULL;
_image_dos_header = (_IMAGE_DOS_HEADER*)FileBufferPoint;
//下面这个别忘记了还有一个PE标记的大小,为4个字节
_image_file_header = (_IMAGE_FILE_HEADER*)(FileBufferPoint + _image_dos_header->e_lfanew + sizeof(PE));
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + _image_file_header->SizeOfOptionalHeader);
RVA += _image_optional_header->ImageBase;
int flag = 0;
if (_image_section_header->VirtualAddress > RVA - _image_optional_header->ImageBase) {
return RVA;
}
for (int i = 0; i < _image_file_header->NumberOfSections; i++) {
if (RVA - _image_optional_header->ImageBase >= _image_section_header->VirtualAddress && RVA - _image_optional_header->ImageBase < _image_section_header->VirtualAddress + _image_section_header->Misc.VirtualSize) {
flag = 1;
break;
}
else {
_image_section_header++;
}
}
if (flag == 0) {
return 0;
}
int TempAddress = RVA - _image_optional_header->ImageBase - _image_section_header->VirtualAddress;
return _image_section_header->PointerToRawData + TempAddress;
}
void PrintExportTables(char* FileBufferPoint) {
//PrintExportTables:打印出导出表的内容
//参数说明:
//FileBufferPoint:指向可执行文件在FileBuffer的地址
//返回值说明:
//没有返回值,直接打印结果
_IMAGE_DOS_HEADER* _image_dos_header = NULL;
_IMAGE_FILE_HEADER* _image_file_header = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
_IMAGE_SECTION_HEADER* _image_section_header = NULL;
_IMAGE_DATA_DIRECTORY* _image_data_directory = NULL;
_IMAGE_EXPORT_DIRECTORY* _image_export_directory = NULL;
_image_dos_header = (_IMAGE_DOS_HEADER*)FileBufferPoint;
//下面这个别忘记了还有一个PE标记的大小,为4个字节
_image_file_header = (_IMAGE_FILE_HEADER*)(FileBufferPoint + _image_dos_header->e_lfanew + sizeof(PE));
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_data_directory = (_IMAGE_DATA_DIRECTORY*)_image_optional_header->DataDirectory;
_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + _image_file_header->SizeOfOptionalHeader);
if (_image_data_directory->VirtualAddress == 0) {
printf("该二进制文件没有导出表");
getchar();
exit(0);
}
//打印出导出表的数据
int FoaExportTables = RvaToFoa(FileBufferPoint, _image_data_directory->VirtualAddress) + (DWORD)FileBufferPoint;
_image_export_directory = (_IMAGE_EXPORT_DIRECTORY*)FoaExportTables;
char* name = (char*)(RvaToFoa(FileBufferPoint, _image_export_directory->Name) + (DWORD)FileBufferPoint);
//这步写的有点臃肿了,一行代码就能行
printf("***************ExportTables****************\n");
printf("Name:%08X --> %s\n", _image_export_directory->Name,name);
printf("Base:%08X\n", _image_export_directory->Base);
printf("NumberOfFunctions:%08X\n", _image_export_directory->NumberOfFunctions);
printf("NumberOfNames:%08X\n", _image_export_directory->NumberOfNames);
printf("AddressOfFunctions:%08X\n", _image_export_directory->AddressOfFunctions);
printf("AddressOfNames:%08X\n", _image_export_directory->AddressOfNames);
printf("AddressOfNameOrdinals:%08X\n\n", _image_export_directory->AddressOfNameOrdinals);
//打印导出函数地址表,注意这里只是打印出函数的地址而已,所以不用二级指针就行
int* AddressOfFunctions = (int*)(RvaToFoa(FileBufferPoint, _image_export_directory->AddressOfFunctions) + (DWORD)FileBufferPoint);
printf("***************AddressOfFunctions****************\n");
for (DWORD i = 0; i < _image_export_directory->NumberOfFunctions; i++) {
printf("%08X\n", *(AddressOfFunctions + i));
}
//打印出函数序号表(记得加Base)
WORD* AddressOfNameOrdinals = (WORD*)(RvaToFoa(FileBufferPoint, _image_export_directory->AddressOfNameOrdinals) + (DWORD)FileBufferPoint);
printf("***************AddressOfNameOrdinals****************\n");
for (int i = 0; i < _image_export_directory->NumberOfNames; i++) {
printf("%04X\n", *(AddressOfNameOrdinals + i) + _image_export_directory->Base);
}
//打印名字
char** functionNameTable = (char**)(RvaToFoa(FileBufferPoint, _image_export_directory->AddressOfNames) + (DWORD)FileBufferPoint);
printf("\n***************FunctionNameTable****************\n");
for (int i = 0; i < _image_export_directory->NumberOfNames; i++) {
printf("%08X --> %s\n", *(functionNameTable + i), (char*)(RvaToFoa(FileBufferPoint, (int)*(functionNameTable + i)) + (DWORD)FileBufferPoint));
}
printf("\n***************全部内容打印完毕****************\n");
}
int SearchByName(char* FileBufferPoint ,char* FucName) {
//SearchByName:根据函数的名字去搜索函数所在的位置
//参数说明:
//FileBufferPoint:指向可执行文件在FileBuffer的地址
//FucName:记录了要搜索的函数的名字
//返回值说明:
//如果搜索到了函数的名字则返回地址,如果函数不存在则返回0
_IMAGE_DOS_HEADER* _image_dos_header = NULL;
_IMAGE_FILE_HEADER* _image_file_header = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
_IMAGE_SECTION_HEADER* _image_section_header = NULL;
_IMAGE_DATA_DIRECTORY* _image_data_directory = NULL;
_IMAGE_EXPORT_DIRECTORY* _image_export_directory = NULL;
_image_dos_header = (_IMAGE_DOS_HEADER*)FileBufferPoint;
//下面这个别忘记了还有一个PE标记的大小,为4个字节
_image_file_header = (_IMAGE_FILE_HEADER*)(FileBufferPoint + _image_dos_header->e_lfanew + sizeof(PE));
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_data_directory = (_IMAGE_DATA_DIRECTORY*)_image_optional_header->DataDirectory;
_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + _image_file_header->SizeOfOptionalHeader);
if (_image_data_directory->VirtualAddress == 0) {
printf("该二进制文件没有导出表");
getchar();
exit(0);
}
int FoaExportTables = RvaToFoa(FileBufferPoint, _image_data_directory->VirtualAddress) + (DWORD)FileBufferPoint;
_image_export_directory = (_IMAGE_EXPORT_DIRECTORY*)FoaExportTables;
int flag = 0;
int index;
int* FunctionNameTables = (int*)(RvaToFoa(FileBufferPoint, _image_export_directory->AddressOfNames) + (DWORD)FileBufferPoint);
for (int i = 0; i < _image_export_directory->NumberOfNames;i++) {
char* TempName = (char*)(RvaToFoa(FileBufferPoint, *FunctionNameTables) + (DWORD)FileBufferPoint);
if (strcmp(TempName, FucName) == 0) {
flag = 1;
index = i;
break;
}
FunctionNameTables++;
}
if (flag == 1) {
WORD* AddressOfNameOrdinals = (WORD*)(RvaToFoa(FileBufferPoint, _image_export_directory->AddressOfNameOrdinals) + (DWORD)FileBufferPoint);
WORD FucIndex = *(AddressOfNameOrdinals + index);
int* AddressOfFunctions = (int*)(RvaToFoa(FileBufferPoint, _image_export_directory->AddressOfFunctions) + (DWORD)FileBufferPoint);
return *(AddressOfFunctions + FucIndex);
}
else {
printf("该功能函数不存在,请检查输入\n");
}
}
int SearchByNum(char* FileBufferPoint,int Num) {
//SearchByNum:根据函数的序号去搜索函数所在的位置
//参数说明:
//FileBufferPoint:指向可执行文件在FileBuffer的地址
//FucNum:记录了要搜索的函数的序号
//返回值说明:
//如果搜索到了函数的序号则返回地址,如果函数不存在则返回0
_IMAGE_DOS_HEADER* _image_dos_header = NULL;
_IMAGE_FILE_HEADER* _image_file_header = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
_IMAGE_SECTION_HEADER* _image_section_header = NULL;
_IMAGE_DATA_DIRECTORY* _image_data_directory = NULL;
_IMAGE_EXPORT_DIRECTORY* _image_export_directory = NULL;
_image_dos_header = (_IMAGE_DOS_HEADER*)FileBufferPoint;
//下面这个别忘记了还有一个PE标记的大小,为4个字节
_image_file_header = (_IMAGE_FILE_HEADER*)(FileBufferPoint + _image_dos_header->e_lfanew + sizeof(PE));
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_data_directory = (_IMAGE_DATA_DIRECTORY*)_image_optional_header->DataDirectory;
_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + _image_file_header->SizeOfOptionalHeader);
if (_image_data_directory->VirtualAddress == 0) {
printf("该二进制文件没有导出表");
getchar();
exit(0);
}
int FoaExportTables = RvaToFoa(FileBufferPoint, _image_data_directory->VirtualAddress) + (DWORD)FileBufferPoint;
_image_export_directory = (_IMAGE_EXPORT_DIRECTORY*)FoaExportTables;
int a = Num - _image_export_directory->Base;
if (Num - _image_export_directory->Base > _image_export_directory->NumberOfFunctions) {
printf("该函数不存在,请检查输入的序号");
getchar();
exit(0);
}
int* AddressOfFunctions = (int*)(RvaToFoa(FileBufferPoint, _image_export_directory->AddressOfFunctions) + (DWORD)FileBufferPoint);
int FucIndex = Num - _image_export_directory->Base;
return *(AddressOfFunctions + FucIndex);
}
下面是对程序代码的验证