任务段
前言:任务段的学习笔记
我这笔记里面记录的TSS段指的就是那个104个字节大小的内存!!!
TSS任务段一个核只有一个,但是TSS段描述符有多个,一个TSS段描述符对应一个任务
前言
在调用门、中断门与陷阱门中,一旦出现权限切换,那么就会有堆栈的切换。而且,由于CS的CPL发生改变,也导致了SS也必须要切换。
切换时,会有新的ESP和SS(CS是由中断门或者调用门指定)这2个值从哪里来的呢?之前老师也有提到过说在讲TSS的时候就知道了,这里学习的就是TSS,也就是任务段
切换的时候新的ESP和SS就是从TSS任务段中来的
什么是TSS任务段
TSS 全称task state segment,是指在操作系统进程管理的过程中,任务(进程)切换时的任务现场信息。
TSS在任务切换过程中起着重要作用,通过它实现任务的挂起和恢复。所谓任务切换是指,挂起当前正在执行的任务,恢复或启动另一任务的执行。
在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到TR(任务寄存器)所指定的TSS中;然后,下一任务的TSS的选择子被装入TR;最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中。由此可见,通过在TSS中保存任务现场各寄存器状态的完整映象,实现任务的切换。
还需要知道的点:
1、TSS是CPU设计的东西,与操作系统无关。
2、一个CPU只有一个TSS,TSS存在的意义在于让一个CPU可以同时执行多个任务。但是操作系统(这里了解到的有linux和windows)并没有使用TSS来进行任务切换,而是直接将任务切换所需要保存的寄存器直接存到了堆栈里。
总结下作用就两点:
1、通过TSS任务段实现单CPU多任务切换
2、TSS任务段能够实现一次性保存/替换许多寄存器
如何观察TSS段描述符/TSS任务段/TR段寄存器
如何读取TR段寄存器?
- windbg指令
r tr,如下图所示,可以看到tr的段选择子为0x28

如果找到TSS段描述符?
- 可以知道从TR段寄存器中进行寻找,如果TR段寄存器的段选择子所指向对应的GDT表中的段描述符,该描述符就是TSS段描述符
上面已经通过r tr,读出来了TR段寄存器的段 选择子0x28,这里通过0x28 -> 0010 1000 ,那么可以知道GDT表中索引值为5的地方就是对应的TSS段描述符

如何找到TSS任务段?
-根据下面的图可以看到,通过TSS段描述符中的base和limit就能够找到对应的TSS段的位置

下面来介绍如何找到TSS段的位置,这里需要先学习TSS段描述符的结构
TSS段描述符
如下图是TSS段描述符,这里的具体找到对应的位置,可以看到还是基于段描述符的结构,就是TYPE域有点不一样,其他的都一样的,那么TSS任务段的地址就是BASE的地址,然后这个limit则指明了这个TSS任务段的结构的大小
注意:
1、图中的TYPE中B表示的意思是刚创建时应为0,任务开始执行,挂起时为1,由硬件管理,防止切换任务切到自己,所以在创建的时候TYPE域应该是1001 -> 9
2、TSS描述符DPL必须为0,只有CPL为0能调用;

我这里的话TSS段描述符是80008b04 200020ab,那么base就是80042000,limit就是020ab

找出GDT表中存储的所有TSS段描述符
X
接着继续学习TSS任务段的结构
TSS任务段的结构
TSS任务段是一块内存,内存结构如下图所示,该结构的大小104字节
previous link:指向上一个TSS段,也就是上一个执行任务的TSS段的结构
ss0:在进入0环之后,0环所使用的ss的地址,之前我们通过提权进入0环的时候,TSS提供的ss也就是这个,以此类推ss1(1环提供) ss2(2环提供)
esp0:在进入0环之后,0环所使用的esp的地址,之前我们通过提权进入0环的时候,TSS提供的esp也就是这个,以此类推esp1(1环提供) esp2(2环提供)
LDT Segment Selector:存储到ldtr段寄存器,一个TSS对应一个LDT表,其中ldtr存储了base limit attribute seletor,其中base是LDT表的地址,limit是LDT表的大小,但是这个在windows中没有使用,了解即可
IO Map Base Address:IO权限位图 与硬件相关 了解即可,值是固定的

这里同样可以通过windbg指令来进行查看dt _KTSS

TR寄存器读写
- 将TSS段描述符加载到TR寄存器
指令:LTR
说明:用LTR指令去装载的话,仅仅是改变TR寄存器的值(96位),并没有真正改变TSS,LTR指令只能在系统层(0环特权指令)使用,加载后TSS段描述符会状态位会发生改变
- 读TR寄存器
指令:STR
说明:如果用STR去读的话,只读了TR的16位 也就是选择子
TSS切换练习
达到的效果就是 自己构建一个TSS段描述符和TSS任务段,然后让CPU进行相关操作实现将自己构建的TSS任务段进行替换原先的TSS任务段。
该练习的描述为"CPU通过TR段寄存器来找到TSS。如果我们想用自己的TSS段来替换原来的寄存器,就要修改TR寄存器,TR寄存器的值又是来自TSS段描述符"
这里先准备好一个TSS段描述符,这个TSS段描述符是放GDT表中进行存放的,这里构建的TSS段描述符的TYPE位1001(刚创建的时候TYPE位需要为9),构造的TSS段描述符为XX0XE9XX XXXX0068
然后继续看地址,到目前的话,TSS段描述符已经构造完了,接下来该TR段寄存器
让TR段寄存器的段选择子指向我们的TSS段描述符,这里需要改TR段寄存器的段选择子来指向GDT表中的TSS段描述符的索引
在Ring0 我们可以通过LTR指令去修改TR寄存器,在Ring3 我们可以通过CALL FAR 或者 JMP FAR指令来修改,所以这里的话两个实验都需要做
CALL FAR 无INT 3实验
需要注意的点:
1、如果通过call far进入的0环切换任务,此时不能使用int 3,因为通过call far调用门进入的0环切换任务EFLAG寄存器中的NT标志位,它会将其NT标志位设置为1,这个NT标志位的作用就是当任务切换的时候,这里的话就是iretd,它会切换到TSS结构体中的PTL(previous task link),而不是正常直接进行返回操作,但是如果你用了INT 3,那么这个NT标志位就会被清0,那么肯定就会触发异常操作,白皮书里面有说明,如下图所示

通过CALL FAR 调用TSS段描述符的时候,其过程就相当于默认将自己构造的TSS段描述符赋值给当前的TR段寄存器
我这里把tss作为全局变量事先进行打印,这个地址作为我们构造新的TSS任务段,printf("current tss addr: %X\n", &tss);,然后上面构造好的TSS段描述符放到GDT表中eq 8003f048 0000E940 D70F0068

在构建TSS结构的时候,0环堆栈我们自己需要位置,所以进入0环后,此时的esp0就是我们在tss中定义的堆栈位置,如果你定义在3环,那么此时0环用的堆栈位置就是在3环的位置

测试代码如下
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
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;
void __declspec(naked) test()
{
__asm
{
iretd;
}
}
char stack[100]={0};
DWORD cr3;
DWORD testFunc;
TSS tss={0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0,
0,
0,0,0,0,0,
((DWORD)stack) - 100, //0环新的堆栈位置,我们自己来定义位置
0,0,0,
0x23,
0x08,
0x10,
0x23,
0x30,
0,0,0x20ac0000
};
int main(int argc,char * argv[])
{
printf("current tss addr: %X\n", &tss);
printf("current testFunc: %p\n", test);
printf("input testFunc: ");
scanf("%X",&tss.eip);
printf("input cr3: ");
scanf("%X", &tss.cr3);
char buf[6]={0,0,0,0,0x48,0};
__asm
{
call fword ptr buf;
}
return 0;
}
可以看到成功的替换了寄存器然后进行执行

