保护模式(三)| 调用门
# 前言
上一篇我们学习了段权限检查和代码跨段跳转。本篇将深入探讨短调用、长调用和调用门的原理与实验。
# 短调用
指令格式:
1 | CALL 立即数/寄存器/内存 |
发生改变的寄存器:ESP、EIP。
# 长调用(跨段不提权)
指令格式:
1 | CALL CS:EIP ; EIP 是废弃的(由段描述符决定) |
发生改变的寄存器:ESP、EIP、CS。
# 长调用(跨段并提权)
指令格式:
1 | CALL CS:EIP ; EIP 是废弃的(由段描述符决定) |
# 执行(CALL)时
发生改变的寄存器:ESP、EIP、CS、SS。
注意:长调用执行后,堆栈已经不是原来的堆栈,而是 0 环的堆栈(ESP0)。
# 返回(RETF)时
发生改变的寄存器:ESP、EIP、CS、SS。
# 小结
- 跨段调用时,一旦有权限切换,就会切换堆栈。
- CS 的权限一旦改变,SS 的权限也要随着改变,CS 与 SS 的等级必须一样。
JMP FAR只能跳转到同级非一致代码段,但CALL FAR可以通过调用门提权,提升 CPL 的权限。
SS 与 ESP 从哪里来?参考 TSS 段(后续文章会详细讲解)。
# 调用门
指令格式:
1 | CALL CS:EIP ; EIP 是废弃的 |
执行步骤:
- 根据 CS 的值查 GDT 表,找到对应的段描述符 —— 这个描述符是一个调用门。
- 在调用门描述符中存储另一个代码段的段选择子。
- 段选择子指向的
段.Base + 偏移地址就是真正要执行的地址。
# 门描述符
Param.Count:代表参数个数。
注意:
- S 位(第 12 位)必须为 0—— 只有当 S=0 时,段描述符才是系统段描述符;此时当 Type 域为
1100时,该描述符就是调用门描述符。 - 低四字节的 16~31 位是决定调用的代码存在于哪个段的段选择子。
- 当长调用执行时:
真正要执行的代码地址=门描述符中段选择子指向的代码段的 Base+Offset,其中Offset由门描述符高四字节的 16~31 位(高 16 位)与低四字节的 0~15 位(低 16 位)拼接而成。
# 实验 1:构造一个调用门(无参)
一、初步构造调用门
1 | Offset in Segment 31:16 = 0x0000 ; 暂定 |
由上述参数构造出的门描述符为: 0000EC00'00080000 。
二、确定 Offset in Segment
编译并执行以下代码,中断在 call 指令位置。
1 |
|
右键查看反汇编(Go To Disassembly),查看 GetRegister 函数地址,本次实验为 00401020 。
至此,门描述符最终确定为: 0040EC00'00081020 。
三、将门描述符写入 GDT 表
四、继续执行第二步代码
运行前,寄存器与堆栈状态:
运行后,虚拟机成功中断至 WinDbg:
寄存器:
堆栈:
五、将代码修改为以下内容
1 |
|
注意:若 GetRegister 函数起始地址发生改变,则需要修改相应的门描述符。
执行后,成功读取高 2G 内存,提权成功。
# 实验 2:构造一个调用门(有参)
一、初步构造参数
1 | Offset in Segment 31:16 = 0x0000 ; 暂定 |
由上述参数构造出的门描述符为: 0000EC03'00080000 。
二、编译并执行以下代码
在 push 1 位置设置断点,查看 CateProc 的起始地址。
1 |
|
至此,门描述符最终确定为: 0040EC03'0008DE30 。
三、将构造好的门描述符写入 GDT。
四、再次运行步骤二的代码,不设置断点,运行结果如下。
传入的参数被成功输出,有参调用门构造成功!
思考: pushad 、 pushfd 、 popfd 、 popad 这几条指令有什么意义?是必须的吗?
# 总结
- 当通过门,权限不变的时候,只会 PUSH 两个值:CS 和 返回地址,新的 CS 值由调用门决定。
- 当通过门,权限改变的时候,会 PUSH 四个值:SS、ESP、CS、返回地址,新的 CS 值由调用门决定,新的 SS 和 ESP 由 TSS 提供。
- 通过门调用时,要执行哪行代码由调用门决定;但使用
RETF返回时,由堆栈中压入的值决定 —— 也就是说,进门时只能按指定路线走,出门时可以 "翻墙"(只要改变堆栈里面的值就可以想去哪去哪)。 - 问:可不可以再建个门出去,也就是再用 CALL 出去?
答:当然可以了,前 "门" 进,后 "门" 出。