保護模式篇——任務段與任務門


寫在前面

  此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的復雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看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 也是如此。但為了保護模式的完整性,故繼續講解。

任務段

什么是任務段

  我們回顧一下之前所學內容,在調用門、中斷門與陷阱門中,一旦出現權限切換,那么就會有堆棧的切換。而且,由於CSCPL發生改變,也導致了SS也必須要切換。切換時,會有新的ESPSS從哪里來的呢?那就是任務狀態段提供的。任務狀態段簡稱任務段,英文縮寫為TSSTask-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位),並沒有真正改變TSSLTR指令只能在系統層使用,加載后TSS段描述符會狀態位會發生改變。

讀取TR寄存器

  • 指令:STR
  • 說明:如果用STR去讀的話,只讀了TR的16位,即選擇子。

修改TR寄存器途徑

  1. 在0環可以通過LTR指令去修改TR寄存器。
  2. 在3環可以通過CALL FAR或者JMP FAR指令來修改。用JMP去訪問一個任務段的時候,如果是TSS段描述符,先修改TR寄存器,在用TR.Base指向的TSS中的值修改當前的寄存器。

CALL 和 JMP 實現任務切換的不同之處

  用CALLJMP實現任務切換,它們之間有什么不同呢?答案就不用說了。如果用CALL,它會把Previous Task Link填寫數值,並EFLAGS寄存器的NT位改為1。如果這個位被改為1iret指令會被當做任務返回,從TSS里的取出Previous Task Link返回;反之則為正常的中斷返回,從堆棧讀值返回。而JMP指令不會做上述事情。

任務門

  任務門的結構如下圖所示:

  任務門的結構我就不想再贅述了,來看看它的執行過程:

  1. 通過INT N的指令進行觸發任務門
  2. IDT表,找到任務門描述符
  3. 通過任務門描述符,查GDT表,找到TSS段描述符
  4. 使用TSS段中的值修改TR寄存器
  5. 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;
}

下一篇

  保護模式篇——段和門小結


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM