DLL的導出函數重定向機制


曾經,調試時跟進HeapAlloc,結果發現直接進入到ntdll的RtlAllocateHeap中,感到很有趣,就使用Dependency Walker查看kernel32.dll的導出函數,結果發現HeapAlloc的地址直接顯示的就是NTDLL.RtlAllocateHeap。於是反匯編查看kernel32.dll文件,發現本以為是匯編代碼的HeapAlloc的函數體就是字符串NTDLL.RtlAllocateHeap。

想想以前也曾經自己實現過GetProcAddress,就是直接從導出表獲取地址返回而已。照這樣來看這樣實現肯定是不完善的。這到底是如何設計的?查閱了一下《Windows PE權威指南》,在導出表一章沒有找到相關說明。又看了一下微軟的PE COFF格式文檔,也沒有找到相關信息。於是決定自己來研究一下。

先仔細的看了一下導出表相關各結構體的定義,沒有覺得有哪個字段標明某個函數是真實的代碼還是重定向字符串。就自己來分析PE文件,在分析文件時注意到PE文件中所有重定向字符串和IMAGE_EXPORT_DIRECTORY結構的位置布局,感覺這些字符串應該是位於數據目錄IMAGE_DIRECTORY_ENTRY_EXPORT包括的地址范圍內,也就是說如果導出函數地址位於此范圍,就是重定向函數,因為數據目錄IMAGE_DIRECTORY_ENTRY_EXPORT不應該包含任何可執行代碼的。

於是照此思路編寫代碼測試了一下,結果與猜想一致。但是這畢竟只是推測,還沒有找到官方證實。想到對導出函數重定向的支持代碼Ldr里肯定會有,就從LdrGetProcedureAddress跟到LdrpSnapThunk去分析,最終在LdrpSnapThunk里找到了相關的代碼,主要邏輯就是先從導出表中找到對應函數的地址,然后判斷函數地址是否在數據目錄IMAGE_DIRECTORY_ENTRY_EXPORT所指的地址范圍內,如果不在則是真實地址,在此范圍則需要進一步重定向。

例如,用如下代碼遍歷kernel32.dll中的重定向導出函數:

#include <tchar.h>
#include <stdio.h>
#include <stddef.h>
#include <Windows.h>

VOID ListRedirects(HMODULE hModule)
{ if(NULL != hModule)   {  PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;  PIMAGE_OPTIONAL_HEADER pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)((PBYTE)hModule + pDosHeader->e_lfanew + offsetof(IMAGE_NT_HEADERS, OptionalHeader));  PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)hModule + pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);  LPCSTR lpstrLibraryName = (LPCSTR)hModule + pExportDirectory->Name;  PDWORD aryAddressOfFunctions = (PDWORD)((PBYTE)hModule + pExportDirectory->AddressOfFunctions);  PDWORD aryAddressOfNames = (PDWORD)((PBYTE)hModule + pExportDirectory->AddressOfNames);  LPWORD aryAddressOfNameOrdinals = (LPWORD)((PBYTE)hModule + pExportDirectory->AddressOfNameOrdinals);  DWORD dwIndex = 0;  while(dwIndex < pExportDirectory->NumberOfNames)   {  PCSTR pstrFunctionName = (PCSTR)hModule + aryAddressOfNames[dwIndex];  PVOID pFunctionAddress = (PBYTE)hModule + aryAddressOfFunctions[aryAddressOfNameOrdinals[dwIndex]];  if((PBYTE)pFunctionAddress > (PBYTE)pExportDirectory && (PBYTE)pFunctionAddress < (PBYTE)pExportDirectory + pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size) //LdrpSnapThunk中也是如此判斷   {  printf("%s.%s -> %s\r\n", lpstrLibraryName, pstrFunctionName, (PCSTR)pFunctionAddress);   }  ++dwIndex;   }   } }  int _tmain(int argc, _TCHAR* argv[]) {  HMODULE hModule = LoadLibraryW(L"kernel32.dll");   ListRedirects(hModule);  return 0; }

在VC中,使用#pragma預處理指令可以制作包含重定向函數的DLL:

#pragma comment(linker,"/export:HeapAlloc=NTDLL.RtlAllocateHeap,@2000")
#pragma comment(linker,"/export:HeapFree=NTDLL.RtlFreeHeap,@2001")
#pragma comment(linker,"/export:HeapReAlloc=NTDLL.RtlReAllocateHeap,@2002")
#pragma comment(linker,"/export:HeapSize=NTDLL.RtlSizeHeap,@2003")

@后面跟的是導出序號。

這樣把一個導出函數重定向到另一個DLL中的某個函數,與通過導入表引入另一個DLL中的某個函數是不同的。重定向函數只要不被其他模塊引入是不會被解析的,哪怕是重定向到一個根本不存在的DLL中或者指向某個根本不存在的函數,也不會影響當前模塊的正常加載。直到這個函數真正被使用,Ldr才會真正去定位它的真實地址,因為重定向的目標函數不會出現在當前模塊的導入表中。但是通過導入表引入的某個模塊或者函數不存在的話,在加載時就會報錯。

看了一下Windows NT 4.0的kernel32.dll,HeapAlloc的重定向已經有了,應該是從NT最初版本(手頭沒有)堆分配函數就已經被轉移到ntdll.dll中了。估計這種重定向技術在NT開發時作為向下兼容的一種手段在PE格式設計就被設計出來了。

Ok. That's all.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM