1. 程式人生 > >Linux保護模式之-CPL、RPL、DPL

Linux保護模式之-CPL、RPL、DPL


較為核心的程式碼和資料放在較高(靠內)的層級中,處理器用此來防止較低特權的任務在不被
允許的情況下訪問處於高特權級的段。為了防止概念混淆,我們不用特權級大小來說明,改
為內層(高),外層(低)來講。
特權級有3 種:CPL,DPL 和RPL,每個都是有4 個等級。
我對他們的關係理解是這樣:一般來說,CPL 代表當前程式碼段的許可權,如果它想要去訪問
一個段或門,首先要看看對方的許可權如何,也就是檢查對方的DPL,如果滿足當前的許可權
比要訪問的許可權高,則有可能允許去訪問,有些情況我們還要檢查選擇子的許可權,即RPL,
因為我們通過選擇子:偏移量的方式去訪問一個段,這算是一個訪問請求動作,因此稱為請
求訪問許可權RPL(Requst Privilege Level)。當請求許可權也滿足條件,那麼訪問就被
允許了。
CPL(Current Privilege Level)
CPL 是當前執行的任務的特權等級,它儲存在CS 和SS 的第0 位和第1 位上。(兩位表示0~3
四個等級)
通常情況下,CPL 等於程式碼所在段的特權等級,當程式轉移到不同的程式碼段時,處理器將改變
CPL。
注意:在遇到一致程式碼段時,情況特殊,一致程式碼段的特點是:可以被等級相同或者更低特權級
的程式碼訪問,當處理器訪問一個與當前程式碼段CPL 特權級不同的一致程式碼段時,CPL 不會改
變。
DPL(Descriptor Privilege Level)
表示門或者段的特權級,儲存在門或者段的描述符的DPL 欄位中。正如上面說的那樣,噹噹前
程式碼段試圖訪問一個段或者門時,其DPL 將會和當前特權級CPL 以及段或門的選擇子比較,
根據段或者門的型別不同,DPL 的含義不同:
1.資料段的DPL:規定了訪問此段的最低許可權。比如一個數據段的DPL 是1,那麼只有運
行在CPL 為0 或1 的程式才可能訪問它。為什麼說可能呢?因為還有一個比較的因素是RPL。
訪問資料段要滿足有效特權級別(上述)高於資料段的DPL.
2.非一致程式碼段的DPL(不使用呼叫門的情況):DPL 規定訪問此段的特權,只有CPL 與
之相等才有可能訪問。
3.呼叫門的DPL,規定了程式或任務訪問該門的最低許可權。與資料段同。
4.一致程式碼段和通過呼叫門訪問的非一致程式碼段,DPL 規定訪問此段的最高許可權。
比如一個段的DPL 為2,那麼CPL 為0 或者1 的程式都無法訪問。
5. TSS 的DPL,同資料段。
RPL(Rquest Privilege Level)
RPL 是通過選擇子的低兩位來表現出來的(這麼說來,CS 和SS 也是存放選擇子的,同時CPL
存放在CS 和SS 的低兩位上,那麼對CS 和SS 來說,選擇子的RPL=當前段的CPL)。處理
器通過檢查RPL 和CPL 來確認一個訪問是否合法。即提出訪問的段除了有足夠的特權級CPL,
如果RPL 不夠也是不行的(有些情況會忽略RPL 檢查)。
為什麼要有RPL?
作業系統往往通過設定RPL 的方法來避免低特權級的應用程式訪問高特權級的內層資料。
例子情景:呼叫者呼叫作業系統的某過程去訪問一個段。
當作業系統(被呼叫過程)從應用程式(呼叫者)接受一個選擇子時,會把選擇子的RPL 設定稱調
用者的許可權等級,於是作業系統用這個選擇子去訪問相應的段時(這時CPL 為作業系統的等級,
因為正在執行作業系統的程式碼),處理器會使用呼叫者的特權級去進行特權級檢查,而不是正在
實施訪問動作的作業系統的特權級(CPL),這樣作業系統就不用以自己的身份去訪問(就防止了
應用去訪問需要高許可權的內層資料,除非應用程式本身的許可權就足夠高)。
那麼RPL 的作用就比較明顯了:因為同一時刻只能有一個CPL,而當低許可權的應用去呼叫擁
有至高許可權的作業系統的功能來訪問一個目標段時,進入作業系統程式碼段時CPL 變成了操作系
統的CPL,如果沒有RPL,那麼許可權檢查的時候就會用CPL,而這個CPL 許可權比應用程式高,
也就可能去訪問需要高許可權才能訪問的資料,這就不安全了。所以引入RPL,讓它去代表訪問
許可權,因此在檢查CPL 的同時,也會檢查RPL.一般來說如果RPL 的數字比CPL 大(許可權比
CPL 的低),那麼RPL 會起決定性作用。
說這麼多不明白都不行啦~真累
下面是引用的一個超棒的關於許可權控制的總結:
引用地址
還有一篇文章在此。
資料訪問時的許可權check
一、 訪問data segment 時(ds、es、fs 及gs)
1、 程式指令要訪問資料時,data segment selector 被載入進 data segment register
(ds、es、fs 和 gs)前,處理器會進行一系列的許可權檢查,通過了才能被載入進入segment
register。處理器分為兩步進行檢查:
★ CPL(當前程式執行的許可權級別)與RPL(位於selector 中的 RPL)作比較,並設定
有效許可權級別為低許可權的一個。
★ 得出的有效許可權級別與DPL(segment descriptor 中的DPL)作比較,有效許可權級
別高於DPL,那麼就通過。低於就不允許訪問。
2、舉個例子:
如果:CPL = 3、RPL = 2、DPL = 2。那麼
EPL = CPL > RPL ? CPL : RPL;
if (EPL <= DPL) {
/* 允許訪問 */
} else {
/* 失敗,#GP 異常生產,執行異常 */
}
或者:
if ((CPL <= DPL) && (RPL <= DPL)) {
/* 允許訪問 */
} else {
/* 失敗,#GP 異常生產,執行異常 */
}
也就是要訪問目標data segment,那麼必須要有足夠的許可權,這個足夠的許可權就是:
當前執行的許可權級別及選擇子的請求許可權級別要高於等於目標data segment 的許可權級
別。
二、 訪問stack segment 時
1、 該問stack 時的許可權檢查更嚴格,CPL、RPL 及DPL 三者必須相等才能通過該問請
求。
2、 舉個例子:
if (CPL == RPL && RPL == DPL && CPL == DPL) {
/* 允許訪問 */
} else {
/* 失敗,#GP 異常生產,執行異常 */
}
也就是說每個許可權級別有相對應的statck segment。不能越權訪問,即使高許可權訪問低權
限也是被拒絕的
控制權的轉移及許可權檢查。
許可權檢查的4 個要素:
★ CPL:處理器當前執行的級別,也就是:當前 CS 的級別,在 CS 的 BIT0 ~ Bit1
★ DPL:訪問目的碼段所需的級別。定義在 segment descriptor 的 DPL 域中
★ RPL: 通過 selector 進行訪問時,selector 內定義的級別。
★ conforming/nonconforming:目的碼屬於 nonconforming 還是 conforming
定義在segment descritptor 的 C 標誌位中
x86 的各方面檢查依賴於目的碼段是 nonconforming(不一致) 還是
conforming(一致) 型別
一、 直接轉移(far call 及 far jmp)
1、 直接轉移定義為不帶gate selector 或 taskselector 的遠呼叫。當執行一條 call
cs:eip 或 jmp cs:eip 指令時,cs 是目的碼段的selector,處理器在載入指令運算元
中的cs 進cs register 前,要進行一系列的許可權檢查,控制權的轉移許可權分兩部分,根據
目的碼段descriptor 定義的兩種情況:
1)、nonconforming target code segment
★ 直接轉移後的許可權級別是不能必改變的。因此,CPL 必須要等於目的碼段的 DPL。
★ 要有足夠的請求許可權進行訪問。因此,目的碼段選擇子的RPL <= CPL
2)、conforming target code segment
★ conforming code segment 允許訪問高許可權級別的程式碼。這裡只需檢
查 CPL >= DPL 即可,RPL 忽略不檢查。
★ 轉移後CPL 不會改變。
2、 以上兩步通過後,處理器載入目的碼段的CS 進入CS register,但許可權級別不
改變,繼而RPL 被忽略。
★ 處理器根據CS selector 在相應的descriptor table 找到 code segment
descriptor。CS 的Bit2(TI 域) 指示在哪個descriptor table 表查詢,CS.TI = 0 時
在GDT 查詢,CS.TI = 1 時在LDT 查詢。
★ CS 的Bit15~Bit3 是selector index 值,處理器基於GDT 或LDT 來查詢segment
descriptor。具體是:GDTR.base 或 LDTR.base + CS.SI × 8 得出code segment
descritpro。
★ 處理器自動載入code segment descriptor 的 base address、segment limit 及
attribute 域進入 CS register 的相應的隱藏域。
★ 轉到CS.base + eip 處執行指令
總結:用程式碼形式來說明直接轉移 call cs:eip 這條指令
例: call 1a:804304c (即cs = 1a, eip = 804304c)
target_cs = 1a;
target_eip = 0x0804304c;
CPL = CS.RPL; /* 當前執行的程式碼段的許可權級別就是CPL */
RPL = target_cs.RPL; /* 目標段 selector 的低3位是RPL */
target_si = target_cs.SI; /* 目標段 selector 的索引是Bit15~Bit3 */
target_ti = target_cs.TI; /* 目標段selector的描述符表索引是Bit2 */
CODESEG_DESCRIPTOR target_descriptor;
if (target_ti == 0) { /* target_cs.TI為0 就是參考到 GDT(全域性描述符表) */
/* 以GDTR暫存器的base 為基地址加上selector的索引乘以8即得出目標
段描述符,目標描述符的DPL就是目標段所需的訪問許可權 */
target_descriptor = GDTR.base + target_si * 8
} else { /* 否則就是參考 LDT (區域性描述符表)*/
/* 以 LDTR暫存器的base 為基地址得出目標段描述符 */
target_descriptor = LDTR.base + target_si * 8;
}
DPL = target_descriptor.DPL; /* 獲取DPL */
if (target_descriptor.type & 0x06) { /* conforming */
if (CPL >= DPL) { /* 允許執行高許可權程式碼 */
/* go ahead */
} else {
/* 引發 #GP 異常 */
goto DO_GP_EXCEPTION;
}
} else { /* nonconforming */
if (CPL == DPL && RPL <= CPL) {
/* go ahead */
} else {
/* 引發 #GP 異常 */
goto DO_GP_EXCEPTION;
}
}
/****** go ahead ⋯ ⋯******/
CS = target_cs; /* 載入目標段CS 進入 CS 暫存器 */
EIP = target_eip; /* 載入目標指令EIP 進入 EIP 暫存器 */
/* 當前執行許可權 CPL 不改變 */
goto target_descriptor.base + target_eip; /* 跳轉到目標地址執行 */
DO_GP_EXCEPTION: /* 執行 #GP異常點 */
⋯ ⋯
二、 使用call gate 進行控制權的轉移
使用call gate 進行轉移控制,目的是建立一個利用gate 進行向高許可權程式碼轉移的一種保
護機制。gate 符相當一個進入高許可權程式碼的一個通道。
對於指令 call cs:eip 來說:
★ 目的碼的selector 是一個門符的選擇子。用來獲取門描述符。
★ 門描述符含目的碼段的selector 及目的碼的偏移量。
★ 目的碼的ip 值被忽略。因為門符已經提供了目的碼的偏移量。
1、 許可權的檢查
★ 首先,必須要有足夠的許可權來訪問gate 符,所以:CPL <= DPLg(門符的DPL)且:
RPL <= DPLg
★ 進前程式碼向高許可權程式碼轉移,所以,對於conforming 型別的程式碼段來說,必須CPL >=
DPLs(目的碼段的DPL)
★ 對於nonconforming 型別程式碼段來說,必須CPL = DPLs
★ call 指令改變當前許可權,而jmp 指令不改變當前許可權。
總結:
if ((CPL <= DPLg) && (RPL <= DPLg)) { /* 足夠的許可權訪問門符 */
if (target.C == CONFORMING) { /* 目的碼屬於 conforming型別 */
/* 向高許可權級別程式碼轉移控制 */
if (CPL >= DPLs) {
/* 通過訪問 */
} else {
/* 失敗,#Gp異常發生 */
}
} else { /* 目的碼屬於 nonconforming 型別 */
/* 平級轉移 */
if (CPL == DPLs) {
/* 通過訪問 */
} else {
/* 失敗,#GP 異常發生 */
}
}
} else { /* 沒有足夠許可權訪問門符 */
/* #GP 異常發生 */
}
2、 控制權的轉移
指令:call 1a:804304c (其中1a 是call gate selector)
gate_selector = 0x1a; /* call gate selector */
RPL = gate_selector.RPL; /* 門符選擇子RPL */
CPL = CS.RPL; /* 當前程式碼段低3位是CPL*/
CALLGATE_DESCRIPTOR call_gate_descriptor; /* 門符的描述符 */
CODESEG_DESCRIPTOR target_cs_descritpor; /* 目的碼段的描述符 */
call_gate_si = gate_selector.SI; /* 門符 selector 的索引 */
call_gate_ti = gate_selector.TI; /* 門符selector的描述符表索引 */
/* 獲取call gate descriptor */
if (call_gate_ti == 0) { /* TI為0 就是參考到 GDT(全域性描述符表) */
/* 以GDTR暫存器的base 為基地址加上selector的索引乘以8即得出門符的描述符,
門符的DPL就是門符的訪問許可權 */
call_gate_descriptor = GDTR.base + call_gate_si * 8;
} else { /* 否則就是參考 LDT (區域性描述符表)*/
/* 以 LDTR暫存器的base 為基地址得出目標段描述符 */
call_gate_descriptor = LDTR.base + call_gate_si * 8;
}
/* 獲取 target code segment descriptor */
target_cs = call_gate_descriptor.selector; /* 獲取門符的目的碼段選擇子
*/
target_cs_si = target_cs.SI; /* 目的碼段的索引 */
target_cs_ti = target_cs.TI; /* 目的碼描述符表索引 */
if (target_cs_ti == 0)
target_cs_descriptor = GDTR.base + target_cs_si * 8;
else
target_cs_descriptor = LDTR.base + target_cs_si * 8;
DPLg = call_gate_descriptor.DPL; /* 獲取門符的DPL */
DPLs = target_cs_descriptor.DPL; /* 獲取目的碼段的DPL */
if (CPL <= DPLg && RPL <= DPLg) { /* 有足夠許可權訪問門符 */
if (target_cs_descriptor.type & 0x06) { /* conforming */
if (CPL >= DPLs) {
/* 允許訪問目的碼段 */
} else {
/* #GP 異常產生 */
}
} else if (CPL == DPLs) { /* nonconforming */
/* 允許訪問目的碼段 */
} else {
/* 拒絕訪問,#GP 異常發生 */
goto DO_GP_EXCEPTION;
}
} else { /* 無許可權訪問門符 */
/* 拒絕訪問, #GP異常發生 */
goto DO_GP_EXCEPTION;
}
/* 允許訪問 */
current_CS = target_cs; /* 載入目的碼段進入CS 暫存器 */
current_CS.RPL = DPLs; /* 改變當前執行段許可權 */
current_EIP = call_gate_descriptor.offset; /* 載入EIP */
/* 跳轉到目的碼執行 */
/* goto current_CS:current_EIP */
goto target_cs_descriptor.base + call_gate_descriptor.offset;
return;
DO_GP_EXCEPTION: /* 執行異常 */
三、 使用中斷門或陷井門進行轉移
中斷門符及陷井門必須存放在IDT 中,IDT 表也可以存放call gate。
1、 中斷呼叫時的許可權檢查
用中斷門符進行轉移時,所作的許可權檢查同call gate 相同,區別在於intterrupt gate 轉
移不需要檢查RPL,因為,沒有RPL 需要檢查。
★ 必須有足夠的許可權訪問門符,CPL <= DPLg
★ 向同級許可權程式碼轉移時,CPL == DPLs,向高許可權程式碼轉移時,CPL > DPLs
總結
if (CPL <= DPLg) { /* 有足夠許可權訪問門符 */
if (CPL >= DPLs) {
/* 允許訪問目的碼頭 */
} else {
/* 失敗,#GP異常發生 */
}
} else {
/* 失敗,#GP異常發生 */
}
2、 控制權的轉移
發生異常或中斷呼叫時
★ 用中斷向量在中斷描述符表查詢描述符:中斷向量×8,然後加上IDT 表基址得出描述
符表。
★ 從查詢到的描述符中得到目的碼段選擇子,並在相應的GDT 或LDT 中獲取目的碼
段描述符。
★ 目的碼段描述符的基址加上門符中的offset,確定最終執行入口點。
中斷或陷井門符轉移的總結:
例: int 0x80 指令發生的情況
vector = 0x80;
INTGATE_DESCRIPTOR gate_descriptor = IDTR.base + vector * 8;
CODESEG_DESCRIPTOR target_descriptor;
TSS tss = TR.base; /* 得到TSS 記憶體塊 */
DPLg = gate_descriptor.DPL;
target_cs = gate_descriptor.selector;
if (CPL <= DPLg) { /* 允許訪問門符 */
if (target_cs.TI == 0) { /* index on GDT */
target_descriptor = GDTR.base + target_cs.SI * 8;
} else { /* index on LDT */
target_descriptor = LDTR.base + target_cs.SI * 8;
}
DPLs = target_descriptor.DPL;
if (CPL > DPLs) { /* 向高許可權程式碼轉移 */
/* 根據目的碼段的DPL值來選取相應許可權的stack結構 */
switch (DPLs) {
case 0 : /* 假如目的碼處理0級,則選0級的stack結構 */
SS = tss.ss0;
ESP = tss.esp0;
break;
case 1:
SS = tss.ss1;
ESP = tss.esp1;
break;
case 2:
SS = tss.ss2;
ESP = tss.esp2;
break;
}
/* 以下必須保護舊的stack結構,以便返回 */
*--esp = SS; /* 將當前SS入棧保護 */
*--esp = ESP; /* 將當前ESP入棧保護 */
} else if (CPL == DPLs) {
/* 同級轉移,繼續向下執行 */
} else {
/* 失敗,#GP異常產生,轉去處理異常 */
}
*--esp = EFLAGS; /* eflags 暫存器入棧 */
/* 分別將 NT、NT、RF及VM標誌位清0 */
EFLAGS.TF = 0;
EFLAGS.NT = 0;
EFLAGS.RF = 0;
EFLAGS.VM = 0;
if (gate_descriptor.type == I_GATE32) { /* 假如是中斷門符 */
EFLAGS.IF = 0; /* 也將IF標誌位清0,遮蔽響應中斷 */
}
*--esp = CS; /* 當前段選擇子入棧 */
*--esp = EIP; /* 當前EIP 入棧 */
CS = target_selector; /* 載入目的碼段 */
CS.RPL = DPLs; /* 改變當前執行許可權級別 */
EIP = gate_descriptor.offset; /* 載入進入EIP */
/* 執行中斷例程 */
goto target_descritptor.base + gate_descriptor.offset;
} else {
/* 失敗,#GP 異常產生,轉去處理異常 */
}
堆疊的切換
控制權發生轉移後,處理器自動進行相應的堆疊切換。
1、 當轉向到同許可權級別的程式碼時,不會進行堆疊級別的調整,也就是不進行堆疊切換。
2、 當轉向高許可權級別時,將發生相應級別的堆疊切換。從TSS 塊獲取相應級別的stack
結構。
例:假如當前執行級別CPL 為2 時,發生了向0 級程式碼轉移時:
TSS tss = TR.base; /* 從TR暫存器中獲取TSS 塊 */
CPL = 2; /* 當前執行級別為2 級*/
DPL = 0; /* 目的碼需要級別為 0 級 */
/* 根據目的碼需要的級別進行選取相應的許可權級別的stack結構 */
switch (DPL) {
case 0:
SS = tss.ss0;
ESP = tss.esp0;
break;
case 1:
SS = tss.ss1;
ESP = tss.esp1;
break;
case 2:
SS = tss.ss2;
ESP = tss.esp2;
break;
}
*--esp = SS; /* 儲存舊的stack結構 */
*--esp = ESP;
/* 然後再作相當的儲存工作,如儲存引數等 */
*--esp = CS; /* 最後儲存返回地址 */
*--esp = EIP;
控制權的返回
當目的碼執行完畢,需要返回控制權給原始碼時,將產生返回控制權行為。返回控制權行
為,比轉移控制權行為簡單得多。因為,一切條件已經在交出控制權之前準備完畢,返回時
僅需出棧就行了。
1、 near call 的返回
近呼叫情況下,段不改變,即CS 不改變,許可權級別不改變。僅需從棧中pop 返回地址就
可以了。
2、 直接控制權轉移的返回(far call 或far jmp)
直接控制權的轉移是一種不改變當前執行級別的行為。只是發生跨段的轉移。這時,CS
被從棧中pop 出來的CS 值載入進去,處理器會檢查CPL 與這個pop 出來的選擇子中的
RPL 進行檢查,相符則返回。不相符則發生 #GP 異常。
總結:假如當前執行的目的碼執行完畢後,將要返回。這時CPL 為2
CPL = 2; /* 當前程式碼執行級別為 2 */
⋯ ⋯
EIP = *esp++; /* pop出原EIP 值 */
CS = *esp++; /* pop出原CS值 */
if (CPL == CS.RPL) {
/* CS.RPL 代表是原來的執行級別,與CPL相符則返回 */
return ;
} else {
/* #GP異常產生,執行異常處理 */
}
3、 利用各種門符進行向高許可權程式碼轉移後的返回
從高許可權程式碼返回低許可權程式碼,須從stack 中pop 出原來的stack 結構。這個stack 結構
屬於低許可權程式碼的stack 結構。然後直接pop 出原返回地址就可以了。
總結:
CPL = 0; /* 當前執行級別為 0 級 */
⋯ ⋯
EIP = *esp++; /* 恢復原地址 */
CS = *esp++; /* 恢復原地址及執行級別 */
ESP = *esp ++; /* 恢復原stack結構 */
SS = *esp++; /* 恢復原stack 結構,同時恢復了原stack訪問級別 */
return ; /* 返回 */