Windows 逆向-导出表

Oyst3r 于 2024-02-25 发布

前言

这块是真的麻烦,真的会有人写代码少了一个*,真的会调试一下午吗???

课堂

简介(引出)

前面学过一个 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  *
};

指向该导出表文件名字符串的 RVA,比如一个 messagebox.dll 的 PE 文件提供函数,那么这个 PE 文件的 Name 指向的字符串为为 messagebox.dll

导出函数起始序号(最小的序号),比如有序号为 14、6、10、8 的导出函数,那么 Base 的值为 6

所有导出函数的个数,注意:上篇文章讲过,这个值是通过导出函数的最大序号 - 最小序号 + 1 算出来的;正常来说这个值是多少,那么此 PE 文件中导出函数的个数就是多少。但是如果使用自定义序号,序号定义时不是连续的,而是中间有空缺的序号,那么此时 NumberOfFunctions 的值会比实际的定义的导出函数个数多,所以这个值是不准确的

函数名字导出的函数个数:比如以动态链接库的方式导出,如果导出时加了 NONAME 关键字,那么就不计数

导出函数名称表 RVA(拉伸后的内存地址偏移,所以要先转成 FOA),即这个地址指向一个表,这个表中记录的是导出函数的名称字符串地址!!不是直接存储名称(且此字符串地址也是 RVA),这个就和之前有一次的指针数组的练习一样

就像 C 语言中的字符串char* name = "abc";,name 变量值为 abc 这个字符串的首地址。所以导出函数名称表中存储的是 name 值(即名称的地址),而不是直接存储的 abc 这个名称

表中元素个数:由 NumberOfNames 决定,且和 AddressOfName 表一一对应的

如果导出时,定义的无名字函数,即Div @13 NONAME,那么函数名称表中就不会有指向 Div 函数名的元素,同样函数序号表中也不会有 Div 的相对序号,但是在函数地址表中会留出来一个元素位置存储 Div 函数地址

由导出表获取函数地址

那个序号表是给名字表查函数值做一个中转,现在比如一共四个函数,三个函数是按名字表导出的,而一个函数是按照序号表导出的,名字表里面肯定只有 3 个,而序号表里面也是只有 3 个,那个函数既然都没有名字,那它肯定也就没有对应的序号表去做中转了,序号查询的话就是完全和这个序号表没有任何关系的

根据函数的名字获取

按序号找函数地址

对比

作业

这三个功能我都放在了一个程序里面

项目的总体结构

头文件如下

#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);

}

下面是对程序代码的验证