11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)

目录

导出表定位以及解析(手动)

代码定位导出表并打印其数据

通过函数名查找导出函数地址

通过导出函数序号查找函数地址

移动导出表


PE中的导出表通常存在于动态链接库文件里.有些EXE也会存在导出表.导出表的主要作用是将PE中存在的函数引出到外部,以便其他人可以使用这些函数,实现代码的重用.导出表的存在可以让程序的开发者很容易清楚PE中到底有多少可以使用的函数.

Windows装载器在进行PE装载时,将与进程相关的DLL加载到对应虚拟地址空间.会根据导入表中登记的与该动态链接库相关的由INT指向的名称或编号来遍历DLL所在虚拟地址空间,通过函数名或编号查找导出表结构,从而确定该导出函数在虚拟地址空间中的起始地址VA,并将VA覆盖导入表的IAT相关项.

导出数据所在的节通常被命名为.edata,它包含了一些可被其他EXE程序访问的符号的相关信息,比如导出函数和资源等.这些符号通常出现在DLL中,但DLL也可以包含导入符号,而且在某些EXE中也可以有导出符号.

导出表定位以及解析(手动)

IMAGE_EXPORT_DIRECTORY

结构及成员含义如下:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory

typedef struct _IMAGE_EXPORT_DIRECTORY {              
    DWORD   Characteristics;        // 未使用      
    DWORD   TimeDateStamp;          // 时间戳      
    WORD    MajorVersion;           // 未使用      
    WORD    MinorVersion;           // 未使用      
    DWORD   Name;                   // 指向该导出表的文件名字符串RVA    
    DWORD   Base;                   // 导出函数的起始序号      
    DWORD   NumberOfFunctions;      // 所有导出函数的个数      
    DWORD   NumberOfNames;          // 以函数名字导出的函数个数      
    DWORD   AddressOfFunctions;     // 导出函数地址表RVA              
    DWORD   AddressOfNames;         // 导出函数名称表RVA              
    DWORD   AddressOfNameOrdinals;  // 导出函数序号表RVA               
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;              

定位导出表:

IMAGE_OPTIONAL_HEADER -> DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT(0)].VirtualAddress + ImageBase.(如果在文件中解析只需将RVA转换为FOA加上当前文件首地址即可定位导出表).

通过WinHex工具查看PE文件默认属性

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第1张图片

上述解析得知:

内存对齐 = 1000h

文件对齐 = 200h

导出表RVA = 0x00116640

转换为FOA = 0x000C8E40(RVA与FOA转换讲解)

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第2张图片

成员含义:

Characteristics;// 未使用

TimeDateStamp;// 时间戳

MajorVersion;// 未使用

MinorVersion;// 未使用

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第3张图片

Name;// 指向该导出表的文件名字符串,该值为RVA

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第4张图片

Base;// 导出函数起始序号.(默认导出序号从1开始,但是通过xxx.def文件可自定义函数导出序号).

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第5张图片

NumberOfFunctions;// 所有导出函数的个数.该值并不一定是真正导出函数个数.计算公式为:导出最大序号 - 导出最小序号 + 1.

这个DLL我自己编写测试的.实际导出函数为4个.序号为2,4,5,6.(6 - 2 + 1就是NumberOfFunctions结果).

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第6张图片

NumberOfNames;// 以函数名字导出的函数个数.(通过xxx.def文件可以定义函数不导出名字).

测试DLL .def定义如下:

LIBRARY
  EXPORTS
    Add     @2
    Sub     @6
    Mul     @4 NONAME
    Div     @5

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第7张图片

AddressOfFunctions;// 导出函数地址表,该值为RVA,该成员相当于指向元素大小为4字节的数组,元素个数由NumberOfFunctions提供,每项元素为导出函数地址的RVA(加上ImageBase才是函数真正的地址).

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第8张图片

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第9张图片

AddressOfNames;// 导出函数名称表,该值为RVA,该成员相当于指向元素大小为4字节的数组,元素个数由 NumberOfNames提供,每项元素为导出函数名的RVA.

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第10张图片

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第11张图片

