Windows(x86)頁表與虛擬空間之我見

Editor發表於2018-09-03

執行環境:

主機系統:Windows 10 x64

目標系統:Windows XP sp3 x86


工具軟體:

虛擬機器:Virtual Box

編譯器:Visual Studio 2017

偵錯程式:Windbg、OllyDBG


參考書籍:

《Windows 核心設計思想》

《Windows 核心程式設計》

《軟體除錯》


參考博文:

《OS 學習筆記》

《詳解Windows記憶體分頁機制》


本文引用了以上書籍、文章的部分觀點,算是一篇學習筆記。在此感謝各位作者!


頁表機制準確的說是CPU實現的特性,由OS加以利用。網上有很多陳述頁表機制的文章,但以Linux居多,Windows偏少,且很多都是理論層面借圖表的闡述,或許對於科班出身的人士來說是小菜一碟,可對我等自學愛好者而言卻經常是一臉懵逼的感嘆!因此,萌發了動手實踐的想法,經過一翻折騰,總算小有心得,就此記錄於網際,希望本文能給和我一樣的愛好者以幫助。本文可能存在很多歪解,請各位看官多多斧正!


本文試圖解決的疑問:

頁有多少個?

頁每個程式都有嗎?

頁多大?

頁放在哪裡?

頁如何檢視?

頁如何修改?

頁如何隔離程式?

頁和虛擬地址的關係?

頁在何時被使用?


從test.exe開始:


// test.exe 原始碼
#include <iostream>
 
int main()
{
    std::cin.get ();           // 等待做手腳
 
    int* p = NULL;
    *p = 0x89abcdef   ;       // 向0指標寫入資料
    std::cout << "Hello nullptr!\n";   // 改編自經典:)
 
    return 0;
}

程式碼很簡單不出大問題的話,編譯執行敲回車直接崩潰QAQ。在預設設定的Win10(17134)cmd下可正常執行和退出,但沒有列印“Hello nullptr”,在XP下報錯終止,錯誤程式碼0xC0000005(訪問違規)。可見Win10在異常處理上有所調整(記得《Windows核心程式設計》有提過在Vista之後即如此)。


有C/C++基礎的碼友一定記得一條原則,0x00000000為空指標,設計思想很簡單,例如呼叫malloc返回0表示失敗,如果返回0指標是可用的,那錯誤用什麼表示呢?


我的執行結果是正常的,我承認我搞事情了,嘿嘿!


Windows(x86)頁表與虛擬空間之我見


在動手完成空指標寫入資料之前先大致瞭解一些基本情況,在Windbg列舉程式。


kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 8a0e59c8  SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 00039000  ObjectTable: e1000d10  HandleCount: 240.
    Image: System
 
PROCESS 89be5020  SessionId: 0  Cid: 05d8    Peb: 7ffd6000  ParentCid: 05e4
    DirBase: 19f3c000  ObjectTable: e1e27cc8  HandleCount:  35.
    Image: cmd.exe
 
PROCESS 89ba8318  SessionId: 0  Cid: 0578    Peb: 7ffde000  ParentCid: 0268
    DirBase: 19f97000  ObjectTable: e1b8ef08  HandleCount:  82.
    Image: taskmgr.exe
 
PROCESS 89f52c10  SessionId: 0  Cid: 07c0    Peb: 7ffde000  ParentCid: 05e4
    DirBase: 1d42f000  ObjectTable: e1ba5700  HandleCount:  77.
    Image: OllyICE.exe
 
PROCESS 89bfc470  SessionId: 0  Cid: 01b8    Peb: 7ffde000  ParentCid: 05d8
    DirBase: 24231000  ObjectTable: e1d764b8  HandleCount:   7.
    Image: test.exe
...

資訊經過刪減,留下了幾個比較熟悉的程式,文中類似資訊也會將無關清除,清除部分會用“...”表示,不再單獨註明。


這些程式分別是:taskmgr.exe工作管理員、cmd.exe控制檯、System系統程式、OllyICE.exe偵錯程式、test.exe測試程式。

每個程式的DirBase項的16進位制數可以發現以下特徵:

1、各程式不同;

2、可被4096整除;

3、以000結尾。


如果你的顯示結果與特徵不符,那是另外一種模式PAE,本文不做解釋。從特徵上分析,可被4096整除說明是4K對齊;各程式不相同說明每個程式的起始頁不同,因此得以隔離程式;000結尾說明PDE沒有屬性,PDE為何物見下文。


