1. 概述
PART1那篇文章基本上已经把段机制交待清楚了,这一部分主要是介绍在PART1里没有介绍的系统段。
我们知道段描述符里的S位为1就是代码段或数据段,如果为0就是系统段。
2. 调用门
2.1 调用门介绍
之前介绍过长调用,想通过长调用提升CPL只能通过调用门,调用门是门描述符的一种,保存在GDT中,Windows系统虽然没有使用调用门,但是为了演示提升CPL,我们可以手动构造一个调用门。大概的步骤如下:
1. 使用指令CALL CS:EIP
2. 分解CS的值,然后查GDT表,在Index位置处我们手动构造一个调用门描述符
3. 在调用门描述符中保存着一个代码段的段选择子(Segment Selector),指向的段描述符Base+调用门描述符中的偏移(Offset inSegment)就是真正要执行的地址
4. 经过调用门的处理以后CPL会变成0
门描述符的结构如下:
上图可以看出S=0,Type=1100,1100就指代调用门,Offset inSegment指代要执行的代码偏移,具体的用法在上面的第3步已经说了。
Segment Selector的值决定了长调用是否提权,如果RPL为0代表提权,RPL为3不提权。
2.2 调用门实例
(要进行以下操作步骤,WinDbg首先要设置好双机调试)
构造一个调用门:
1. 在WinDbg中使用r gdtr查看系统GDT表,显示它的地址是0x8003f000
2. 使用dq 8003f000显示GDT表内容
3. 使用eq 8003f048 0040ec00`00081030将构造好的调用门写入到地址0x8003f048处(调用门需要根据门描述符的结构来构造,如果你不知道这个调用门是怎么构造的,你将它转成2进制,再跟门描述符表中的成员进行对照就会明白的)
构造好调用门以后,使用长调用的方式访问GDT中的调用门,就可以实现权限的提升。
int main()
{
char buff[6];
*(DWORD *)&buff[0] = 0x12345678;
*(WORD *)&buff[4] = 0x48;//赋值给高2个字节的CS段寄存器,0x48是段选择子
_asm
{
call fword ptr[buff] ;//6个字节的指针,高2个字节是CS,低4个字节是IP,可以忽略低4个字节
}
getchar();
return 0;
}
最后再通过调用门描述符中的偏移处的代码验证是否提权成功(要先找到裸函数在内存中的地址,然后填进构造好的调用门描述符中作为它的偏移,用前面说的方式将GDT表中的门描述符修改成0040ec00 000810d0)。
void __declspec(naked) GetRegister()
{
_asm
{
int 3
retf
}
}
运行程序,程序会中断在函数GetRegister()的int 3处,在WinDbg中注意观察SS、ESP、CS的值,会发现在这时已经改变了,分析它的选择子发现CPL已经变成0了,权限成功提升。
权限提升以后就可以访问高2G的内存了,我们将GetRegister()重写如下:
BYTE GDT[6] = {0};
DWORD dwH2GValue;
void __declspec(naked) GetRegister()
{
_asm
{
pushad
pushfd
mov eax,0x8003f00c
mov ebx,[eax]
mov dwH2GValue,ebx
sgdt GDT;
popfd
popad
retf
}
}
void PrintRegister()
{
DWORD GDT_ADDR = *(PDWORD)(&GDT[2]);
WORD GDT_LIMIT = *(PWORD)(&GDT[0]);
printf("%x %x %x\n",dwH2GValue,GDT_ADDR,GDT_LIMIT);
}
3. 中断门
Windows系统大量使用中断门,主要应用有:
1. 老的CPU在使用系统调用时要从3环进入0环,使用的就是中断门
2. int 3就是执行中断门的指令
中断门保存在IDT中,IDT是由多个描述符组成的,跟GDT不同的是,IDT的第一个元素不是NULL,IDT中全部都是门描述符,它们分别是任务门描述符、中断门描述符、陷阱门描述符。
在WinDbg中查看IDT的基址使用r idtr指令,查看IDT的长度使用r idtl。
中断门描述符结构如下图所示:
中断门的调用可以直接使用INT 0x20这样的指令,0x20代表IDT表中的下标,下标处的中断门描述符决定了是否提权和接下来要执行的代码偏移。
4. 陷阱门
陷阱门跟中断门基本是一样的,主要的区别是中断门执行时,IF位会被清零,但陷阱门不会。IF位是EFLAGS寄存器里下标为9的位,IF位为0的话程序不会再接收可屏蔽中断,什么是可屏蔽中断?即可以被屏蔽的中断,什么意思?举个例子,键盘是通过向CPU发送中断来让系统感知的,但是这类中断是可以被屏蔽的,屏蔽了以后按键就不会再被接收了。
5.TSS段
之前说过,当CS的CPL改变时,SS也会随之改变,ESP跟着也会产生变化,那么这两个的值是从哪里来的呢?答案是TSS,即任务状态段。
需要注意的是TSS、TSS段描述符、TR寄存器是三个不同的结构
5.1 TSS
TSS是一块内存,大小104字节,它不在CPU中,它在内存中,它的结构如下:
TSS虽然叫任务段,Intel最初设计它的时候希望用于任务切换,但是Windows并没有采用,TSS在Windows中只是用于换掉一堆寄存器。
5.2 TR寄存器
CPU中有一个TR寄存器,它是段寄存器的一种,它的选择子保存了它在GDT表中的位置,通过加载TSS段描述符到TR寄存器中给寄存器赋值,当系统需要访问TSS的时候,系统会通过TR寄存器中的Base和Limit获取到TSS的位置和大小。
5.3 TSS段描述符
TSS段描述符是系统段的一种,它的结构跟其他段描述符差别不大
通过LTR指令可以将TSS段描述符加载到TR寄存器,但是LTR指令只能在0环使用,使用STR指令可以读TR寄存器,但是只能读TR寄存器可见的16位选择子。
5.4 修改TSS
我们可以通过LTR指令修改TR寄存器,修改了TR寄存器就可以修改TSS,但是LTR指令只能在0环使用。
我们知道使用JMP FAR访问一个代码段的时候,改变的是CS和IP,当JMP FAR访问的是一个TSS段的时候,改变的就是TR寄存器了,比如说JMP 0x48:0x12345,0x48是段选择子,如果这个段选择子内部的Index处是一个TSS段描述符,系统就会将这个描述符加载到TR寄存器中。
6.任务门
通过任务门可以访问TSS,任务门描述符如下图:
任务门中保存了一个TSS段的选择子,这个选择子用来找到GDT表中的TSS段描述符。