windows系统调用(三)

简介:这篇延续上一篇windows系统调用过程,不过这篇是64位系统是如何调用的,虽然整体差不多还是有一些差异,我们重点说下这些差异的地方。

前言

前面两篇介绍了关于在x86系统架构下,从用户层发起系统调用的过程,从进入内核前,进入后,调用函数及调用后整体把主脉络过了一遍,这篇给大家介绍下从64位系统重,系统调用又有什么区别,因为整体的调用思路基本差不多,只是函数和指令有一些区别,那么我们重点会放在对这些区别的介绍,相似的地方可以看前面两篇:

windows系统调用(一)https://daliu.net/posts/20250105/

windows系统调用(二)https://daliu.net/posts/20250106/

准备过程(用户层)

在用户层的调用过程中,整体相差不多,我们还是x64dbg中随便打开一个64位的应用,然后Ctrl+G,输入CloseHandle,跳转到这个函数,然后进入到最后的跳转前,就来到了下图的位置:

2

还是很熟悉的布局,一段段重复的代码,简单来看下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 保存rcx,因为64位是rcx保存返回值
mov r10,rcx
// 系统服务号(函数编号)
mov eax,F                    
test byte ptr ds:[7FFE0308],1           
jne ntdll.7FFD9676D1C5  

// 系统调用的指令                
syscall                                 
ret                                     
00007FFD9676D1C5 |int 2E            
00007FFD9676D1C7 |ret       

syscall

上文中,涉及到这两步的时候,感觉没什么,因为这个地址的值总是0,应该就不会跳转,看后面它如果为1,就要跳转到后面的int 2E,也就是通过中断门的方式来进入内核,所以应该是判断是否可以用这个syscall指令来进入内核状态。

1
2
test byte ptr ds:[7FFE0308],1           
jne ntdll.7FFD9676D1C5

考虑到7FFE0308这个地址,跟上一篇说的_KUSER_SHARED_DATA 可能有关系我就在内存中顺势查了查,首先赋上官方文档该结构体的链接:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/ddi/ntddk/ns-ntddk-kuser_shared_data 或者更加方便的可以看这个https://www.vergiliusproject.com/kernels/x64/windows-10/22h2/_KUSER_SHARED_DATA

其中:0x30的位置是NtSystemRoot,其实就是系统内核的根文件夹,从结果看是c:\windows,那么这个地址在64位置依然是共享的结构体地址:

3

那么我们可以继续向下观察,这个对应0x308的位置到底是什么: ULONG SystemCall; 官方文档给的解释是:在 AMD64 上,如果系统使用系统服务调用机制的已更改视图运行,则此值将初始化为非零值。大概理解是AMD64的某些情况下无法用syscall的快速调用的指令,其他都OK。

后面,就执行syscall指令,并切入内核了,但是这个syscall指令到底都做了什么呢,这时候我们就需要查看intel的白皮书了,看看CPU到底干了什么:

4

以上是描述syscall指令具体做了什么,我们简要的概括出来:

1. 从IA32_LSTAR 这个MSR寄存器加载了RIP(也就是地址指针),并把返回地址赋给RCX,具体这步骤的理解就是,执行sysenter指令,然后RIP地址++,然后RIP赋值给RCX

这个寄存器的地址是什么呢,在白皮数里也有找到,就是C0000082

5

那么我们通过windbg查一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 读取MSR值
0: kd> rdmsr C0000082
msr[c0000082] = fffff802`38c10c40

// 反编译下这个读取的地址
0: kd> u fffff802`38c10c40

// 切入内核的首地址所对应的函数
nt!KiSystemCall64:
fffff802`38c10c40 0f01f8          swapgs
fffff802`38c10c43 654889242510000000 mov   qword ptr gs:[10h],rsp
fffff802`38c10c4c 65488b2425a8010000 mov   rsp,qword ptr gs:[1A8h]
fffff802`38c10c55 6a2b            push    2Bh
fffff802`38c10c57 65ff342510000000 push    qword ptr gs:[10h]
fffff802`38c10c5f 4153            push    r11
fffff802`38c10c61 6a33            push    33h
fffff802`38c10c63 51              push    rcx

2. 先用R11寄存器保存rflags,然后用IA32_FMASK的MSR寄存器的值通过掩码的方式覆盖rflags寄存器,也就是rflags 与上 取反之后的寄存器的值。

1
2
R11 := RFLAGS;
RFLAGS := RFLAGS AND NOT(IA32_FMASK);