AddressOfNameOrdinals;// 导出函数序号表,该值为RVA,该成员相当于指向元素大小为2字节的数组,元素个数由 NumberOfNames提供,元素值加上Base为函数导出序号.通过函数名查找函数地址需要通过此值.

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第12张图片

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第13张图片

上述解析全解如下图:

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第14张图片

导出表相对简单,只需要了解三张表结构以及含义很容易理解其对应关系.

代码定位导出表并打印其数据

读取文件代码

PVOID FileToMem(IN PCHAR szFilePath, OUT LPDWORD dwFileSize)
{
	//打开文件
	FILE* pFile = fopen(szFilePath, "rb");
	if (!pFile)
	{
		printf("FileToMem fopen Fail \r\n");
		return NULL;
	}
 
	//获取文件长度
	fseek(pFile, 0, SEEK_END);			//SEEK_END文件结尾
	DWORD Size = ftell(pFile);
	fseek(pFile, 0, SEEK_SET);			//SEEK_SET文件开头
 
	//申请存储文件数据缓冲区
	PCHAR pFileBuffer = (PCHAR)malloc(Size);
	if (!pFileBuffer)
	{
		printf("FileToMem malloc Fail \r\n");
		fclose(pFile);
		return NULL;
	}
 
	//读取文件数据
	fread(pFileBuffer, Size, 1, pFile);
 
	//判断是否为可执行文件
	if (*(PSHORT)pFileBuffer != IMAGE_DOS_SIGNATURE)
	{
		printf("Error: MZ \r\n");
		fclose(pFile);
		free(pFileBuffer);
		return NULL;
	}
 
	if (*(PDWORD)(pFileBuffer + *(PDWORD)(pFileBuffer + 0x3C)) != IMAGE_NT_SIGNATURE)
	{
		printf("Error: PE \r\n");
		fclose(pFile);
		free(pFileBuffer);
		return NULL;
	}
 
	if (dwFileSize)
	{
		*dwFileSize = Size;
	}
 
	fclose(pFile);
 
	return pFileBuffer;
}

输出文件代码

VOID MemToFile(IN PCHAR szFilePath, IN PVOID pFileBuffer, IN DWORD dwFileSize)
{
	//打开文件
	FILE* pFile = fopen(szFilePath, "wb");
	if (!pFile)
	{
		printf("MemToFile fopen Fail \r\n");
		return;
	}
 
	//输出文件
	fwrite(pFileBuffer, dwFileSize, 1, pFile);
 
	fclose(pFile);
}

定位并输出导出表数据

VOID PrintExportInfo()
{
	//读取文件二进制数据
	DWORD dwFileSize = 0;
	PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize);
	if (!pFileBuffer)
	{
		return;
	}

	//定位结构
	PIMAGE_DOS_HEADER        pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
	PIMAGE_NT_HEADERS        pNth = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
	PIMAGE_FILE_HEADER		 pFil = (PIMAGE_FILE_HEADER)((PUCHAR)pNth + 4);
	PIMAGE_OPTIONAL_HEADER   pOpo = (PIMAGE_OPTIONAL_HEADER)((PUCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);

	//判断该PE文件是否有导出表
	if (!pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
	{
		printf("该PE文件不存在导出表 \r\n");
		return;
	}
	PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(pFileBuffer + RvaToFoa(pFileBuffer, pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));

	//基本导出表信息
	printf("IMAGE_EXPORT_DIRECTORY.Characteristics  -> [0x%08x] \r\n", pExp->Characteristics);
	printf("IMAGE_EXPORT_DIRECTORY.TimeDateStamp  -> [0x%08x] \r\n", pExp->TimeDateStamp);
	printf("IMAGE_EXPORT_DIRECTORY.MajorVersion  -> [0x%04x] \r\n", pExp->MajorVersion);
	printf("IMAGE_EXPORT_DIRECTORY.MinorVersion  -> [0x%04x] \r\n", pExp->MinorVersion);
	printf("IMAGE_EXPORT_DIRECTORY.Name -> [%s] \r\n", pFileBuffer + RvaToFoa(pFileBuffer, pExp->Name));
	printf("IMAGE_EXPORT_DIRECTORY.Base  -> [0x%08x] \r\n", pExp->Base);
	printf("IMAGE_EXPORT_DIRECTORY.NumberOfFunctions  -> [0x%08x] \r\n", pExp->NumberOfFunctions);
	printf("IMAGE_EXPORT_DIRECTORY.NumberOfNames  -> [0x%08x] \r\n", pExp->NumberOfNames);

	//函数地址表
	printf("IMAGE_EXPORT_DIRECTORY.AddressOfFunctions  -> [0x%08x] \r\n", pExp->AddressOfFunctions);
	LPDWORD pAddFunc = (LPDWORD)(pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfFunctions));
	for (size_t i = 0; i < pExp->NumberOfFunctions; i++)
	{
		printf("AddressOfFunctions[%d] -> [0x%08x] \r\n", i, pAddFunc[i]);
	}

	//函数名称表
	printf("IMAGE_EXPORT_DIRECTORY.AddressOfNames  -> [0x%08x] \r\n", pExp->AddressOfNames);
	LPDWORD pAddName = (LPDWORD)(pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfNames));
	for (size_t i = 0; i < pExp->NumberOfNames; i++)
	{
		printf("AddressOfNames[%d] -> [%s] \r\n", i, pFileBuffer + RvaToFoa(pFileBuffer, pAddName[i]));
	}

	//函数序号表
	printf("IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals  -> [0x%08x] \r\n", pExp->AddressOfNameOrdinals);
	LPWORD pAddOrdi = (LPWORD)(pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfNameOrdinals));
	for (size_t i = 0; i < pExp->NumberOfNames; i++)
	{
		printf("AddressOfNameOrdinals[%d] -> [0x%04x] \r\n", i, pAddOrdi[i]);
	}

	//PE工具解析如下
	pAddFunc = (LPDWORD)(pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfFunctions));
	pAddName = (LPDWORD)(pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfNames));
	pAddOrdi = (LPWORD)(pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfNameOrdinals));
	size_t i, j;
	BOOL bFlag = FALSE;

	for (i = 0; i < pExp->NumberOfFunctions; i++)
	{
		//函数地址表中值为空直接跳过本次循环
		if (pAddFunc[i] == NULL)
		{
			printf("Ordinal[%04x] RVA[0x%08x] Name[NULL] \r\n", i + pExp->Base, pAddFunc[i]);
			continue;
		}

		//判断导出方式
		bFlag = FALSE;

		for ( j = 0; j < pExp->NumberOfNames; j++)
		{
			if (pAddOrdi[j] == i)
			{
				//通过函数名导出
				bFlag = TRUE;
				printf("Ordinal[%04x] RVA[0x%08x] Name[%s] \r\n", pAddOrdi[j] + pExp->Base, pAddFunc[pAddOrdi[j]], pFileBuffer + RvaToFoa(pFileBuffer, pAddName[j]));
				break;
			}
		}

		if (!bFlag)
		{
			//通过序号导出
			printf("Ordinal[%04x] RVA[0x%08x] Name[NULL] \r\n", i + pExp->Base, pAddFunc[i]);
		}

	}

}

通过函数名查找导出函数地址

  • 通过导出表结构定位函数名称表(RVA),通过函数名称表中的值(RVA)定位导出函数名,函数名称表可以看作4字节数组,通过遍历数组查找是否有对应函数名.
  • 如果函数名称表中查找到对应函数名,将函数名称表中索引值作为下标去函数序号表中查找对应值. 
  • 函数序号表中的值作为函数地址表中的下标即可得到导出函数地址(RVA).

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第15张图片