瞭解了程式資訊之後,再來看看程式的地址、佔用空間大小等資訊,著重觀察test.exe。


Windows(x86)頁表與虛擬空間之我見


在工作管理員視窗紅色框部分,是各程式使用實體記憶體佔用大小,單位Kb,這些數都可以被4整除,所得商即該程式使用的記憶體頁數。


當test.exe在前臺時(最大化)使用了193(772K/4)頁記憶體,在後臺時(最小化)使用9(36/4)頁,由此可見當程式非活動狀態或需求非常少時,實體記憶體會歸還給OS。從test.exe程式碼可知,程式阻塞在cin.get ()呼叫,基本上沒什麼執行需求。(切換的視窗是cmd,因test執行於此) 


OD視窗紅色框部分,是test.exe的邏輯地址和空間大小資訊,此空間大小是程式自身可能用到的大小,粗略合計約1000(4M/4K)頁,通過OD的簡要資訊,可獲知地址處都存放了什麼,如主棧、PE頭、.text程式碼段等。這裡的空間大小基本是固定的。


經過以上分析得到一個結果,程式虛擬空間遠大於物理佔用,實際上程式在執行時,不會將所有內容都放入實體記憶體,僅將當前執行所需要的程式碼、資料放入物理頁。


有了這些線索,我們通過實驗來進一步印證。利用PROCESS項的16進位制數將test.exe切換至當前環境。預設當前環境是System程式,所以查詢顯示資訊將是System程式的。


kd> !process 0 0
...
PROCESS 89bfc470  SessionId: 0  Cid: 01b8    Peb: 7ffde000  ParentCid: 05d8
    DirBase: 24231000  ObjectTable: e1d764b8  HandleCount:   7.
    Image: test.exe
...
kd> .process /i /p 89bfc470
kd> g
kd> r cr3
cr3=24231000

使用r指令檢視了cr3暫存器,顯示的資訊與DirBase項一致,實際上OS在後臺不斷的自動切換cr3,讓每個程式都得到一點執行時間。


實驗所需的知識點簡單介紹一下(以下僅是小頁面,大頁面請翻閱書籍):每個程式都有一頁(4K)存放一個頁目錄(Page Directory Entry,PDE),PDE佔4K空間,分成1024項,每項4B,每項描述一個頁表(Page Table Entr,PTE),PTE的高20位是基址域,低12位是屬性域;PTE也佔4K,同樣可分成1024項,每項4B,每項描述一頁(Page),Page的高20位是基址域,低12位是屬性域。


根據上述可得

1024(PTE 個數)*1024(Page 個數)*4096(Page 大小)=4G(總空間);

1024(PTE 個數)*1024(Page個數)*4(描述項大小)=4M(總佔用);

1024(PTE個數)*1024(Page個數)=1M(總頁數);

實際總佔用僅是理論值,一般程式不可能達到4M,下面實驗會得到證實。


檢視test.exe的PDE,我將PDE分成兩部分,前512項和後512項分別進行分析,因PDE管理4G虛擬空間,但其高2G(後512項)是核心態使用,剩餘低2G(前512項)由使用者態使用(PAE模式使用者態擁有更多的空間)。


kd> !process 0 0
...
PROCESS 89bfc470  SessionId: 0  Cid: 01b8    Peb: 7ffde000  ParentCid: 05d8
    DirBase: 24231000  ObjectTable: e1d764b8  HandleCount:   7.
    Image: test.exe
...
kd> !dd 24231000 l200
#24231000 24766067 245e0067 00000000 00000000
...
#242317c0 00000000 00000000 247cf067 00000000
...
#242317f0 00000000 2459a067 00000000 2451e067
...

PDE前512項中只有5個非空PTE,前面說過1個PTE佔用4K實體記憶體,含1024個Page描述,也就是說1個PTE維護4M(1024*4096)空間,因此可認為OS給test.exe分配了20M(4*5)虛擬空間。


我把以上資料和OD的邏輯地址整理成表進行對比。


Windows(x86)頁表與虛擬空間之我見

 

我相信你已經找到了相似特徵,邏輯地址與PTE是對映關係。更準確的說邏輯地址值是一個三段式資料結構,關於該結構我用一段程式碼來說明。


// demo.exe 原始碼
#include <iostream>
 
int main()
{
    struct {
        ULONG offset : 12;
        ULONG pteIndex : 10;
        ULONG pdeIndex : 10;
    } va { 0 };
 
    using namespace std;
 
    while (true) {
        cin >> hex >> *(ULONG*)&va;
        cout << hex << "邏輯地址:0x" << *(ULONG*)&va << endl
             << "PDE索引:0x" << va.pdeIndex
             << " PDE物理偏移:0x" << va.pdeIndex * 4 << endl
             << "PTE索引:0x" << va.pteIndex
             << " PTE物理偏移:0x" << va.pteIndex * 4 << endl
             << "頁內偏移:0x" << va.offset << endl;
    }
 
    return 0;
}

Windows(x86)頁表與虛擬空間之我見


PDE可通過cr3得到,PDE+偏移即可找到PTE描述,PTE+偏移又可找到Page描述,Page+頁內偏移可指向具體資料,這就是用邏輯地址搜尋頁表的過程。


進一步檢視PTE索引1(第二個PTE)的內容:


kd> !process 0 0
...
PROCESS 89bfc470  SessionId: 0  Cid: 01b8    Peb: 7ffde000  ParentCid: 05d8
    DirBase: 24231000  ObjectTable: e1d764b8  HandleCount:   7.
    Image: test.exe
...
kd> !dd 24231000
#24231000 24766067 245e0067 00000000 00000000
...
kd> !dd 245e0000 l400
#245e0000 24430025 2456c025 2436d025 243ee025
#245e0010 242af025 24470025 24271025 244f2025
#245e0020 242b3025 242f4025 24375025 242b6025
#245e0030 23fb7025 242b8025 240f9025 2423a025
#245e0040 243fb025 241bc025 241bd025 242be025
#245e0050 242bf025 24540025 243c1025 24502025
#245e0060 24543025 248c4025 24705025 27da8025
#245e0070 2459e025 24647025 00000000 24708025
#245e0080 24349025 00000000 2444a025 2454b025
#245e0090 2450c025 00000000 00000000 00000000
#245e00a0 24362025 24523025 24664025 2465f067
#245e00b0 24660067 24625025 00000000 00000000
...

擷取部分中共41項非空Page描述(省略的都是空Page),第二個PTE管理的4M空間範圍是0x00400000~0x007FFFFF,回顧OD截圖在這4M範圍內的大小合計是46頁,這裡相差5個頁,原因是什麼我不清楚,猜測可能是軟體之間的誤差,有知道的請告知!


PTE和Page的低12位是屬性域,資料上對應的如025、067等,屬性域其中一位描述了資料是在實體記憶體還是在硬碟的虛擬記憶體,OS通過屬性域控制記憶體,PTE可控制4M,Page可控制4K。更多屬性的內容請翻閱書籍。


在OD截圖可知0x00401000是.text段的起始,從demo程式的顯示結果獲知該邏輯地址PTE的索引1偏移4,現在來看該Page內容:


kd> !process 0 0
...
PROCESS 89bfc470  SessionId: 0  Cid: 01b8    Peb: 7ffde000  ParentCid: 05d8
    DirBase: 24231000  ObjectTable: e1d764b8  HandleCount:   7.
    Image: test.exe
...
kd> !dd 24231000
#24231000 24766067 245e0067 00000000 00000000
...
kd> !dd 245e0000
#245e0000 24430025 2456c025 2436d025 243ee025
...
kd> !db 2456c000
#2456c000 b9 a0 c1 42 00 e8 24 2b-00 00 68 29 b5 41 00 e8
#2456c010 d2 25 00 00 59 c3 68 33-b5 41 00 e8 c6 25 00 00
#2456c020 59 c3 68 3d b5 41 00 e8-ba 25 00 00 59 c3 6a 01
#2456c030 6a 00 68 a8 c2 42 00 b9-00 c3 42 00 e8 82 33 00
#2456c040 00 68 47 b5 41 00 e8 9b-25 00 00 59 c3 56 57 6a
#2456c050 00 e8 ee b9 00 00 59 bf-a8 c2 42 00 8b f0 8b cf
#2456c060 e8 ca 33 00 00 6a 00 56-8b cf c7 05 a8 c2 42 00
#2456c070 18 ce 41 00 e8 98 37 00-00 68 51 b5 41 00 e8 63

或許有人會問這些資料是什麼?我們來看一張截圖,看完之後自然豁然開朗……!


Windows(x86)頁表與虛擬空間之我見

所謂的頁內偏移就是以上資料頁位元組的位置,Page[0]=b9,Page[1]=a0……Page[15]=e8,偏移的範圍是0~4095(4K頁內)。


上文書說到:“令貴妃魏瓔珞為救五阿哥永琪……”。咦!好像是走錯片場了……,好吧,書接上文……。