同理,可以读取下这个寄存器的值:

6

1
2
0: kd> rdmsr C0000084
msr[c0000084] = 00000000`0000470

3. 将IA32_STAR 这个 MSR寄存器的第32-第47位赋给CS寄存器当做选择子,

其次,SS的值就是CS寄存器的值增加8

7

而且这个MSR寄存器的值是:C0000081H

8

同样可以用windbg读一下:

1
2
3
4
0: kd> rdmsr C0000081
msr[c0000081] = 00230010`00000000

// 也就是0010和0018

此外上文还特别强调,rsp也就是栈地址,该指令是不会进行操作RSP的,所以需要内核来完成rsp保存的工作。

内核部分

基本上syscall做的主要的动作就介绍到这里,在第一条命令中设置的RIP,也就是下一步要执行的代码位置,我们反编译后发现,对应的是一个函数,这个函数就类似于我们在x86系统分析的时候说的KiFastCallEntry函数。

KiSystemCall64

那我们就接着看下KiSystemCall64这个函数到底做了什么,跟KiFastCallEntry相比有什么区别,依旧是首先用IDA打开64位的内核文件,我们这次用win10的内核文件(还是ntoskrnl.exe),并查找这个函数,首先把第一段代码放出来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
.text:0000000140410C40                 swapgs
.text:0000000140410C43                 mov     gs:10h, rsp
.text:0000000140410C4C                 mov     rsp, gs:1A8h
.text:0000000140410C55                 push    2Bh ; '+'
.text:0000000140410C57                 push    qword ptr gs:10h
.text:0000000140410C5F                 push    r11
.text:0000000140410C61                 push    33h ; '3'
.text:0000000140410C63                 push    rcx
.text:0000000140410C64                 mov     rcx, r10
.text:0000000140410C67                 sub     rsp, 8
.text:0000000140410C6B                 push    rbp
.text:0000000140410C6C                 sub     rsp, 158h
.text:0000000140410D73                 lea     rbp, [rsp+80h]
.text:0000000140410D7B                 mov     [rbp+0C0h], rbx
.text:0000000140410D82                 mov     [rbp+0C8h], rdi
.text:0000000140410D89                 mov     [rbp+0D0h], rsi
.text:0000000140410D90                 test    byte ptr cs:KeSmapEnabled, 0FFh
.text:0000000140410D97                 jz      short loc_140410DA5
.text:0000000140410D99                 test    byte ptr [rbp+0F0h], 1
.text:0000000140410DA0                 jz      short loc_140410DA5
.text:0000000140410DA2                 stac

第一个指令就是:swapgs,也就是切换gs寄存器,这个跟在x86是一样的fs是一样的,用户层fs是teb线程环境块,内核层就是KPCR,也就是CPU控制区域,gs也是一样的存放的还是KPCR

转换成结构体的形式可以查看,这其实就还是在填充KTRAP_FRAME结构体,改结构体的样子可以查看这个地址:https://www.vergiliusproject.com/kernels/x64/windows-10/22h2/_KTRAP_FRAME

1
2
3
4
5
6
// 跟x86不同,首先保存rsp没有区别
// 然后内核的rsp位置,存放在了kpcr的其他位置,而x86是存放在的KTSS结构体
.text:0000000140410D43                 mov     gs:_KPCR.___u0.__s1.UserRsp, rsp
.text:0000000140410D4C                 mov     rsp, gs:_KPCR.Prcb.RspBase

// 在x86里,指向的位置是KTRAP_FRAME结构体的SegSs元素的位置,同样的这里应该也是这个位置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 保存SegSs的值
.text:0000000140410C55                 push    2Bh ; '+'

// 保存用户层Rsp的值
.text:0000000140410D57                 push    gs:_KPCR.___u0.__s1.UserRsp

// 保存用户层的rflags的值
.text:0000000140410C5F                 push    r11

// 保存用户层SegCs的值
.text:0000000140410C61                 push    33h ; '3'

// 保存返回地址(syscall过程赋值的,上文有写)
.text:0000000140410C63                 push    rcx
.text:0000000140410C64                 mov     rcx, r10

// 保存用户层的rpb
.text:0000000140410C67                 sub     rsp, 8
.text:0000000140410C6B                 push    rbp

// 直接到KTRAP_FRAME结构体的首地址
.text:0000000140410C6C                 sub     rsp, 158h

然后继续这段的最后一小部分:

