# 前言

上一篇我们了解了 3 环进 0 环的两种方式。本篇将分析进入 0 环后,操作系统如何保存 3 环的寄存器现场。

API 进入 0 环后调用的函数:

  1. 中断门KiSystemService
  2. 快速调用KiFastCallEntry

# Trap Frame 结构

  1. 无论是通过中断门还是快速调用进入 0 环,进入 0 环前(3 环)的所有寄存器都会存到这个结构体中。
  2. 这个结构体本身处于 0 环,由 Windows 操作系统进行维护。
  3. 当程序通过中断门从 3 环进入 0 环时,ESP 指向 TrapFrame + 0x64 的位置。
  4. 当程序通过快速调用从 3 环进入 0 环时,ESP 指向 TrapFrame + 0x78 的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
kd> dt _KTRAP_FRAME
ntdll!_KTRAP_FRAME

// 调试等其他作用
+0x000 DbgEbp : Uint4B
+0x004 DbgEip : Uint4B
+0x008 DbgArgMark : Uint4B
+0x00c DbgArgPointer : Uint4B
+0x010 TempSegCs : Uint4B
+0x014 TempEsp : Uint4B
+0x018 Dr0 : Uint4B
+0x01c Dr1 : Uint4B
+0x020 Dr2 : Uint4B
+0x024 Dr3 : Uint4B
+0x028 Dr6 : Uint4B
+0x02c Dr7 : Uint4B
+0x030 SegGs : Uint4B
+0x034 SegEs : Uint4B
+0x038 SegDs : Uint4B
+0x03c Edx : Uint4B
+0x040 Ecx : Uint4B
+0x044 Eax : Uint4B

// Windows 中非易失性寄存器需要在中断例程中先保存
+0x048 PreviousPreviousMode : Uint4B
+0x04c ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x050 SegFs : Uint4B
+0x054 Edi : Uint4B
+0x058 Esi : Uint4B
+0x05c Ebx : Uint4B
+0x060 Ebp : Uint4B
+0x064 ErrCode : Uint4B

// 中断发生时,保存被中断的代码段和地址,iret 返回到此地址
+0x068 Eip : Uint4B
+0x06c SegCs : Uint4B
+0x070 EFlags : Uint4B

// 中断发生时,若发生权限切换,则要保存旧堆栈
+0x074 HardwareEsp : Uint4B
+0x078 HardwareSegSs : Uint4B

// 虚拟 8086 方式下需要保存的段寄存器(保护模式下不使用)
+0x07c V86Es : Uint4B
+0x080 V86Ds : Uint4B
+0x084 V86Fs : Uint4B
+0x088 V86Gs : Uint4B

注意

  1. 保护模式下,最后四个成员( 0x7C ~ 0x88 )并没有被使用,只有在虚拟 8086 模式下才会用到。
  2. 中断门执行时,3 环的 SS、ESP、EFLAGS、CS、EIP 会被硬件自动存储到结构体的 0x68 ~ 0x78 中。执行快速调用时,这些值需要由内核代码手动填充。

# 线程相关的结构体

# ETHREAD(Executive Thread,执行线程)

ETHREAD 块包含线程的状态、调度信息、堆栈和上下文等关键信息。它的第一个成员是 _KTHREAD 结构体。

1
2
3
4
5
6
7
8
9
10
11
kd> dt _ETHREAD
ntdll!_ETHREAD
+0x000 Tcb : _KTHREAD
+0x1c0 CreateTime : _LARGE_INTEGER
+0x1c8 ExitTime : _LARGE_INTEGER
+0x1d0 ExitStatus : Int4B
+0x1ec Cid : _CLIENT_ID
+0x220 ThreadsProcess : Ptr32 _EPROCESS
+0x224 StartAddress : Ptr32 Void
+0x228 Win32StartAddress : Ptr32 Void
// ... 省略其余成员 ...

# KTHREAD(Kernel Thread,内核线程)

