任务段tss(task state segment)是针对于CPU的一个概念。
举一个简单的例子,你一个电脑,肯定是同时会运行多个程序把,比如说QQ,微信,LOL。哪我们知道每个进程的内容是不一样的,那么,这个时候如果说只有一块CPU,这个CPU肯定不能只执行一个进程吧,不然这么多个程序就得卡死了,等LOL打完才能进行微信视频聊天,这样对很多和女朋友视频的男生非常不友好吧。就CPU肯定是会切换的,这里的task是一个比较广的概念,不仅仅局限于什么进程线程这样子,可以抽象为当前做的事情。
就比如说一个人是CPU,他穿了一套打篮球的装备在打篮球,那么突然要去踢足球,是不是得换一套踢足球的装备再去踢足球啊。
这里的TSS就可以类比为人身上穿的东西,然后人做的事就可以类比为任务。就你切换任务的时候,根据任务不同你还得穿不同的TSS。
再通过OS和CPU的内容来描述,就是TSS是任务的状态,每个任务有每个状体。当切换成task时,tss就会赋值为别的tss。
所以我们就可以利用这个机制,把我们的代码作为一种切换,让CPU切换的我们的代码,并且TSS采用我们写的TSS进行赋值,这样来作为一种提权,修改RPL和CPL。
这个结构体一共有104个字节。
字段 | 内容 |
---|---|
灰色字段 | 保留字段,用0填充。 |
Previous Task Link | 上一个TSS的段选择子。 |
ESP0 SS0到 ESP2 SS2 | ESP0 SS0到ESP2 SS2,这个就是CPU内部的设置了,因为intel有3中特权级,0 1 2 3,所以这个esp0 ss0就是对应的0环的栈空间。然后根据特权级,来使用不同特权级的栈空间到ss和esp寄存器来使用。 |
GS-EIP | 普通的段寄存器和寄存器。 |
其它字段 | 其它字段暂时用不到,感兴趣的可以去看intel手册,这里只是为了一段提权代码。 |
tss就是一个结构体,肯定是保存在这个内存里面的,那么我们如何拿到它呢。
这里需要引入一个寄存器,叫tr寄存器,它是一个段寄存器,和CS,ES等等一样,存放的也是段选择子的内容。然后可以拿到段描述符的内容。
这里我们再从头到尾解析一下:
首先输出tr寄存器:
kd> r tr tr=00000028
得到的段选择子为0x28。然后解析它:
0x28 = 0000 0000 0010 1 0 00
0x28 = 0000 0000 00101(GDT的第5个) 0(GDT) 00(RPL)
然后通过段选择子结构体,找到对应的段描述符:
kd> dq gdtr 80b93000 00000000`00000000 00cf9b00`0000ffff 80b93010 00cf9300`0000ffff 00cffa00`0000ffff 80b93020 00cff300`0000ffff 80008b1e`400020ab 80b93030 834093f6`dd003748 0040f200`00000fff 80b93040 0000f200`0400ffff 00000000`00000000 80b93050 830089f6`b0000068 830089f6`b0680068 80b93060 00000000`00000000 00000000`00000000 80b93070 800092b9`300003ff 00000000`00000000
tr段描述符对应的就是gdt表中的第五个值的内容:
80008b1e`400020ab
然后通过tss的段描述符结构进行解析:
TSS的地址就是几个base address加起来的结果。
这里通过前面的段描述符来找内容:
80 00 8b 1e `40 00 20 ab 地址为:801e4000
然后查看一下当前tss的内容:
手动添加一个任务切换
自己构造一个tr,然后把tr寄存器赋值为我们自己的tr,再进行任务切换,这样就会把tr结构体里的值赋值给切换后的环境的值,从而达到一个提权的作用。
那么首先我们得构造一个tss结构体:
typedef struct _KTSS { USHORT link; //0x0 USHORT Reserved0; //0x2 ULONG esp0; //0x4 USHORT ss0; //0x8 USHORT Reserved1; //0xa ULONG notUsed1[4]; //0xc ULONG CR3; //0x1c ULONG eip; //0x20 ULONG eflags; //0x24 ULONG eax; //0x28 ULONG ecx; //0x2c ULONG edx; //0x30 ULONG ebx; //0x34 ULONG esp; //0x38 ULONG ebp; //0x3c ULONG esi; //0x40 ULONG edi; //0x44 USHORT es; //0x48 USHORT Reserved2; //0x4a USHORT cs; //0x4c USHORT Reserved3; //0x4e USHORT ss; //0x50 USHORT Reserved4; //0x52 USHORT ds; //0x54 USHORT Reserved5; //0x56 USHORT fs; //0x58 USHORT Reserved6; //0x5a USHORT gs; //0x5c USHORT Reserved7; //0x5e USHORT LDT; //0x60 USHORT Reserved8; //0x62 USHORT flags; //0x64 USHORT IoMapBase; //0x66 }TSS;
然后开始构造这个段描述符:
构建段描述符
由于tss是我们自己构建的,所以首先得获取我们的tss首地址
首先要注意是这个tss的首地址的问题,我采用的方式是在vs里面创建一个tss,并把地址写死,然后把这个程序里的tss的地址作为这个段描述符中的地址来处理。修改了代码这个值还是会改变。
需要进行两个设置:
首先是随机基址改为否:
然后是这个启用增量链接改为否:
//获取构建的tss地址 #include<iostream> #include<Windows.h> using namespace std; typedef struct _KTSS { USHORT link; //0x0 USHORT Reserved0; //0x2 ULONG esp0; //0x4 USHORT ss0; //0x8 USHORT Reserved1; //0xa ULONG notUsed1[4]; //0xc ULONG CR3; //0x1c ULONG eip; //0x20 ULONG eflags; //0x24 ULONG eax; //0x28 ULONG ecx; //0x2c ULONG edx; //0x30 ULONG ebx; //0x34 ULONG esp; //0x38 ULONG ebp; //0x3c ULONG esi; //0x40 ULONG edi; //0x44 USHORT es; //0x48 USHORT Reserved2; //0x4a USHORT cs; //0x4c USHORT Reserved3; //0x4e USHORT ss; //0x50 USHORT Reserved4; //0x52 USHORT ds; //0x54 USHORT Reserved5; //0x56 USHORT fs; //0x58 USHORT Reserved6; //0x5a USHORT gs; //0x5c USHORT Reserved7; //0x5e USHORT LDT; //0x60 USHORT Reserved8; //0x62 USHORT flags; //0x64 USHORT IoMapBase; //0x66 }TSS; TSS tss{ 0 }; int main() { printf("tss的地址为: %x\n", &tss); system("pause"); return 0; }
然后我这里的结果是00403378:
构建tss段描述符:
高32位: Base 31:24 00 G:0(0表示tss大小以字节为单位,1表示以页为单位) AVL:这里暂时没找到解释,采用0就好 Limit 19:16 采用0,因为104个字节用不到这么高位 p: 1表示可用,0为不可用 DPL:特权级,这里采用3 Type中的B表示这个段是否在被使用中,这里用0 表示空闲 Base23:16: 40 低32位: 前16位地址:3378 Segment Limit(15-0):0068 总和下来就是 0000e940 33780068
方式tss段描述符需要根据段选择子来确定,因为需要确定放置在gdt表的位置。这里段描述符我采用之前用的 0x48来处理。
这里的0x48就表明这个段描述符要在gdt表里的第10个,这个不会的建议回头看看前面的知识。
对结构体进行赋值
我们在切换任务的时候,会把tss的值赋值给当前的环境,所以我们的tss肯定是要赋值的。
//申请0环栈空间 BYTE esp0[0x2000]; //申请普通栈空间 BYTE esp[0x2000]; memset(esp0, 0, 0x2000);//清空0环栈空间 memset(esp, 0, 0x2000);//情况普通栈空间 tss.eip = (ULONG)test;//执行地址 tss.cs = 0x08; //赋予0环的RPL,别的不用管 tss.ss = 0x10; //赋予0环的RPL,别的不用管 tss.ds = 0x23; //数据段,不用管 tss.es = 0x23; //这个也不用管 tss.fs = 0x30;//0环是30,三环是3B tss.esp0 = (ULONG)esp0+2000; //赋值esp给前面开辟的空间 tss.esp = (ULONG)esp + 2000;//赋值给前面开辟的esp空间
这里还需要一个关键值CR3,别的值可以直接忽略掉了。但是这个CR3比较麻烦,就直接先说一下怎么找吧,这里采用一个直接输入的方式来添加cr3:
(完整代码)
#include<iostream> #include<Windows.h> using namespace std; typedef struct _KTSS { USHORT link; //0x0 USHORT Reserved0; //0x2 ULONG esp0; //0x4 USHORT ss0; //0x8 USHORT Reserved1; //0xa ULONG notUsed1[4]; //0xc ULONG CR3; //0x1c ULONG eip; //0x20 ULONG eflags; //0x24 ULONG eax; //0x28 ULONG ecx; //0x2c ULONG edx; //0x30 ULONG ebx; //0x34 ULONG esp; //0x38 ULONG ebp; //0x3c ULONG esi; //0x40 ULONG edi; //0x44 USHORT es; //0x48 USHORT Reserved2; //0x4a USHORT cs; //0x4c USHORT Reserved3; //0x4e USHORT ss; //0x50 USHORT Reserved4; //0x52 USHORT ds; //0x54 USHORT Reserved5; //0x56 USHORT fs; //0x58 USHORT Reserved6; //0x5a USHORT gs; //0x5c USHORT Reserved7; //0x5e USHORT LDT; //0x60 USHORT Reserved8; //0x62 USHORT flags; //0x64 USHORT IoMapBase; //0x66 }TSS; TSS tss{ 0 }; //申请0环栈空间 BYTE esp0[0x2000]; //申请普通栈空间 BYTE esp[0x2000]; void _declspec(naked)test() { _asm { int 3 iretd } } int main() { BYTE code[6] = {0,0,0,0,0x48,0 }; printf("tss的地址为: %x\n", &tss); system("pause"); memset(esp0, 0, 0x2000);//清空0环栈空间 memset(esp, 0, 0x2000);//情况普通栈空间 tss.eip = (ULONG)test;//执行地址 tss.cs = 0x08; //赋予0环的RPL,别的不用管 tss.ss = 0x10; //赋予0环的RPL,别的不用管 tss.ds = 0x23; //数据段,不用管 tss.es = 0x23; //这个也不用管 tss.fs = 0x30;//0环是30,三环是3B tss.ss0 = 0x10;//赋予0环的RPL,别的不用管 tss.esp0 = (ULONG)esp0+0x1600; //赋值esp给前面开辟的空间 tss.esp = (ULONG)esp + 0x1600;//赋值给前面开辟的esp空间 DWORD cr3 = 0; printf("please input cr3\n"); scanf_s("%x", &cr3); _asm { call far fword ptr code } system("pause"); return 0; }
这里建议查看前面的tss全局变量是否地址改变,如果改变了需要重新配置段描述符。
前面我们还有个坑cr3没有处理。
先让程序在虚拟机里跑起来,然后WinDbg把虚拟机断下来,来找cr3:
!process 0 0 //查看所有进程,找到我们的程
这里的DirBase就是cr3。具体原因就先不解释了。
正常运行首先得先安装段描述符,但是需要注意的是如果我们代码修改了,前面的tss全局变量的地址可能是会改变的,需要重新编写段描述符,然后这个cr3也是会改变的,每次都得重新添加才行。
这样再运行就会跑到我们写好的int 3中断里:
同时再查看一下寄存器的内容:
就成功了。
这里有一些补充知识:这里的esp就是给写好的代码段进行正常使用的esp,然后esp0就是给你代码要执行0环的代码的时候采用的。
然后这个previou task link会自动赋值为上一个的tss段选择子。
还有就是iretd这个指令,它会根据EFLAG寄存器的nt位来不同的选择返回方式:
如果nt为0,就会通过esp来返回,和retf差不多,如果为1就会通过tss来返回。但是如果执行了int 3就会把eflag寄存器的nt位清零。
tss作为x86架构中的一个段,主要作用是用来保存环境。我感觉这个x86我其实没学明白,准备后面几天把x86架构阅读一遍,再来重新修改这些内容,不好意思各位,可能这里没讲清楚,等我后面再来修改这系列x86 CPU架构的博客。