1
2
3
4
5
6
7
// rbp存储了rsp+80的位置,跟x86不一样,没有直接存储ktrap_frame的结构体首地址
.text:0000000140410D73                 lea     rbp, [rsp+80h]

// 有点困惑为啥不直接从结构体首地址往下而是从之间位置的相对位置去赋值
.text:0000000140410D7B                 mov     [rbp+0C0h], rbx
.text:0000000140410D82                 mov     [rbp+0C8h], rdi
.text:0000000140410D89                 mov     [rbp+0D0h], rsi

接下来是一段不太涉及主要调用过程的功能,涉及一些补丁和权限判断,就忽略简写了。

 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
// 判断是用户层和是内核,这个参数涉及到CPU读写用户层权限
.text:0000000140410D90                 test    byte ptr cs:KeSmapEnabled, 0FFh
.text:0000000140410D97                 jz      short loc_140410DA5
.text:0000000140410D99                 test    byte ptr [rbp+0F0h], 1
.text:0000000140410DA0                 jz      short loc_140410DA5
.text:0000000140410DA2                 stac

// 保存其他几个寄存器
.text:0000000140410DA5                 mov     [rbp-50h], rax
.text:0000000140410DA9                 mov     [rbp-48h], rcx
.text:0000000140410DAD                 mov     [rbp-40h], rdx

// 然后是一段涉及到补丁,漏洞相关的访问控制,Spectre或Meltdown漏洞相关的控制寄存器更新等。
// 通过当先线程及进程获取数据保存到对应的KPCR中
.text:0000000140410DB1                 mov     rcx, gs:_KPCR.Prcb.CurrentThread
.text:0000000140410DBA                 mov     rcx, [rcx+_ETHREAD.Tcb.Process]
.text:0000000140410DC1                 mov     rcx, [rcx+_EPROCESS.SecurityDomain]
.text:0000000140410DC8                 mov     gs:_KPCR.Prcb.___u42.__s0.TrappedSecurityDomain, rcx
.text:0000000140410DD1                 mov     cl, gs:_KPCR.Prcb.___u47.__s0.BpbRetpolineExitSpecCtrl
.text:0000000140410DD9                 mov     gs:_KPCR.Prcb.___u47.__s0.BpbTrappedRetpolineExitSpecCtrl, cl
.text:0000000140410DE1                 mov     cl, gs:_KPCR.Prcb.___u42.__s0.BpbState
.text:0000000140410DE9                 mov     gs:_KPCR.Prcb.___u47.__s0.BpbTrappedBpbState, cl
.text:0000000140410DF1                 movzx   eax, gs:_KPCR.Prcb.___u42.__s0.BpbKernelSpecCtrl
.text:0000000140410DFA                 cmp     gs:_KPCR.Prcb.___u42.__s0.BpbCurrentSpecCtrl, al
.text:0000000140410E02                 jz      short loc_140410E15
.text:0000000140410E04                 mov     gs:_KPCR.Prcb.___u42.__s0.BpbCurrentSpecCtrl, al
.text:0000000140410E0C                 mov     ecx, 48h ; 'H'
.text:0000000140410E11                 xor     edx, edx
.text:0000000140410E13                 wrmsr

// 然后是针对各种情况判断如何处理堆栈等,直接略过到后面。

接着开始新的正文部分:

 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
// 这个相对位置赋值非常的恶心,不是结构体起始位置,这个地方的值是ExceptionActive
.text:0000000140410F7A                 mov     byte ptr [rbp-55h], 2

// 当前线程块的地址给了rbx
.text:0000000140410F7E                 mov     rbx, gs:_KPCR.Prcb.CurrentThread

// prefetchw 类似于预读取指令,把内存的值读取到CPU缓存区(并不影响执行流)
.text:0000000140410F87                 prefetchw byte ptr [rbx+_ETHREAD.Tcb.TrapFrame]

// 需修改控制寄存器,MXCSR
.text:0000000140410F8E                 stmxcsr dword ptr [rbp-54h]
.text:0000000140410F92                 ldmxcsr gs:_KPCR.Prcb._MxCsr

// 判断是否为调试情况
.text:0000000140410F9B                 cmp     [rbx+_ETHREAD.Tcb.Header.___u0.__s7.DebugActive], 0
.text:0000000140410F9F                 mov     word ptr [rbp+80h], 0

// 如果不是调试就跳转了
.text:0000000140410FA8                 jz      loc_14041107E