CALL FAR 有INT 3实验
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
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;
void __declspec(naked) test()
{
__asm
{
int 3;
iretd;
}
}
char stack[100]={0};
DWORD cr3;
DWORD testFunc;
TSS tss={0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0,
0,
0,0,0,0,0,
((DWORD)stack) - 100,
0,0,0,
0x23,
0x08,
0x10,
0x23,
0x30,
0,0,0x20ac0000
};
int main(int argc,char * argv[])
{
printf("current tss addr: %X\n", &tss);
printf("current testFunc: %p\n", test);
printf("input testFunc: ");
scanf("%X",&tss.eip);
printf("input cr3: ");
scanf("%X", &tss.cr3);
char buf[6]={0,0,0,0,0x48,0};
__asm
{
call fword ptr buf;
}
return 0;
}

然后再用g,可以看到直接蓝屏了,所以需要注意这个NT标志位

JMP FAR实验
JMP FAR不影响EFLAG的NT标志位
用JMP去访问一个代码段的时候,改变的是CS和EIP :
-
JMP 0x48:0x123456 如果0x48是代码段
-
执行后:CS-->0x48 EIP-->0x123456
用JMP去访问一个任务段的时候:
- 如果0x48是TSS段描述符,先修改TR寄存器,在用TR.Base指向的TSS中的值修改当前的寄存器
这里挑选的GDT表的位置是如下,地址为0x8003f048

#include <stdio.h>
#include <Windows.h>
void __declspec(naked) test()
{
__asm
{
int 3;
iretd;
}
}
int main()
{
char stack[100]={0};
DWORD dwCr3;
printf("your input cr3: ");
scanf("%X",&dwCr3);
DWORD tss[0x68]={
0x0,
0x0,
0x0,
0x0,
0x0,
0x0,
0x0,
cr3, // cr3,老师说切换进程的标识符
(DWORD)test, // 进入0环之后的ESP0
0,
0,
0,
0,
0,
((DWORD)stack) - 100,
0,
0,
0,
0x23, //ES
0x08, //CS
0x10, //SS
0x23, //DS
0x30, //FS
0,
0,
0x20ac0000 // 默认写死的
};
char buf[6]={0,0,0,0,0x48,0};
__asm
{
jmp fword ptr buf;
}
return 0;
}
构造完之后运行,发现直接蓝屏重启了

原因就是此时的iretd已经不适合我们了,因为我们走的是自己用的堆栈,最后的话默认的还是空堆栈,那么此时进行iretd,肯定会蓝屏,如下图所示

这里的话就是需要先保存TR段寄存器中保存的段选择子,通过STR事先保存,然后后面0环的执行完之后,手动再通过JMP段间跳转,跳回去即可模拟任务切换
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
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;
typedef struct seg{
char a;
char b;
char c;
char d;
char e;
char f;
} SEG;
DWORD dwHigh2GValue;
char stack[100]={0};
DWORD cr3;
DWORD testFunc;
WORD chCs;
SEG seg1 = {0,0,0,0,0,0};
SEG seg2 = {0,0,0,0,0x48,0};
char buffer1[6] = {0};
char buffer2[6]={0,0,0,0,0x48,0};
TSS tss =
{
0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0,
0,
0,0,0,0,0,
((DWORD)stack) - 100,
0,0,0,
0x23,
0x08,
0x10,
0x23,
0x30,
0,0,0x20ac0000
};
void __declspec(naked) test()
{
__asm
{
int 3;
pushad;
pushfd;
mov eax, 0x8003f008;
mov ebx,[eax];
mov dwHigh2GValue, ebx;
popfd;
popad;
jmp fword ptr buffer1;
}
}
int main(int argc,char * argv[])
{
__asm
{
STR chCs;
}
*((PDWORD)&buffer1+1) = chCs;
printf("current tss addr: %X\n", &tss);
printf("current testFunc: %p\n", test);
printf("input testFunc: ");
scanf("%X",&tss.eip);
printf("input cr3: ");
scanf("%X", &tss.cr3);
__asm
{
jmp fword ptr buffer2;
}
printf("dwHigh2GValue: %d\n", dwHigh2GValue);
printf("----ok----\n");
return 0;
}


浙公网安备 33010602011771号