寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的復雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統內核——簡述 ,方便學習本教程。
看此教程之前,問幾個問題,基礎知識儲備好了嗎?上一節教程學會了嗎?上一節課的練習做了嗎?沒有的話就不要繼續了。
🔒 華麗的分割線 🔒
練習及參考
本次答案均為參考,可以與我的答案不一致,但必須成功通過。答案注釋中有一些思考題,將會在本文結束后給出答案。
1️⃣ 構造無參的調用門,實現提權后讀取高2G的地址並分析堆棧情況。
🔒 點擊查看答案 🔒
本人在8003f090
作為段描述符存儲地址,值為00CF9A00`0000FFFF
,在8003f098
作為調用門的存儲地址, 值為0040EC00`00901030
,注意一定填好正確的函數地址,否則會容易藍屏。
在本題目中的示例代碼,運行,設置的斷點將會斷在WinDbg之中,說明調用門成功。如下圖所示:
堆棧分析如下圖所示:
🔒 點擊查看代碼 🔒
#include "stdafx.h"
int a=0;
void __declspec(naked) test() //生成裸函數,不生成 ebp尋址 等代碼
{
_asm
{
int 3; //讓斷點停在WinDbg中
pushad;
pushfd; //pushad 和 pushfd 到底是必須的嗎?作用是什么?
mov eax,0x8003f00c; //讀取高2G的地址
mov ebx,[eax];
mov dword ptr ds:[a],ebx;
popfd;
popad;
retf;
}
}
const char buffer[6]={0,0,0,0,0x9B,0};
int main(int argc, char* argv[])
{
_asm
{
call fword ptr ds:[buffer]; //在此處下斷點,填寫正確的調用門
}
return 0;
}
2️⃣ 構造有參的調用門,實現提權后正確依次取出參數並分析堆棧情況。
🔒 點擊查看答案 🔒
本人在8003f090
作為段描述符存儲地址,值為00CF9A00`0000FFFF
,在8003f098
作為調用門的存儲地址, 值為0040EC03`0090D740
,注意一定填好正確的函數地址和平好堆棧,否則會導致藍屏。
在本題目中的示例代碼,運行,設置的斷點將會斷在WinDbg之中,說明調用門成功。如下圖所示:
堆棧分析如下圖所示:
🔒 點擊查看代碼 🔒
#include "stdafx.h"
int a=0;
void __declspec(naked) testarg()
{
_asm
{
int 3;
pushad;
pushfd;
mov eax,[esp+0x24+0x8+0x8]; //請思考我為什么這樣寫
mov ebx,[esp+0x24+0x8+0x4];
mov ecx,[esp+0x24+0x8+0x0];
popfd;
popad;
retf 0xC; //注意平棧,防止藍屏
}
}
const char buffer[6]={0,0,0,0,0x9B,0};
int main(int argc, char* argv[])
{
_asm
{
push 1;
push 2;
push 3;
call fword ptr ds:[buffer];
}
return 0;
}
3️⃣ 構造調用門,提權后,實現“FQ”,即不按原函數地址返回。
🔒 點擊查看答案 🔒
本人在8003f090
作為段描述符存儲地址,值為00CF9A00`0000FFFF
,在8003f098
作為調用門的存儲地址, 值為0040EC00`00901030
,注意一定填好正確的函數地址,否則會容易藍屏。
在本題目中的示例代碼,運行,設置的斷點將會斷在WinDbg之中,說明調用門成功。如下圖所示:
程序顯示結果如下圖所示:
🔒 點擊查看代碼 🔒
#include "stdafx.h"
int a=0;
void __declspec(naked) test()
{
_asm
{
int 3;
pushad;
pushfd;
mov eax,0x8003f00c; //讀取高2G的地址
mov ebx,[eax];
mov dword ptr ds:[a],ebx;
mov dword ptr [esp+0x24],0x401088; //這個地址自己要填好
popfd;
popad;
retf;
}
}
const char buffer[6]={0,0,0,0,0x9B,0};
int backdoor()
{
printf("a的值為:%d——成功FQ!!!",a); //FQ的時候要填調用該函數過程的地址
return 0;
}
int main(int argc, char* argv[])
{
_asm
{
call fword ptr ds:[buffer]; //在此處下斷點,填寫正確的調用門
}
return 0;
}
4️⃣ 構造中斷門,實現提權后讀取高2G的地址並分析堆棧情況。
🔒 點擊查看答案 🔒
本人在8003f090
作為段描述符存儲地址,值為00CF9A00`0000FFFF
,在8003f4a0
作為中斷門的存儲地址, 值為0040EE00`00901020
,注意一定填好正確的函數地址,否則會容易藍屏。
在本題目中的示例代碼,運行,設置的斷點將會斷在WinDbg之中,說明調用門成功。如下圖所示:
堆棧分析如下圖所示:
🔒 點擊查看代碼 🔒
#include "stdafx.h"
int a=0;
void __declspec(naked) test()
{
_asm
{
int 3;
pushad;
pushfd;
mov eax,0x8003f00c; //讀取高2G的地址
mov ebx,[eax];
mov dword ptr ds:[a],ebx;
popfd;
popad;
iretd;
}
}
int main(int argc, char* argv[])
{
_asm
{
int 0x14;
}
return 0;
}
5️⃣ 構造陷阱門,實現提權后讀取高2G的地址並分析堆棧情況。
🔒 點擊查看答案 🔒
本人在8003f090
作為段描述符存儲地址,值為00CF9A00`0000FFFF
,在8003f4a0
作為陷阱門的存儲地址, 值為0040EF00`00901020
,注意一定填好正確的函數地址,否則會容易藍屏。
陷阱門的分析和代碼和陷阱門完全一樣,故不再贅述。
Windows 並沒有完全利用任務段和使用任務門實現CPU所謂的任務切換,Linux 也是如此。但為了保護模式的完整性,故繼續講解。
任務段
什么是任務段
我們回顧一下之前所學內容,在調用門、中斷門與陷阱門中,一旦出現權限切換,那么就會有堆棧的切換。而且,由於CS
的CPL
發生改變,也導致了SS
也必須要切換。切換時,會有新的ESP
和SS
從哪里來的呢?那就是任務狀態段提供的。任務狀態段簡稱任務段,英文縮寫為TSS
,Task-state segment
。
TSS
是一塊內存,大小為104
字節,內存結構如下圖所示:
TSS 的作用
Intel
的設計TSS
目的,用官方的話說就是實現所謂的任務切換。CPU
的任務在操作系統的方面就是線程。任務一切換,執行需要的環境就變了,即所有寄存器里面的值,需要保存供下一次切換到該任務的時候再換回去重新執行。
說到底,TSS
的意義就在於可以同時換掉一堆寄存器。本質上和所謂的任務切換沒啥根本聯系。而操作系統嫌棄Intel
的設計過於麻煩,自己實現了所謂的任務切換,即線程切換。具體將會在后面的教程進行講解。
CPU 如何找到 TSS
TSS
是一個內存塊,並不在CPU
中,那么它是怎樣找到正確的TSS
呢?那就是之前提到的TR
段寄存器。CPU
通過TR
寄存器索引TSS
是示意圖如下圖所示:
TSS段描述符
TSS段描述符
的結構和普通的段描述符沒啥區別,就不詳細介紹了,如下圖所示:
TR寄存器讀寫
加載TSS
- 指令:
LTR
- 說明:用
LTR
指令去裝載,僅僅是改變TR
寄存器的值(96位),並沒有真正改變TSS
。LTR
指令只能在系統層使用,加載后TSS
段描述符會狀態位會發生改變。
讀取TR寄存器
- 指令:
STR
- 說明:如果用
STR
去讀的話,只讀了TR
的16位,即選擇子。
修改TR寄存器途徑
- 在0環可以通過LTR指令去修改TR寄存器。
- 在3環可以通過CALL FAR或者JMP FAR指令來修改。用JMP去訪問一個任務段的時候,如果是TSS段描述符,先修改TR寄存器,在用TR.Base指向的TSS中的值修改當前的寄存器。
CALL 和 JMP 實現任務切換的不同之處
用CALL
和JMP
實現任務切換,它們之間有什么不同呢?答案就不用說了。如果用CALL
,它會把Previous Task Link
填寫數值,並EFLAGS
寄存器的NT
位改為1
。如果這個位被改為1
,iret
指令會被當做任務返回,從TSS里的取出Previous Task Link
返回;反之則為正常的中斷返回,從堆棧讀值返回。而JMP
指令不會做上述事情。
任務門
任務門的結構如下圖所示:
任務門的結構我就不想再贅述了,來看看它的執行過程:
- 通過
INT N
的指令進行觸發任務門 - 查
IDT
表,找到任務門描述符 - 通過任務門描述符,查
GDT
表,找到TSS
段描述符 - 使用
TSS
段中的值修改TR
寄存器 IRETD
返回
本篇思考解答
1️⃣ pushad 和 pushfd 到底是必須的嗎?作用是什么?
🔒 點擊查看答案 🔒
如果你不改寄存器或者主動還原的話,這東西不是必要的。這兩個匯編是為了保存所有必要保存寄存器的現場。保存后可以肆意修改。最后的時候還原回去,防止出現潛在的錯誤。
2️⃣ 在有參調用門調用取參的時候,為什么用下面的代碼?
mov eax,[esp+0x24+0x8+0x8];
mov ebx,[esp+0x24+0x8+0x4];
mov ecx,[esp+0x24+0x8+0x0];
🔒 點擊查看答案 🔒
0x24:是十六進制的 36 ,先看看怎么來的吧。pushfd 會將 8 個 32 位寄存器壓入堆棧中,即 32 個字節。 pushfd 會將 EFLAG 寄存器壓入堆棧中,也是 4 個字節,總和即為 36 個字節。
0x8:是返回地址和 CS 所占的總字節數。
本節練習
本節的答案將會在下一節進行講解,務必把本節練習做完后看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到后面,不做練習的話容易夾生了,開始還明白,后來就真的一點都不明白了。本節練習很少,請保質保量的完成。
1️⃣ 自己構造任務段通過CALL
實現任務切換,要求使用0環的段,下面是一個代碼模板,代碼里面有坑,並且坑很深,看看自己能不能自行解決。
#include "stdafx.h"
#include <Windows.h>
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
void __declspec(naked) test()
{
dwOK=1;
__asm
{
int 3;
mov eax,esp;
mov dwESP,eax;
mov word ptr [dwCS],cs;
iretd;
}
}
int main(int argc,char * argv[])
{
char stack[100]={0}; //自己構造一個堆棧使用
DWORD cr3=0;
printf("請輸入CR3:\n");
scanf("%x",&cr3); //通過WinDbg指令進行獲取:!process 0 0
//下一步構造TSS,標有*說明必填有效值
DWORD tss[0x68]={
0x0, //link
0x0, //esp0
0x0, //ss0
0x0, //esp1
0x0, //ss1
0x0, //esp2
0x0, //ss2
cr3, //*
(DWORD)test, //eip *
0, //eflags
0, //eax
0, //ecx
0, //edx
0, //ebx
((DWORD)stack) + 100, //esp *
0, //ebp
0, //esi
0, //edi
0x23, //es *
0x08, //cs *
0x10, //ss *
0x23, //ds *
0x30, //fs *
0, //gs
0, //idt
0x20ac0000 //IO權限位圖,VISTA之后不再用了,從其他結構體拷貝出來
};
char buffer[6];//構造任務段
__asm
{
call fword ptr [buffer];
}
printf("切換成功,獲取的值:dwESP=%d\tdwCS=%d\n",dwESP,dwCS);
return 0;
}