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

写在前面

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

你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。

  看此教程之前,问几个问题,基础知识储备好了吗?上一节教程学会了吗?上一节课的练习做了吗?没有的话就不要继续了。


🔒 华丽的分割线 🔒


写在前面

  如果您看本系列进行学习保护模式的话,抱歉给您的长期等待带来的不便。最近主要是忙于学业,作业比较多。其次就是回顾之前的总结有没有疏漏,也花费了不少时间。总之,该写的博客,还是得写的。

练习及参考

本次答案均为参考,可以与我的答案不一致,但必须成功通过。第一题的答案可以比我的更详细。

1️⃣ 自己构造任务段通过CALL实现任务切换,要求使用0环的段。

🔒 点击查看答案 🔒

  本人在8003f090作为任务段存储地址,值为0000E912`FD740068,注意一定构造正确的TSS并填写正确的TSS地址,否则会容易蓝屏。

首先填写的地址就是任务切换所需要的EIP,注意填对了,如下图所示:

  然后显示出TSS的地址,这个至关重要,填好在GDT表中,如下图所示:

  其次填CR3,通过!process 0 0获得并写入,如下图所示:

  然后运行跑起来,任务切换成功,如下图所示:

🔒 点击查看代码 🔒
#include "stdafx.h"
#include <Windows.h>
#include <stdlib.h>

DWORD dwOK;
DWORD dwESP;
DWORD dwCS;

void __declspec(naked) test()
{
    dwOK=1;
    __asm
    {
        //int 3;
        //为什么不要上面的指令呢?因为断到WinDbg,走几步就会蓝屏。
        //因为EFLAGS的NT位被改了,所以不要它。注意用Call的时候一定不要调试。

        mov eax,esp;
        mov dwESP,eax;
        mov word ptr [dwCS],ax;
        iretd;
    }
}

int main(int argc,char * argv[])
{
    char stack[100]={0};    //自己构造一个堆栈使用
    DWORD cr3=0;
    DWORD addr=0;

    printf("请输入目标地址:\n");
    scanf("%x",&addr);

    DWORD tss[0x68]={
        0x0,        //link
        0x0,        //esp0
        0x0,        //ss0
        0x0,        //esp1
        0x0,        //ss1
        0x0,        //esp2
        0x0,        //ss2
        0,        //cr3 *
        (DWORD)addr,        //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之后不再用了,从其他结构体拷贝出来。
        };

    printf("tss地址:%x\n",tss);

    printf("请输入CR3:\n");
    scanf("%x",&cr3);    //通过WinDbg指令进行获取:!process 0 0

    tss[7]=cr3;

    char buffer[6]={0};    //构造任务段
    *(WORD*)(&buffer[4])=0x93;

    __asm
    {
        call fword ptr [buffer];
    }

    printf("切换成功,获取的值:dwESP=%x\tdwCS=%x\n",dwESP,dwCS);
    system("pause");
    return 0;
}

结语

  到现在我们学习了什么是段,什么是门,什么是段的机制,到现在您应该有一些比较清晰的了解和掌握。然而读取内存不仅得过的了段这一关,还得过的了页这一关。什么是页,什么是页的机制。将会到下一篇进行讲解。
  为什么要写这一篇小节呢?一是总结自己之前学过的东西,回头一看其实也不太难,不过说实在的刚开始学起来挺头秃的。二是提醒大家要学习完一个阶段,每天回头看看我写的教程。因为这教程是我学习过程中写的,可能有不太完善的地方,一旦有漏洞就会及时修改。保护模式仅仅靠我写的东西是学不完的,还有很多细节的东西我还未涉及,但我学到的部分会尽快的补充。三是提醒大家要及时完成练习,因为后面还是会用到的。
  学完段的内容,难道没有个小节作业呢?这个是额外的练习,可以不做,但还是建议做一做,下面的题目将是对之前未讲到的细节的补充。

练习

1️⃣ 用代码获得GDT表的基地址和大小。

🔒 点击查看答案 🔒
#include "stdafx.h"
#include <stdlib.h>

int main(int argc, char* argv[])
{
    char gdt[6]={0};
    _asm
    {
        sgdt gdt;
    }

    printf("base : %X\nlimit : %X\n",*(unsigned int*)&gdt[2],*(unsigned short*)gdt);
    system("pause");
    return 0;
}

2️⃣ 构造一个3环的代码段描述符,我想把它用请求权限为3的选择子加载到CS当中。比如0x1B指向的段描述符,我用0x18请求CS的值会变成0x1B吗?

🔒 点击查看答案 🔒
 这个我就不详细描述了,自己实验一下就知道了,还是`0x18`。

3️⃣ 使用调用门我们都知道需要用retf将堆栈中的压入的值重新弹到寄存器,从而返回,从此有一个大胆的想法,我们自己构造压入的值,使用0环的,用retf能提权吗?

🔒 点击查看答案 🔒


  可以说想法很美好,现实很残酷。先看看实现结果如下图:

为什么不能,我们看一下白皮书,可以看到如下句子:

A far return that requires a privilege-level change is only allowed when returning to a less privileged level (that is,the DPL of the return code segment is numerically greater than the CPL).

  远返回系列指令只能降级返回所以通过这个方式提权别想了。


4️⃣ 一天通过中断门提权,使用下面的代码,发现运行后蓝屏了,是为什么呢?

//构造段描述符:00809A00`0000FFFF ,填写在 0x8003f090 ,如果用0环的权限选择子为 0x90
//构造中断门:0040EE00`00901020 ,填写在 0x8003f4f0 ,即索引为 0x1E

