保護模式篇——分頁基礎
- 2021 年 10 月 16 日
- 筆記
- 羽夏看Win系統內核
寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎回饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並聲明我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統內核——簡述 ,方便學習本教程。
看此教程之前,問幾個問題,基礎知識儲備好了嗎?前面的教程學會了嗎?沒有的話就不要繼續了。
🔒 華麗的分割線 🔒
前言
在講解分頁基礎之前,我們先大體了解CPU
是如何在保護模式下訪問數據的,如下圖所示:
比如我們執行mov eax,ds:[0x12345678]
這句彙編指令的時候,0x12345678
這個線性地址會傳遞給CPU
,先查詢TLB
和快取
有沒有,有的話直接取出來返回;如果沒有,經過MMU(記憶體管理單元)
處理得到物理地址,通過固定的分頁模式直接找到,取出數據返回。
分頁
前面的教程講解了段的機制,接下來將介紹頁的機制。CPU為了方便管理物理記憶體,按照頁的方式進行管理記憶體。可用的所有記憶體可以類比為一本書,而所有的記憶體被分為這本書的一個頁。對於32位來說,它有10-10-12
分頁和2-9-9-12
分頁。其中10-10-12
分頁最為簡單,故拿其作為詳細講解,作為分頁講解的基礎。
我們都了解一個進程都有4GB的虛擬地址空間,它們並不是真正的地址,而是個索引。它通過某種方式進行轉換,從而指向真正的物理地址,示意圖如下所示:
而虛擬地址也被稱作線性地址。舉個例子,比如某個進程裡面我想讀取一個0x12345678
,它就是線性地址,通過一些轉換,找到了對應的物理地址0x10101010
,如下圖所示:
每個進程都有一個CR3
,準確的說是都一個CR3
的值。CR3
本身是個暫存器,一核一套。CR3
裡面放的是一個真正的物理地址,指向一個物理頁,一共4096位元組
,如下圖所示:
對於10-10-12
分頁來說,線性地址對應的物理地址是有對應關係的,它被分成了三個部分,每個部分都有它具體的含義。線性地址分配的結構如下圖所示:
第一個部分指的是PDE
在PDT
的索引,第二部分是PTE
在PTT
的索引,第三個部分是在PTE指向的物理頁的偏移。PDT
被稱為頁目錄表,PTT
被稱為頁表。PDE
和PTE
分別是它們的成員,大小為4個位元組。我們接下來將詳細介紹每一個部分是咋用的。
直接純理論的講解挺抽象的,我們先通過一個實現初步探測一下它們的存在。注意在學習之前需要對作業系統的調試設置進行修改,因為系統默認的是2-9-9-12
分頁。如下圖所示修改並重啟作業系統即可啟用10-10-12
分頁:
實驗探測
這次就需要我們的CheatEngine
,它是一個強大的記憶體搜索工具。我們打開一個記事本,然後打開CheatEngine 6.3
,如下圖所示:
然後我們按照下圖所示來打開notepad
進程:
然後在記事本中寫入一個字元串This is a test
,然後在CheatEngine
搜索這個字元串,注意選中Unicode
,否則字元串搜不到。
然後隨便改一下記事本最後一個字元串的字母,如下圖所示,即可定位到真正的存儲記事本填寫內容的線性地址。
找到線性地址後,打開WinDbg
,找到記事本的CR3
,如下圖所示:
我們按位元組讀取線性地址的記憶體時,在WinDbg的指令是dd [地址]
。如果是物理地址的記憶體的話,需要在前面加一個英文嘆號,即為!dd [地址]
,查詢內容的流程如下圖所示:
10-10-12 分頁整體結構
通過實驗我們了解了它們的結構,接下來將詳細介紹了。根據實驗結果的體驗,可以給出如下圖:
分頁並不是由作業系統決定的,而是由CPU
決定的。只是作業系統遵守了CPU
的約定來實現的。物理頁是什麼?物理頁是作業系統對可用的物理記憶體的抽象,按照4KB
的大小進行管理(Intel
是按照這個值做的,別的CPU
就不清楚了),和真實硬體層面上的記憶體有一層的映射關係,這個不是保護模式的範疇,故不介紹。
PDE 與 PTE
前面我們簡單了解PDE
和PTE
,接下來將學習它們的屬性結構,結構如下:
P 位
表示PDE
或者PTE
是否有效,如果有效為1
,反之為0
。
R/W 位
如果R/W = 0
,表示是只讀的,反之為可讀可寫。
U/S 位
如果U/S = 0
,則為特權用戶(super user),即非3環許可權。反之,則為普通用戶,即為3環許可權。
PS位
這個位只對PDE
有意義。如果PS == 1
,則PDE
直接指向物理頁,不再指向PTE
,低22位是頁內偏移。它的大小為4MB
,俗稱「大頁」。
A 位
是否被訪問,即是否被讀或者寫過,如果被訪問過則置1
。
D 位
臟位,指示是否被寫過。若沒有被寫過為0
,被寫過為1
。
注意,下面的三個位的講解將涉及 TLB 和控制暫存器相關知識,為了保證文章的完整性,故先介紹。之後將會詳細講解。
G 位
表示是否為全局頁。它的作用是什麼呢?舉個例子,作業系統的進程的高2G
映射基本不變,如果Cr3
改了,TLB
刷新重建高2G
以上很浪費。所以PDE
和PTE
中有個G
位,如果為1,刷新TLB
時將不會刷新它指向的頁。
PWT 位
當PWT = 1
,寫快取的時候也要將數據寫入記憶體中。
PCD 位
當PCD = 1
時,禁止某個頁寫入快取,直接寫記憶體。比如,做頁表用的頁,已經存儲在TLB中了,可能不需要再快取了。
注意事項
PTE
可以沒有物理頁,且只能對應一個物理頁。- 多個
PTE
也可以指向同一個物理頁。 PDE
和PTE
重合的屬性共同決定著最終物理頁的屬性。比如P位
,如果有一個是0,那麼最終的物理頁就是無效的。但是PDE
和PTE
它們的屬性的影響範圍是不一樣的。數值上:物理頁的屬性 = PDE屬性 & PTE屬性。
頁目錄表基址與頁表基址
在學習本部分之前,請把練習的第一題做一下,以加深對此的印象。
如果系統要保證某個線性地址是有效的,必須為其填充正確的PDE
與PTE
,如果我們想填充PDE
與PTE
那麼必須能夠訪問。有的人會想,直接拿CR3
去填寫就行了,還需要頁目錄表基址幹嘛?這裡我強調一下:作業系統只能用線性地址,不能用物理地址。CR3
存儲的是物理地址,這個是給CPU
看的,不是給作業系統看的。作業系統訪問它就必須知道它的線性地址才行。CPU
可不幫我們掛物理頁,它做不到這點,只能提供要求標準,而作業系統按照標準進行辦事。於是乎頁目錄表基址與頁表基址這兩個東西就出現了。
通過頁目錄表基址,作業系統可以幫我們程式掛上正確的PDE
,通過頁表基址掛上正確的PTE
,然後指向正確的物理頁。
先說一下頁目錄表基址,我們先拆分一下這個線性地址:0xC0300000
。請讀者先自行拆分,並用虛擬機查看物理記憶體後,再查看下面的結果。
🔒 華麗的分割線 🔒
我在虛擬機啟動了一個notepad進程,中斷作業系統轉到Windbg中。查看進程可以得到它的CR3
。然後我們查看它的物理地址,即可看到它指向的PDT
。拆一下該線性地址,我們可以得到如下結果:
PDI -- 11 0000 0000
PTI -- 11 0000 0000
物理頁偏移 -- 0x000
為什麼查詢的後面加0xC00
,而不是0x300
呢。是因為PDE
和PTE
的大小都是4
個位元組,乘上4
後就是0xC00
了。最後查看到,到最後內容又指向了自己。即0xC0300000
存儲的就是帶PTE
屬性的CR3
。我們根據當前所學的概念可以重新定義10-10-12
分頁的結構框圖:
同理,作業系統也需要頁表基址。如果你老老實實地做了第一道題,你就知道我們找PDT
的時候就已經發現了指向PTT
的線性地址了,那就是0xC0000000
。具體實驗我就不再贅述了,最終我們得到下面的結構圖:
好了,我們思考一下有了0xC0300000
和0xC0000000
能做什麼?如果我們掌握了這兩個地址,就掌握了一個進程所有的物理記憶體讀寫許可權。尋找PDE和PTE的公式總結如下:
- 訪問頁目錄表的公式:
0xC0300000 + PDI * 4
- 訪問頁表的公式:
0xC0000000 + PDI * 4096 + PTI * 4
練習
本節的答案將會在下一節進行講解,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習比較多,請保質保量的完成。
1️⃣ 拆兩個進程的4GB
物理頁。(建議閱讀頁目錄表基址與頁表基址
這個部分前完成)
2️⃣ 定義一個只讀類型的變數,再另一個線性地址指向相同的物理頁,通過修改PDE
/PTE
屬性,實現可寫。
3️⃣ 分析0x8043F00C
線性地址的PDE
屬性。
4️⃣ 修改一個高2G線性地址的PDE
/PTE
屬性,實現Ring3
可讀。
5️⃣ 在0
線性地址掛上物理頁並執行shellcode
調用MessageBox
。
6️⃣ 逆向分析MmIsAddressValid
函數。
下一篇
保護模式篇——PAE分頁