最近整理PE文件相關代碼的時候,想到如果能在PE剛剛讀進內存的時候再去修改內存PE鏡像,那不是比直接對PE文件進行操作隱秘多了么?
PE文件在運行時會根據導入表來進行dll庫的“動態鏈接”,那么如果我們修改PE導入表結構,就可以實現對我們自己動態庫的導入,從而實現注入。
那么問題來了,選擇一個合適的時機顯得很重要,網上搜索了一下,大部分都是直接在文件上進行修改,有位同學說用LoadImageNotifyRoutine可以來實現。
每一個模塊加載前都能觸發SetLoadImageNotifyRoutine注冊的回調函數,然后獲得PE文件基地址,構造PE文件就可以實現注入了。
下面簡單復習一下PE文件導入表以及系統回調。
PE文件導入表
微軟對導入表結構體的定義
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) } DUMMYUNIONNAME; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and realdate\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp ofDLL bound to (Old BIND) DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR
值得注意的是:上述結構的是導入表數組中的一項,每個導入的 DLL 都會有一個結構,也就是說,一個這樣的結構對應一個導入的 DLL。
Characteristics 和 OriginalFirstThunk:一個聯合體,如果是數組的最后一項 Characteristics 為0,否則 OriginalFirstThunk 保存一個 RVA,指向一個 IMAGE_THUNK_DATA 的數組, 這個數組中的每一項表示一個導入函數。
TimeDateStamp: 映象綁定前,這個值是0,綁定后是導入模塊的時間戳。
ForwarderChain: 轉發鏈,如果沒有轉發器,這個值是-1。
Name: 一個 RVA,指向導入模塊的名字,所以一個 IMAGE_IMPORT_DESCRIPTOR 描 述一個導入的 DLL。
FirstThunk : 也是一個RVA,也指向一個IMAGE_THUNK_DATA 數組 。
既然OriginalFirstThunk與FirstThunk都指向一個IMAGE_THUNK_DATA數組,而且這兩個域的名字都長得很像,他倆有什么區別呢?
為了解答這個問題, 先來認識一下 IMAGE_THUNK_DATA 結構:
typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // PBYTE DWORD Function; // PDWORD DWORD Ordinal; DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
ForwarderString :是轉發用的,暫時不用考慮。
Function : 表示函數地址。
Ordinal : 如果是按序號導入 Ordinal 就有用了。如果 Ordinal 的最高位是1, 就是按序號導入的,這時候,低16位就是導入序號,如果最高位是0,則 AddressOfData 是一個RVA,指向一個IMAGE_IMPORT_BY_NAME結構,用來保存名字信息,
AddressOfData: 若是按名字導入 便指向名字信息。
可以看出這個結構體 就是一個大的 union,大家都知道 union 雖包含多個域但是在不同時刻代表不同的意義那到 底應該是名字還是序號,該如何區分呢?可以通過 Ordinal 判斷,由於Ordinal 和 AddressOfData 實際上是同一個內存空間,所以 AddressOfData 其實只有低31位可以表示RVA,但是一個 PE 文件不可能超過2G,所以最高位永遠為0,這樣設計很合理的利用了空間 。 實際編寫代碼的時候微軟提供兩個宏定義處理序號導入:IMAGE_SNAP_BY_ORDINAL 判斷是否按序號導入,IMAGE_ORDINAL 用來獲取導入序 號。
這時我們可以回頭看看 OriginalFirstThunk 與 FirstThunk,OriginalFirstThunk 指向的 IMAGE_THUNK_DATA 數組包含導入信息,在這個數組中只有 Ordinal 和 AddressOfData 是有用的,因此可以通過 OriginalFirstThunk 查找到函數的地址。FirstThunk 則略有不同, 在 PE 文件加載以前或者說在導入表未處理以前,他所指向的數組與 OriginalFirstThunk 中 的數組雖不是同一個,但是內容卻是相同的,都包含了導入信息,而在加載之后,FirstThunk 中的 Function 開始生效,他指向實際的函數地址,因為 FirstThunk 實際上指向 IAT 中的一 個位置,IAT 就充當了 IMAGE_THUNK_DATA 數組,加載完成后,這些 IAT 項就變成了實 際的函數地址,即 Function 的意義。
一圖勝千言:


