在上一版 LdrLayoutPe 中,VirtualAlloc 直接封裝在函數內部。這導致兩大局限:
VirtualAlloc,需改用 MmAllocateIndependentPages 或 ExAllocatePoolWithTag + MDL 映射。重構後的 LdrLayoutPe 將內存管理職責上移至調用方,實現「純邏輯映射」,大幅提升復用性。
/* 將文件內容按虛擬地址信息映射到預分配的地址空間
* 參數:
* pImage - 源PE文件緩衝區
* FileSize - 文件大小
* pRealImageBase- 調用方預分配的目標基址 (已提交、可寫)
* SizeOfImage - 目標區域大小 (需 >= OptionalHeader.SizeOfImage)
* 返回: TRUE/FALSE 表示映射成敗
*/
BOOL LdrLayoutPe(char* pImage, DWORD FileSize, char* pRealImageBase, DWORD SizeOfImage)
{
PIMAGE_FILE_HEADER pFileHeader;
PIMAGE_SECTION_HEADER pSectionHeader;
char* pSrc;
char* pDst;
DWORD CopySize;
DWORD i;
// 1. 校驗源文件合法性
if(!LdrValidateImage(pImage, FileSize))
return FALSE;
// 2. 解析頭部結構
pFileHeader = _GetFileHeaderFromPe(pImage);
pSectionHeader = _GetSectionHeaderFromPe(pFileHeader);
// 3. 拷貝文件頭 (含 Section Table)
pDst = pRealImageBase;
pSrc = pImage;
CopySize = (char*)pSectionHeader - pImage + pFileHeader->NumberOfSections * sizeof(IMAGE_SECTION_HEADER);
// 邊界檢查:防止頭部拷貝溢出目標緩衝區
if(pDst + CopySize >= pRealImageBase + SizeOfImage)
return FALSE;
RtlCopyMemory(pDst, pSrc, CopySize);
// 4. 逐節區拷貝數據,帶嚴格邊界保護
for(i = 0; i < pFileHeader->NumberOfSections; i++){
if(!pSectionHeader[i].PointerToRawData)
continue; // 無文件數據的節區 (如 .bss) 跳過拷貝,內存已由 VirtualAlloc 零初始化
pDst = pRealImageBase + pSectionHeader[i].VirtualAddress;
pSrc = pImage + pSectionHeader[i].PointerToRawData;
CopySize = pSectionHeader[i].SizeOfRawData;
// 關鍵防禦:確保拷貝不超出 SizeOfImage 邊界
if(pDst + CopySize >= pRealImageBase + SizeOfImage)
return FALSE;
RtlCopyMemory(pDst, pSrc, CopySize);
}
return TRUE;
}
| 場景 | 調用方實現策略 |
|---|---|
| 用戶態普通加載 | VirtualAlloc(NULL, SizeOfImage, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE) |
| 指定基址加載 (需重定位) | VirtualAlloc(pPreferredBase, ...) 失敗則 fallback |
| 大頁內存優化 | VirtualAlloc(..., MEM_LARGE_PAGES) 需鎖定頁面權限 |
| 內核驅動加載 | MmAllocateIndependentPages + MmMapLockedPagesSpecifyCache |
| 共享內存映射 | CreateFileMapping + MapViewOfFile 實現進程間映像共享 |
| 單元測試 Mock | 預分配堆緩衝區 malloc(SizeOfImage) 注入測試 |
LdrValidateImage 確保輸入非畸形 PE。pDst + CopySize >= pRealImageBase + SizeOfImage 徹底杜緩衝區溢出,這在處理惡意構造的 PE (如異常大的 VirtualAddress) 時至關重要。PAGE_READWRITE;映射完成後,調用方應根據節區屬性調用 VirtualProtect 降權 (RX/RW/RO)。.bss 類節區 (PointerToRawData == 0) 依賴 VirtualAlloc 的零頁特性;若調用方使用 malloc 需自行 memset。pRealImageBase,若與 ImageBase 不同,調用方可立即調用 LdrProcessRelocs(pRealImageBase, delta) 完成重定位。BOOL LoadPeImage(char* pFileBuffer, DWORD dwFileSize, HMODULE* phMod)
{
// 1. 解析 SizeOfImage (需先輕量解析 OptionalHeader)
DWORD sizeOfImage = GetPeSizeOfImage(pFileBuffer);
if(!sizeOfImage) return FALSE;
// 2. 分配目標內存 (此處演示系統自動選址)
LPVOID pMem = VirtualAlloc(NULL, sizeOfImage, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
if(!pMem) return FALSE;
// 3. 純邏輯映射
if(!LdrLayoutPe(pFileBuffer, dwFileSize, (char*)pMem, sizeOfImage)){
VirtualFree(pMem, 0, MEM_RELEASE);
return FALSE;
}
// 4. 後續處理: 重定位 -> IAT綁定 -> TLS回調 -> EntryPoint調用
// ...
*phMod = (HMODULE)pMem;
return TRUE;
}
通過「內存分配外置化」,LdrLayoutPe 從一個耦合 Win32 API 的工具函數蛻變為平台無關的 PE 映射核心。這不僅滿足驅動層、EFI、裸機、跨平台模擬器等異構環境的需求,更貫徹了單一職責原則與依賴倒置原則,是系統級組件庫設計的典範範例。