保护模式篇——任务段与任务门

写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读 羽夏看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[26]={
        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;
}

下一篇

  保护模式篇——段和门小结

posted @ 2021-09-29 16:49  寂静的羽夏  阅读(1979)  评论(4编辑  收藏  举报