這也就是為什么說導入表的是雙橋結構了。
1.導入表其實是一個 IMAGE_IMPORT_DESCRIPTOR 的數組,每個導入的 DLL 對應 一個 IMAGE_IMPORT_DESCRIPTOR。
2. IMAGE_IMPORT_DESCRIPTOR 包含兩個 IMAGE_THUNK_DATA 數組,數組中 的每一項對應一個導入函數。
3. 加載前OriginalFirstThunk與FirstThunk的數組都指向名字信息,加載后FirstThunk 數組指向實際的函數地址。
好了,回顧了這么多PE導入表知識點,下面看看系統回調。
系統回調
系統回調就是由系統執行回調函數,這個回調函數可以是用戶編寫的,但是必須是由系統調用
比如下面這幾種
LoadImageNotifyRoutine 模塊加載回調
CreateProcessNotifyRoutine 進程創建回調
CreateThreadNotifyRoutine 線程創建回調
CmRegisterCallback 注冊表回調
IoRegisterFsRegistrationChange 文件系統回調
......
由程序員注冊回調,系統函數在觸發條件下調用
所以就提供了注冊模塊加載回調然后獲得修改PE文件的條件
下面看看在回調函數中做了些什么
VOID Start ( IN PUNICODE_STRING FullImageName, IN HANDLE ProcessId, // where image is mapped IN PIMAGE_INFO ImageInfo ) { NTSTATUS ntStatus; PIMAGE_IMPORT_DESCRIPTOR pImportNew; HANDLE hProcessHandle; int nImportDllCount = 0; int size; IMAGE_IMPORT_DESCRIPTOR Add_ImportDesc; PULONG ulAddress; ULONG oldCr0; ULONG Func; PIMAGE_IMPORT_BY_NAME ptmp; IMAGE_THUNK_DATA *pOriginalThunkData; IMAGE_THUNK_DATA *pFirstThunkData; PIMAGE_BOUND_IMPORT_DESCRIPTOR pBoundImport; if(wcsstr(FullImageName->Buffer,L"calc.exe")!=NULL) { lpBuffer = NULL; lpDllName = NULL; lpExportApi = NULL; lpTemp = NULL; lpTemp2=NULL; g_eprocess = PsGetCurrentProcess(); g_ulPid = (ULONG)ProcessId; ulBaseImage = (ULONG)ImageInfo->ImageBase;// 進程基地址 pDos = (PIMAGE_DOS_HEADER) ulBaseImage; pHeader = (PIMAGE_NT_HEADERS)(ulBaseImage+(ULONG)pDos->e_lfanew); pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((ULONG)pHeader->OptionalHeader.DataDirectory[1].VirtualAddress + ulBaseImage); nImportDllCount = pHeader->OptionalHeader.DataDirectory[1].Size / sizeof(IMAGE_IMPORT_DESCRIPTOR); // 把原始值保存。 g_psaveDes = pImportDesc; ntStatus = ObOpenObjectByPointer(g_eprocess, OBJ_KERNEL_HANDLE, NULL, PROCESS_ALL_ACCESS , //PROCESS_WRITECOPY NULL, KernelMode, &hProcessHandle); if(!NT_SUCCESS(ntStatus)) return ; // 加上一個自己的結構。 size = sizeof(IMAGE_IMPORT_DESCRIPTOR) * (nImportDllCount + 1); // 分配導入表 ntStatus = ZwAllocateVirtualMemory(hProcessHandle, &lpBuffer, 0, &size, MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE); if(!NT_SUCCESS(ntStatus)) { ZwClose(hProcessHandle); return ; } RtlZeroMemory(lpBuffer,sizeof(IMAGE_IMPORT_DESCRIPTOR) * (nImportDllCount + 1)); size = 20; // 分配當前進程空間。 ntStatus = ZwAllocateVirtualMemory(hProcessHandle, &lpDllName, 0, &size, MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE); if(!NT_SUCCESS(ntStatus)) { ZwClose(hProcessHandle); return ; } RtlZeroMemory(lpDllName,20); size = 20; ntStatus = ZwAllocateVirtualMemory(hProcessHandle, &lpExportApi, 0, &size, MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE); if(!NT_SUCCESS(ntStatus)) { ZwClose(hProcessHandle); return ; } RtlZeroMemory(lpExportApi,20); // 分配當前進程空間。 size = 20; ntStatus = ZwAllocateVirtualMemory(hProcessHandle, &lpTemp, 0, &size, MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE); if(!NT_SUCCESS(ntStatus)) { ZwClose(hProcessHandle); return ; } RtlZeroMemory(lpTemp,20); // 分配當前進程空間。 size = 20; ntStatus = ZwAllocateVirtualMemory(hProcessHandle, &lpTemp2, 0, &size, MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE); if(!NT_SUCCESS(ntStatus)) { ZwClose(hProcessHandle); return ; } RtlZeroMemory(lpTemp2,20); pImportNew = lpBuffer; // 把原來數據保存好。 RtlCopyMemory(pImportNew , pImportDesc, sizeof(IMAGE_IMPORT_DESCRIPTOR) * nImportDllCount ); // 構造自己的DLL IMAGE_IMPORT_DESCRIPTOR結構 pOriginalThunkData = (PIMAGE_THUNK_DATA)lpTemp; pFirstThunkData = (PIMAGE_THUNK_DATA)lpTemp2; ptmp = (PIMAGE_IMPORT_BY_NAME)lpExportApi; ptmp->Hint = 0; // 至少要一個導出API RtlCopyMemory(ptmp->Name,"HelloShine",strlen("HelloShine")); pOriginalThunkData[0].u1.AddressOfData = (ULONG)ptmp-ulBaseImage; pFirstThunkData[0].u1.AddressOfData = (ULONG)ptmp-ulBaseImage; Add_ImportDesc.FirstThunk = (ULONG)pFirstThunkData-ulBaseImage; Add_ImportDesc.TimeDateStamp = 0; Add_ImportDesc.ForwarderChain = 0; // // DLL名字的RVA RtlCopyMemory(lpDllName,"D:\\Dll.dll",strlen("D:\\Dll.dll")); Add_ImportDesc.Name = (ULONG)lpDllName-ulBaseImage; Add_ImportDesc.Characteristics = (ULONG)pOriginalThunkData-ulBaseImage; pImportNew += (nImportDllCount-1); RtlCopyMemory(pImportNew, &Add_ImportDesc, sizeof(IMAGE_IMPORT_DESCRIPTOR)); pImportNew += 1; RtlZeroMemory(pImportNew, sizeof(IMAGE_IMPORT_DESCRIPTOR)); __asm { cli; mov eax, cr0; mov oldCr0, eax; and eax, not 10000h; mov cr0, eax } // 改導出表 pHeader->OptionalHeader.DataDirectory[1].Size += sizeof(IMAGE_IMPORT_DESCRIPTOR); pHeader->OptionalHeader.DataDirectory[1].VirtualAddress = (ULONG)( pImportNew - nImportDllCount) - ulBaseImage; pBoundImport = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((ULONG)pHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress + ulBaseImage); if( (ULONG)pBoundImport != ulBaseImage) { //取消綁定輸入表里的所有東西 pHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress = 0; pHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].Size = 0; } __asm { mov eax, oldCr0; mov cr0, eax; sti; } ZwClose(hProcessHandle); hProcessHandle = NULL; } }
*需要注意一點:綁定導入表
當時實踐的時候怎么都不成功,熬了一晚上最后都沒有結果,真是崩潰,最后再次查看《WindowsPE權威指南》才發現綁定導入表的問題。
學知識看來總是得多實踐才能發現問題,以前總以為自己知道綁定導入表的問題,可是真正遇到問題就忘了,更坑的是有些問題搜索不到或者寥寥無幾。
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11
指向一個 IMAGE_BOUND_IMPORT_DESCRIPTOR結構數組,對應於這個映像綁定的每個DLL。數組元素中的時間戳允許加載器快速判斷綁定是否是新的。如果不是,加載器忽略綁定信息並且按正常方式解決導入API。
也就是說,綁定導入是提高PE加載的一項技術,如果PE文件中導入的函數比較多,PE加載速度就會變慢。綁定導入的目的就是把由Windows加載程序負責的IAT地址修正工作提前到之前進行。
所以也就是說在取消綁定導入表后,強制操作系統按導入表進行導入。那么也就成功了。
