保护模式篇——任务段与任务门
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看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[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;
}
下一篇
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15330063.html