可选PE头的最后一个字段是数据目录表,是大小为16的结构体数组
该表记录了导出的函数,记录当前的PE文件有哪些函数可以被别人使用
一般而言导出表存在于DLL
中给EXE
使用,但这也不是绝对
xxxxxxxxxx
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
xxxxxxxxxx
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
通过VirtualAddress
找到每一项在内存中的偏移,数据目录第一个结构体就是导出表
导出表的结构
xxxxxxxxxx
typedef 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
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
该表项中的值是RVA
(加上ImageBase
才是函数真正的地址)
以上部分字段对应的关系
开头和之前的文章类似,解析DOS
和NT
头,拿到需要的可选PE头
xxxxxxxxxx
if (pFileBuffer == NULL)
{
return;
}
if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE)
{
return;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)
{
return;
}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
拿到第一个数据目录结构体,对应导出表
xxxxxxxxxx
pDataDirectory = pOptionHeader->DataDirectory;
将第一个数据目录的VirtualAddress
转为FOA
地址(文件偏移)
RvaToFileOffset
这个函数在PE学习(6)
文章中已经详细介绍:如何将内存相对偏移转为文件偏移
xxxxxxxxxx
FoaAddress = RvaToFileOffset(pFileBuffer, pDataDirectory->VirtualAddress);
通过这个地址即可得到文件中导出表的地址
xxxxxxxxxx
pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader + FoaAddress);
简单格式化打印下
xxxxxxxxxx
printf("***********************************************************\n");
printf("pExportDirectory->AddressOfFunctions : \t\t%#010X \n", pExportDirectory->AddressOfFunctions);
printf("pExportDirectory->AddressOfNames : \t\t%#010X \n", pExportDirectory->AddressOfNames);
printf("pExportDirectory->AddressOfNameOrdinals : \t%#010X \n", pExportDirectory->AddressOfNameOrdinals);
printf("pExportDirectory->Base : \t\t\t%#010X \n", pExportDirectory->Base);
printf("pExportDirectory->Characteristics : \t\t%#010X \n", pExportDirectory->Characteristics);
printf("pExportDirectory->MajorVersion : \t\t%#010X \n", pExportDirectory->MajorVersion);
printf("pExportDirectory->MinorVersion : \t\t%#010X \n", pExportDirectory->MinorVersion);
printf("pExportDirectory->Name : \t\t\t%#010X \n", pExportDirectory->Name);
printf("pExportDirectory->NumberOfFunctions : \t\t%#010X \n", pExportDirectory->NumberOfFunctions);
printf("pExportDirectory->NumberOfNames : \t\t%#010X \n", pExportDirectory->NumberOfNames);
printf("***********************************************************\n");
文章开头已经提到,导出表的Name
以及各种Address
都是内存偏移地址,需要使用RvaToFileOffset
函数得到FOA
文件偏移地址,进一步通过FileBuffer
拿到真正的值
xxxxxxxxxx
DWORD dwNameFoa = RvaToFileOffset(pFileBuffer, pExportDirectory->Name);
DWORD dwAddressOfNamesFoa = RvaToFileOffset(pFileBuffer, pExportDirectory->AddressOfNames);
DWORD dwAddressOfFunctionsFoa = RvaToFileOffset(pFileBuffer, pExportDirectory->AddressOfFunctions);
DWORD dwAddressOfOrdinalsFoa = RvaToFileOffset(pFileBuffer, pExportDirectory->AddressOfNameOrdinals);
拿到相对于FileBuffer
的指针,指向文件中导出表的内容
注意:序号表指针必须是PWORD
类型,否则后续操作会有问题(上图也有说明宽度是2)
xxxxxxxxxx
PBYTE pDllOrExeName = (PBYTE)((DWORD)pDosHeader + dwNameFoa);
printf("导出表的名称: %s \n", pDllOrExeName);
PDWORD pAddressFunctionTable = (PDWORD)((DWORD)pDosHeader + dwAddressOfFunctionsFoa);
PWORD pOrdinaFunctionTable = (PWORD)((DWORD)pDosHeader + dwAddressOfOrdinalsFoa);
PDWORD pNameFunctionTable = (PDWORD)((DWORD)pDosHeader + dwAddressOfNamesFoa);
遍历函数地址表,根据函数地址索引尝试在序号表里查找
xxxxxxxxxx
for (DWORD n = 0; n < pExportDirectory->NumberOfFunctions; n++)
{
BOOL indexNumIsExist = FALSE;
WORD nNameIndex = 0;
for (nNameIndex = 0; nNameIndex < pExportDirectory->NumberOfNames; nNameIndex++)
{
if ((WORD)n == (WORD)pOrdinaFunctionTable[nNameIndex])
{
indexNumIsExist = TRUE;
break;
}
}
// ...
}
如果成功在序号表里找到该函数的索引,可以进一步去函数名表根据序号索引拿到名称地址dwNameFoa
,这是一个RVA
地址,进行转换后从FileBuffer
开头处计算偏移地址,该处记录了具体的函数字符串
另外函数序号规定,函数导出序号 = 序号表保存的序号 + Base值
xxxxxxxxxx
if (indexNumIsExist == TRUE)
{
DWORD dwNameFoa = pNameFunctionTable[nNameIndex];
PBYTE pFunctionName = (PBYTE)((DWORD)pDosHeader + RvaToFileOffset(pFileBuffer, dwNameFoa));
WORD ord = (WORD)n + (WORD)pExportDirectory->Base;
printf("函数地址RVA: %#010X \t 函数序号: %d \t Name: [%s]",
pAddressFunctionTable[n],ord, pFunctionName);
}
如果根据函数地址索引找不到对应的序号,且这个函数地址记录不为空,则这是一个隐藏函数
xxxxxxxxxx
else
{
if (pAddressFunctionTable[n] != 0)
{
printf("隐藏函数");
}
}
我从电脑上随便找了一个DLL做实验
整体代码
xxxxxxxxxx
VOID FileBufferPrintDataDirectory(IN LPVOID pFileBuffer)
{
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL;
DWORD FoaAddress = 0;
if (pFileBuffer == NULL)
{
return;
}
if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE)
{
return;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)
{
return;
}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pDataDirectory = pOptionHeader->DataDirectory;
FoaAddress = RvaToFileOffset(pFileBuffer, pDataDirectory->VirtualAddress);
printf("FoaAddress: %#010X \n", FoaAddress);
pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader + FoaAddress);
printf("***********************************************************\n");
printf("pExportDirectory->AddressOfFunctions : \t\t%#010X \n", pExportDirectory->AddressOfFunctions);
printf("pExportDirectory->AddressOfNames : \t\t%#010X \n", pExportDirectory->AddressOfNames);
printf("pExportDirectory->AddressOfNameOrdinals : \t%#010X \n", pExportDirectory->AddressOfNameOrdinals);
printf("pExportDirectory->Base : \t\t\t%#010X \n", pExportDirectory->Base);
printf("pExportDirectory->Characteristics : \t\t%#010X \n", pExportDirectory->Characteristics);
printf("pExportDirectory->MajorVersion : \t\t%#010X \n", pExportDirectory->MajorVersion);
printf("pExportDirectory->MinorVersion : \t\t%#010X \n", pExportDirectory->MinorVersion);
printf("pExportDirectory->Name : \t\t\t%#010X \n", pExportDirectory->Name);
printf("pExportDirectory->NumberOfFunctions : \t\t%#010X \n", pExportDirectory->NumberOfFunctions);
printf("pExportDirectory->NumberOfNames : \t\t%#010X \n", pExportDirectory->NumberOfNames);
printf("***********************************************************\n");
DWORD dwNameFoa = RvaToFileOffset(pFileBuffer, pExportDirectory->Name);
DWORD dwAddressOfNamesFoa = RvaToFileOffset(pFileBuffer, pExportDirectory->AddressOfNames);
DWORD dwAddressOfFunctionsFoa = RvaToFileOffset(pFileBuffer, pExportDirectory->AddressOfFunctions);
DWORD dwAddressOfOrdinalsFoa = RvaToFileOffset(pFileBuffer, pExportDirectory->AddressOfNameOrdinals);
PBYTE pDllOrExeName = (PBYTE)((DWORD)pDosHeader + dwNameFoa);
printf("导出表的名称: %s \n", pDllOrExeName);
PDWORD pAddressFunctionTable = (PDWORD)((DWORD)pDosHeader + dwAddressOfFunctionsFoa);
PWORD pOrdinaFunctionTable = (PWORD)((DWORD)pDosHeader + dwAddressOfOrdinalsFoa);
PDWORD pNameFunctionTable = (PDWORD)((DWORD)pDosHeader + dwAddressOfNamesFoa);
for (DWORD n = 0; n < pExportDirectory->NumberOfFunctions; n++)
{
BOOL indexNumIsExist = FALSE;
WORD nNameIndex = 0;
for (nNameIndex = 0; nNameIndex < pExportDirectory->NumberOfNames; nNameIndex++)
{
if ((WORD)n == (WORD)pOrdinaFunctionTable[nNameIndex])
{
indexNumIsExist = TRUE;
break;
}
}
if (indexNumIsExist == TRUE)
{
DWORD dwNameFoa = pNameFunctionTable[nNameIndex];
PBYTE pFunctionName = (PBYTE)((DWORD)pDosHeader + RvaToFileOffset(pFileBuffer, dwNameFoa));
WORD ord = (WORD)n + (WORD)pExportDirectory->Base;
printf("函数地址RVA: %#010X \t 函数序号: %d \t Name: [%s]",
pAddressFunctionTable[n],ord, pFunctionName);
}
else
{
if (pAddressFunctionTable[n] != 0)
{
printf("隐藏函数");
}
}
printf("\n");
}
return;
}
主函数
xxxxxxxxxx
int main()
{
LPVOID pFileBuffer = NULL;
ReadPEFile((CHAR*)TEST_DLL, &pFileBuffer);
FileBufferPrintDataDirectory(pFileBuffer);
return 0;
}
读者可能看完这篇文章感觉云里雾里,可以参考下一篇文章,或许可以解开疑惑
另外,这里用到的代码,都仅限于32位,在64位中有另外的方式和办法