#include "stdafx.h"

void __declspec(naked) test()
{
    _asm
    {
        int 3;
        iretd;
    }
}

int main(int argc, char* argv[])
{
    _asm
    {
        int 0x1E;
    }
    return 0;
}

🔒 点击查看答案 🔒


  这道题是专门给给搞不清楚iretiretd的同志下的坑。

  如下是《Intel白皮书》的原话,它在 PDF 中的第1059页:

 IRET and IRETD are mnemonics for the same opcode. The IRETD mnemonic (interrupt return double) is intended for use when returning from an interrupt when using the 32-bit operand size; however, most assemblers use the IRET mnemonic interchangeably for both operand sizes.

  简单是说IRETIRETD是一样的硬编码IRETD是用来32位返回用的。但是大多数汇编程序使用两种操作数大小的 IRET 助记符可以互换,OD 就很好的落实到了这点:

  但是不幸的是,它们是不一样的指令,Intel只说是Opcode是一样的,没说前缀不一样呀。在继续之前我们先看白皮书的一句话:

  The instruction prefix 66H can be used to select an operand size other than the default.

  简单点说,0x66这一个前缀指令,不明白的可以看一下 羽夏笔记——硬编码(32位) ,里面比较详细的介绍了硬编码是基础,也略微提及到了这一个前缀。我们来看看正确的解释是啥:

  看到没有,iretd指令比iret指令多一个0x66这个指令,这个指令正式反转默认寻址方式的前缀。在32位系统默认寻址32位,如果有这个指令,就用16位寻址。这会明白为什么我会下这个坑吧?所以答案就是把iret改为iretd


5️⃣ 验证当3环提权到0环,不管怎么破坏能够不蓝屏回去后,重新再次进入后也不会蓝屏。

🔒 点击查看代码 🔒
#include "stdafx.h"
#include <windows.h>

DWORD _ESP=0;

void __declspec(naked) test()
{
    _asm
    {
        mov edx,[esp];    //保存返回地址
        sub esp,0x10;    //破坏堆栈平衡
        /*自己重新模拟一个调用门调用的0环堆栈*/
        push 0x23;    //ss,通过OD获得
        mov eax,_ESP;
        push eax;
        push 0x1B;    //cs,通过OD获得
        push edx;
        retf;    //不平栈,直接返回
    }
}

char buffer[6]={0,0,0,0,0x9B,0};

int main(int argc, char* argv[])
{
    _asm
    {
        mov _ESP,esp;
        call fword ptr [buffer];    //第一次调用
        mov _ESP,esp;
        call fword ptr [buffer];    //第二次调用
    }
    return 0;
}
🔒 点击查看答案 🔒


  代码我已详细写了注释了,我用的调用门是0040EC00`0090D480,被填写到0x8003f098,而调用门使用的段描述符的值为:00CF9A00`0000FFFF,被填写到0x8003f090

至于为什么不蓝屏,看看每次进0环的时候的esp,你就为什么不蓝屏,也感受到TSS的魅力了。


6️⃣ 将某一代码片运行到1环。

