来深入挖掘一下Windows系统调用的过程

KiSystemService分析

这个函数是通过中断门进的,中断门本身保存了 CSEIP,跨段提权后通过 TSS 拿到零环的 SSESP。此时为了维护三环的上下文状态,则会将各种寄存器保存到堆栈,也就是 Trap_Frame 结构体,中断门提权之后本身就会按顺序压入 SSESPELFAGSCSEIP。此时比较一下上一篇文章中提到的 Trap_Frame 结构图,大概就能知道为什么这么排布了,随后系统调用的入口则会按顺序保存这些信息。

开头的一堆 push

1
2
3
4
5
6
.text:00434FEA 6A 00                                   push    0
.text:00434FEC 55 push ebp
.text:00434FED 53 push ebx
.text:00434FEE 56 push esi
.text:00434FEF 57 push edi
.text:00434FF0 0F A0 push fs

正对应了结构体的 ErrorCodeEbpEbxEsiEdiSegFs

随后对应也将 FS 切换到了 R0 的值 0x30,再把对应的 ExceptionListPreviousPreviousMode 压入,也就是这里的 fs:[0] 和 [fs:[0x124]+0x13A]

fs 前面说过了,是在 R3 指向 TEB 的段选择子,它的基址就在 TEB 上,在 R0 会指向 KPCR 结构,这个结构上篇文章分析过了,那么来看看fs:[0][[KPCR+0x124]+0x13A] 的值是否如我们所想,根据上篇文章的分析,可以得知 fs:[0] 就是 ExceptionList 字段,而 fs:[0x124] 指向 CurrentThread 成员,而 KTHREAD+0x13A 正是当前线程的 PreviousMode 字段,完美对应上。压入之后,还会给当前 ExceptionList 赋值为 -1。

1
2
3
4
5
6
7
8
9
10
11
.text:00435022 83 EC 48                                sub     esp, 48h
.text:00435025 8B 5C 24 6C mov ebx, dword ptr [esp+68h+arg_0]
.text:00435029 83 E3 01 and ebx, 1
.text:0043502C 88 9E 3A 01 00 00 mov [esi+13Ah], bl
.text:00435032 8B EC mov ebp, esp
.text:00435034 8B 9E 28 01 00 00 mov ebx, [esi+128h]
.text:0043503A 89 5D 3C mov [ebp+3Ch], ebx
.text:0043503D F6 45 6C 01 test byte ptr [ebp+6Ch], 1
.text:00435041 75 1E jnz short loc_435061
.text:00435043 0F AE E8 lfence
.text:00435046 E9 A8 01 00 00 jmp loc_4351F3

保存完成这些字段之后, esp 额外被减了 0x48,刚好是 Trap_Frame 剩余字段的大小(不包括虚拟8086模式的字段)。将 Ebp 提上来,随后拿到 CurrentThread+0x128,通过查找可以得知是 Trap_Frame 指针,保存到了 ebp+0x3C 的位置上,此时 ESP 指向了栈中保存的 Trap_Frame 结构,也就是将 CurrentThreadTrap_Frame 存到了栈中的 Trap_Frame+0x3CEdx 字段所处的位置。

随后拿到栈中 Trap_Frame 中的 SegCs 字段(+0x6C),&1 后判断是否为 0,因为 Windows 没有实现 R1 和 R2,所以这里可以简单认为如果发出中断的线程不为内核线程则跳转。通常情况下,发出系统调用的线程都是 R3 的,所以着重分析跳转的分支。(To Be Continued…)

SystemServiceTable

之前我们讲到进0环后,3环的各种寄存器的值都会保留到_Trap_Frame结构体中,接下来我将会讲解:如何根据系统服务号(eax中存储)找到要执行的内核函数?调用时参数是存储到3环的堆栈,如何传递给内核函数?首先我们得知道一个结构体,用来描述内核函数信息的表:SystemServiceTable,即系统服务表

可以看出这个表由4部分组成,ServiceTable指向的是函数地址数组,每个成员四个字节;Count表示调用次数,没啥意义;ServiceLimit表示这张表有几个函数;ArgumentTable指向对应函数有几个字节参数,每个成员一个字节。

从图中可以看出,Windows提供了两张表:上面的表是用来处理一般内核函数的,下面这张表是用来处理与GUI相关的内核函数。

这个表会存在 ETHREAD+0xbc (ETHREAD的头部就是KTHREAD)偏移的位置。

1
2
3
4
5
6
kd> dt _KTHREAD
ntdll!_KTHREAD
//...
+0x0b8 ThreadFlags : Int4B
+0x0bc ServiceTable : Ptr32 Void
//...

拿到了系统调用号如何去寻找对应的系统函数呢,看如下示意图

APIService分析

从系统调用的代码往后分析,可以找到系统调用入口对调用号的处理

顺着看一遍

其中 ESI+0xBC 就对应了线程的 ServiceTable 成员,加上 edi 刚刚好,因为表的大小刚好就是 0x10,所以如果是这里直接加上去,edi 的结果只能是 0 或者 0x10,这样加上去 edi 最终都指向了正确的表。

然后拿到 eaxServiceLimit 相比较,如果 eax>=ServiceLimit 则直接报错,很好理解。

随后 (eax>>8 & 0x10)==0x10 其实就是判断第 12 位是否为1,如果为 1 则走 loc_4354F8 分支调用 win32k.sys 的函数,我们直接往下分析:

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
.text:004354E6                         _KiSystemServiceAccessTeb@0 proc near   ; DATA XREF: KiPreprocessAccessViolation(x,x,x)+3D↓o
.text:004354E6
.text:004354E6 ; FUNCTION CHUNK AT .text:0043579E SIZE 0000000A BYTES
.text:004354E6
.text:004354E6 0B B1 70 0F 00 00 or esi, [ecx+0F70h]
.text:004354EC 74 0A jz short loc_4354F8
.text:004354EE 52 push edx
.text:004354EF 50 push eax
.text:004354F0 FF 15 0C 5A 56 00 call ds:_KeGdiFlushUserBatch
.text:004354F6 58 pop eax
.text:004354F7 5A pop edx
.text:004354F8
.text:004354F8 loc_4354F8: ; CODE XREF: KiEndUnexpectedRange()+63C↑j
.text:004354F8 ; KiSystemServiceAccessTeb()+6↑j
.text:004354F8 64 FF 05 B0 06 00 00 inc large dword ptr fs:6B0h
.text:004354FF 8B F2 mov esi, edx
.text:00435501 33 C9 xor ecx, ecx
.text:00435503 8B 57 0C mov edx, [edi+0Ch]
.text:00435506 8B 3F mov edi, [edi]
.text:00435508 8A 0C 10 mov cl, [eax+edx]
.text:0043550B 8B 14 87 mov edx, [edi+eax*4]
.text:0043550E 2B E1 sub esp, ecx
.text:00435510 C1 E9 02 shr ecx, 2
.text:00435513 8B FC mov edi, esp
.text:00435515 F6 45 72 02 test byte ptr [ebp+72h], 2
.text:00435519 75 06 jnz short loc_435521
.text:0043551B F6 45 6C 01 test byte ptr [ebp+6Ch], 1
.text:0043551F 74 0C jz short _KiSystemServiceCopyArguments@0 ; KiSystemServiceCopyArguments()
.text:00435521
.text:00435521 loc_435521: ; CODE XREF: KiSystemServiceAccessTeb()+33↑j
.text:00435521 3B 35 34 58 56 00 cmp esi, ds:_MmUserProbeAddress
.text:00435527 0F 83 71 02 00 00 jnb loc_43579E
.text:00435527 _KiSystemServiceAccessTeb@0 endp
.text:00435527
.text:0043552D
.text:0043552D ; =============== S U B R O U T I N E =======================================
.text:0043552D
.text:0043552D
.text:0043552D ; int __usercall KiSystemServiceCopyArguments@<eax>(void (*)(void)@<edx>, int@<ecx>, unsigned int@<ebx>, int@<ebp>, void *@<edi>, const void *@<esi>)
.text:0043552D _KiSystemServiceCopyArguments@0 proc near
.text:0043552D ; CODE XREF: KiSystemServiceAccessTeb()+39↑j
.text:0043552D ; DATA XREF: KiPreprocessAccessViolation(x,x,x):loc_4D84BC↓o
.text:0043552D
.text:0043552D ; FUNCTION CHUNK AT .text:00435964 SIZE 0000000C BYTES
.text:0043552D
.text:0043552D F3 A5 rep movsd
.text:0043552F F6 45 6C 01 test byte ptr [ebp+6Ch], 1
.text:00435533 74 16 jz short loc_43554B
.text:00435535 64 8B 0D 24 01 00 00 mov ecx, large fs:124h
.text:0043553C 8B 3C 24 mov edi, [esp+0]
.text:0043553F 89 99 3C 01 00 00 mov [ecx+13Ch], ebx
.text:00435545 89 B9 2C 01 00 00 mov [ecx+12Ch], edi
.text:0043554B
.text:0043554B loc_43554B: ; CODE XREF: KiSystemServiceCopyArguments()+6↑j
.text:0043554B 8B DA mov ebx, edx
.text:0043554D F6 05 C8 2F 53 00 40 test byte ptr ds:dword_532FC8, 40h
.text:00435554 0F 95 45 12 setnz byte ptr [ebp+12h]
.text:00435558 0F 85 06 04 00 00 jnz loc_435964
.text:0043555E
.text:0043555E loc_43555E: ; CODE XREF: KiSystemServiceCopyArguments()+43E↓j
.text:0043555E FF D3 call ebx
.text:0043555E _KiSystemServiceCopyArguments@0 endp

然后已经知道了 edi 指向了正确的表 mov edi,[edi] 就让 edi 指向了函数表,mov edx,[edi+eax*4] 随后通过 eax 选择执行的函数,最后 call

上面还有一个步骤是复制参数,因为中断门进来的参数还在 R3,并没有被带到 R0,复制参数是通过:

mov edx,[edi+0Ch] 拿到参数个数表,再 mov cl,[eax+edx] 拿到具体参数字节数,然后 shr ecx,2 右移两位得到参数个数(假设所有参数都是四字节大小)。然后前面分别给 ediesi 赋值,最后使用了 rep movsd 指令,这个指令会根据 ecx 的值为步长,每次从 esi 指向的地址搬运四个字节到 edi 指向的地址,所以搬运参数仅通过这一条指令就完成了。

至此我们分析完了系统调用的入口。

后续还需要分析函数调用完成之后退出系统调用的过程,由于这一部分知识需要学习 APC 之后才能看,所以今天就到这里,分析完一遍系统调用的过程收获还是挺大的。

参考文献