可选PE头的最后一个字段是数据目录表,是大小为16的结构体数组
该表记录了导出的函数,记录当前的PE文件有哪些函数可以被别人使用
一般而言导出表存在于DLL中给EXE使用,但这也不是绝对
xxxxxxxxxxIMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];xxxxxxxxxxtypedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;通过VirtualAddress找到每一项在内存中的偏移,数据目录第一个结构体就是导出表
导出表的结构
xxxxxxxxxxtypedef 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头
xxxxxxxxxxif (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);拿到第一个数据目录结构体,对应导出表
xxxxxxxxxxpDataDirectory = pOptionHeader->DataDirectory;将第一个数据目录的VirtualAddress转为FOA地址(文件偏移)
RvaToFileOffset这个函数在PE学习(6)文章中已经详细介绍:如何将内存相对偏移转为文件偏移
xxxxxxxxxxFoaAddress = RvaToFileOffset(pFileBuffer, pDataDirectory->VirtualAddress);通过这个地址即可得到文件中导出表的地址
xxxxxxxxxxpExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader + FoaAddress);
简单格式化打印下
xxxxxxxxxxprintf("***********************************************************\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拿到真正的值
xxxxxxxxxxDWORD 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)
xxxxxxxxxxPBYTE 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);
遍历函数地址表,根据函数地址索引尝试在序号表里查找
xxxxxxxxxxfor (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值
xxxxxxxxxxif (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);}如果根据函数地址索引找不到对应的序号,且这个函数地址记录不为空,则这是一个隐藏函数
xxxxxxxxxxelse{ if (pAddressFunctionTable[n] != 0) { printf("隐藏函数"); }}我从电脑上随便找了一个DLL做实验

整体代码
xxxxxxxxxxVOID 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位中有另外的方式和办法