2020-11-24(Windows保护模式学习笔记(1)—— 段寄存器与段权限检查)
X86 CPU的三個模式:實模式、保護模式和虛擬8086模式
0x01 段寄存器
通常情況下,我們認為有8個段寄存器,他們分別是
ES CS SS DS FS GS LDTR TR
其中,后四個寄存器沒有處理器定義,是由操作系統運行它們來賦予目的的。
段寄存器具有以下結構
(紅色字體視環境而定)
(注意:在64位模式下,Base會占64位,且可由用戶設定,且Limit對于所有段寄存器均無效)
在對段寄存器進行讀寫時,通常我們采用以下格式
讀:MOV AX,ES 寫:MOV DS,AX但是從段寄存器結構中我們可以看到,段寄存器有96位,而AX寄存器只有16位,那在讀的時候我們可以正常的取低16位(Selector)讀入AX,而在寫時,就不太一樣了,仍然會將96位數據寫入Dst,那剩下的80位數據從何而來就是關鍵了。 在寫段寄存器的值時,系統會先取ax的值,拆分出索引位,到gdt表里去找對應的值,最后修改整個Dst的值
0x02 GDT表與LDT表
為了解決上面所說關于剩下80位數據哪里來的問題,就要引入GDT的概念GDT(Global Descriptor Table 全局描述符表)。通常情況下,在設計程序時,我們認為段寄存器為16-bit(雖然每個段寄存器事實上有一個64-bit長的不可見部分,但對于程序員來說,段寄存器就是16-bit),但是為了描述一個段,還需要【Base Address, Limit, Attr】三方面因素,它們加在一起被放在一個64-bit長的數據結構中,被稱為段描述符(上面所說的缺少了80位數據,但是這里只存儲64位數據是因為80位中只有部分位是有效的)。也就是說,本應該需要64-bit的段寄存器來存儲段描述符,但Inter為了向下兼容而規定段寄存器為16-bit,這就需要程序在運行時能通過這16bit數據來找到64bit的段描述符。
怎么解決呢? 解決方法就是將這個64-bit的段描述符放入一個數組中,而將段寄存器中的值作為下標索引來間接引用(事實上,是將段寄存器中的高13 -bit的內容作為索引)。這個全局的數組就是GDT。 但是,GDT這個數組中,存放的不僅僅是段描述符,還有其他的一些64-bit長的描述符。
GDT是Protected Mode所必須的數據結構,也是唯一的。且正如它的名字(GGDTlobal Descriptor Table)所示,它是全局可見的,對任何一個任務而言都是這樣。
GDT存在哪?怎么找?
GDT可以被放在內存的任何位置,那么當程序員通過段寄存器來引用一個段描述符時,CPU必須知道GDT的入口,也就是基地址放在哪里。這就引入了 GDTR 概念。 為了解決這個問題,Intel的設計者們提供了一個名為GDTR的寄存器用來存放GDT的入口地址,程序員將GDT設定在內存中某個位置之后,可以通過LGDT指令將GDT的入口地址裝入此寄存器,從此以后,CPU就根據此寄存器中的內容作為GDT的入口來訪問GDT了。
LDT:
除了GDT之外,IA-32還允許程序員構建與GDT類似的數據結構,它們被稱作LDT(Local Descriptor Table,局部描述符表),但與GDT不同的是,LDT在系統中可以存在 多個,并且從LDT的名字可以得知,LDT不是全局可見的,它們只對引用它們的任務可見,每個任務最多可以擁有一個LDT。另外,每一個LDT自身作為一個段存在,它們的段描述符被放在GDT中。
LDT只是一個可選的數據結構,你完全可以不用它。使用它或許可以帶來一些方便性,但同時也帶來復雜性,如果你想讓你的OS內核保持簡潔性,以及可移植性,則最好不要使用它。
同樣的,GDT有GDTR,LDT也有LDTR
IA-32為LDT的入口地址也提供了一個寄存器LDTR,因為在任何時刻只能有一個任務在運行,所以LDT寄存器全局也只需要有一個。如果一個任務擁有自身的LDT,那么當它需要引用自身的LDT時,它需要通過lldt指令將其LDT的段描述符裝入此寄存器。lldt指令與lgdt指令不同的時,lgdt指令的操作數是一個32-bit的內存地址,這個內存地址處存放的是一個32-bit GDT的入口地址,以及16-bit的GDT Limit。而lldt指令的操作數是一個16-bit的選擇子,這個選擇子主要內容是:被裝入的LDT的段描述符在GDT中的索引值。
實模式&&保護模式
看到這里,很可能你會有疑惑,在匯編課上,或者開發中,學到的尋址方式就是段基址左移4位+偏移地址,這個段基址也就是段寄存器的值,但是上面的說法卻顛覆原來所學的知識。這里就需要補充一下實模式和保護模式的知識。其實這兩種說法都是正確的,只是一種是實模式一種是保護模式。保護模式是在實模式上建立起來的,他們有一個區別就是如何得到段基址。實模式是將基址直接存在段寄存器中,而保護模式是將段基址統一存放在表中,這個表就是我們前面所說的GDT和IDT。明白了這個應該能解決不少疑惑。
0x03 段選擇子
前面我們說了,我們通過使用段選擇子作為索引去GDT/LDT取得保存該段信息的80位數據。那么問題來了,系統該如何區分數據保存在GDT還是LDT,請求的權限是多少,索引值是選擇子的一部分還是全部? 這就需要了解段選擇子的結構。
字段說明:
我們可以看到,Index是表示所需要的段的描述符在描述符表的位置,由這個位置再根據在GDTR/LDTR中存儲的描述符表基址就可以找到相應的描述符。然后用描述符表中的段基址加上邏輯地址(SEL:OFFSET)的OFFSET就可以轉換成線性地址。
0x04 段與權限
CPU與ring環
這幅圖描述的是CPU的分級概念。一般來說,我們在 Windows 中寫的代碼,都運行在 3 環(應用層)。只有在調用一些系統接口的時候,或者中斷等情況的時候,才會進入 0 環(內核)。也就是為什么平時我們稱應用程序為3環,系統程序為0環。
為什么要讓CPU給代碼賦予不同的權限?這是為了防止你隨意更改一些內核中的重要數據,以避免藍屏。為此,CPU 把執行權限分成了4個等級,0,1,2,3,這就是所謂的ring環, 其中 0 表示最高級,可以執行任意代碼,而執行權限為 3 的程序,只能執行普通的指令。Windows 和 Linux 只使用了 0 和 3 這兩種。而 CPU 的有些指令和對于某些特殊內存的訪問,只能在 0 環執行,這就是所謂的保護。
下面我們需要思考,ring環由什么決定,如何確定某個應用程序是處于哪一環的。
進程特權級別
當前特權級(CPL)
前面在段選擇子結構中,我們講到了 CS寄存器的末兩位是CPL,即當前進程的特權級(注意:段選擇子SS和CS的后兩位比特位相同),而其他段的末兩位則是RPL,這也非常好理解,畢竟CS是代碼段,運行的程序的代碼即在這個段上,這個段已經沒有RPL存在的意義了,故此處保存當前進程的特權級最為合適。同時,我們可以理解為是當前正在執行的代碼所在的段的特權級,可以看成是段描述符未加載入CS前,該段的DPL,加載入CS后就存入CS的低兩位,所以叫做CPL,其值就等于原段DPL的值。
其計算方式如下
請求特權級(RPL)
RPL作為段選擇子的一部分,是針對段選擇子而言的,每個段的選擇子都有自己的RPL,其表示用什么權限去訪問一個段,也意味著它的改變不會對你所要訪問哪個段產生影響(影響你訪問哪個段的僅僅由Index決定)。RPL可以實現一些權限的控制,比如當前進程的CPL是0,但我想以一個只讀的形式訪問一個數據段,就可以用RPL來降低權限(如RPL置3)
段特權級(DPL)
DPL保存在段描述符的段屬性中,其規定了訪問所在段描述符所需要的特權級別是多少。它與ring環一樣,都是數值越大,權限越小。
(注意:在Windows中,DPL只會出現兩種情況,要么全為0,要么全為1,即0或3)
數據段權限檢查
既然有權限,那就必然會有檢查。段的權限檢查要求 CPL<= DPL 并且 RPL<= DPL
即段的特權級必然小于等于當前進程的特權級,且我們請求的特權級要大與等于該段的特權級。
數據段權限檢查,本質上就是檢查能不能把段選擇子代入到段寄存器。如果代入成功,表明權限檢查通過。如果代入不成功,說明權限不夠
0x05 代碼跨段跳轉
通常,CS寄存器在被載入后,往往不會再去用指令對其值進行修改,同時8086匯編也明確規定了 “除CS外,其他的段寄存器都可以通過MOV,LES,LSS,LDS,LFS,LGS指令進行修改”。那么,為什么CS寄存器不可以直接修改呢?EIP寄存器,記錄的其實也是一個偏移,相對于CS的偏移,若是我們對CS進行了修改,改變CS的同時也必須修改EIP,而上述的指令都不具備改變EIP的效果,所以這些指令不能用于CS寄存器。
一致代碼段與非一致代碼段
在介紹代碼段跳轉前,需要先了解一下代碼段的種類。代碼段分為兩種,分別是 一致代碼段 與 非一致代碼段,而且判定依據——“一致位”存在段描述符表示段屬性的第五字節上。這里來回頭重新看一下段屬性
P:為Persent存在位。1 表示段在內存中存在;0表示段在內存中不存在。
S: 表示描述符的類型。1 數據段和代碼段描述符;0 系統段描述符和門描述符
TYPE域:
當S = 1時,即段描述符為代碼段或數組段描述符時,Type域結構圖如下:
換成人話說,當 S=1 時TYPE中的4個二進制位情況:
這就非常明顯了,是一致代碼段還是非一致代碼段由一致位是否置1決定。那這兩者之間究竟有什么區別呢?
一致代碼段:簡單理解,就是操作系統拿出來被共享的代碼段,可以被低特權級的用戶直接調用訪問的代碼。通常這些共享代碼,是”不訪問”受保護的資源和某些類型異常處理。比如一些數學計算函數庫,為純粹的數學運算計算,被作為一致代碼段。如果選擇一致代碼段,低級別的程序就可以在不提升CPL權限等級的情況下進行訪問,并且不會破壞內核態的數據
非一致代碼段:為了避免低特權級的訪問而被操作系統保護起來的系統代碼。
一致代碼段的限制作用:
特權級高的程序不允許訪問特權級低的數據:核心態不允許調用用戶態的數據.
特權級低的程序可以訪問到特權級高的數據.但是特權級不會改變:用戶態還是用戶態.
非一致代碼段的限制作用:
只允許同級間訪問,絕對禁止不同級訪問:核心態不用用戶態.用戶態也不使用核心態.
權限檢查
一致代碼段:要求 CPL >= DPL;
非一致代碼段:要求 CPL == DPL并且 RPL <= DPL
可以看到,代碼段的權限檢查與數據段權限檢查有很大的區別。數據段不允許低權限對高權限的訪問;而代碼段不允許高權限對低權限的訪問,可視C位是否置1而決定是否允許低權限對高權限的訪問。至于為什么會這樣,我的理解是不可以將代碼段的權限理解為權力,而是安全等級。
代碼段是我們所需要運行的,而內核態比用戶態更需要安全,如果允許了安全級別更高的代碼(內核態)訪問了安全級別低的代碼(用戶態),低安全級別代碼的情況對于高安全級別的代碼是不清楚的,也就是說,假如用戶態的東西有問題,內核態調用了有問題的用戶態,就會有崩潰的風險。而用戶態調用內核態的代碼,必然是用戶態確實有這個需求要調用,如果發生了崩潰,那一定是用戶態代碼存在問題,這樣的設計將保證我內核態沒有問題。
總結:一致代碼段可以當當前特權級低于目標代碼段特權級時也可以直接進行跳轉,但是不會改變特權級(用戶態還是用戶態);非一致代碼段低特權級程序要跳轉到高特權級的代碼段需要用過“調用門”等方式暫時提升CPL后才可進行跳轉;在代碼段的 跳轉中 絕對禁止 高特權級程序向低特權級代碼段進行跳轉(內核態跳至用戶態)
0x06 學習資料
lzyddf師傅的博客
https://blog.csdn.net/wrx1721267632/article/details/52056910
https://my.oschina.net/u/1777508/blog/1821254
https://www.cnblogs.com/geason/p/5774088.html
https://www.jianshu.com/p/68433e97f864
總結
以上是生活随笔為你收集整理的2020-11-24(Windows保护模式学习笔记(1)—— 段寄存器与段权限检查)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2020-11-24(dll注入的N种搞
- 下一篇: 2020-11-26((《深入理解计算机