学习一下任务门相关的知识

从堆栈切换开始说起,不管是中断、陷入还是调用,提权之后 ESP 和 SS 都会被切换到对应权限的栈,那么必然有一个地方会维护这个栈所在的位置,其实就是使用任务段去维护的。

任务段

任务段介绍

任务状态段简称任务段,英文缩写为TSSTask-state segment,任务段的结构体如下所示,大小为 104 字节。

观察结构体成员,可以很明显地看到有 SS2,ESP2,SS0,ESP0 等字段,没错,这就是保存相对应任务2环和0环的堆栈信息的。Intel 的设计 TSS 目的肯定主要就是实现任务切换。CPU 的任务在操作系统的方面就是线程。任务一切换,执行需要的环境就变了,即所有寄存器里面的值,需要保存供下一次切换到该任务的时候再换回去重新执行。但是事实上,线程切换并不走 TSS,而是操作系统自己实现了线程切换的逻辑[1],据说是因为 intel 自带的任务切换逻辑过慢[2]。

CPU 要找到 TSS 需要通过 TR 段寄存器,TR 也是一个内核寄存器,CPU 通过 TR 寄存器找到 TSS 的方式如下图所示:

可以看到,CPU 保存任务段选择子在 TR 寄存器中,将具体的任务段描述符保存在 GDT 表中。

任务段描述符的段描述符结构如下

其余位基本一样了,注意这里的 B 表示任务段是否被加载进 TR 寄存器中,B=0 表示没有被加载(available)。

读写 TR 寄存器指令

读写分别对应 StoreLoad 操作,也就对应 STRLTR 两个指令。

同样的,读指令是可以在三环下运行,但是只能读到任务段选择子。写指令只能在零环下运行,需要提供 96 位的数据去装载任务段描述符,且加载后会导致任务段描述符的 TYPE 发生改变(B 位从 0 变为 1)。

任务门

还是先来看看任务门的结构:

很简单,具体字段也不赘述了。因为任务门是在 idt 表中的,所以必然是通过 int 指令去调用。

实验

先写一个代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// 文件:SwitchTSS.cpp
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>

char stack[10] = {0}; // 0x4050a0

DWORD g_esp;
DWORD g_cs;
typedef struct TSS {
DWORD link; // 保存前一个 TSS 段选择子,使用 call 指令切换寄存器的时候由CPU填写。
// 这 6 个值是固定不变的,用于提权,CPU 切换栈的时候用
DWORD esp0; // 保存 0 环栈指针
DWORD ss0; // 保存 0 环栈段选择子
DWORD esp1; // 保存 1 环栈指针
DWORD ss1; // 保存 1 环栈段选择子
DWORD esp2; // 保存 2 环栈指针
DWORD ss2; // 保存 2 环栈段选择子
// 下面这些都是用来做切换寄存器值用的,切换寄存器的时候由CPU自动填写。
DWORD cr3;
DWORD eip;
DWORD eflags;
DWORD eax;
DWORD ecx;
DWORD edx;
DWORD ebx;
DWORD esp;
DWORD ebp;
DWORD esi;
DWORD edi;
DWORD es;
DWORD cs;
DWORD ss;
DWORD ds;
DWORD fs;
DWORD gs;
DWORD ldt;
DWORD io_map;
} TSS;
TSS tss = {// 0x00405000
0x00000000,//link
(DWORD)stack,//esp0
0x00000010,//ss0
0x00000000,//esp1
0x00000000,//ss1
0x00000000,//esp2
0x00000000,//ss2
0x00000000,//cr3
0x00401000,//eip
0x00000000,//eflags
0x00000000,//eax
0x00000000,//ecx
0x00000000,//edx
0x00000000,//ebx
(DWORD)stack,//esp
0x00000000,//ebp
0x00000000,//esi
0x00000000,//edi
0x00000023,//es
0x00000008,//cs
0x00000010,//ss
0x00000023,//ds
0x00000030,//fs
0x00000000,//gs
0x00000000,//ldt
0x20ac0000
};

void __declspec(naked) func() {//00401000
__asm {
int 3
mov g_esp, esp
mov eax, 0
mov ax, cs
mov g_cs, eax
iretd
}
}
int main(int argc, char* argv[])
{
printf("func=%x tss=%x stack=%x\n",func,&tss,stack);
printf("please input cr3:\n");
scanf("%x", &(tss.cr3));

char buffer[6] = {0, 0, 0, 0, 0x48, 0};
__asm {
call fword ptr [buffer]
}
printf("g_cs = %08x\ng_esp = %08x\n", g_cs, g_esp);
return 0;
}

然后向 0x48 这个段描述符构造一个任务段,段描述符为 0000e940`5000ffff。

然后找到该进程 cr3 的值,这里 cr3 寄存器是保存程序页表的物理地址使用的,windbg中输入 !process 0 0 即可查看,dirbase 即是该值。

但是试了很多次,发现都会直接导致虚拟机关闭 or 蓝屏,这个进行不下去了可能得先一放下,下篇开始学分页。

参考文献