外掛、木馬、病毒等可能需要讀取其他進程的數據,windows提供了OpenProcess、ReadProcessMemory等函數。但越是大型的軟件,防護做的越好,大概率會做驅動保護,比如hook SSDT表等,這些系統調用都會先被過濾一次,導致返回的數據不是想要的;為了確保能讀到目標進程數據,最好重寫ReadProcessMemory;要想讀取其他進程的內容,思路大概有一下幾種:
- 注冊PsSetLoadImageNotifyRoutine函數,其他進程加載模塊會時調用我們注冊的函數,這個時候已經進入目標進程的空間,可用memcpy復制數據
- KeStackAttachProcess可以切換到目標進程,然后用memcpy復制數據,最后調用KeUnstackDetachProcess切換回來
- 利用進程ID查找EPROCESS,根據名稱得到目標EPROCESS后再讀取CR3,最用利用目標進程的CR3讀取其內存數據;
前兩種方式要調用大家熟知的函數,目的性比較明顯,這些函數肯定會被大廠家重點關注,返回的結果可能在邏輯上有誤;而第三種方式僅僅根據ID查詢EPROCESS,相對前兩種更加“人畜無害”,被攔截的概率要小很多,今天詳細介紹第三種方式。
1、為了驗證讀取數據是否正確,先在notepad隨便寫一些內容,然后用CE查看地址,如下,后續就選這個地址來測試了;
這里多說幾句:CE為了防止被針對,自己重寫了關鍵的內存掃描/讀寫、進程打開函數,並未使用windows原生自帶的系統調用函數;
2、遍歷進程,根據ID查找EPROCESS,進而得到進程名、CR3等關鍵信息,這塊也比較簡單,如下:
PEPROCESS LookupProcess(HANDLE Pid) { PEPROCESS eprocess = NULL; NTSTATUS Status = STATUS_UNSUCCESSFUL; Status = PsLookupProcessByProcessId(Pid, &eprocess); if (NT_SUCCESS(Status)) return eprocess; return NULL; }
因不同版本EPROCESS結構體可能有細微區別,保險起見最好在windbg下通過dt _EPROCESS查看一下CR3的偏移,我這里是0x28,那么CR3的獲取代碼:
ULONG64 target_CR3 = *(PULONG64)((ULONG64)eproc + 0x28);
3、目標CR3都拿到了,接下來就可以讀進程數據了,這里的環境是win10 x64,默認開啟了PAE;最初的想法很簡單,系統用的是9-9-9-9-12分頁,那么先把虛擬地址拆分,再用CR3一步一步跟蹤不就得到物理地址了嗎? 物理地址都有了,再讀取數據豈不是探囊取物般簡單了么?
所以剛開始的代碼是這樣的:(1)先拆分虛擬地址,得到各個層級的偏移 (2)再仿造windbg種逐步計算的方式一步一步得到物理地址; (3)這里有點要注意:& 和移位的優先級較低,注意使用括號,避免計算的邏輯錯誤;
ULONG64 PML4E_index = (virtualAddress >> 39) & 0x1ff; ULONG64 PDPE_index = (virtualAddress >> 30) & 0x1ff; ULONG64 PDE_index = (virtualAddress >> 21) & 0x1ff; ULONG64 PTE_index = (virtualAddress >> 12) & 0x1ff; ULONG64 physical_offset = virtualAddress & 0xfff; ULONG64 PML4E = CR3 + (PML4E_index << 3); ULONG64 PDPE = (*(PULONG64)PML4E & 0x00000007fffff000) + (PDPE_index << 3);//上一級table entry的12~35位提供下一級table物理基地址的高24位,此時36~51是保留位,必須置0,低12位補零 ULONG64 PDE = (*(PULONG64)PDPE & 0x00000007fffff000) + (PDE_index << 3); ULONG64 PTE = (*(PULONG64)PDE & 0x00000007fffff000) + (PTE_index << 3); ULONG64 physicalAddress = (*(PULONG64)PTE & 0x00000007fffff000) + physical_offset;
然而一運行就藍屏,通過下斷點逐步跟蹤,發現罪魁禍首在這行:ULONG64 PDPE = (*(PULONG64)PML4E & 0x00000007fffff000) + (PDPE_index << 3)
這樣代碼做的運算有好幾個,為了徹查到底是哪個運算導致的藍屏,繼續把代碼拆分地更細,最終發現導致藍屏的真凶:*(PULONG64)PML4E;
這行代碼本身很簡單,就是把PML4E轉換成指針,再讀取其指向的內容,這么簡單的操作,為啥會導致藍屏了?這就牽扯到win10 x64下用戶態CR3和內核態CR3的區別了;本案例用的是驅動,在0環內核態執行,自然用的是內核態CR3。但代碼讀取數據的虛擬地址明顯是3環的,屬於用戶態。不同的CR3會映射到不同的物理地址,最終出錯。問題找到了,怎么解決了?既然是CR3不對,那么讀取內存之前先切換一下不就行了?
由於vs2019對於x64的程序不允許內聯匯編,這里單獨新建一個asm文件來切換CR3,如下:
_swapCR3 PROC mov cr3,rcx; ret _swapCR3 Endp
然后繼續單步調試,切換CR3的函數執行完后,CR3成功更改:
但是在執行*(PULONG64)PML4E前查看時CR3又變回了內核態CR3:
繼續執行不出意外又藍屏:
為什么函數 _swapCR3 執行完后,CR3又變回了內核態?經過多次反復嘗試,發現都是斷點惹的禍:代碼里面設置了斷點,執行到斷點時會被系統進程接管,然后CR3自然就切換;
既然ret后CR3又被改回,那么只能在匯編代碼里面讀取虛擬地址的內容了,這次重新寫一個_ReadVirtualMemory代碼,如下(這里直接使用mov rax,[rdx],還省去了繁瑣的地址轉換,這部分直接讓cpu自動做了):
_ReadVirtualMemory PROC push rax; push rbx; mov rbx,cr3;保存舊的CR3 mov cr3,rcx; mov rax,[rdx];第二個參數,是VirtualMemory mov data,rax mov cr3,rbx;還原舊的CR3 pop rbx; pop rax; ret _ReadVirtualMemory Endp
在這里讀的數據終於對了,效果如下:0x7ffd350ca309 地址前2個字節的內容確實是CC,和通過CE查找到的一致;
完整代碼如下:主文件
#include "ReadProcessMemory.h" ULONG64 data; // 根據進程ID返回進程EPROCESS結構體,失敗返回NULL PEPROCESS LookupProcess(HANDLE Pid) { PEPROCESS eprocess = NULL; NTSTATUS Status = STATUS_UNSUCCESSFUL; Status = PsLookupProcessByProcessId(Pid, &eprocess); if (NT_SUCCESS(Status)) return eprocess; return NULL; } VOID EnumProcess() { PEPROCESS eproc = NULL; for (int temp = 0; temp < 100000; temp += 4) { eproc = LookupProcess((HANDLE)temp); if (eproc != NULL ) { if (!strcmp(PsGetProcessImageFileName(eproc),"notepad.exe")) { DbgPrint("進程名: %s --> 進程PID = %d --> 父進程PPID = %d -->EPROCESS地址:%p \r\n", PsGetProcessImageFileName(eproc), PsGetProcessId(eproc), PsGetProcessInheritedFromUniqueProcessId(eproc), eproc); ULONG64 target_CR3 = *(PULONG64)((ULONG64)eproc + 0x28); KdBreakPoint(); _ReadVirtualMemory(target_CR3, 0x7ffd350ca309); DbgPrint("存儲內容:%c \r\n", data); ObDereferenceObject(eproc); return; } } } } VOID UnDriver(PDRIVER_OBJECT driver) { DbgPrint(("Uninstall Driver Is OK \n")); } NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath) { EnumProcess(); Driver->DriverUnload = UnDriver; return STATUS_SUCCESS; }
頭文件:
#ifndef READPROCESSMEMORY_ #define READPROCESSMEMORY_ #include <ntifs.h> NTKERNELAPI UCHAR* PsGetProcessImageFileName(IN PEPROCESS Process); //未公開的進行導出即可 NTKERNELAPI HANDLE PsGetProcessInheritedFromUniqueProcessId(IN PEPROCESS Process);//未公開進行導出 extern void Dbg_Break(); extern void _swapCR3(ULONG64 target_CR3); extern void _ReadVirtualMemory(ULONG64 target_CR3, ULONG64 VirtualMemory); extern ULONG64 data; #endif
asm文件:這里切換CR3的時候為了保險起見,建議先保存舊CR3,用完了再切回去;
EXTERN data:qword .code Dbg_Break Proc int 3 ret Dbg_Break Endp _swapCR3 PROC mov cr3,rcx; ret _swapCR3 Endp _ReadVirtualMemory PROC push rax; push rbx; mov rbx,cr3;保存舊的CR3 mov cr3,rcx; mov rax,[rdx];第二個參數,是VirtualMemory mov data,rax mov cr3,rbx;還原舊的CR3 pop rbx; pop rax; ret _ReadVirtualMemory Endp END
最后注意: VS2019在x64的默認調用約定是_stdcall,不是_fastcall,建議改成_fastcall;