//通过函数名查找导出函数地址
PVOID GetFunctionAddrByName(PCHAR pFileBuffer, PCHAR szName)
{
  //定位结构
  PIMAGE_DOS_HEADER        pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
  PIMAGE_NT_HEADERS        pNth = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
  PIMAGE_FILE_HEADER     pFil = (PIMAGE_FILE_HEADER)((PUCHAR)pNth + 4);
  PIMAGE_OPTIONAL_HEADER   pOpo = (PIMAGE_OPTIONAL_HEADER)((PUCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);

  //判断该PE文件是否有导出表
  if (!pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
  {
    printf("该PE文件不存在导出表 \r\n");
    return;
  }
  PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(pFileBuffer + RvaToFoa(pFileBuffer, pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));

  //定位地址表,名称表,序号表
  LPDWORD pAddFunc = (LPDWORD)(pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfFunctions));
  LPDWORD pAddName = (LPDWORD)(pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfNames));
  LPWORD pAddOrdi = (LPWORD)(pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfNameOrdinals));

  for (size_t i = 0; i < pExp->NumberOfNames; i++)
  {
    if (strcmp(szName,(PCSTR)(pFileBuffer + RvaToFoa(pFileBuffer, pAddName[i]))) == 0)
    {
      //此值+ImageBase为真正函数地址
      return pAddFunc[pAddOrdi[i]];
    }
  }

  return NULL;
}

通过导出函数序号查找函数地址

  • 查询序号 - IMAGE_EXPORT_DIRECTORY.Base = Index 
  • Index作为函数地址表中的下标即可得到导出函数地址.   