KTHREAD 块包含与内核模式线程相关的信息,如调度、同步和内核态资源的管理。以下列出关键成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kd> dt _KTHREAD
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x018 InitialStack : Ptr32 Void // 初始栈地址
+0x01c StackLimit : Ptr32 Void // 栈下限
+0x020 Teb : Ptr32 Void // 线程环境块
+0x028 KernelStack : Ptr32 Void // 内核栈
+0x02c DebugActive : UChar // 调试状态
+0x034 ApcState : _KAPC_STATE
+0x0e0 ServiceTable : Ptr32 Void // 系统服务表指针
+0x134 TrapFrame : Ptr32 _KTRAP_FRAME // Trap Frame 指针
+0x140 PreviousMode : Char // 先前模式
+0x168 StackBase : Ptr32 Void // 栈基地址
// ... 省略其余成员 ...

# CPU 相关的结构体

# KPCR(Kernel Processor Control Region,CPU 控制区)

  1. KPCR 用于存储与当前处理器相关的内核信息,如中断向量表、当前处理器的上下文以及其他调度和处理器状态信息。
  2. 每一个 CPU 都有一个 KPCR,一核一个。
  3. FS 段寄存器3 环时指向 TEB(Thread Environment Block,线程环境块),在 0 环时指向 KPCR
1
2
3
4
5
6
7
8
9
10
11
kd> dt _KPCR
nt!_KPCR
+0x000 NtTib : _NT_TIB // 线程信息块
+0x01c SelfPcr : Ptr32 _KPCR // 指向自身
+0x020 Prcb : Ptr32 _KPRCB // 处理器控制块指针
+0x024 Irql : UChar
+0x038 IDT : Ptr32 _KIDTENTRY
+0x03c GDT : Ptr32 _KGDTENTRY
+0x040 TSS : Ptr32 _KTSS
+0x051 Number : UChar // 处理器编号
+0x120 PrcbData : _KPRCB // 处理器控制块

# 查看 CPU 数量

1
kd> dd KeNumberProcessors L1

# 查看 KPCR

1
kd> dd KiProcessorBlock L2

若第二个成员有值,说明当前 CPU 有两个核。

# _NT_TIB(Thread Information Block,线程信息块)

_NT_TIB 是 KPCR 结构体的第一个成员,用于保存线程的栈信息和异常处理信息。

1
2
3
4
5
6
kd> dt _NT_TIB
ntdll!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB

# KPRCB(Kernel Processor Control Block,处理器控制块)

KPRCBKPCR 结构体的成员之一,包含当前处理器的状态、CPU 调度器的信息、当前运行线程的信息等。以下列出关键成员:

1
2
3
4
5
6
7
8
9
kd> dt _KPRCB
ntdll!_KPRCB
+0x000 MinorVersion : Uint2B
+0x002 MajorVersion : Uint2B
+0x004 CurrentThread : Ptr32 _KTHREAD // 当前线程
+0x008 NextThread : Ptr32 _KTHREAD // 下一个线程
+0x00c IdleThread : Ptr32 _KTHREAD // 空闲线程
+0x518 KeSystemCalls : Uint4B // 系统调用计数
// ... 省略其余成员 ...

# 实验 1:分析 KiSystemService

注意:当进入 KiSystemService 时,3 环的 SS、ESP、EFLAGS、CS、EIP 已经被硬件存储到 Trap Frame 结构体的 0x68 ~ 0x78 位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.text:004067C1 _KiSystemService proc near

; 中断门产生权限切换时一般向堆栈压入 5 个值
; 但有些情况会压入 6 个值(第六个为 Error Code)
; 通过 INT 2E 进入 0 环时并没有压入 Error Code,操作系统为了对齐自己补了个 0

; _KTRAP_FRAME + 0x064 ErrCode
.text:004067C1 push 0

; _KTRAP_FRAME + 0x060 Ebp
.text:004067C3 push ebp

; _KTRAP_FRAME + 0x05c Ebx
.text:004067C4 push ebx

; _KTRAP_FRAME + 0x058 Esi
.text:004067C5 push esi

; _KTRAP_FRAME + 0x054 Edi
.text:004067C6 push edi

; _KTRAP_FRAME + 0x050 SegFs
.text:004067C7 push fs

; 为 FS 寄存器赋值,指向 KPCR 结构体
; 0x30 = 0b00110000 → Index = 6
; 加载 GDT 表中下标为 6 的段描述符到 FS
.text:004067C9 mov ebx, 30h
.text:004067CE mov fs, ebx

此时可以在 WinDbg 中查看 GDT 表中下标为 6 的段描述符,fs.Base = 0xFFDFF000 (即 KPCR):

继续分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
; 保存老的 ExceptionList(异常列表)
; KPCR.NtTib.ExceptionList → _KTRAP_FRAME.ExceptionList
.text:004067D0 push large dword ptr fs:0

; 将异常列表置为空(0xFFFFFFFF)
.text:004067D7 mov large dword ptr fs:0, 0FFFFFFFFh

; 获取当前线程:KPCR + 0x120(PrcbData)+ 0x4(CurrentThread)
; CurrentThread 为 KTHREAD 结构体
.text:004067E2 mov esi, large fs:124h

; 保存老的先前模式(PreviousMode)到堆栈
; KTHREAD + 0x140 → _KTRAP_FRAME + 0x048
.text:004067E9 push dword ptr [esi+140h]

; 提升堆栈空间:ESP 下移 0x48 字节
; 执行后 ESP 等于 _KTRAP_FRAME 结构体的头部
.text:004067EF sub esp, 48h

; 取出 3 环压入的 CS(_KTRAP_FRAME + 0x6C)
.text:004067F2 mov ebx, [esp+68h+4]

; 权限检查:0 环最低位为 0,3 环最低位为 1
.text:004067F6 and ebx, 1

; 将权限检查的结果存储到 KTHREAD.PreviousMode 中
; 目的是记录调用该代码前程序处于几环
.text:004067F9 mov [esi+140h], bl

; ebp = esp = _KTRAP_FRAME 结构指针
.text:004067FF mov ebp, esp

; 保存旧的 TrapFrame 指针(KTHREAD + 0x134)到临时位置
.text:00406801 mov ebx, [esi+134h]
.text:00406807 mov [ebp+3Ch], ebx

; 将新的 _KTRAP_FRAME 指针赋值给 KTHREAD.TrapFrame
.text:0040680A mov [esi+134h], ebp

.text:00406810 cld

; 将 3 环的 ebp 和 eip 存储到 TrapFrame 的 Dbg 字段
.text:00406811 mov ebx, [ebp+60h] ; 3 环 Ebp
.text:00406814 mov edi, [ebp+68h] ; 3 环 Eip
.text:00406817 mov [ebp+0Ch], edx ; DbgArgPointer(3 环参数指针)
.text:0040681A mov dword ptr [ebp+8], 0BADB0D00h ; DbgArgMark
.text:00406821 mov [ebp+0], ebx ; DbgEbp
.text:00406824 mov [ebp+4], edi ; DbgEip

; 检测当前线程是否处于调试状态
; KTHREAD + 0x02C DebugActive
.text:00406827 test byte ptr [esi+2Ch], 0FFh

; 若处于调试状态,则跳转到 Dr_kss_a 为 Dr0~Dr7 赋值
; 若不处于调试状态,则继续向下执行
.text:0040682B jnz Dr_kss_a

.text:00406831 loc_406831:
; 开启中断
.text:00406831 sti

; 跳转到 KiSystemService 与 KiFastCallEntry 的共同代码
; 学习系统服务表时再进行分析
.text:00406832 jmp loc_406922
.text:00406832 _KiSystemService endp

# 实验 2:分析 KiFastCallEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
.text:0040688F _KiFastCallEntry proc near

; 加载段寄存器
.text:0040688F mov ecx, 23h
.text:00406894 push 30h
.text:00406896 pop fs ; FS → KPCR
.text:00406898 mov ds, ecx ; DS → GDT[4]
.text:0040689A mov es, ecx ; ES → GDT[4]

; 从 TSS 获取 0 环栈顶
; KPCR + 0x40 = TSS → TSS + 0x4 = Esp0
.text:0040689C mov ecx, large fs:40h
.text:004068A3 mov esp, [ecx+4]

; 手动构造 TrapFrame 中硬件应压入的值
; +0x078 HardwareSegSs = 0x23(3 环 SS)
.text:004068A6 push 23h

; +0x074 HardwareEsp = edx(3 环参数指针 / 栈顶)
.text:004068A8 push edx

; 保存旧的 EFLAGS
.text:004068A9 pushf

; 清除 EFLAGS 中的敏感标志位
.text:004068AA push 2
; edx 原本指向 3 环 ESP,+8 跳过两个返回地址,使其指向实际参数
.text:004068AC add edx, 8
.text:004068AF popf

; 设置 IF 位(允许中断)
.text:004068B0 or [esp+0Ch-0Bh], 2

; +0x06C SegCs = 0x1B(3 环 CS)
.text:004068B5 push 1Bh

; +0x068 Eip = SystemCallReturn(从 KUSER_SHARED_DATA 取得)
.text:004068B7 push dword ptr ds:0FFDF0304h

; +0x064 ErrCode = 0
.text:004068BD push 0

; +0x060 Ebp
.text:004068BF push ebp

; +0x05C Ebx
.text:004068C0 push ebx

; +0x058 Esi
.text:004068C1 push esi

; +0x054 Edi
.text:004068C2 push edi

; KPCR.SelfPcr → ebx
.text:004068C3 mov ebx, large fs:1Ch

; +0x050 SegFs = 0x3B(3 环 FS)
.text:004068CA push 3Bh

; 获取当前线程
; KPCR + 0x120 + 0x4 = CurrentThread(KTHREAD 结构体)
.text:004068CC mov esi, [ebx+124h]

; 保存老的异常列表
; KPCR.NtTib.ExceptionList → _KTRAP_FRAME.ExceptionList
.text:004068D2 push dword ptr [ebx]

; 将异常列表置为空
.text:004068D4 mov dword ptr [ebx], 0FFFFFFFFh

; 获取 KTHREAD.InitialStack
.text:004068DA mov ebp, [esi+18h]

; 压入老的先前模式(直接设为 1,表示来自 3 环)
; +0x048 PreviousPreviousMode = 1
.text:004068DD push 1

; 提升堆栈空间
.text:004068DF sub esp, 48h

; 计算期望的 TrapFrame 地址
.text:004068E2 sub ebp, 29Ch

; 设置新的 PreviousMode = 1(来自 3 环)
.text:004068E8 mov byte ptr [esi+140h], 1

; 检查 ebp 是否等于 esp(验证 TrapFrame 指针一致性)
.text:004068EF cmp ebp, esp
.text:004068F1 jnz loc_40685C ; 不相等则跳转异常处理

; 清除 TrapFrame 中的 Dr7
.text:004068F7 and dword ptr [ebp+2Ch], 0

; 检测当前线程是否处于调试状态
.text:004068FB test byte ptr [esi+2Ch], 0FFh

; 将 TrapFrame 指针写入 KTHREAD.TrapFrame
.text:004068FF mov [esi+134h], ebp

; 若处于调试状态,则跳转为 Dr0~Dr7 赋值
.text:00406905 jnz Dr_FastCallDrSave

.text:0040690B loc_40690B:
; 取出 3 环的 ebp 和 eip 存入 Dbg 字段
.text:0040690B mov ebx, [ebp+60h]
.text:0040690E mov edi, [ebp+68h]
.text:00406911 mov [ebp+0Ch], edx
.text:00406914 mov dword ptr [ebp+8], 0BADB0D00h
.text:0040691B mov [ebp+0], ebx ; DbgEbp
.text:0040691E mov [ebp+4], edi ; DbgEip

; 开启中断
.text:00406921 sti

; 跳转到共同代码部分(分析系统服务表)
.text:00406922 loc_406922:
; ... 共同代码(下篇分析)

# 总结

  1. 当程序通过中断门从 3 环进入 0 环时,ESP 指向 TrapFrame + 0x64 的位置。
  2. 当程序通过快速调用从 3 环进入 0 环时,ESP 指向 TrapFrame + 0x78 的位置。
  3. 若通过中断门进入 0 环,在 KiSystemService 函数开始执行时,3 环的 SS、ESP、EFLAGS、CS、EIP 就已经被硬件存储到 TrapFrame 中了。
  4. TrapFrame 结构体的其它成员由 KiSystemServiceKiFastCallEntry 通过软件方式赋值。
  5. 不管是 KiSystemService 还是 KiFastCallEntry ,最终都要执行一部分相同的代码。分为两个函数是因为进入 0 环时堆栈里的值不一样,统一走同一个函数会出问题。

上一篇:系统调用(二)| 3 环进 0 环
下一篇:系统调用(四)| 系统服务表