前面两篇介绍了关于在x86系统架构下,从用户层发起系统调用的过程,从进入内核前,进入后,调用函数及调用后整体把主脉络过了一遍,这篇给大家介绍下从64位系统重,系统调用又有什么区别,因为整体的调用思路基本差不多,只是函数和指令有一些区别,那么我们重点会放在对这些区别的介绍,相似的地方可以看前面两篇:
windows系统调用(一)https://daliu.net/posts/20250105/
windows系统调用(二)https://daliu.net/posts/20250106/
在用户层的调用过程中,整体相差不多,我们还是x64dbg中随便打开一个64位的应用,然后Ctrl+G,输入CloseHandle,跳转到这个函数,然后进入到最后的跳转前,就来到了下图的位置:

还是很熟悉的布局,一段段重复的代码,简单来看下:
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
|
上文中,涉及到这两步的时候,感觉没什么,因为这个地址的值总是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位置依然是共享的结构体地址:

那么我们可以继续向下观察,这个对应0x308的位置到底是什么:
ULONG SystemCall; 官方文档给的解释是:在 AMD64 上,如果系统使用系统服务调用机制的已更改视图运行,则此值将初始化为非零值。大概理解是AMD64的某些情况下无法用syscall的快速调用的指令,其他都OK。
后面,就执行syscall指令,并切入内核了,但是这个syscall指令到底都做了什么呢,这时候我们就需要查看intel的白皮书了,看看CPU到底干了什么:

以上是描述syscall指令具体做了什么,我们简要的概括出来:
1. 从IA32_LSTAR 这个MSR寄存器加载了RIP(也就是地址指针),并把返回地址赋给RCX,具体这步骤的理解就是,执行sysenter指令,然后RIP地址++,然后RIP赋值给RCX
这个寄存器的地址是什么呢,在白皮数里也有找到,就是C0000082

那么我们通过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);
|
同理,可以读取下这个寄存器的值:

1
2
|
0: kd> rdmsr C0000084
msr[c0000084] = 00000000`0000470
|
3. 将IA32_STAR 这个 MSR寄存器的第32-第47位赋给CS寄存器当做选择子,
其次,SS的值就是CS寄存器的值增加8

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

同样可以用windbg读一下:
1
2
3
4
|
0: kd> rdmsr C0000081
msr[c0000081] = 00230010`00000000
// 也就是0010和0018
|
此外上文还特别强调,rsp也就是栈地址,该指令是不会进行操作RSP的,所以需要内核来完成rsp保存的工作。
基本上syscall做的主要的动作就介绍到这里,在第一条命令中设置的RIP,也就是下一步要执行的代码位置,我们反编译后发现,对应的是一个函数,这个函数就类似于我们在x86系统分析的时候说的KiFastCallEntry函数。
那我们就接着看下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
|
继续向下看,已经快要开始处理服务了
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的操作,依然是调用前保存环境,调用后恢复环境

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

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

直到最后一段恢复状态,然后跳转回去:
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的执行过程倒过来。具体不赘述了,可以直接看这个图片。

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