🔒 点击查看提示 🔒
Win系统没有自主提供 ESP1 和 SS1 怎么办?我想你应该知道咋办了。
🔒 点击查看代码 🔒
//eq 8003f0d8 0040E912FD740068    ;TSS描述符 D9,注意正确
//eq 8003f0b0 00CFBB000000FFFF    ;cs:B1
//eq 8003f0b8 00CFB3000000FFFF    ;ss:B9
//eq 8003f0c0 FFC0B3DFF0000001    ;fs:C1,这个东西是关键,运气不好容易因线程切换蓝屏

#include "stdafx.h"
#include <Windows.h>

DWORD dwOK;
DWORD dwESP;
DWORD dwCS;

void __declspec(naked) test()
{
    dwOK=1;
    __asm
    {
        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;
    DWORD addr=0;

    printf("请输入目标地址:\n");
    scanf("%x",&addr);


    //下一步构造TSS,标有*说明必填有效值

    DWORD tss[0x68]={
        0x0,        //link
        0x0,        //esp0
        0x0,        //ss0
        ((DWORD)stack) + 100,        //esp1 *
        0xB9,        //ss1 *
        0x0,        //esp2
        0x0,        //ss2
        0,        //cr3 *
        (DWORD)addr,        //eip *
        0,        //eflags
        0,        //eax
        0,        //ecx
        0,        //edx
        0,        //ebx
        ((DWORD)stack) + 100,        //esp
        0,        //ebp
        0,        //esi
        0,        //edi
        0x23,        //es *
        0xB1,        //cs *
        0xB9,        //ss *
        0x23,        //ds *
        0xC1,        //fs *
        0,        //gs
        0,        //idt
        0x20ac0000        //IO权限位图,VISTA之后不再用了,从其他结构体拷贝出来。
    };

    printf("tss地址:%x\n",tss);

    printf("请输入CR3:\n");
    scanf("%x",&cr3);    //通过WinDbg指令进行获取:!process 0 0

    tss[7]=cr3;

    char buffer[6]={0};    //构造任务段
    *(WORD*)(&buffer[4])=0xDB;

    __asm
    {
        push fs;
        call fword ptr [buffer];
        pop fs;
    }

    printf("切换成功,获取的值:dwESP=%x\tdwCS=%x\n",dwESP,dwCS);
    system("pause");
    return 0;
}

7️⃣ 逆向int3/int 8中断(注:只把第一层看明白逻辑即可,就算看不懂也没有太大的关系,剩下的部分都会在后面的教程逐步讲解深入)。

🔒 点击查看答案 🔒


  在看详细答案之前,我们先搞懂3号中断和8号中断是什么,看下面的图:

  3号中断正好是我们已知的断点,而8号中断是双重错误。说到双重错误,那么什么是双重错误呢?在程序执行过程中,可能会出现一个异常。当异常产生时,即错误产生时,CPU会走指定中断进行异常处理。但在异常处理过程中,也可能会发生错误。如果再次发生错误,CPU将会走8号中断,即所谓的双重错误。对于Windows系统来说,基本就是以蓝屏的形式出现。

  既然我要分析代码,必然要找到函数地址。根据中断门的结构不难找到它的函数地址。我们先用Windbg来找到函数。先以int 8为例。

  根据IDT表和索引找到中断门804e8e00`00080d02后,得到地址804e0d02。然后输入uf 804e0d02即可显示。

  虽然 Windbg 能够很快的找到函数,但它不太方便。都知道大名鼎鼎的 IDA ,如何通过它找到我需要分析的函数呢?

  首先我们先找到自己的内核文件,找到C:\WINDOWS\system32这个路径,找到的内核文件如下图所示:

  为什么是两个内核程序呢?这是由于分页的原因。WinXP有两种分页模式:10-10-12分页和2-9-9-12分页。10-10-12分页使用的内核程序是ntoskrnl.exe,2-9-9-12分页是ntkrnlpa.exe。我们把它们拖到实体机进行分析。

  打开 IDA ,然后把ntoskrnl.exe拖进去,如下图所示:

  点击确定,然后IDA询问检测到调试信息,是否从微软服务器下载符号,点击否。因为WinXP的符号微软早不搞了。如下图所示:

  然后我们自己加载符号,根据下图所示操作,打开加载符号文件:

  按照图示操作进行加载符号,注意找到自己的符号安装位置再参考下图:

  不清楚自己当前的内核文件是啥咋么办?可以用 PCHunter 进行检查:

  根据WinDbg给出的函数,根据其特征找到指定函数,具体分析过程就不再赘述了。


下一篇

  保护模式篇——分页基础

posted @ 2021-10-08 15:36  寂静的羽夏  阅读(912)  评论(0编辑  收藏  举报