在用戶態進行虛擬空間地址向物理空間地址的轉換

http://flier_lu.blogone.net/?id=1428057


    在《自動獲取 NT 系統服務描述表與函數名映射表》一文中,我給出了一個從虛地址向物理地址轉換的經驗函數。

以下爲引用:

PHYSICAL_ADDRESS TPhysicalMemoryMapping::LinearAddressToPhysicalAddress(LPCVOID lpVirtualAddress)
{
  PHYSICAL_ADDRESS addr = { 0, 0 };

  if((DWORD)lpVirtualAddress < 0x80000000L || (DWORD)lpVirtualAddress >= 0xA0000000L)
    addr.QuadPart = (DWORD)lpVirtualAddress & 0x0FFFF000;
  else
    addr.QuadPart = (DWORD)lpVirtualAddress & 0x1FFFF000;

  return addr;
}



    這個函數實際上只處理了0x8000000 - 0xA0000000這段內存地址的轉換,而對0xA0000000以上內存,則只是用 PA = VA & 0x0FFFF000 保障訪問不會出錯,這實際上並不能獲取這段內存的實際內容。瞭解 WinNT/2K 內存佈局的朋友應該知道,爲了系統實現簡便並保障地址轉換效率,WinNT將0x80000000 - 0xA0000000內存端直接映射到物理內存,轉換算法就是一個簡單的 PA = VA & 0x1FFFF000;而對 0xA0000000 以上的虛擬地址,則是使用兩級頁表索引來實現虛地址頁向物理地址頁的映射(如啓用 AWE 則爲三級,這裏暫不討論,實現原理類似)。因此要真正訪問 0xA0000000 以上虛地址內存,必須讀取進程的 PDE/PTE 表。
    但問題是PDE頁表一般存放在虛地址 0xC0300000,PTE 頁表則從虛地址 0xC0000000 開始。也就是說這些頁表本身,就存放在以頁表才能訪問的虛地址上。要查PDE/PTE表進行虛地址向物理地址轉換,首先要知道 0xC0300000 和 0xC0000000 映射在哪個物理頁上。這樣一來就變成了先有雞還是先有蛋的問題了,呵呵。 NT 內核本身,則可以通過進程 CR3 寄存器中保存的 PDE 頁表起始物理地址直接訪問,但不巧的是,訪問 CR3 需要有 Ring 0 的狀態。因此大多數介紹虛地址向物理地址轉換的文章,在描述完兩級映射之後,都是提供 Ring 0 層的代碼演示實現,例如 webcrazy 的大作《小議Windows NT/2000分頁機制》
    因爲此類的文章較多了,這裏我就不在羅嗦內存結構和轉換算法了,有興趣研究的朋友可以參考 Inside Win2K 3nd 和 webcrazy 的相關文章。

    既然無法知道 PDE/PTE 的物理地址,也不能直接訪問 CR3,我就開始琢磨各種迂迴的途徑。昨天折騰了大半天,翻越了 ntos/ke 和 ntos/mm 目錄下的一堆源碼,在大大體驗了一把 Open Source Windows NT 的優勢之後,終於找到了一個還算完美的解決方法 :P

    我們知道 PDE/PTE 是進程相關的。因爲每個進程都有自己的虛擬空間,因此必須有一整套獨立的頁表備查。核心在進行線程切換時,如果兩個線程在一個進程內,無需做頁表的切換;如果兩個線程跨越了進程,就必須將目標線程所在進程的Context載入,這其中就包括我們所需要的 CR3 的內容。而在翻越代碼後,我發現 CR3 的內容被保存到 EPROCESS::KPROCESS::DirectoryTableBase[0] 中。這個變量保存了 PDE 頁表物理地址和 hyber space PTE 頁表物理地址(以後再詳細介紹)。
    於是實現思路就清晰了:
    1.取當前進程的 EPROCESS
    2.讀取 PDE 頁表物理地址
    3.通過分解虛地址獲取 PDE/PTE 表的索引
    4.查表獲得目標物理頁地址,讀取物理頁內容。

1.取當前進程的 EPROCESS

    當前進程的 EPROCESS 地址在 ntoskrnl.exe 的空間中,因此可以通過直接映射內存訪問。使用OpenProcess打開當前進程,獲得句柄;使用NTDLL::ZwQuerySystemInformation函數獲取所有核心句柄表,線性搜索到進程句柄,其指向的內核對象就是 EPROCESS。

2.讀取 PDE 頁表物理地址

    PDE 頁表物理地址在 EPROCESS 結構的偏移 0x18 處

3.通過分解虛地址獲取 PDE/PTE 表的索引

    將目標虛地址如 0xA01A8148 分解爲三部分:

    DDDDDDDDDDTTTTTTTTTTBBBBBBBBBBBB
    01234567890123456789012345678901

    高10位是PDE索引;中間10位是PTE索引;末尾12位是頁內字節索引。

4.查表獲得目標物理頁地址,讀取物理頁內容。

    PDE/PTE表項都是一個DWORD,其高20位定義下一級的索引。

以下爲引用:

typedef struct _MMPTE_HARDWARE {
    ULONG Valid : 1;
    ULONG Write : 1;  // UP version
    ULONG Owner : 1;
    ULONG WriteThrough : 1;
    ULONG CacheDisable : 1;
    ULONG Accessed : 1;
    ULONG Dirty : 1;
    ULONG LargePage : 1;
    ULONG Global : 1;
    ULONG CopyOnWrite : 1; // software field
    ULONG Prototype : 1;   // software field
    ULONG reserved : 1;  // software field
    ULONG PageFrameNumber : 20;
} MMPTE_HARDWARE, *PMMPTE_HARDWARE;



    具體查錶轉換算法如下

以下爲引用:

#define GetPdeAddress(base, va) (LPCVOID)((base) + (((ULONG)(va) >> 22) << 2))
#define GetPteAddress(base, va) (LPCVOID)((base) + ((((ULONG)(va) >> 12) & 0x3FF) << 2))

const PHYSICAL_ADDRESS TPhysicalMemoryMapping::LinearAddressToPhysicalAddress(LPCVOID lpVirtualAddress)
{
  PHYSICAL_ADDRESS addr = { 0, 0 };

  if((DWORD)lpVirtualAddress >= 0x80000000L && (DWORD)lpVirtualAddress < 0xA0000000L)
  {
    addr.QuadPart = (DWORD)lpVirtualAddress & 0x1FFFF000;
  }
  else
  {
    MMPTE_HARDWARE PDE, PTE;

    m_pManager->ReadPhysicalMemoryPage(GetPdeAddress(m_pManager->PTE.LowPart, lpVirtualAddress), &PDE, sizeof(PDE));
    m_pManager->ReadPhysicalMemoryPage(GetPteAddress(PDE.PageFrameNumber << PAGE_SHIFT, lpVirtualAddress), &PTE, sizeof(PTE));

    addr.LowPart = (PTE.PageFrameNumber << PAGE_SHIFT) + BYTE_OFFSET(lpVirtualAddress);
  }
  return addr;
}



    知道了目標頁面的物理地址,就可以通過讀取 /Device/PhysicalMemory 直接獲取了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章