系统调用(二)| 3 环进 0 环
# 前言
上一篇我们分析了 ReadProcessMemory 的调用过程,了解到 3 环函数最终通过 0x7FFE0300 地址处的函数进入 0 环。本篇将深入分析进入 0 环的两种方式。
# 0x7FFE0300 的含义
0x7FFE0300 就是 _KUSER_SHARED_DATA 结构体的成员 SystemCall ,它的作用是进入 0 环,而具体进入 0 环的方式由 CPU 决定。
当通过 eax=1 执行 CPUID 指令时,处理器的特征信息被放在 ecx 和 edx 寄存器中,其中 edx 包含了一个 SEP 位(第 11 位),该位指明了当前处理器是否支持 sysenter / sysexit 指令:
- 支持:
ntdll.dll!KiFastSystemCall() - 不支持:
ntdll.dll!KiIntSystemCall()
# 实验:判断 CPU 是否支持快速调用
一、在 OllyDbg 中修改 EAX = 1。
二、将当前汇编指令修改为 CPUID。
三、清空 ECX 与 EDX。
四、执行 CPUID,观察结果。
五、分析 EDX 的第 11 位(SEP 位)。
1 | EDX = 0x0BFF |
SEP = 1,说明当前 CPU 支持 sysenter / sysexit 指令。
# 进 0 环注意事项
进入 0 环时,以下寄存器需要更新:
- CS:权限由 3 变为 0,需要新的 CS
- SS:SS 与 CS 的权限永远一致,需要新的 SS
- ESP:权限切换时堆栈也一定会切换,需要新的 ESP
- EIP:进 0 环后要执行内核代码,需要新的 EIP
# 中断门进 0 环
# KiIntSystemCall
1 | .text:7C92E500 _KiIntSystemCall@0 proc near |
该函数只执行了两行代码:
- 将 3 环堆栈中第一个参数的地址放入 EDX(
[esp+8]跳过返回地址和调用者返回地址) - 调用 0x2E 中断(所有 API 通过中断门进内核时,统一的中断号为
0x2E)
注意:在执行 KiIntSystemCall 之前,系统调用号已被写入 EAX。
# INT 0x2E 的执行过程
在 IDT 表中找到 0x2E 号门描述符,可以发现指向的地址是 0x804DE7C1 :
CS/SS/ESP/EIP 的来源:
| 寄存器 | 来源 |
|---|---|
| CS | 门描述符的段选择子部分( 0x0008 ) |
| SS | 从 TSS 表中取出 |
| ESP | 从 TSS 表中取出 |
| EIP | 门描述符中的偏移地址( 0x804DE7C1 ) |
查看门描述符指向的代码, nt 前缀表示当前函数为内核函数:
# 快速调用进 0 环
# 快速调用原理
- 中断门进 0 环时,需要的 CS、EIP 在 IDT 表中,需要查内存(SS 与 ESP 由 TSS 提供)。
- 如果 CPU 支持
sysenter指令,操作系统会提前将 CS/SS/ESP/EIP 的值存储在 MSR 寄存器中。sysenter指令执行时,CPU 会将 MSR 寄存器中的值直接写入相关寄存器,没有读内存的过程,所以叫快速调用。两种方式的本质是一样的。
# KiFastSystemCall
1 | .text:7C92E4F0 _KiFastSystemCall@0 proc near |
该函数只执行了两行代码:
- 将当前栈顶(ESP)的值放入 EDX 中
- 执行 sysenter 指令
注意:在执行 KiFastSystemCall 函数前,系统调用号已被写入 EAX。
# MSR 寄存器
在执行 sysenter 指令之前,操作系统必须指定 0 环的 CS 段、SS 段、EIP 以及 ESP。其中 CS、EIP 和 ESP 来自 MSR 寄存器。
可以通过 RDMSR / WRMSR 来进行读写:
1 | kd> rdmsr 174 |
查看 EIP 所在地址的反汇编, nt 前缀表示当前函数为内核函数:
注意:
- 在执行
sysenter指令时,只有 CS、ESP、EIP 三个寄存器的值可从 MSR 寄存器中获得,其中并不包括 SS。 SS = IA32_SYSENTER_CS + 8- 这些操作与操作系统无关,而是由 ** 硬件(CPU)** 完成的(详情参考 Intel 白皮书第二卷)。
# 总结
# API 通过中断门进 0 环
- 固定中断号为 0x2E
- CS/EIP 由门描述符提供,ESP/SS 由 TSS 提供
- 进入 0 环后执行的内核函数:nt!KiSystemService
# API 通过 sysenter 指令进 0 环
- CS/ESP/EIP 由 MSR 寄存器提供(SS 由 CS + 8 算出)
- 进入 0 环后执行的内核函数:nt!KiFastCallEntry
# 内核模块
| 分页模式 | 内核文件 |
|---|---|
| 10-10-12 分页 | ntoskrnl.exe |
| 2-9-9-12 分页 | ntkrnlpa.exe |
# 后续待分析的问题
通过 IDA 找到 KiSystemService 和 KiFastCallEntry 函数并分析:
- 进 0 环后,原来的寄存器存在哪里?
- 如何根据系统调用号(EAX 中存储)找到要执行的内核函数?
- 调用时参数存储在 3 环的堆栈中,如何传递给内核函数?
- 两种调用方式是如何返回到 3 环的?
上一篇:系统调用(一)| API 调用过程
下一篇:系统调用(三)| 保存现场