// 一样的判断
.text:0000000140410FAE                 test    [rbx+_ETHREAD.Tcb.Header.___u0.__s7.DebugActive], 3
// 保存寄存器
.text:0000000140410FB2                 mov     [rbp-38h], r8
.text:0000000140410FB6                 mov     [rbp-30h], r9
.text:0000000140410FBA                 jz      short loc_140410FC1
.text:0000000140410FBC                 call    KiSaveDebugRegisterState

以上整体看气啦就是在判断是否在调试,也就是是否下了硬件端点,如果是,就需要保存很多环境,如果不是就往下走。那么我们接着向下去看。

 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
// 这一大段,涉及到几个问题,是否调试,异常派发,以及ums线程的处理。就简单略过了哈。
.text:0000000140410FC1                 test    [rbx+_ETHREAD.Tcb.Header.___u0.__s7.DebugActive], 24h
.text:0000000140410FC5                 jz      short loc_14041101D
.text:0000000140410FC7                 mov     [rbp-20h], r10
.text:0000000140410FCB                 mov     [rbp-28h], r10
.text:0000000140410FCF                 movaps  xmmword ptr [rbp-10h], xmm0
.text:0000000140410FD3                 movaps  xmmword ptr [rbp+0], xmm1
.text:0000000140410FD7                 movaps  xmmword ptr [rbp+10h], xmm2
.text:0000000140410FDB                 movaps  xmmword ptr [rbp+20h], xmm3
.text:0000000140410FDF                 movaps  xmmword ptr [rbp+30h], xmm4
.text:0000000140410FE3                 movaps  xmmword ptr [rbp+40h], xmm5
.text:0000000140410FE7                 sti
.text:0000000140410FE8                 mov     rcx, rsp
.text:0000000140410FEB                 call    PsAltSystemCallDispatch
.text:0000000140410FF0                 cmp     al, 1
.text:0000000140410FF2                 jz      short loc_14041101D
.text:0000000140410FF4                 mov     rax, [rbp-50h]
.text:0000000140410FF8                 jl      short loc_14041100E
.text:0000000140410FFA                 mov     ecx, 0C000001Ch
.text:0000000140410FFF                 xor     edx, edx
.text:0000000140411001                 mov     r8, [rbp+0E8h]

// 异常派发
.text:0000000140411008                 call    KiExceptionDispatch
.text:000000014041100D                 int     3               ; Trap to Debugger

.text:000000014041100E loc_14041100E:                          ; CODE XREF: KiSystemCall64+2B8↑j
.text:000000014041100E                 test    byte ptr [rbx+3], 4
.text:0000000140411012                 jz      KiSystemServiceExit
.text:0000000140411018                 jmp     KiSystemServiceExitPico
.text:000000014041101D ; ---------------------------------------------------------------------------
.text:000000014041101D
.text:000000014041101D loc_14041101D:                          ; CODE XREF: KiSystemCall64+285↑j
.text:000000014041101D                                         ; KiSystemCall64+2B2↑j
.text:000000014041101D                 test    byte ptr [rbx+3], 80h
.text:0000000140411021                 jz      short loc_14041106B
.text:0000000140411023                 mov     ecx, 0C0000102h
.text:0000000140411028                 rdmsr
.text:000000014041102A                 shl     rdx, 20h
.text:000000014041102E                 or      rax, rdx
.text:0000000140411031                 cmp     rax, cs:MmUserProbeAddress
.text:0000000140411038                 cmovnb  rax, cs:MmUserProbeAddress
.text:0000000140411040                 cmp     [rbx+0F0h], rax
.text:0000000140411047                 jz      short loc_14041106B
.text:0000000140411049                 mov     rdx, [rbx+1F0h]
.text:0000000140411050                 bts     dword ptr [rbx+74h], 8
.text:0000000140411055                 dec     word ptr [rbx+1E6h]
.text:000000014041105C                 mov     [rdx+80h], rax
.text:0000000140411063                 sti
// ums线程调用
.text:0000000140411064                 call    KiUmsCallEntry
.text:0000000140411069                 jmp     short loc_140411076

.text:000000014041106B ; ---------------------------------------------------------------------------
.text:000000014041106B
.text:000000014041106B loc_14041106B:                          ; CODE XREF: KiSystemCall64+2E1↑j
.text:000000014041106B                                         ; KiSystemCall64+307↑j
.text:000000014041106B                 test    byte ptr [rbx+3], 40h
.text:000000014041106F                 jz      short loc_140411076
.text:0000000140411071                 bts     dword ptr [rbx+74h], 10h

