This page looks best with JavaScript enabled

Windows保护模式学习笔记(3)—— 任务状态段与任务门

 ·  ☕ 4 min read · 👀... views

0x01 任务状态段(Task-state segment, TSS)

之前讲到了,因为SS段的CPL必须与CS段的CPL一致,所以当使用调用门、中断门或陷阱门并产生权限切换时,必然会引起堆栈的切换。通过门描述符的学习可以知道CPL切换时,CS由门描述符决定,但是新的SS和ESP由何而来的呢?答案是 任务状态段(Task-state segment, TSS)

任务状态段是一块大小为104字节的内存,上面存储了一堆寄存器的值

其结构体就是这样的

 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
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;

通过观察这个结构体可以看到有两个DWORD变量叫ss0和esp0,当我们将代码从三环跨到0环时,CPU就会从这个TSS里把SS0和ESP0取出来,放到 ss 和 esp 寄存器中。

TR段寄存器

那么,CPU如何找到TSS段呢?
CPU可以通过 gdtr 寄存器来知道 GDT表在哪里,通过 idtr 寄存器知道 IDT 表在哪里。同样的,CPU通过 tr 寄存器来确定 TSS 的位置。但是! tr寄存器是不同于 gdtr和 idtr 的,tr寄存器在分类上是属于 段寄存器,是长度为96位的段寄存器。TR寄存器的值是当操作系统启动时,就从TSS段描述符中加载出来的,TSS段描述符存储在GDT表中。

将TSS段描述符加载到TR寄存器

指令:LTR

说明:

  1. 用LTR指令去装载的话 仅仅是改变TR寄存器的值(96位)
  2. 并没有真正改变TSS
  3. LTR指令只能在系统层(ring0)使用
  4. 加载后TSS段描述符的状态位(Type位)会发生改变

用途:

  1. 保存0环、1环和2环的栈段选择子和栈顶指针
  2. 一次性切换一堆寄存器。通过观察 TSS 的结构还发现 TSS 不仅存储了不同特权级下的 SS 和 ESP,还有 cs, esp, ss, esp 等等,这些后面不带数字的变量名,有着各自的用途。可以通过 call/jmp + TSS段选择子指令 一次性 把这些值加载到 CPU 对应的寄存器中。同时,旧值将保存在旧的 TSS 中。

GDT 表中可以存放多个TSS描述符,这意味着内存中可以存在多份不同的TSS。总有一个 TSS 是在当前使用中的,也就是 tr 寄存器指向的那个 TSS。Inter设计TSS段的初衷就是任务切换(虽然现代操作系统在做任务切换时都没用上它,因为切换速度过慢)。当使用 call/jmp + TSS段选择子的时候,CPU做了以下几件事情。

  1. 把当前所有寄存器(TSS结构中有的那些寄存器)的值填写到当前 tr 段寄存器指向的 TSS 中
  2. 把新的 TSS 段选择子指向的段描述符加载到 tr 段寄存器中
  3. 把新的 TSS 段中的值覆盖到当前所有寄存器(TSS结构中有的那些寄存器)中

读TR寄存器

指令:STR
说明:如果用STR指令去读的话,只读了TR的低16位,也就是段选择子部分

TSS段描述符

TSS段描述符是系统段描述符中的一种,其结构如下

Type = 二进制1001:说明该TSS段描述符未被加载到TR段寄存器中
Type = 二进制1011:说明该TSS段描述符已被加载到TR段寄存器中

0x02 任务门(Task Gate)

任务门的存在就是为了实现任务的切换(实际是一种抢占式任务切换),而任务切换的本质就是暂停/终止当前任务的执行切换,切换到另一任务执行。任务门存在于 IDT表 中,其中保存着TSS段选择子,使得我们可以通过访问任务门达到切换TSS的目的。 任务门的描述符如下图所示:

其中:

  1. TSS选择子: 执行任务切换时,必须找到新任务的选择子。
  2. P位:任务门的P位指示该门 是否有效 ,p=0时,不允许使用此门实施任务切换;
  3. DPL:任务门描述符的特权级,但是对因中断而发起的任务切换不起作用,处理器不按特权级施加任何保护。当以非中断的方式使用任务门进行任务切换,就需要用到DPL

任务门执行过程

  1. INT N(N为IDT表索引号)
  2. 系统通过用户指定的索引查找IDT表,找到对应的门描述符
  3. 门描述符若为任务门描述符,则根据任务门描述符中TSS段选择子查找GDT表,找到新的TSS段描述符
  4. 在执行新任务前,处理器会把当前任务状态保存起来。也就是将当前任务的状态(一堆寄存器的值)保存到TR寄存器指向的TSS状态段。
  5. 将TSS段描述符中的内容加载到TR段寄存器
  6. TR段寄存器通过Base和Limit找到TSS
  7. 使用TSS中的值修改寄存器
  8. 一旦新任务开始执行,处理器固件会自动将其TSS描述符的B位置1,表示该任务状态为忙。
  9. IRETD返回

PS: 关于这里是先将TSS段描述符载入TR寄存器还是先用新TSS段值修改寄存器的值的这一先后顺序存疑,搜到的资料答案不一,若有师傅知道可以在下面留言回复我,非常感谢

0x03 学习资料

  1. lzyddf师傅的博客

  2. https://blog.csdn.net/q1007729991/article/details/52650822

  3. https://blog.csdn.net/qq_37375427/article/details/85046543

Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer