學習開源代碼ReflectiveDLLInjection時做的一些思路總結。
代碼大家可自行在github上面下載。
利用CreateFileA加載reflective_dll得到句柄
通過GetFileSize得到reflective_dll文件大小,分配一塊堆內存利用ReadFile去將reflective_dll讀入進程內存空間。
提權
利用OpenProcess函數去打開目標進程 必須帶有以下標志
/*A handle to the process in which the thread is to be created. The handle must have the
PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights*/
LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, NULL );
- 取ReflectiveLoader函數在DLL文件中的文件偏移通過傳參進來的基地址 強制類型轉換成PIMAGE_DOS_HEADER類型指向e_lfanew,加上基地址得到NT Header。 找到導出表 從導出表中通過AdressOfName表找到 ReflectiveLoader,計算出他的索引值,從而在AdressOfFuction表中找到該函數地址,再轉換為在DLL中的文件偏移
- Rva2Offset通過傳參進來的基地址 強制類型轉換成PIMAGE_DOS_HEADER類型指向e_lfanew,加上基地址得到NT Header。
- RVA(相對虛擬地址)和FOA(文件偏移)的具體含義大家可以看看《Windows PE 權威指南》或者是小甲魚的PE結構詳解視頻,我相信大家看完之后一定會理解的,我這里就不寫這些概念了。之所以會產生兩者的轉換,是因為同一個文件在硬盤和內存中的對齊方式不一樣,我們可以通過IMAGE_OPTIONAL_HEADER結構體的SectionAlignment(內存對齊方式)和FileAlignment(文件對齊方式)這兩個字段,知道它們的對齊方式分別是以200h進行對齊和以1000h對齊的,那么當我們知道內存相對虛擬地址時,想轉換成文件偏移時,還需要借助IMAGE_SECTION_HEADER這個結構體。該結構體記錄了該Section在文件中的起始偏移(PointerToRawData)和內存映像中的起始RVA(VirtualAddress),這是我們可以找到它們唯一聯系的地方。
我們轉換的思路是:模擬內存對齊方式,看需要轉換的虛擬地址是否在該區段之間。轉化的步驟:1. 找到可執行文件中的Section的數目dwSectionCount,這個可以通過IMAGE_FILE_HEADER結構體中的NumberOfSections字段獲取。2. 找到Section的對齊大小,可以通過IMAGE_OPTIONAL_HEADER的SectionAlignment字段獲取這個值。3. 對dwSectionCount的循環,在這個循環中我們需要判斷RVA位於哪個Section中,diff = 需要轉換的虛擬地址-VirtualAddr計算出距離該節起始地址的偏移。4 PointerToRawData + diff就是轉換后的結果。
在目標進程空間VirtualAllocEx分配一塊PAGE_EXECUTE_READWRITE虛擬內存,
WriteProcessMemory 將DLL寫入目標進程的虛擬內存
算出ReflectiveLoader函數在目標進程中的地址
hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId );
HANDLE WINAPI CreateRemoteThread(
__in HANDLE hProcess,
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress,
__in LPVOID lpParameter,
__in DWORD dwCreationFlags,
__out LPDWORD lpThreadId
);
|
hProcess [in]
線程所屬進程的進程句柄.
該句柄必須具有 PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE,和PROCESS_VM_READ 訪問權限.
lpThreadAttributes [in]
一個指向 SECURITY_ATTRIBUTES 結構的指針, 該結指定了線程的安全屬性.
dwStackSize [in]
線程初始大小,以字節為單位,如果該值設為0,那么使用系統默認大小.
lpStartAddress [in]
在遠程進程的地址空間中,該線程的線程函數的起始地址.
lpParameter [in]
傳給線程函數的參數.
dwCreationFlags [in]
線程的創建標志.
創建遠程監控線程 要求是lpReflectiveLoader(函數必須在進程空間存在)
開始執行ReflectiveLoader
先獲得我們寫入目標進程的DLL的首地址
//__declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); } 返回當前調用函數返回的地址,即函數下一條指令地址
//利用當前調用函數返回的地址去尋找DLL首地址
uiLibraryAddress = caller();//獲得當前調用函數返回的地址
while( TRUE )
{
//找到DLL在進程空間中的地址 要注意這里的dll也是寫進來的而不是加載進來的。
//利用當前調用函數返回的地址去尋找DLL首地址
if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE )
{
uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
// some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'),
// we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems.
if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 )
{
uiHeaderValue += uiLibraryAddress;
// break if we have found a valid MZ/PE header
if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE )
break;
}
}
uiLibraryAddress--;
}
通過PEB查找已加載模塊,找到模塊中3我們所需的函數地址 pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache
獲得peb——>ldr-->InMemoryOrderModuleList.Flink
typedef struct _LDR_DATA_TABLE_ENTRY
{
//LIST_ENTRY InLoadOrderLinks; // As we search from PPEB_LDR_DATA->InMemoryOrderModuleList we dont use the first entry.
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STR FullDllName;
UNICODE_STR BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashTableEntry;
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
利用獲得的函數動態申請一塊內存。PAGE_EXECUTE_READWRITE屬性,可讀可寫可執行
- 先將PE頭寫入內存中
- 然后在將節寫入(開始重定位)
uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );
uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );
uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;while( uiValueD-- )*(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
映射導入表
OriginalFirstThunk
FirstThunk
現在可以看成是相同的(現在),它們都指向一個包含一系列IMAGE_THUNK_DATA結構的數組,數組中的每個IMAGE_THUNK_DATA結構定義了一個導入函數的信息,數組最后以一個內容為0的IMAGE_THUNK_DATA結構作為結束。
現在他們放的東西還是一樣的,因為他們是被讀進內存的
開始重寫FirstThunk
判斷OriginalFirstThunk->u1.Ordinal的最高位 從而確定重寫方法
當 IMAGE_THUNK_DATA 值的最高位為 1時,表示函數以序號方式輸入,這時候低 31位被看作一個函數序號。(讀者可以用預定義值IMAGE_ORDINAL_FLAG32或80000000h來對最高位進行測試)
當 IMAGE_THUNK_DATA 值的最高位為 0時,表示函數以字符串類型的函數名方式輸入,這時雙字的值是一個 RVA,指向IMAGE_IMPORT_BY_NAME 結構。
IMAGE_IMPORT_BY_NAME STRUCT
Hint WORD ?
Name1 BYTE ?
IMAGE_IMPORT_BY_NAME ENDS
假如是序號方式 序號減去BASE基址得到索引,從導入表中獲得函數偏移 在FirstThunk對應IMAGE_THUNK_DATA 上寫入函數地址
假如是以字符串類型的函數名方式輸入 利用已獲得的函數指針 pGetProcAddress 獲得 IMAGE_IMPORT_BY_NAME STRUCT 中的函數名Name1 所指的函數地址 在FirstThunk對應IMAGE_THUNK_DATA 上寫入
- 重定位表(重寫)
重定位的算法可以描述為:將直接尋址指令中的雙字地址加上模塊實際裝入地址與模塊建議裝入地址之差。為了進行這個運算,需要有3個數據,首先是需要修正的機器碼地址;其次是模塊的建議裝入地址;最后是模塊的實際裝入地址。
通過數據目錄找到找到重定位表的首地址
每個重定位塊以一個IMAGE_BASE_RELOCATION結構開頭,后面跟着在本頁面中使用的所有重定位項,每個重定位項占用16位的地址(也就是一個word),結構的定義是這樣的:
IMAGE_BASE_RELOCATION STRUCT
VirtualAddress dd ? ;重定位內存頁的起始RVA
SizeOfBlock dd ? ;重定位塊的長度
IMAGE_BASE_RELOCATION ENDS
( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );獲得重定位項的數量
IMAGE_RELOC 結構大小為WORD
typedef struct
{
WORD offset:12;
WORD type:4;
} IMAGE_RELOC, *PIMAGE_RELOC;
判斷type的類型來確定取模塊實際裝入地址與模塊建議裝入地址之差的位數
最后重寫對應重定位項地址
最后利用已獲得的函數指針pNtFlushInstructionCache 刷新緩存
注入成功
找到程序入口點,執行DLLMAIN