处理服务及SSDT

继续向下看,已经快要开始处理服务了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 将上文中保存的内容再重新赋回来
// 因为上文中间对应参数被各种其他处理过程使用过,所以需要再保存回来
.text:0000000140411076                                         ; KiSystemCall64+32F↑j
.text:0000000140411076                 mov     r8, [rbp-38h]
.text:000000014041107A                 mov     r9, [rbp-30h]

.text:000000014041107E                 mov     rax, [rbp-50h]
.text:0000000140411082                 mov     rcx, [rbp-48h]
.text:0000000140411086                 mov     rdx, [rbp-40h]
//开中断
.text:000000014041108A                 sti

// 开始保存参数地址和系统服务号
.text:000000014041108B                 mov     [rbx+_ETHREAD.Tcb.FirstArgument], rcx
.text:0000000140411092                 mov     [rbx+_ETHREAD.Tcb.SystemCallNumber], eax

// 不止怎地有一个空指令
.text:0000000140411098                 nop     dword ptr [rax+rax+00000000h]

继续向后看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 保存堆栈地址,KTRAP_FRAME的地址,经此算是保存完了用户层的上下文状态
.text:00000001404110A0                 mov     [rbx+_ETHREAD.Tcb.TrapFrame], rsp

// 开始运算系统服务号
.text:00000001404110A7                 mov     edi, eax
// 在x86是右移8,与0x10
// 之所以有差异是因为下面的两个服务表的大小有了变化
.text:00000001404110A9                 shr     edi, 7
.text:00000001404110AC                 and     edi, 20h
.text:00000001404110AF                 and     eax, 0FFFh
.text:00000001404110B4                 lea     r10, KeServiceDescriptorTable
.text:00000001404110BB                 lea     r11, KeServiceDescriptorTableShadow

上文说到会有一些区别,其实主要目的就是选择用哪个表,是用非UI的还是用UI的SSDT表,通过在windbg里观察发现,KeServiceDescriptorTableShadow里面就包含了KeServiceDescriptorTable的函数表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
1: kd> dq KeServiceDescriptorTable
fffff804`568018c0  fffff804`55ac7780 00000000`00000000
fffff804`568018d0  00000000`000001d9 fffff804`55ac7ee8
fffff804`568018e0  00000000`00000000 00000000`00000000
fffff804`568018f0  00000000`00000000 00000000`00000000
fffff804`56801900  fffff804`55e0a7c0 fffff804`55e0ab00
fffff804`56801910  fffff804`55e0fc80 fffff804`55e0ffc0
fffff804`56801920  fffff804`55e10300 fffff804`55e10d40
fffff804`56801930  fffff804`55e10880 00000000`00000000
1: kd> dq KeServiceDescriptorTableShadow
fffff804`566fca40  fffff804`55ac7780 00000000`00000000
fffff804`566fca50  00000000`000001d9 fffff804`55ac7ee8
fffff804`566fca60  ffff89b9`33f29000 00000000`00000000
fffff804`566fca70  00000000`00000524 ffff89b9`33f2a9bc
fffff804`566fca80  00000000`00000000 00000000`00000000
fffff804`566fca90  00007fff`047f0f60 00007fff`047f103b
fffff804`566fcaa0  ffffce88`37eca560 00000000`00000000
fffff804`566fcab0  00000000`00000000 00000000`1298b528

因为当前SSDT表的结构体成员都是8个字节大小,当edi向右移动7,然后与上0x20,结果无非就是0x20或者0x0,以上操作的目的就是根据服务号是否超过0x1000判断为UI还是非UI,如果超过就可以用类似于,KeServiceDescriptorTableShadow表地址+计算结果+成员位置,来确定需要找哪个成员表了。

 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
// 通过线程块判断是否是GUI线程,
.text:00000001404110C2                 test    dword ptr [rbx+_ETHREAD.Tcb.___u16.__s0._bf_4], 80h
.text:00000001404110C9                 jz      short loc_1404110DE
.text:00000001404110CB                 test    dword ptr [rbx+_ETHREAD.Tcb.___u16.__s0._bf_4], 200000h
.text:00000001404110D2                 jz      short loc_1404110DB

