調用門有一個關鍵的作用,就是用來提權。調用門其實就是一個段。
調用門:
這是段描述符的結構體,里面的s字段用來標記是代碼段還是數據段還是系統段,前面解析的時候講的是 S==1的情況,也就是Code or data的情況,這次的調用門就是當s==0時的情況。
當s==0時,type的內容如下:
那么調用門其實就是 type為12的情況下的一個段寄存器。
32-Bit Call Gate。32位情況下的一個調用門。
就是一個 段描述符下且S==0 && type==12的一個段。
調用門的段描述符:
利用調用門提權
我們可以通過構造段選擇子和段描述符來自己添加一個調用門也就是添加一個段來自己使用。
因為段寄存器無非就是擁有一個起始地址然后作為一個段的內容來讓你使用。用匯編寫過代碼的肯定知道如果構造一個段:
sample PROC ret sample ENDP
那么我們自己構造一個系統段來執行我們的代碼這不就行了嗎。段寄存器只是系統默認的方便我們使用,我們自己通過 segment:eip這樣來跳轉使用不就完事了嗎。
段寄存器比如說 ss(stack segment)無法也就是把棧段的內容通過段選擇子和段描述符來集成了而已。
所以這里我們直接添加自己的段,也就是這里所謂的調用門。
解析調用門描述符:
我們要自己配一個調用們,肯定需要知道怎么配,段選擇子我們知道了,這里還需要知道段描述符:
這個和前面的段寄存器的段描述符大相徑庭,只是有一些不一樣。
和前面的段描述符一樣,上面的是高32位地址,下面的是低32位地址。
字段 | 內容 |
---|---|
offset in segment | 要跳轉的函數的地址,或者是要跳轉的地址 |
segment selector | 段選擇子,要變成的段選擇子(提權的關鍵) |
Param Count | 函數參數個數 |
高位5-7 | 固定的三個0 |
Type | 系統段只能是1100(10進制的12) |
高12地址 | 就是段描述符的S字段,就系統調用的必須是0 |
DPL | 肯定賦值為3呀,這樣ring3才能。 |
p | 和段描述符一樣表示該段是否有效,當P為0時無效,1時有效。 |
構造段描述符:
segment selector段描述符字段,可以通過WinDbg附加Windows雙機調試時查看cs段寄存器的值就行了,因為此時系統正在啟動肯定是在r0的情況下的:
所以這里的段選擇子就構造為: 0008(因為段選擇子是兩個字節16位)
然后再根據前面的解析我們目前的構造是這樣的:
高32位: 0-3: 0(十六進制) 4-7: 0(十六進制) 8-11: C(十六進制) 12-15: E(十六進制) 16-31: 函數地址的高地址:xxxx 低32: 0-15: 函數地址的低地址:xxxx 16-31: 0008 合集: xxxxEC000008xxxx
目前就是函數地址需要解決,這個函數地址的話其實就是我們的代碼的起始地址,然后就處理這個函數的內容了。
我們可以寫一個函數不就完了嘛,但是需要修改成固定基址,這樣函數地址就不會改變了。需要采用release版本,因為debug版本有個jmp,然后還得修改優化和隨機基址:
#include<iostream> #include<Windows.h> using namespace std; void _declspec(naked) test() { _asm { //這里我們訪問一個0環才能訪問的地址 //這樣就知道是否是拿到了0環的權限 push eax mov eax,0x80b95040 mov eax,[eax] pop eax ret } } int main() { printf("%x\n", test);//輸出函數的地址 system("pause"); return 0; }
這里我的函數地址是:00401080。
所以完整的段描述符值為:
0040EC0000081080
將段描述符添加到gdt表:
這個段描述就到了gdt表偏移9的地方了。
使用調用門
前面調用們的段描述符我們已經配置好了。
現在需要使用調用門了,需要學習兩個指令:
call far ;跨段調用 長調用
jmp far ;跨段跳轉 長跳轉
調用門只能采用call far,jmp far無法,因為jmp far無法做到越級這個作用。
然后還要配一個段選擇子:

RPL: 00//采用0環 TI: 0 Index 1001 0100 1011
最終的段選擇子就是:0x48
那么應該call far的地址為: 0x48:xxxx
但是在內聯匯編里不能這樣寫,只能這樣寫:
BYTE code[] = {0,0,0,0,0x48,0}; //0,0,0,0 是eip,然后0x0048是段選擇子,采用的是小端字節序 //這里用0000是因為這里不會用到 //因為段描述符里面已經指定了函數的地址了會自動跳轉,所以寫啥都行 _asm { call far fwrod ptr code }
完整代碼:
#include<iostream> #include<Windows.h> using namespace std; void _declspec(naked) test() { _asm { //這里我們訪問一個0環才能訪問的地址 //這樣就知道是否是拿到了0環的權限 push eax mov eax,0x80b95040 mov eax,[eax] pop eax ret } } int main() { //跳轉 BYTE code[] = {0,0,0,0,0x48,0}; //段選擇子為 0000 _asm { call far fwrod ptr code } system("pause"); return 0; }
然后拿到虛擬機里運行一下:
直接系統出錯WinDbg捕獲了並且藍屏了,但是至少有一個可以確定,就是我們肯定是跑到內核去執行了,不然怎么會導致系統出錯呢,r3的應用層代碼肯定不會導致系統的問題的。
就出現問題很正常,打幾個斷點觀察下:
#include<iostream> #include<Windows.h> using namespace std; void _declspec(naked) test() { _asm { //這里我們訪問一個0環才能訪問的地址 //這樣就知道是否是拿到了0環的權限 int 3 push eax mov eax, 0x80b95040 mov eax, [eax] pop eax ret } } int main() { //跳轉 BYTE code[6] = {0,0,0,0,0x48,0}; //段選擇子為0x0048 __asm { call far fword ptr code } printf("%x\n", test); system("pause"); return 0; }
通過單步調試匯編代碼發現是,這個ret的原因,call far或者jmp far,需要采用retf來使用。
改成retf:
#include<iostream> #include<Windows.h> using namespace std; void _declspec(naked) test() { _asm { //這里我們訪問一個0環才能訪問的地址 //這樣就知道是否是拿到了0環的權限 int 3 push eax mov eax, 0x80b95040 mov eax, [eax] pop eax retf } } int main() { //跳轉 BYTE code[6] = {0,0,0,0,0x48,0}; //段選擇子為0x0048 __asm { call far fword ptr code } printf("%x\n", test); system("pause"); return 0; }
這樣之后我們函數內的代碼是正常執行了,但是還是藍屏了,通過我的觀察,是段寄存器的問題,這里就通過od的觀察看進入前和進入后的問題就可以判斷是這個問題了。
所以這里我直接不要這個int 3斷點了,可能在r0和r3下的int 3斷點執行的東西不一樣把:
#include<iostream> #include<Windows.h> using namespace std; void _declspec(naked) test() { _asm { //這里我們訪問一個0環才能訪問的地址 //這樣就知道是否是拿到了0環的權限 push eax mov eax, 0x80b93040 mov eax, [eax] pop eax retf } } int main() { //跳轉 BYTE code[6] = {0,0,0,0,0x48,0}; //段選擇子為0x0048 __asm { call far fword ptr code } printf("%x\n", test); system("pause"); return 0; }
這樣我們的程序就可以美美的執行結束了:
小結
段是一個很重要的概念用來進行內存分割,一個段的描述有很多信息保存在段描述符里面,由於段很多所有就有段描述符表,然后這個表呢由一個段選擇子來指向獲取表內哪一個段描述符,而段寄存器是CPU為了方便使用的一個寄存器,用來保存的是段選擇子這個東西。所以這里我們用的是一個系統調用段來執行我們的指令就是操作系統內部的操作所允許也是intel架構的內容,比較復雜,這里的話我們就明白到這里就可以了想深入的可以閱讀本博客后面的參考文獻。
參考文獻:
參考文獻: