在某些時(shí)候我們需要讀寫的進(jìn)程可能存在虛擬內(nèi)存保護(hù)機(jī)制,在該機(jī)制下用戶的CR3
以及MDL
讀寫將直接失效,從而導(dǎo)致無法讀取到正確的數(shù)據(jù),本章我們將繼續(xù)研究如何實(shí)現(xiàn)物理級(jí)別的尋址讀寫。
首先,驅(qū)動(dòng)中的物理頁讀寫是指在驅(qū)動(dòng)中直接讀寫物理內(nèi)存頁(而不是虛擬內(nèi)存頁)。這種方式的優(yōu)點(diǎn)是它能夠更快地訪問內(nèi)存,因?yàn)樗苊饬颂摂M內(nèi)存管理的開銷,通過直接讀寫物理內(nèi)存,驅(qū)動(dòng)程序可以繞過虛擬內(nèi)存的保護(hù)機(jī)制,獲得對系統(tǒng)中內(nèi)存的更高級(jí)別的訪問權(quán)限。
想要實(shí)現(xiàn)物理頁讀寫,第一步則是需要找到UserDirectoryTableBase
的實(shí)際偏移地址,你一定會(huì)問這是個(gè)什么?別著急,聽我來慢慢解釋;
在操作系統(tǒng)中,每個(gè)進(jìn)程都有一個(gè)KPROCESS
結(jié)構(gòu)體,它是進(jìn)程的內(nèi)部表示。該結(jié)構(gòu)體中包含了一些重要的信息,包括UserDirectoryTableBase
字段,它指向進(jìn)程的頁表目錄表(Page Directory Table)
,也稱為DirectoryTable
頁目錄表。
Page Directory Table
是一種數(shù)據(jù)結(jié)構(gòu),它在虛擬內(nèi)存管理中起著重要的作用。它被用來存儲(chǔ)將虛擬地址映射到物理地址的映射關(guān)系,其內(nèi)部包含了一些指向頁表的指針,每個(gè)頁表中又包含了一些指向物理頁面的指針。這些指針一起構(gòu)成了一個(gè)樹形結(jié)構(gòu),它被稱為頁表樹(Page Table Tree)
。
kd> dt _KPROCESS
ntdll!_KPROCESS
+0x278 UserTime : Uint4B
+0x27c ReadyTime : Uint4B
+0x280 UserDirectoryTableBase : Uint8B
+0x288 AddressPolicy : UChar
+0x289 Spare2 : [71] UChar
#define GetDirectoryTableOffset 0x280
UserDirectoryTableBase
字段包含了進(jìn)程的頁表樹的根節(jié)點(diǎn)的物理地址,通過它可以找到進(jìn)程的頁表樹,從而實(shí)現(xiàn)虛擬內(nèi)存的管理。在WinDbg
中,通過輸入dt _KPROCESS
可以查看進(jìn)程的KPROCESS
結(jié)構(gòu)體的定義,從而找到UserDirectoryTableBase
字段的偏移量,這樣可以獲取該字段在內(nèi)存中的地址,進(jìn)而獲取DirectoryTable
的地址。不同操作系統(tǒng)的KPROCESS
結(jié)構(gòu)體定義可能會(huì)有所不同,因此它們的UserDirectoryTableBase
字段的偏移量也會(huì)不同。
通過上述原理解釋,我們可知要實(shí)現(xiàn)物理頁讀寫需要實(shí)現(xiàn)一個(gè)轉(zhuǎn)換函數(shù),因?yàn)樵趹?yīng)用層傳入的還是一個(gè)虛擬地址,通過TransformationCR3
函數(shù)即可實(shí)現(xiàn)將虛擬地址轉(zhuǎn)換到物理地址,函數(shù)內(nèi)部實(shí)現(xiàn)了從虛擬地址到物理地址的轉(zhuǎn)換過程,并返回物理地址。
// 從用戶層虛擬地址切換到物理頁地址的函數(shù)
// 將 CR3 寄存器的末尾4個(gè)比特清零,這些比特是用于對齊的,不需要考慮
/*
參數(shù) cr3:物理地址。
參數(shù) VirtualAddress:虛擬地址。
*/
ULONG64 TransformationCR3(ULONG64 cr3, ULONG64 VirtualAddress)
{
cr3 &= ~0xf;
// 獲取頁面偏移量
ULONG64 PAGE_OFFSET = VirtualAddress & ~(~0ul << 12);
// 讀取虛擬地址所在的三級(jí)頁表項(xiàng)
SIZE_T BytesTransferred = 0;
ULONG64 a = 0, b = 0, c = 0;
ReadPhysicalAddress((PVOID)(cr3 + 8 * ((VirtualAddress >> 39) & (0x1ffll))), &a, sizeof(a), &BytesTransferred);
// 如果 P(存在位)為0,表示該頁表項(xiàng)沒有映射物理內(nèi)存,返回0
if (~a & 1)
{
return 0;
}
// 讀取虛擬地址所在的二級(jí)頁表項(xiàng)
ReadPhysicalAddress((PVOID)((a & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 30) & (0x1ffll))), &b, sizeof(b), &BytesTransferred);
// 如果 P 為0,表示該頁表項(xiàng)沒有映射物理內(nèi)存,返回0
if (~b & 1)
{
return 0;
}
// 如果 PS(頁面大?。?,表示該頁表項(xiàng)映射的是1GB的物理內(nèi)存,直接計(jì)算出物理地址并返回
if (b & 0x80)
{
return (b & (~0ull << 42 >> 12)) + (VirtualAddress & ~(~0ull << 30));
}
// 讀取虛擬地址所在的一級(jí)頁表項(xiàng)
ReadPhysicalAddress((PVOID)((b & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 21) & (0x1ffll))), &c, sizeof(c), &BytesTransferred);
// 如果 P 為0,表示該頁表項(xiàng)沒有映射物理內(nèi)存,返回0
if (~c & 1)
{
return 0;
}
// 如果 PS 為1,表示該頁表項(xiàng)映射的是2MB的物理內(nèi)存,直接計(jì)算出物理地址并返回
if (c & 0x80)
{
return (c & ((~0xfull << 8) & 0xfffffffffull)) + (VirtualAddress & ~(~0ull << 21));
}
// 讀取虛擬地址所在的零級(jí)頁表項(xiàng),計(jì)算出物理地址并返回
ULONG64 address = 0;
ReadPhysicalAddress((PVOID)((c & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 12) & (0x1ffll))), &address, sizeof(address), &BytesTransferred);
address &= ((~0xfull << 8) & 0xfffffffffull);
if (!address)
{
return 0;
}
return address + PAGE_OFFSET;
}
這段代碼將輸入的CR3
值和虛擬地址作為參數(shù),并將CR3
值和虛擬地址的偏移量進(jìn)行一系列計(jì)算,最終得出物理地址。
其中,CR3
是存儲(chǔ)頁表的物理地址,它保存了虛擬地址到物理地址的映射關(guān)系。該函數(shù)通過讀取CR3
中存儲(chǔ)的頁表信息,逐級(jí)訪問頁表,直到找到對應(yīng)的物理地址。
該函數(shù)使用虛擬地址的高9位
確定頁表的索引,然后通過讀取對應(yīng)的頁表項(xiàng),得到下一級(jí)頁表的物理地址。該過程重復(fù)執(zhí)行,直到讀取到頁表的最后一級(jí),得到物理地址。
最后,該函數(shù)將物理地址的低12位
與虛擬地址的偏移量進(jìn)行OR
運(yùn)算,得到最終的物理地址,并將其返回。
需要注意的是,該函數(shù)還會(huì)進(jìn)行一些錯(cuò)誤處理,例如在讀取頁表項(xiàng)時(shí),如果該項(xiàng)沒有被設(shè)置為有效,函數(shù)將返回0,表示無法訪問對應(yīng)的物理地址。
此時(shí)用戶已經(jīng)獲取到了物理地址,那么讀寫就變得很容易了,當(dāng)需要讀取數(shù)據(jù)時(shí)調(diào)用ReadPhysicalAddress
函數(shù),其內(nèi)部直接使用MmCopyMemory
對內(nèi)存進(jìn)行拷貝即可,而對于寫入數(shù)據(jù)而言,需要通過調(diào)用MmMapIoSpace
先將物理地址轉(zhuǎn)換為一個(gè)用戶空間的虛擬地址,然后再通過RtlCopyMemory
向內(nèi)部拷貝數(shù)據(jù)即可實(shí)現(xiàn)寫入,這三段代碼的封裝如下所示;
#include <ntifs.h>
#include <windef.h>
#define GetDirectoryTableOffset 0x280
#define bit64 0x28
#define bit32 0x18
// 讀取物理內(nèi)存封裝
// 這段代碼實(shí)現(xiàn)了將物理地址映射到內(nèi)核空間,然后將物理地址對應(yīng)的數(shù)據(jù)讀取到指定的緩沖區(qū)中。
/*
address:需要讀取的物理地址;
buffer:讀取到的數(shù)據(jù)需要保存到的緩沖區(qū);
size:需要讀取的數(shù)據(jù)大小;
BytesTransferred:實(shí)際讀取到的數(shù)據(jù)大小。
*/
NTSTATUS ReadPhysicalAddress(PVOID address, PVOID buffer, SIZE_T size, SIZE_T* BytesTransferred)
{
MM_COPY_ADDRESS Read = { 0 };
Read.PhysicalAddress.QuadPart = (LONG64)address;
return MmCopyMemory(buffer, Read, size, MM_COPY_MEMORY_PHYSICAL, BytesTransferred);
}
// 寫入物理內(nèi)存
// 這段代碼實(shí)現(xiàn)了將數(shù)據(jù)寫入物理地址的功能
/*
參數(shù) address:要寫入的物理地址。
參數(shù) buffer:要寫入的數(shù)據(jù)緩沖區(qū)。
參數(shù) size:要寫入的數(shù)據(jù)長度。
參數(shù) BytesTransferred:實(shí)際寫入的數(shù)據(jù)長度。
*/
NTSTATUS WritePhysicalAddress(PVOID address, PVOID buffer, SIZE_T size, SIZE_T* BytesTransferred)
{
if (!address)
{
return STATUS_UNSUCCESSFUL;
}
PHYSICAL_ADDRESS Write = { 0 };
Write.QuadPart = (LONG64)address;
// 將物理空間映射為虛擬空間
PVOID map = MmMapIoSpace(Write, size, (MEMORY_CACHING_TYPE)PAGE_READWRITE);
if (!map)
{
return STATUS_UNSUCCESSFUL;
}
// 開始拷貝數(shù)據(jù)
RtlCopyMemory(map, buffer, size);
*BytesTransferred = size;
MmUnmapIoSpace(map, size);
return STATUS_SUCCESS;
}
// 從用戶層虛擬地址切換到物理頁地址的函數(shù)
// 將 CR3 寄存器的末尾4個(gè)比特清零,這些比特是用于對齊的,不需要考慮
/*
參數(shù) cr3:物理地址。
參數(shù) VirtualAddress:虛擬地址。
*/
ULONG64 TransformationCR3(ULONG64 cr3, ULONG64 VirtualAddress)
{
cr3 &= ~0xf;
// 獲取頁面偏移量
ULONG64 PAGE_OFFSET = VirtualAddress & ~(~0ul << 12);
// 讀取虛擬地址所在的三級(jí)頁表項(xiàng)
SIZE_T BytesTransferred = 0;
ULONG64 a = 0, b = 0, c = 0;
ReadPhysicalAddress((PVOID)(cr3 + 8 * ((VirtualAddress >> 39) & (0x1ffll))), &a, sizeof(a), &BytesTransferred);
// 如果 P(存在位)為0,表示該頁表項(xiàng)沒有映射物理內(nèi)存,返回0
if (~a & 1)
{
return 0;
}
// 讀取虛擬地址所在的二級(jí)頁表項(xiàng)
ReadPhysicalAddress((PVOID)((a & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 30) & (0x1ffll))), &b, sizeof(b), &BytesTransferred);
// 如果 P 為0,表示該頁表項(xiàng)沒有映射物理內(nèi)存,返回0
if (~b & 1)
{
return 0;
}
// 如果 PS(頁面大?。?,表示該頁表項(xiàng)映射的是1GB的物理內(nèi)存,直接計(jì)算出物理地址并返回
if (b & 0x80)
{
return (b & (~0ull << 42 >> 12)) + (VirtualAddress & ~(~0ull << 30));
}
// 讀取虛擬地址所在的一級(jí)頁表項(xiàng)
ReadPhysicalAddress((PVOID)((b & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 21) & (0x1ffll))), &c, sizeof(c), &BytesTransferred);
// 如果 P 為0,表示該頁表項(xiàng)沒有映射物理內(nèi)存,返回0
if (~c & 1)
{
return 0;
}
// 如果 PS 為1,表示該頁表項(xiàng)映射的是2MB的物理內(nèi)存,直接計(jì)算出物理地址并返回
if (c & 0x80)
{
return (c & ((~0xfull << 8) & 0xfffffffffull)) + (VirtualAddress & ~(~0ull << 21));
}
// 讀取虛擬地址所在的零級(jí)頁表項(xiàng),計(jì)算出物理地址并返回
ULONG64 address = 0;
ReadPhysicalAddress((PVOID)((c & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 12) & (0x1ffll))), &address, sizeof(address), &BytesTransferred);
address &= ((~0xfull << 8) & 0xfffffffffull);
if (!address)
{
return 0;
}
return address + PAGE_OFFSET;
}
有了如上封裝,那么我們就可以實(shí)現(xiàn)驅(qū)動(dòng)讀寫了,首先我們實(shí)現(xiàn)驅(qū)動(dòng)讀取功能,如下這段代碼是Windows
驅(qū)動(dòng)程序的入口函數(shù)DriverEntry
,主要功能是讀取指定進(jìn)程的虛擬地址空間中指定地址處的4
個(gè)字節(jié)數(shù)據(jù)。
代碼首先通過 PsLookupProcessByProcessId
函數(shù)獲取指定進(jìn)程的 EPROCESS
結(jié)構(gòu)體指針。然后獲取該進(jìn)程的 CR3
值,用于將虛擬地址轉(zhuǎn)換為物理地址。接下來,循環(huán)讀取指定地址處的 4
個(gè)字節(jié)數(shù)據(jù),每次讀取 PAGE_SIZE
大小的物理內(nèi)存數(shù)據(jù)。最后輸出讀取到的數(shù)據(jù),并關(guān)閉對 EPROCESS
結(jié)構(gòu)體指針的引用。
需要注意的是,該代碼并沒有進(jìn)行有效性檢查,如沒有檢查讀取的地址是否合法、讀取的數(shù)據(jù)是否在用戶空間,因此存在潛在的風(fēng)險(xiǎn)。另外,該代碼也沒有考慮內(nèi)核模式下訪問用戶空間數(shù)據(jù)的問題,因此也需要進(jìn)行進(jìn)一步的檢查和處理。
// 驅(qū)動(dòng)卸載例程
extern "C" VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
UNREFERENCED_PARAMETER(pDriver);
DbgPrint("Uninstall Driver \n");
}
// 驅(qū)動(dòng)入口地址
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING path)
{
DbgPrint("Hello LyShark \n");
// 通過進(jìn)程ID獲取eprocess
PEPROCESS pEProcess = NULL;
NTSTATUS Status = PsLookupProcessByProcessId((HANDLE)4116, &pEProcess);
if (NT_SUCCESS(Status) && pEProcess != NULL)
{
ULONG64 TargetAddress = 0x401000;
SIZE_T TargetSize = 4;
SIZE_T read = 0;
// 分配讀取空間
BYTE* ReadBuffer = (BYTE *)ExAllocatePool(NonPagedPool, 1024);
// 獲取CR3用于轉(zhuǎn)換
PUCHAR Var = reinterpret_cast<PUCHAR>(pEProcess);
ULONG64 CR3 = *(ULONG64*)(Var + bit64);
if (!CR3)
{
CR3 = *(ULONG64*)(Var + GetDirectoryTableOffset);
}
DbgPrint("[CR3] 寄存器地址 = 0x%p \n", CR3);
while (TargetSize)
{
// 開始循環(huán)切換到CR3
ULONG64 PhysicalAddress = TransformationCR3(CR3, TargetAddress + read);
if (!PhysicalAddress)
{
break;
}
// 讀取物理內(nèi)存
ULONG64 ReadSize = min(PAGE_SIZE - (PhysicalAddress & 0xfff), TargetSize);
SIZE_T BytesTransferred = 0;
// reinterpret_cast 強(qiáng)制轉(zhuǎn)為PVOID類型
Status = ReadPhysicalAddress(reinterpret_cast<PVOID>(PhysicalAddress), reinterpret_cast<PVOID>((PVOID *)ReadBuffer + read), ReadSize, &BytesTransferred);
TargetSize -= BytesTransferred;
read += BytesTransferred;
if (!NT_SUCCESS(Status))
{
break;
}
if (!BytesTransferred)
{
break;
}
}
// 關(guān)閉引用
ObDereferenceObject(pEProcess);
// 輸出讀取字節(jié)
for (size_t i = 0; i < 4; i++)
{
DbgPrint("[讀入字節(jié) [%d] ] => 0x%02X \n", i, ReadBuffer[i]);
}
}
// 關(guān)閉引用
UNREFERENCED_PARAMETER(path);
// 卸載驅(qū)動(dòng)
pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
編譯并運(yùn)行上述代碼片段,則會(huì)讀取進(jìn)程ID為4116
的0x401000
處的地址數(shù)據(jù),并以字節(jié)的方式輸出前四位,輸出效果圖如下所示;
寫出數(shù)據(jù)與讀取數(shù)據(jù)基本一致,只是調(diào)用方法從ReadPhysicalAddress
變?yōu)榱?code>WritePhysicalAddress其他的照舊,但需要注意的是讀者再使用寫出時(shí)需要自行填充一段堆用于存儲(chǔ)需要寫出的字節(jié)集。
// 驅(qū)動(dòng)卸載例程
extern "C" VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
UNREFERENCED_PARAMETER(pDriver);
DbgPrint("Uninstall Driver \n");
}
// 驅(qū)動(dòng)入口地址
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING path)
{
DbgPrint("Hello LyShark \n");
// 物理頁寫
PEPROCESS pEProcess = NULL;
NTSTATUS Status = PsLookupProcessByProcessId((HANDLE)4116, &pEProcess);
// 判斷pEProcess是否有效
if (NT_SUCCESS(Status) && pEProcess != NULL)
{
ULONG64 TargetAddress = 0x401000;
SIZE_T TargetSize = 4;
SIZE_T read = 0;
// 申請空間并填充寫出字節(jié)0x90
BYTE* ReadBuffer = (BYTE *)ExAllocatePool(NonPagedPool, 1024);
for (size_t i = 0; i < 4; i++)
{
ReadBuffer[i] = 0x90;
}
// 獲取CR3用于轉(zhuǎn)換
PUCHAR Var = reinterpret_cast<PUCHAR>(pEProcess);
ULONG64 CR3 = *(ULONG64*)(Var + bit64);
if (!CR3)
{
CR3 = *(ULONG64*)(Var + GetDirectoryTableOffset);
// DbgPrint("[CR3] 寄存器地址 = 0x%p \n", CR3);
}
while (TargetSize)
{
// 開始循環(huán)切換到CR3
ULONG64 PhysicalAddress = TransformationCR3(CR3, TargetAddress + read);
if (!PhysicalAddress)
{
break;
}
// 寫入物理內(nèi)存
ULONG64 WriteSize = min(PAGE_SIZE - (PhysicalAddress & 0xfff), TargetSize);
SIZE_T BytesTransferred = 0;
Status = WritePhysicalAddress(reinterpret_cast<PVOID>(PhysicalAddress), reinterpret_cast<PVOID>(ReadBuffer + read), WriteSize, &BytesTransferred);
TargetSize -= BytesTransferred;
read += BytesTransferred;
// DbgPrint("[寫出數(shù)據(jù)] => %d | %0x02X \n", WriteSize, ReadBuffer + read);
if (!NT_SUCCESS(Status))
{
break;
}
if (!BytesTransferred)
{
break;
}
}
// 關(guān)閉引用
ObDereferenceObject(pEProcess);
}
// 關(guān)閉引用
UNREFERENCED_PARAMETER(path);
// 卸載驅(qū)動(dòng)
pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
如上代碼運(yùn)行后,會(huì)向進(jìn)程ID為4116
的0x401000
處寫出4字節(jié)的0x90
機(jī)器碼,讀者可通過第三方工具驗(yàn)證內(nèi)存,輸出效果如下所示;文章來源:http://www.zghlxwxcb.cn/news/detail-500498.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-500498.html
到了這里,關(guān)于驅(qū)動(dòng)開發(fā):內(nèi)核物理內(nèi)存尋址讀寫的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!