// 上文的这个位置是一个联合体,具体定义如下:
union
    {
        struct
        {
            ULONG ThreadFlagsSpare:2;                                       //0x78
            ULONG AutoAlignment:1;                                          //0x78
            ULONG DisableBoost:1;                                           //0x78
            ULONG AlertedByThreadId:1;                                      //0x78
            ULONG QuantumDonation:1;                                        //0x78
            ULONG EnableStackSwap:1;                                        //0x78
            //这个就是其第8位置
            ULONG GuiThread:1;                                              //0x78
            ULONG DisableQuantum:1;                                         //0x78
            ULONG ChargeOnlySchedulingGroup:1;                              //0x78
            ULONG DeferPreemption:1;                                        //0x78
            ULONG QueueDeferPreemption:1;                                   //0x78
            ULONG ForceDeferSchedule:1;                                     //0x78
            ULONG SharedReadyQueueAffinity:1;                               //0x78
            ULONG FreezeCount:1;                                            //0x78
            ULONG TerminationApcRequest:1;                                  //0x78
            ULONG AutoBoostEntriesExhausted:1;                              //0x78
            ULONG KernelStackResident:1;                                    //0x78
            ULONG TerminateRequestReason:2;                                 //0x78
            ULONG ProcessStackCountDecremented:1;                           //0x78
            // 这个位置是第22位
            ULONG RestrictedGuiThread:1;                                    //0x78
            ULONG VpBackingThread:1;                                        //0x78
            ULONG ThreadFlagsSpare2:1;                                      //0x78
            ULONG EtwStackTraceApcInserted:8;                               //0x78
        };
        volatile LONG ThreadFlags;                                          //0x78
    };
...


.text:00000001404110D4                 lea     r11, KeServiceDescriptorTableFilter

以上通过判断是否GUI线程来确定要找哪一个表,然后继续执行,假设是非GUI线程,继续走

1
2
3
//把服务号与SSDT表的数量比较看是否有效
.text:00000001404110DE                 cmp     eax, [r10+rdi+10h]
.text:00000001404110E3                 jnb     loc_1404118B3

跳转的这条指令如下:jnb loc_1404118B3,

上文判断大于系统服务号如果大于当前结构体了存储的函数个数,跳转到下文,下文并没有直接走退出机制,而是再次判断是否为0x20,如果不是走退出机制,如果是先保存寄存器,然后走KiConvertToGuiThread这个函数,将其转为GUI线程,然后再返回去刚刚的KiSystemServiceRepeat函数,也就是保存环境之后的部分。

 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
.text:00000001404118B3                 cmp     edi, 20h ; ' '
.text:00000001404118B6                 jnz     short loc_140411913
.text:00000001404118B8                 mov     [rbp-80h], eax
.text:00000001404118BB                 mov     [rbp-78h], rcx
.text:00000001404118BF                 mov     [rbp-70h], rdx
.text:00000001404118C3                 mov     [rbp-68h], r8
.text:00000001404118C7                 mov     [rbp-60h], r9
.text:00000001404118CB                 call    KiConvertToGuiThread
.text:00000001404118D0                 or      eax, eax
.text:00000001404118D2                 mov     eax, [rbp-80h]
.text:00000001404118D5                 mov     rcx, [rbp-78h]
.text:00000001404118D9                 mov     rdx, [rbp-70h]
.text:00000001404118DD                 mov     r8, [rbp-68h]
.text:00000001404118E1                 mov     r9, [rbp-60h]
.text:00000001404118E5                 mov     [rbx+90h], rsp
.text:00000001404118EC                 jz      KiSystemServiceRepeat
.text:00000001404118F2                 lea     rdi, xmmword_140CFCA60
.text:00000001404118F9                 mov     esi, [rdi+10h]
.text:00000001404118FC                 mov     rdi, [rdi]
.text:00000001404118FF                 cmp     eax, esi
.text:0000000140411901                 jnb     short loc_140411913
.text:0000000140411903                 lea     rdi, [rdi+rsi*4]
.text:0000000140411907                 movsx   eax, byte ptr [rdi+rax]
.text:000000014041190B                 or      eax, eax
.text:000000014041190D                 jle     KiSystemServiceExit

然后就是获取函数地址的计算过程,这里是重点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 获取SSDT中函数表的地址
.text:00000001404110E9                 mov     r10, [r10+rdi]
// 获取函数表赌赢索引位置的值,注意 rax * 4,这个值不是8字节而是4字节
// 保存到rax中,判断这是一个偏移
.text:00000001404110ED                 movsxd  r11, dword ptr [r10+rax*4]
.text:00000001404110F1                 mov     rax, r11

// r11的值,算数右移,也就是看符号位的方式补位
// 最高位1,右移过程中,最高位补1
// 最高位是0,右移过程中,最高位补0
.text:00000001404110F4                 sar     r11, 4
// 将r10 + 右移之后的值,也就是SSDT表首位置 + 右移运算后的值
.text:00000001404110F8                 add     r10, r11
//判断是否为GUI线程,不是GUI就跳转走
.text:00000001404110FB                 cmp     edi, 20h ; ' '
.text:00000001404110FE                 jnz     short loc_140411150

可见在64位情况下,这个SSDT表里的函数地址是做了处理的,并没有那么直接的给到你,同样下面的参数处理也有一些区别:

 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
// eax存储的是上文的r11,也就是首次获取到的对应函数表索引位置的值
// 取出最后低4位来查看,这个低4位有实际意义的
// 在x64位里,为了方便函数调用,一般优先用rcx rdx r8 r9来存储参数
// 如果超出的部分再用堆栈
// 所以这4位实际是表达除了头四个参数外,还剩余多少个参数
.text:0000000140411150                 and     eax, 0Fh
// 如果没有,就直接跳转到参数处理函数那边了
.text:0000000140411153                 jz      KiSystemServiceCopyEnd
// 如果有,就需要分配堆栈,右移3位,就是乘以8,也就是需要分配的堆栈大小
.text:0000000140411159                 shl     eax, 3
// 获取新堆栈地址
.text:000000014041115C                 lea     rsp, [rsp-70h]
.text:0000000140411161                 lea     rdi, [rsp+18h]
//获取用户参数地址
.text:0000000140411166                 mov     rsi, [rbp+100h]
.text:000000014041116D                 lea     rsi, [rsi+20h]

// 该值是SegCs,判断是否用户层还是内核层
.text:0000000140411171                 test    byte ptr [rbp+0F0h], 1
.text:0000000140411178                 jz      short loc_140411190
.text:000000014041117A                 cmp     rsi, cs:MmUserProbeAddress
.text:0000000140411181                 cmovnb  rsi, cs:MmUserProbeAddress
.text:0000000140411189                 nop     dword ptr [rax+00000000h]
.text:0000000140411190
.text:0000000140411190 loc_140411190:                          ; CODE XREF: KiSystemCall64+438↑j

.text:0000000140411190                 lea     r11, KiSystemServiceCopyEnd
.text:0000000140411197                 sub     r11, rax
.text:000000014041119A                 jmp     r11

关于上文最后这三行是非常巧妙的地方,首先可以看下面的代码,KiSystemServiceCopyStart和KiSystemServiceCopyEnd分别是复制参数列表到对阵的代码段的开始和结尾。

首先,赋值给r11,KiSystemServiceCopyEnd,也就是复制参数代码段的结尾地址

然后,将这个地址减去刚刚所谓的分配堆栈的大小,其实既是分配堆栈的大小,同时又是复制参数的代码段所开始的位置。

也就是说,减去的多,代码其实位置就比较靠前。而刚刚好这段代码的指令长度跟堆栈长度一样,一条指令4字节,复制一次是两条指令刚刚好是8个字节,而且也就是堆栈的一条。

 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
.text:00000001404111A0 KiSystemServiceCopyStart:               ; DATA XREF: KiSystemServiceHandler+1A↑o
.text:00000001404111A0                 mov     rax, [rsi+70h]
.text:00000001404111A4                 mov     [rdi+70h], rax
.text:00000001404111A8                 mov     rax, [rsi+68h]
.text:00000001404111AC                 mov     [rdi+68h], rax
.text:00000001404111B0                 mov     rax, [rsi+60h]
.text:00000001404111B4                 mov     [rdi+60h], rax
.text:00000001404111B8                 mov     rax, [rsi+58h]
.text:00000001404111BC                 mov     [rdi+58h], rax
.text:00000001404111C0                 mov     rax, [rsi+50h]
.text:00000001404111C4                 mov     [rdi+50h], rax
.text:00000001404111C8                 mov     rax, [rsi+48h]
.text:00000001404111CC                 mov     [rdi+48h], rax
.text:00000001404111D0                 mov     rax, [rsi+40h]
.text:00000001404111D4                 mov     [rdi+40h], rax
.text:00000001404111D8                 mov     rax, [rsi+38h]
.text:00000001404111DC                 mov     [rdi+38h], rax
.text:00000001404111E0                 mov     rax, [rsi+30h]
.text:00000001404111E4                 mov     [rdi+30h], rax
.text:00000001404111E8                 mov     rax, [rsi+28h]
.text:00000001404111EC                 mov     [rdi+28h], rax
.text:00000001404111F0                 mov     rax, [rsi+20h]
.text:00000001404111F4                 mov     [rdi+20h], rax
.text:00000001404111F8                 mov     rax, [rsi+18h]
.text:00000001404111FC                 mov     [rdi+18h], rax
.text:0000000140411200                 mov     rax, [rsi+10h]
.text:0000000140411204                 mov     [rdi+10h], rax
.text:0000000140411208                 mov     rax, [rsi+8]
.text:000000014041120C                 mov     [rdi+8], rax
.text:0000000140411210
.text:0000000140411210 KiSystemServiceCopyEnd:                 ; CODE XREF: KiSystemCall64+413↑j
.text:0000000140411210                                         ; DATA XREF: KiSystemServiceHandler+27↑o ...
.text:0000000140411210                 test    cs:KiDynamicTraceMask, 1
.text:000000014041121A                 jnz     loc_140411951
.text:0000000140411220                 test    dword ptr cs:PerfGlobalGroupMask+8, 40h
.text:000000014041122A                 jnz     loc_1404119C5
.text:0000000140411230                 mov     rax, r10
.text:0000000140411233                 call    rax

结束之后就是是服务的调用了:

1
2
3
4
5
6
7
8
9
// 其他准备工作,暂不展开了
.text:0000000140411210                 test    cs:KiDynamicTraceMask, 1
.text:000000014041121A                 jnz     loc_140411951
.text:0000000140411220                 test    dword ptr cs:PerfGlobalGroupMask+8, 40h
.text:000000014041122A                 jnz     loc_1404119C5

// r10是函数的入口地址
.text:0000000140411230                 mov     rax, r10
.text:0000000140411233                 call    rax

调用完成

调用完其实更多就是还原的工作了,因为篇幅过长,不过多赘述,大概的步骤跟进入的时候一样,就是倒过来的一个过程:

涉及到APC的操作,依然是调用前保存环境,调用后恢复环境

9

涉及计数器,ums线程,调试状态存储等

10

又是一长条最初堆栈处理异常派发那边遇到的一样的部分。

11

直到最后一段恢复状态,然后跳转回去:

 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
// 从KTRAP_FRAME中保存的用户层信息赋到对应寄存器中
// 保存rax
.text:0000000140411584                 mov     rax, [rbp-50h]

// 保存Rsp和rbp的值
.text:0000000140411588                 mov     r8, [rbp+100h]
.text:000000014041158F                 mov     r9, [rbp+0D8h]

// 清零对应寄存器
.text:0000000140411596                 xor     edx, edx
.text:0000000140411598                 pxor    xmm0, xmm0
.text:000000014041159C                 pxor    xmm1, xmm1
.text:00000001404115A0                 pxor    xmm2, xmm2
.text:00000001404115A4                 pxor    xmm3, xmm3
.text:00000001404115A8                 pxor    xmm4, xmm4
.text:00000001404115AC                 pxor    xmm5, xmm5

// 保存的是RIP
.text:00000001404115B0                 mov     rcx, [rbp+0E8h]
// 保存的是eflags
.text:00000001404115B7                 mov     r11, [rbp+0F8h]

.text:00000001404115BE                 test    cs:KiKvaShadow, 1
.text:00000001404115C5                 jnz     KiKernelSysretExit

// 复制给rbp和rsp对应KTRAP_FRAME中取出的对应值
.text:00000001404115CB                 mov     rbp, r9
.text:00000001404115CE                 mov     rsp, r8
// 交换gs寄存器,变为用户层的寄存器值
.text:00000001404115D1                 swapgs
// 返回用户层
.text:00000001404115D4                 sysret

这个sysret指令,与syscall正好相反,从白皮书里粘贴了这段描述,基本就是把syscall的执行过程倒过来。具体不赘述了,可以直接看这个图片。

12

以上就是系统调用的全部内容了,虽然篇幅很长,但是还有很多点没有具体展开,但是总体的流程应该都梳理的比较清楚了,后续如果有一定的积累,会单独把对没有展开的内容再展开介绍下,那么今天就到这里了。

updatedupdated2025-01-082025-01-08