保护模式篇之任务段与任务门,详细介绍段任务段与任务门的基础知识。



写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。






练习及参考

本次答案均为参考,可以与我的答案不一致,但必须成功通过。答案注释中有一些思考题,将会在本文结束后给出答案。

1️⃣ 构造无参的调用门,实现提权后读取高2G的地址并分析堆栈情况。

???? 点击查看答案 ????

  本人在​​8003f090​​作为段描述符存储地址,值为​​00CF9A00`0000FFFF​​,在​​8003f098​​作为调用门的存储地址,值为​ ​0040EC00`00901030​​,注意一定填好正确的函数地址,否则会容易蓝屏。

  在本题目中的示例代码,运行,设置的断点将会断在WinDbg之中,说明调用门成功。如下图所示:

保护模式篇——任务段与任务门_堆栈

  堆栈分析如下图所示:

保护模式篇——任务段与任务门_寄存器_02

  ???? 点击查看代码 ????

#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之中,说明调用门成功。如下图所示:

保护模式篇——任务段与任务门_描述符_03

  堆栈分析如下图所示:

保护模式篇——任务段与任务门_#include_04

  ???? 点击查看代码 ????

#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之中,说明调用门成功。如下图所示:

保护模式篇——任务段与任务门_描述符_05

  程序显示结果如下图所示:

保护模式篇——任务段与任务门_#include_06

  ???? 点击查看代码 ????

#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之中,说明调用门成功。如下图所示:

保护模式篇——任务段与任务门_蓝屏_07

  堆栈分析如下图所示:

保护模式篇——任务段与任务门_#include_08

  ???? 点击查看代码 ????

#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​​字节,内存结构如下图所示:

保护模式篇——任务段与任务门_描述符_09

TSS 的作用

  ​​Intel​​的设计​​TSS​​目的,用官方的话说就是实现所谓的任务切换。​​CPU​​的任务在操作系统的方面就是线程。任务一切换,执行需要的环境就变了,即所有寄存器里面的值,需要保存供下一次切换到该任务的时候再换回去重新执行。

  说到底,​TSS的意义就在于可以同时换掉一堆寄存器。本质上和所谓的任务切换没啥根本联系。而操作系统嫌弃​​Intel​​的设计过于麻烦,自己实现了所谓的任务切换,即线程切换。具体将会在后面的教程进行讲解。

CPU 如何找到 TSS

  ​​TSS​​是一个内存块,并不在​​CPU​​中,那么它是怎样找到正确的​​TSS​​呢?那就是之前提到的​​TR​​段寄存器。​​CPU​​通过​​TR​​寄存器索引​​TSS​​是示意图如下图所示:

保护模式篇——任务段与任务门_描述符_10

TSS段描述符

  ​​TSS段描述符​​的结构和普通的段描述符没啥区别,就不详细介绍了,如下图所示:

保护模式篇——任务段与任务门_#include_11

TR寄存器读写

加载TSS

  • 指令:​​LTR​
  • 说明:用​​LTR​​指令去装载,仅仅是改变​​TR​​寄存器的值(96位),并没有真正改变​​TSS​​。​​LTR​​指令只能在系统层使用,加载后​​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 实现任务切换的不同之处

  用​​CALL​​和​​JMP​​实现任务切换,它们之间有什么不同呢?答案就不用说了。如果用​​CALL​​,它会把​​Previous Task Link​​填写数值,并​​EFLAGS​​寄存器的​​NT​​位改为​​1​​。如果这个位被改为​​1​​,​​iret​​指令会被当做任务返回,从TSS里的取出​​Previous Task Link​​返回;反之则为正常的中断返回,从堆栈读值返回。而​​JMP​​指令不会做上述事情。

保护模式篇——任务段与任务门_堆栈_12

任务门

  任务门的结构如下图所示:

保护模式篇——任务段与任务门_寄存器_13

  任务门的结构我就不想再赘述了,来看看它的执行过程:

  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],ax;
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;
}


下一篇

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


作者:寂静的羽夏​