前面分析了PDE前512項,基本清楚了含蓋的內容,現在來分析PDE的後512項,我將test.exe的後512項PTE與taskmgr、cmd、System、OllyICE等4個程式的後512項PTE進行了對比,發現這些PTE只有2項不同,其它完全一致,共有414項非空PTE(含2項不同),按每項管理4M來計算(414-2)*4=1648M,可認為這些空間是所有程式共享的。


不同項和偏移如下:


kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 8a0e59c8  SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 00039000  ObjectTable: e1000d10  HandleCount: 240.
    Image: System
 
PROCESS 89be5020  SessionId: 0  Cid: 05d8    Peb: 7ffd6000  ParentCid: 05e4
    DirBase: 19f3c000  ObjectTable: e1e27cc8  HandleCount:  35.
    Image: cmd.exe
 
PROCESS 89ba8318  SessionId: 0  Cid: 0578    Peb: 7ffde000  ParentCid: 0268
    DirBase: 19f97000  ObjectTable: e1b8ef08  HandleCount:  82.
    Image: taskmgr.exe
 
PROCESS 89f52c10  SessionId: 0  Cid: 07c0    Peb: 7ffde000  ParentCid: 05e4
    DirBase: 1d42f000  ObjectTable: e1ba5700  HandleCount:  77.
    Image: OllyICE.exe
 
PROCESS 89bfc470  SessionId: 0  Cid: 01b8    Peb: 7ffde000  ParentCid: 05d8
    DirBase: 24231000  ObjectTable: e1d764b8  HandleCount:   7.
    Image: test.exe
...
kd> !dd 00039000+c00 l2
#   39c00 00039067 0a640063
kd> !dd 19f3c000+c00 l2
#19f3cc00 19f3c063 19ffd063
kd> !dd 19f97000+c00 l2
#19f97c00 19f97063 19f58063
kd> !dd 1d42f000+c00 l2
#1d42fc00 1d42f063 1d5f0063
kd> !dd 24231000+c00 l2
#24231c00 24231063 244b2063

從資料上看不同的第一個PTE基址域都指向了自己,而屬性域指明瞭該PTE歸核心態空間所有,第二個不同PTE沒有深挖。如有知者請告知。將這個位置轉換成邏輯地址是0xC0000000,可以想見PTE是由核心態空間儲存的。


現在來說下test.exe向0地址寫入資料的方法,首先將0x00000000邏輯地址分解成三段式結構,通過所得結果找到對應頁一探究竟。


kd> !process 0 0
...
PROCESS 89bfc470  SessionId: 0  Cid: 01b8    Peb: 7ffde000  ParentCid: 05d8
    DirBase: 24231000  ObjectTable: e1d764b8  HandleCount:   7.
    Image: test.exe
...
// 修改之前
kd> !dd 24231000 
#24231000 24766067 245e0067 00000000 00000000
...
// 原因是沒有分配頁
kd> !dd 24766000
#24766000 00000000 00000000 00000000 00000000
...
// 修改
kd> !ed 24766000 29e3c067
 
// 修改之後
kd> !dd 24766000
#24766000 29e3c067 00000000 00000000 00000000

或許有人會問“29e3c067”是哪裡來的,《OS 學習筆記》的作者是將主棧的描述掛到此處,但我認為主棧涉及區域性變數、呼叫引數和返回地址等,修改可能造成程式報錯,因此我的方法是用OD的外掛分配一頁堆空間,並將堆邏輯地址解析成三段式找到對應的Page描述將其掛到此處,“29e3c067”就是該堆頁的描述。


總結:

test.exe分析結果統計,1個PDE,5個PTE,佔6個4K物理頁,後512項那些PTE或複製或對映,總之這些都不屬於當前程式,5個PTE所描述的是20M虛擬空間,程式僅用4M,20M是5120頁,4M是1024頁,急需執行的193頁放入實體記憶體。



原文作者: khristian (看雪ID)

原文連結:https://bbs.pediy.com/thread-246611.htm

轉載請註明:轉自看雪論壇





看雪推薦:


1、[原創] PEDIY-JD CTF 2018 Windows CrackMe 題目及設計思路


2、[原創]覺醒之戰Ⅰ:洞察HW程式設計師的腦洞


3、[原創]幾種常見的注入姿勢


4、[翻譯]StaDynA:解決Android APP安全分析中的動態程式碼更新問題


5、[原創]新手——win32程式的半生(CreateProcess)



相關文章