# 前言

上一篇我们分析了 ReadProcessMemory 的调用过程,了解到 3 环函数最终通过 0x7FFE0300 地址处的函数进入 0 环。本篇将深入分析进入 0 环的两种方式。

# 0x7FFE0300 的含义

0x7FFE0300 就是 _KUSER_SHARED_DATA 结构体的成员 SystemCall ,它的作用是进入 0 环,而具体进入 0 环的方式由 CPU 决定。

当通过 eax=1 执行 CPUID 指令时,处理器的特征信息被放在 ecxedx 寄存器中,其中 edx 包含了一个 SEP 位(第 11 位),该位指明了当前处理器是否支持 sysenter / sysexit 指令:

  • 支持ntdll.dll!KiFastSystemCall()
  • 不支持ntdll.dll!KiIntSystemCall()

# 实验:判断 CPU 是否支持快速调用

一、在 OllyDbg 中修改 EAX = 1。

二、将当前汇编指令修改为 CPUID。

三、清空 ECX 与 EDX。

四、执行 CPUID,观察结果。

五、分析 EDX 的第 11 位(SEP 位)。

1
2
3
4
5
6
7
EDX = 0x0BFF

二进制(低 12 位):
位: 11 10 9 8 7 6 5 4 3 2 1 0
值: 1 0 1 1 1 1 1 1 1 1 1 1

第 11 位(SEP)= 1

SEP = 1,说明当前 CPU 支持 sysenter / sysexit 指令。

# 进 0 环注意事项

进入 0 环时,以下寄存器需要更新:

  1. CS:权限由 3 变为 0,需要新的 CS
  2. SS:SS 与 CS 的权限永远一致,需要新的 SS
  3. ESP:权限切换时堆栈也一定会切换,需要新的 ESP
  4. EIP:进 0 环后要执行内核代码,需要新的 EIP

# 中断门进 0 环

# KiIntSystemCall

1
2
3
4
5
6
7
.text:7C92E500 _KiIntSystemCall@0 proc near

; edx 为参数指针,系统调用号已在 eax 寄存器中
.text:7C92E500 lea edx, [esp+arg_4]
.text:7C92E504 int 2Eh
.text:7C92E506 retn
.text:7C92E506 _KiIntSystemCall@0 endp

该函数只执行了两行代码:

  1. 3 环堆栈中第一个参数的地址放入 EDX[esp+8] 跳过返回地址和调用者返回地址)
  2. 调用 0x2E 中断(所有 API 通过中断门进内核时,统一的中断号为 0x2E

注意:在执行 KiIntSystemCall 之前,系统调用号已被写入 EAX

# INT 0x2E 的执行过程

在 IDT 表中找到 0x2E 号门描述符,可以发现指向的地址是 0x804DE7C1

CS/SS/ESP/EIP 的来源:

寄存器 来源
CS 门描述符的段选择子部分( 0x0008
SS 从 TSS 表中取出
ESP 从 TSS 表中取出
EIP 门描述符中的偏移地址( 0x804DE7C1

查看门描述符指向的代码, nt 前缀表示当前函数为内核函数

# 快速调用进 0 环

# 快速调用原理

  1. 中断门进 0 环时,需要的 CS、EIP 在 IDT 表中,需要查内存(SS 与 ESP 由 TSS 提供)。
  2. 如果 CPU 支持 sysenter 指令,操作系统会提前将 CS/SS/ESP/EIP 的值存储在 MSR 寄存器中。 sysenter 指令执行时,CPU 会将 MSR 寄存器中的值直接写入相关寄存器,没有读内存的过程,所以叫快速调用。两种方式的本质是一样的。

# KiFastSystemCall

1
2
3
4
5
6
7
8
9
.text:7C92E4F0 _KiFastSystemCall@0 proc near

; 3 环栈顶,系统调用号已在 eax 寄存器中
.text:7C92E4F0 mov edx, esp

; 通过 sysenter 指令进入 0 环
.text:7C92E4F2 sysenter

.text:7C92E4F2 _KiFastSystemCall@0 endp

该函数只执行了两行代码:

  1. 将当前栈顶(ESP)的值放入 EDX
  2. 执行 sysenter 指令

注意:在执行 KiFastSystemCall 函数前,系统调用号已被写入 EAX

# MSR 寄存器

在执行 sysenter 指令之前,操作系统必须指定 0 环的 CS 段、SS 段、EIP 以及 ESP。其中 CS、EIP 和 ESP 来自 MSR 寄存器

可以通过 RDMSR / WRMSR 来进行读写:

1
2
3
4
5
6
kd> rdmsr 174
msr[174] = 00000000`00000008 ; CS
kd> rdmsr 175
msr[175] = 00000000`f7c5a000 ; ESP
kd> rdmsr 176
msr[176] = 00000000`804de88f ; EIP

查看 EIP 所在地址的反汇编, nt 前缀表示当前函数为内核函数

注意

  1. 在执行 sysenter 指令时,只有 CSESPEIP 三个寄存器的值可从 MSR 寄存器中获得,其中并不包括 SS
  2. SS = IA32_SYSENTER_CS + 8
  3. 这些操作与操作系统无关,而是由 ** 硬件(CPU)** 完成的(详情参考 Intel 白皮书第二卷)。

# 总结

# API 通过中断门进 0 环

  1. 固定中断号为 0x2E
  2. CS/EIP 由门描述符提供,ESP/SS 由 TSS 提供
  3. 进入 0 环后执行的内核函数:nt!KiSystemService

# API 通过 sysenter 指令进 0 环

  1. CS/ESP/EIP 由 MSR 寄存器提供(SS 由 CS + 8 算出)
  2. 进入 0 环后执行的内核函数:nt!KiFastCallEntry

# 内核模块

分页模式 内核文件
10-10-12 分页 ntoskrnl.exe
2-9-9-12 分页 ntkrnlpa.exe

# 后续待分析的问题

通过 IDA 找到 KiSystemServiceKiFastCallEntry 函数并分析:

  1. 进 0 环后,原来的寄存器存在哪里?
  2. 如何根据系统调用号(EAX 中存储)找到要执行的内核函数?
  3. 调用时参数存储在 3 环的堆栈中,如何传递给内核函数?
  4. 两种调用方式是如何返回到 3 环的?

上一篇:系统调用(一)| API 调用过程
下一篇:系统调用(三)| 保存现场