//通过导出函数序号查找函数地址
PVOID GetFunctionAddrByOrdinals(PCHAR pFileBuffer, DWORD dwOrdinal)
{
  //定位结构
  PIMAGE_DOS_HEADER        pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
  PIMAGE_NT_HEADERS        pNth = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
  PIMAGE_FILE_HEADER     pFil = (PIMAGE_FILE_HEADER)((PUCHAR)pNth + 4);
  PIMAGE_OPTIONAL_HEADER   pOpo = (PIMAGE_OPTIONAL_HEADER)((PUCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);

  //判断该PE文件是否有导出表
  if (!pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
  {
    printf("该PE文件不存在导出表 \r\n");
    return;
  }
  PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(pFileBuffer + RvaToFoa(pFileBuffer, pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));

  //定位地址表,名称表,序号表
  LPDWORD pAddFunc = (LPDWORD)(pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfFunctions));
  
  //判断序号是否正常
  if (dwOrdinal - pExp->Base > pExp->NumberOfFunctions)
  {
    return NULL;
  }

  return pAddFunc[dwOrdinal - pExp->Base];
}

移动导出表

  • 新增节用来存储新导出表
  • 复制默认导出表数据到新增节起始位置,并修正IMAGE_OPTION_HEADER目录项中VirtualAddress
  • 修正导出表结构中Name成员(RVA)
  • 复制导出函数地址表.直接复制即可,并修正AddressOfFunctions.
  • 复制导出函数名称表.修正AddressOfNames.通过NumberOfNames循环拷贝默认函数名并修正其对应RVA.
  • 复制导出函数序号表.直接复制即可,并修正AddressOfNameOrdinals.
VOID MoveExportTable()
{
	//读取文件二进制数据
	DWORD dwFileSize = 0;
	PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize);
	if (!pFileBuffer)
	{
		return;
	}

	//新增节存储导出表
	pFileBuffer = AddNewSection(pFileBuffer, 0x2000, &dwFileSize);

	//定位结构
	PIMAGE_DOS_HEADER        pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
	PIMAGE_NT_HEADERS        pNth = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
	PIMAGE_FILE_HEADER		 pFil = (PIMAGE_FILE_HEADER)((PUCHAR)pNth + 4);
	PIMAGE_OPTIONAL_HEADER   pOpo = (PIMAGE_OPTIONAL_HEADER)((PUCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);
	PIMAGE_SECTION_HEADER    pSec = (PIMAGE_SECTION_HEADER)((PUCHAR)pOpo + pFil->SizeOfOptionalHeader);

	//判断该PE文件是否有导出表
	if (!pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
	{
		printf("该PE文件不存在导出表 \r\n");
		return;
	}
	PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(pFileBuffer + RvaToFoa(pFileBuffer, pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));

	//导出地址表,名称表,序号表大小
	DWORD dwSizeOfAddrFunc = 4 * pExp->NumberOfFunctions;
	DWORD dwSizeOfAddrName = 4 * pExp->NumberOfNames;
	DWORD dwSizeOfAddrOrdi = 2 * pExp->NumberOfNames;

	//修复可选PE头中目录项指向
	PUCHAR pCurrent = pFileBuffer + pSec[pFil->NumberOfSections - 1].PointerToRawData;
	pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress = FoaToRva(pFileBuffer, pCurrent - pFileBuffer);

	//拷贝导出表结构
	PIMAGE_EXPORT_DIRECTORY pNewExp = (PIMAGE_EXPORT_DIRECTORY)pCurrent;
	memcpy(pCurrent, pExp, sizeof(IMAGE_SECTION_HEADER));
	pCurrent += sizeof(IMAGE_SECTION_HEADER);

	//拷贝导出表结构中Name成员	
	DWORD dw = RvaToFoa(pFileBuffer, pExp->Name);
	dw += (DWORD)pFileBuffer;
	PUCHAR pOldDllName = pFileBuffer + RvaToFoa(pFileBuffer, pExp->Name);
	DWORD dwDllNameLength = strlen(pOldDllName);
	memcpy(pCurrent, pOldDllName, dwDllNameLength);
	//修复导出表结构中Name指向
	pNewExp->Name = FoaToRva(pFileBuffer, pCurrent - pFileBuffer);
	pCurrent = pCurrent + dwDllNameLength + 1;

	//拷贝地址表数据
	PUCHAR pOldAddrFunc = pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfFunctions);
	memcpy(pCurrent, pOldAddrFunc, dwSizeOfAddrFunc);
	//修复导出表结构中AddressOfFunctions指向
	pNewExp->AddressOfFunctions = FoaToRva(pFileBuffer, pCurrent - pFileBuffer);
	pCurrent += dwSizeOfAddrFunc;

	//拷贝名称表数据
	PDWORD pOldAddrName = pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfNames);
	memcpy(pCurrent, pOldAddrName, dwSizeOfAddrName);
	//修复导出表结构中AddressOfNames指向
	pNewExp->AddressOfNames = FoaToRva(pFileBuffer, pCurrent - pFileBuffer);
	//指向当前导出名称表
	PDWORD pNewAddrName = pCurrent;
	pCurrent += dwSizeOfAddrName;
	for (size_t i = 0; i < pExp->NumberOfNames; i++)
	{
		//遍历并修复导出表名称表的指向

		//默认函数地址表中RVA转FOA并获取文件中地址
		PUCHAR pOldNameAddr = pFileBuffer + RvaToFoa(pFileBuffer, pOldAddrName[i]);
		//获取函数名长度
		DWORD dwFunNameSize = strlen(pOldNameAddr) + 1;
		//拷贝函数名
		memcpy(pCurrent, pOldNameAddr, dwFunNameSize);
		//修正新导出名称表中地址
		pNewAddrName[i] = FoaToRva(pFileBuffer, pCurrent - pFileBuffer);

		pCurrent += dwFunNameSize;
	}

	//拷贝序号表数据
	PUCHAR pOldAddrOrdi = pFileBuffer + RvaToFoa(pFileBuffer, pExp->AddressOfNameOrdinals);
	memcpy(pCurrent, pOldAddrOrdi, dwSizeOfAddrOrdi);
	//修复导出表结构中AddressOfNameOrdinals指向
	pNewExp->AddressOfNameOrdinals = FoaToRva(pFileBuffer, pCurrent - pFileBuffer);
	pCurrent += dwSizeOfAddrOrdi;

	//将二进制数据输出到文件
	MemToFile(FILE_PATH_OUT, pFileBuffer, dwFileSize);
}

查看移动后导出表位置

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第16张图片

移动后解析数据无误

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第17张图片

获取函数地址成功

11.PE文件之导出表(IMAGE_EXPORT_DIRECTORY)_第18张图片

你可能感兴趣的:(Pe文件解析,PE文件,安全,安全漏洞,C语言,C++)