avatar

Catalog
任务段

要点回顾

在调用门、中断门与陷阱门中,一旦发生权限切换,那么就必有堆栈的切换。而且,由于CS的CPL发生改变,也导致了SS也必须要切换。这时问题来了,我们知道EIP和CS的值都可以通过门描述符获得,那么ESP和SS是哪来的?这就是引出了今天的内容:TSS(Task-state segment),任务状态段。

TSS设计初衷

想要学习一类知识,首先要了解它被设计出来的目的,这样就能找到方向,更好的了解它。CPU在运行时会频繁的切换任务,每次切换任务时,还没执行完的任务怎么办?总不能下次重新执行吧,于是需要保存上一个任务的上下文环境,于是,TSS诞生了,TSS是一块大小为104(0x68)个字节的内存,没错,TSS不是什么寄存器,就是一段内存,或者说是一个段,这段内存可以保存32-Bit下所有通用寄存器以及段寄存器的值,这样CPU就可以切换到新任务时,仍然保留上一个任务的环境,方便新任务执行完后,能够完好的回到先前的任务继续执行。

注意,TSS是一个段,有段就有段寄存器和段描述符哟!比如我们熟悉的CS,CS就是段寄存器,它描述的是代码段,同时,它会通过段选择子从GDT表的代码段描述符中载入段的相关信息,通常情况下,代码段的范围是0~FFFFFFFF(大小是4GB);这样一对比,就可以理解了,TSS也是如此,因为TSS也是一个段(大小是104字节),所以应该存在一个描述TSS的段寄存器从一个段描述符里加载TSS相关信息。这就是今天会依次介绍的TR寄存器和TSS段描述符。

这里补充一点知识,尽管Intel设计TSS的初衷的为了方便任务切换(CPU层面叫做任务切换,操作系统层面叫做线程切换),但是Windows认为这个TSS设计的不好,因此并没有采用这个结构进行线程切换,并且Linux也没有采用TSS进行线程切换,这俩操作系统用的都是堆栈进行线程切换的。那么Windows用到了这个结构没有,当然是用到了,但仅仅用到了ESP0和SS0这两个值。

TSS结构

先来看看TSS的结构

大部分都应该比较熟悉,这里介绍几个较为陌生的字段:

  • Previous Task Link:这里保存的是上一个TSS的段选择子,比如任务发生了切换,新任务执行完后,如何才能找到先前未能执行完的任务呢?就得依靠这个值了(前面不是说了Windows不用TSS进行线程切换嘛,是呀,但是这里讲解的这个字段的作用,设计初衷就是为了任务切换)
  • ESP0/SS0:当发生提权时,0环的ESP和SS的值就是从这里取的
  • CR3:有人会问,我哪知道取哪个TSS的ESP0和SS0呢?就是这个值的作用了,这个值帮我们确定当前位于哪个线程,之后在页的篇章中会学到CR3的相关内容。
  • LDT:这个值通常都是0,Windows没有用到LDT表,因为LDT表只对当前的线程有用
  • I/O Map:这个位置涉及到硬件IO了,值一般是固定的

TSS段描述符

TSS段描述符只能存在GDT表中,不能存到LDT或者IDT中,所以它的结构和之前介绍的段描述符是类似的,区别在于个别位的不同

  • G位:在代码段/数据段描述符中,这个位置通常位1,因为这两个段的范围通常是4GB,而TSS的大小是104字节,单位是字节,因此这个值为0
  • Type域:这个值为1011或1001,其中B位是Busy位,置1时说明该TSS是否被载入或者嵌套,载入说明CPU正在执行该任务;嵌套则表明该任务处理了一半,切换到了另一个任务中去,但该任务并未执行完。
  • Base/Limit:Base确定TSS段的起始位置,Limit确定TSS段的大小,Base~Base+Limit就是TSS段的范围。

TR寄存器

在一开始的探究段寄存器的文章中提过,CPU共有8个段寄存器,TR就是其中之一。先前提到过,TR寄存器的工作原理和其它段寄存器一样,通过段选择子加载GDT表的TSS段描述符中的信息,方便CPU找到TSS的位置,具体工作原理如下:

这里介绍两个操作TR段寄存器的指令LTR和STR:

  • LTR:这是一个特权指令,只有0环的程序才能调用;作用是将段选择子写入TR寄存器
  • STR:STR不是特权指令,这个指令3环程序也可以调用,所用是读取TR寄存器的值

需要注意的一点,修改TR寄存器的值,只是会载入新的TSS段描述符信息,并不会对修改前的TSS段造成影响。

实现任务切换

虽然说了Windows并没有将TSS用来进行线程切换,但是我们仍然可以手动实现任务的切换。

直接修改TR寄存器是不能做到任务切换的,但是可以通过JMP FAR或者CALL FAR来加载TSS段描述符来实现。下面分别使用两种方法来实现(两种实现方法细节有很多差别)

一般情况下,任务切换发生在下列四种情况之一:

  • 当前程序,任务或者进程执行JMP/CALL语句,且参数是位于GDT表中的TSS段描述符
  • 当前程序,任务或者进程执行JMP/CALL语句,且参数是位于GDT表或者当前LDT表中的任务门描述符
  • 一个中断或者异常触发了在IDT表中的任务门描述符
  • 当前任务执行IRET指令,且EFLAGS寄存器的NT位为1时

CALL FAR实现

本次CALL FAR实现采用第一种任务切换的情况。

设计一个TSS,存储任务切换时必要的信息,其中包括ESP0,SS0,CR3,EIP,ESP,段寄存器,位图控制。

  • ESP0:也就是任务切换后的堆栈,可以自己创一个空数组,然后写入数组的首地址,就可以作为堆栈使用。
  • 段寄存器及SS0:这些值,在0环通常都一样,可以进入Windbg参考其它TSS的值,这里使用的是SS/SS0 = 0x10,ES/DS = 0x23,CS = 0x8,FS = 0x30,GS = 0x0
  • EIP:这就是要跳转后执行的地方,可以写一个裸函数来验证是否切换成功,直接取裸函数地址即可,我这里的值为0x401020(每个人机器可能不一样)
  • CR3:这个值,需要在执行前,中断到Windbg寄存器中,通过!process 0 0指令获取。
  • 位图控制:这是一个默认值为0x20AC0000

构造完的TSS如下:

c
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
DWORD tss[0x68] = {
0x00000000, //Previous Task Link
(DWORD)stack, //ESP0
0x00000010, //SS0
0x00000000, //ESP1
0x00000000, //SS1
0x00000000, //ESP2
0x00000000, //SS2
(DWORD)Cr3, //Cr3
0x00401020, //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 //IO_MAP
};

然后我们需要根据这个TSS的地址,来构造我们的TSS段描述符

地址为0x12fd70

因此TSS段描述符为:0000e912`fd700068,e->DPL=3:是为了3环的程序可以访问这个段描述符,0x68就是104字节,9说明未被载入。然后让我们填入段描述符

接着运行程序,需要采集Cr3的值,先中断到Windbg,再通过!process 0 0指令获取,取最后的一个值

在程序中填入cr3的值后回车,发现可以成功取到任务切换后ESP,CS,SS,并且均为我们设定的值,实验成功。

完整代码如下:

c
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
#include "stdafx.h"
#include

int saveEax, newESP;
short newCS, newSS;

__declspec(naked) void Get_Value() {
__asm {
mov saveEax, eax
mov newESP, esp
mov ax, cs
mov newCS, ax
mov ax, ss
mov newSS, ax
mov eax, saveEax
iret
}
}


int main(int argc, char* argv[])
{
char stack[100] = {0};
char buffer[6] = {0x0, 0x0, 0x0, 0x0, 0x4B, 0x0};
int Cr3 = 0;
printf("Input: ");
scanf("%x", &Cr3);
getchar();

DWORD tss[0x68] = {
0x00000000, //Previous Task Link
(DWORD)stack, //ESP0
0x00000010, //SS0
0x00000000, //ESP1
0x00000000, //SS1
0x00000000, //ESP2
0x00000000, //SS2
(DWORD)Cr3, //Cr3
0x00401020, //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 //IO_MAP
};

_asm {
call fword ptr buffer
}

printf("ESP: %x, cs: %x, ss: %x", newESP, newCS, newSS);
getchar();
return 0;
}

以上是通过Call Far实现的任务切换。还有另一种方法,是通过JMP FAR来实现,而且JMP FAR实现会更加困难一些。这里简要概括一下,当使用CALL FAR时,CPU会自动帮你用当前任务的段选择子填写你TSS的Previous Task Link字段,同时给你的Eflags的NT位置1,这个NT位有什么用呢,就是关系到iret这个指令的意义,当Elfags的NT为1时,iret表示根据Previous Task Link的值,从当前任务返回到前一个任务中去,当NT为0时,这是一个中断返回指令。而当你使用JMP FAR实现时,你需要手动给Pervious Task Link字段赋上前一个任务的段选择子,此外你需要手动给Eflags的NT位置1,当然这可以通过

Code
1
2
3
4
5
pushfd
mov eax, [esp]
or eax, 0x4000
mov [esp], eax
popfd

来实现,此外,还需要确保前一个TSS段的段描述符Busy位的值为1,这样才能确保该任务处在嵌入的状态。

总结

这是开博客以来,最艰难的一篇了,看视频楞是没看明白,然后又去翻Intel白皮书,看明白了然后开始代码实现,CALL FAR的任务切换实现的还算顺利,但是JMP FAR的问题就比较大了,一下午都没整出来,蛋疼啊~ 不想再弄了,看了群友的代码,感觉自己好像也没写错,就是一直死。明天打算整一整任务门吧,完了就到页的知识了,那边掌握的还算不错,可以轻松一阵子了,坚持呀!

参考资料:《Intel白皮书卷3-第七章》

Author: cataLoc
Link: http://cata1oc.github.io/2020/03/16/%E4%BB%BB%E5%8A%A1%E6%AE%B5/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