x86的保护模式(2)——调用门,中断门,陷阱门与门描述符
今天开始学习各种门与门描述符
先解决一下上节课的存疑。
段选择子的检验
尝试将段选择子装入 CS 或 SS 时,会进行检查,通常会产生一个保护异常。而装入其它的段寄存器不会立即检查,会在尝试访问的时候检查权限。
前面提到,段描述符当 s=0
时,是一个系统段,而系统段根据 TYPE 域的变化有如下的区别
其中就有各种各样的门描述符,包括调用门、中断门、陷阱门,门描述符的结构如下所示
长调用和短调用,长跳转与短跳转
在学习调用门之前,先来了解一下长跳(jmp far),短跳(jmp)与长调用(call far),短调用(call)的区别。我们平时使用的比较多的指令其实都是短调用和短跳转,几乎很少用到长跳和长调用。长调用和长跳事实上会修改段寄存器 CS,CS不能通过一般的赋值指令修改,只能通过长调用或长跳修改。
长跳不会改变进程 CPL,长调用会。
短跳:
1 | jmp 立即数/寄存器/内存 |
长跳:
1 | jmp far cs:eip |
事实上,长调用在现实情况中也很少见。
短调用:
1 | call cs:eip |
长调用:
1 | call far cs:eip (eip参数在这里不发挥作用) |
跨段不提权
这里编写代码测试一下
1 |
|
下断点调试,进入之后可以发现在执行 call 的时候不但压入了 EIP,同时将段寄存器 CS 也压入栈中。
在 retf 之后,同样会将 EIP 和 CS 一块弹出来,恢复栈。
跨段提权
通常是改变了进程自身的权限,至于前面提到了跨段之后提权,自然是会有一些校验的。对于用户层来说,我们跨段提权仅仅是修改 CS 和 EIP。如果我们修改了 CS 提权,同时能够任意指定 EIP,那将会产生很严重的安全漏洞。
因此,跨段调用提权需要通过调用门来进行,我们看到调用门描述符结构
- ParamCount 有五位,可以指定该调用门最多拥有31个参数。
- TYPE=C 表明是调用门。
- P=1表示有效。
- S=0表示为系统段。
- DPL指示了该调用门需要请求的权限。
- 调用门还存储了新的段选择子指示该调用门调用之后 CS 新的值。
- 其余 32 位数据指示了新的 EIP 的位置。
因此我们说跨段提权的情况中,EIP 被废弃,我们只需要指示 CS 为目标调用门即可,而目的地址则在对应的门描述符中,因此我们通过调用门来进行跨段提权无法指示目标代码的执行位置(至少在用户层)。
调用门
跨段提权具体过程
对于一致代码段而言,低级别的程序可以在不通过提升 CPL 的情况下访问。对于非一致代码段而言,禁止不同级别进行访问,要想访问则必须通过调用门先提升 CPL 再调用。
从上面分析我们可以总结调用门的执行过程(个人理解,难免有误,敬请指正),在调用 call far 的时候会进行如下操作:
- 根据 CALL 给出的 CS 在 GDT 中找到对应的对描述符,CPU验证该段描述符是一个调用门描述符。
- 检查 CPL 与调用门 DPL 是否满足调用条件
- 将 SS 和 ESP 压栈。
- 取出调用门当中指定的新段选择子,给CS,当前 CS,EIP 压栈。
- SS 会变为当前 CS+8 的值,此时栈会被切换。
- 栈切换之前,会根据 ParamCount 字段获取原栈中参数个数,并将它压入新的栈中。
- 将调用前的 CS,EIP也压入新的栈中。
- 根据 CS 新的段选择子的 Base + 调用门指定的 Offset 将 EIP 设置到指定的位置。
这样就完成了跨段提权的过程。
调用门权限检查
它会检查这几个字段
- CPL
- 门选择子 RPL(也就是你尝试 CALL 的所指定的门选择子的低2位)
- 调用门描述符 DPL
- 目标代码段 DPL
同时还要检查目标代码段的一致性位。
与前面访问数据差不多,CALL 调用门权限要满足以下条件:
- CPL<=调用门DPL
- RPL<=调用门DPL
- 目标代码段DPL<=CPL。
这里会发现有点奇怪,为什么 CPL >= 目标代码段 DPL,这是为了防止通过调用门以高特权级去执行用户代码。也就是说,使用 call far
尝试调用调用门时,只允许向高特权级的代码去转移而不能向低特权级的代码转移。相对应的,call far
有对应的 retf
指令与之对应,但是 retf
只能向同权限或者低权限去转移。
对于 JMP 来说,除了满足前两个条件外,如果目标是非一致代码段,则不允许低权限的 CPL 进来访问,只允许同级访问,因为 jmp far
不会改变当前进程的 CPL。对于一致代码段,访问并不会受限,此时也不会改变当前进程的特权级。
retf
指令它能确保跨段提权之后,恢复所有栈的情况,包括提权之后可能压入的 CS,EIP,SS,ESP以及参数的清理等。
中断门与陷阱门
中断
中断(Interrupt)指的是当出现需要的时候,CPU放弃处理当前运行的程序转而去处理的过程。比如常见的除零(0号中断),断点(3号中断),系统调用(0x2e中断,Linux使用0x80中断)以及异常处理都会引发中断,调用相应的处理例程去处理中断事件。而操作系统需要维护这样的一个例程表,于是就有了 IDT。
中断门与调用门类似,也会指定新的段选择子和一个中断处理程序,所以中断门也可以用于提权,提权的规则与检测与调用门几乎相同,在某些细节有略微的差异。
中断描述符表
与gdt一样,同样有一个寄存器 IDTR 维护了一张中断描述符表(IDT),同样,该寄存器是 48 位的寄存器,存储了 IDT 表的位置(4字节)和大小(2字节),IDT的长度字段默认是 0x7FF,也就是IDT总长为 2048 字节,能够存入 256 个中断描述符。
IDT 主要存储三种门描述符
- 任务门描述符
- 中断门描述符
- 陷阱门描述符
中断描述符结构如下图所示
其中,TYPE 域的 D 位决定了它是 16 位(0)还是 32 位(1)的中断门。除了 ParamCount 字段,其余字段跟调用门几乎是一致的。
IDT可以存入以下三种描述符:
- 任务门描述符:用于任务切换,里面包含用于选择任务状态段(TSS)的段选择子。可以使用JMP或CALL指令通过任务门来切换到任务门所指向的任务,当CPU因为中断或异常转移到任务门时,也会切换到指定任务。
- 中断门描述符:用于描述中断例程的入口。
- 陷阱门描述符:用于描述异常处理例程的入口。
中断门调用过程
通过中断门进入处理程序有很多种方法,通过 int xx
指令进入应该属于熟知的方式,除此之外,以下行为均会通过中断门进入中断处理过程:
- int 指令
- 外部设备中断
- 软件异常
同样,如果是跨段提权,那么需要进行栈切换,保存原 CS 等等操作,除此之外,需要额外保留原来的 EFLAGS,栈的情况如下:
1 | | SS | <- 用户模式的栈段选择子 |
对应的,从中断处理程序返回使用 iret
指令返回到被中断的位置。iret
指令和 retf
一样,
可以完美地还原压栈的参数。
这里需要注意的是,iret
指令并不单单还原栈的参数,还会做一件事情,就是开中断。因为通过中断门调用进去之后,会屏蔽其它中断( eflags.TF=0
)。如果仅仅使用 retf 4
进行长返回的话,会导致应用层程序的 eflags.TF=0
,如果此时它出现其它异常,会导致无法中断这个程序从而出现蓝屏。并且,eflags.TF
标志位无法在用户层修改,只能通过特权指令 cli
(关中断)和 sti
(开中断)去修改。
关中断指令只能屏蔽可屏蔽中断,电源掉电等不可屏蔽中断(NMI)CPU无法屏蔽,其它硬件也可以向CPU报告紧急事件,通过CPU的NMI引脚去触发,CPU一旦收到必须立刻处理。
陷入
与中断几乎一样,唯一的区别是陷阱门调用之后不会关中断,也就是说它可以被其它中断打断。