Syscall Journey
本文记录一下syscall的“旅程”。
这里使用的是win10x64 22H2 19045.3086
-
与其他进程、内存、驱动器或文件系统交互时,使用
Kernel32.dll
中的函数 -
与 Windows GUI 交互,使用的是
user32.dll
和gdi32.dll
中的函数 -
并非所有
Kernel32
、user32
和gdi32
函数都以直接系统调用结束
Nt*
、 Zw*
、 NtUser*
和 NtGdi*
函数的“真正”代码在内核模式下运行。
在本文中, Nt*
和 Zw*
函数将被称为 Native functions
。 NtUser*
和 NtGdi*
函数将被称为 GUI functions
直接系统调用的代码也称为 syscall stub
syscall stub
的作用是将函数的执行流程转发到内核中的相关代码( kernel routine
)。这是用户模式的最后一步。由 syscall
完成的,该数字称为 syscall ID
或 syscall number
或 system service number (SSN)
在64位系统上,当执行 syscall
指令时, LSTAR
寄存器中的地址被放入 RIP
寄存器中
图示:
LSTAR
寄存器的唯一目的是存储 syscall
指令之后在内核模式下执行的第一个函数的地址。 LSTAR
寄存器是称为 MSR(模型特定寄存器 Model-specific register)的许多其他寄存器之一。
系统中, LSTAR
被标识为 msr[c0000082]
,使用命令 rdmsr
读取MSR
0: kd> rdmsr c0000082
msr[c0000082] = fffff801`54a751c0
1: kd> ln fffff801`54a751c0
(fffff801`54a751c0) nt!KiSystemCall64 | (fffff801`54a753eb) nt!KiSystemServiceUser
执行syscall后进入内核态,之后大致流程为:
- KiSystemCall64Shadow
- KiSystemServiceUser
- KiSystemServiceStart
- KiSystemServiceRepeat
- KiSystemServiceGdiTebAccess
- KiSystemServiceCopyStart
- KiSystemServiceCopyEnd
本文重点关注KiSystemServiceUser
、 KiSystemServiceStart
、 KiSystemServiceRepeat
和 KiSystemServiceCopyEnd
函数
KiSystemServiceUser()
KiSystemCall64
结束后,执行KiSystemServiceUser
,获取当前线程KTHREAD结构体地址
KiSystemServiceUser:
mov byte ptr [rbp-55h], 2
mov rbx, gs:KPCR.Prcb.CurrentThread
prefetchw byte ptr [rbx+90h]
stmxcsr dword ptr [rbp-54h]
ldmxcsr dword ptr gs:180h
cmp byte ptr [rbx+3], 0
mov word ptr [rbp+KPCR.KernelReserved], 0
jz loc_1401C44C7
1: kd> dt nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 GdtBase : Ptr64 _KGDTENTRY64
+0x008 TssBase : Ptr64 _KTSS64
+0x010 UserRsp : Uint8B
+0x018 Self : Ptr64 _KPCR
+0x020 CurrentPrcb : Ptr64 _KPRCB
+0x028 LockArray : Ptr64 _KSPIN_LOCK_QUEUE
+0x030 Used_Self : Ptr64 Void
+0x038 IdtBase : Ptr64 _KIDTENTRY64
+0x040 Unused : [2] Uint8B
+0x050 Irql : UChar
+0x051 SecondLevelCacheAssociativity : UChar
+0x052 ObsoleteNumber : UChar
+0x053 Fill0 : UChar
+0x054 Unused0 : [3] Uint4B
+0x060 MajorVersion : Uint2B
+0x062 MinorVersion : Uint2B
+0x064 StallScaleFactor : Uint4B
+0x068 Unused1 : [3] Ptr64 Void
+0x080 KernelReserved : [15] Uint4B
+0x0bc SecondLevelCacheSize : Uint4B
+0x0c0 HalReserved : [16] Uint4B
+0x100 Unused2 : Uint4B
+0x108 KdVersionBlock : Ptr64 Void
+0x110 Unused3 : Ptr64 Void
+0x118 PcrAlign1 : [24] Uint4B
+0x180 Prcb : _KPRCB
lkd> dt nt!_KPRCB
+0x000 MxCsr : Uint4B
+0x004 LegacyNumber : UChar
+0x005 ReservedMustBeZero : UChar
+0x006 InterruptRequest : UChar
+0x007 IdleHalt : UChar
+0x008 CurrentThread : Ptr64 _KTHREAD
+0x010 NextThread : Ptr64 _KTHREAD
+0x018 IdleThread : Ptr64 _KTHREAD
....
_KPRCB
与 _KPCR
一样,包含处理器特定的数据,它还包含指向当前、下一个和空闲执行线程计划的指针。
KiSystemServiceUser()
的末尾,KTHREAD
结构中初始化了两个值:
.text:00000001401C44C7 loc_1401C44C7: ; CODE XREF: KiSystemCall64+259↑j
.text:00000001401C44C7 mov rax, [rbp-50h]
.text:00000001401C44CB mov rcx, [rbp-48h]
.text:00000001401C44CF mov rdx, [rbp-40h]
.text:00000001401C44D3 sti
.text:00000001401C44D4 mov [rbx+_KTHREAD.FirstArgument], rcx
.text:00000001401C44DB mov [rbx+_KTHREAD.SystemCallNumber], eax
.text:00000001401C44E1 db 66h, 66h, 66h, 66h, 66h, 66h
.text:00000001401C44E1 nop word ptr [rax+rax+00000000h]
现在rcx
= FirstArgument
,eax
= SystemCallNumber
,之后是KiSystemServiceStart
.text:00000001401C44F0 KiSystemServiceStart: ; DATA XREF: KiServiceInternal+5A↑o
.text:00000001401C44F0 ; .data:00000001403FF340↓o
.text:00000001401C44F0 mov [rbx+_KTHREAD.TrapFrame], rsp
.text:00000001401C44F7 mov edi, eax
.text:00000001401C44F9 shr edi, 7
.text:00000001401C44FC and edi, 20h
.text:00000001401C44FF and eax, 0FFFh
Syscall Number 和 KiSystemServiceStart()
System Call Number包含两部分,Table Identifier
和 System Call Index
这里以NtQueryVirtualMemory()
举例,系统调用号为0x23
mov edi, eax# edi = 23h = 0010 0011
shr edi, 7#右移6位 = 0000 0000
and edi, 20h # edi = 00h
and eax, 0FFFh # eax = 23h
NtQueryVirtualMemory()
的 System Call Index
是 0x23
对于 win32u.dll
的函数 NtUserSetMenu()
来说, System Call Number
是 0x1496h
mov edi, eax // edi = 1496h = 0001 0100 1001 0110
shr edi, 7 // 右移6位
and edi, 20h // edi = 29h
and eax, 0FFFh // eax = 1496h
NtUserSetMenu()
的 System Call Index
是 496h
。
如今,在 Windows 10 和 11 中, Table Identifier
上的初始值只能导致结果 20h
或 00h
。
- 如果数字在
1000h
和1FFFh
之间,则Table Identifier
为20h
。这种可以在win32u.dll
上找到。与 Windows GUI 相关。 - 如果
Syscall Number
位于0h
和FFFh
之间,则Table Identifier
为00h
。这种可以在ntdll.dll
上找到。与Native Function
相关。
后面会用到这里写的东西。
KiSystemServiceRepeat()
接下来是KiSystemServiceRepeat
.text:00000001401C4504 KiSystemServiceRepeat: ; CODE XREF: KiSystemCall64+8DE↓j
.text:00000001401C4504 lea r10, KeServiceDescriptorTable
.text:00000001401C450B lea r11, KeServiceDescriptorTableShadow
.text:00000001401C4512 test dword ptr [rbx+78h], 80h
.text:00000001401C4519 jz short loc_1401C452E
.text:00000001401C451B test dword ptr [rbx+78h], 200000h
.text:00000001401C4522 jz short loc_1401C452B
.text:00000001401C4524 lea r11, KeServiceDescriptorTableFilter
先加载了KeServiceDescriptorTable
和KeServiceDescriptorTableShadow
,统称为SDT(Service Descriptor Table)
SDT
包含4个SERVICE_DESCRIPTOR_TABLE
:
typedef struct tag_SERVICE_DESCRIPTOR_TABLE {
SYSTEM_SERVICE_TABLE item1;
SYSTEM_SERVICE_TABLE item2;
SYSTEM_SERVICE_TABLE item3;
SYSTEM_SERVICE_TABLE item4;
} SERVICE_DESCRIPTOR_TABLE;
SERVICE_DESCRIPTOR_TABLE
包含4个元素:
typedef struct tag_SYSTEM_SERVICE_TABLE {
PULONG ServiceTable;//
PULONG_PTR CounterTable;
ULONG_PTR ServiceLimit;//
PBYTE ArgumentTable;
} SYSTEM_SERVICE_TABLE;
SYSTEM_SERVICE_TABLE
由 4 个元素组成,但对我们来说只有 ServiceTable
和 ServiceLimit
有用
0: kd> dps nt!KeServiceDescriptorTable
fffff801`54e0b880 fffff801`54caf4c0 nt!KiServiceTable
fffff801`54e0b888 00000000`00000000
fffff801`54e0b890 00000000`000001cf
fffff801`54e0b898 fffff801`54cafc00 nt!KiArgumentTable
fffff801`54e0b8a0 00000000`00000000
fffff801`54e0b8a8 00000000`00000000
fffff801`54e0b8b0 00000000`00000000
fffff801`54e0b8b8 00000000`00000000
fffff801`54e0b8c0 fffff801`54a6f780 nt!KiBreakpointTrap
fffff801`54e0b8c8 fffff801`54a6fa80 nt!KiOverflowTrap
fffff801`54e0b8d0 fffff801`54a74100 nt!KiRaiseSecurityCheckFailure
fffff801`54e0b8d8 fffff801`54a74440 nt!KiRaiseAssertion
fffff801`54e0b8e0 fffff801`54a74780 nt!KiDebugServiceTrap
fffff801`54e0b8e8 fffff801`54a751c0 nt!KiSystemCall64
fffff801`54e0b8f0 fffff801`54a74d00 nt!KiSystemCall32
fffff801`54e0b8f8 00000000`00000000
0: kd> dps nt!KeServiceDescriptorTableShadow
fffff801`54df4980 fffff801`54caf4c0 nt!KiServiceTable
fffff801`54df4988 00000000`00000000
fffff801`54df4990 00000000`000001cf
fffff801`54df4998 fffff801`54cafc00 nt!KiArgumentTable
fffff801`54df49a0 ffffb34b`e904b000
fffff801`54df49a8 00000000`00000000
fffff801`54df49b0 00000000`000004da
fffff801`54df49b8 ffffb34b`e904c84c
fffff801`54df49c0 00000000`00111311
fffff801`54df49c8 00000000`00000000
fffff801`54df49d0 ffffffff`80000018
fffff801`54df49d8 00000000`00000000
fffff801`54df49e0 00000000`00000000
fffff801`54df49e8 00000000`00000000
fffff801`54df49f0 00000000`00000000
fffff801`54df49f8 00000000`00000000
图示:
之后test dword ptr [rbx+78h], 80h
,其中,KTHREAD偏移0x78的位置是:
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
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
ULONG RestrictedGuiThread:1; //0x78
ULONG VpBackingThread:1; //0x78
ULONG ThreadFlagsSpare2:1; //0x78
ULONG EtwStackTraceApcInserted:8; //0x78
};
volatile LONG ThreadFlags; //0x78
};
0x80
为检查 GuiThread
标志是否已设置,如果设置,检查test dword ptr [rbx+78h], 200000h
,之后巴拉巴拉…..但是第一次没有设置GuiThread
,所以走loc_1401C452E
:
.text:00000001401C4512 test dword ptr [rbx+78h], 80h
.text:00000001401C4519 jz short loc_1401C452E
........
.text:00000001401C452E cmp eax, [r10+rdi+10h]
.text:00000001401C4533 jnb loc_1401C4A65
.text:00000001401C4A65 loc_1401C4A65: ; CODE XREF: KiSystemCall64+373↑j
.text:00000001401C4A65 cmp edi, 20h ; ' '
.text:00000001401C4A68 jnz short loc_1401C4AC5
.text:00000001401C4A6A mov [rbp-80h], eax
.text:00000001401C4A6D mov [rbp-78h], rcx
.text:00000001401C4A71 mov [rbp-70h], rdx
.text:00000001401C4A75 mov [rbp-68h], r8
.text:00000001401C4A79 mov [rbp-60h], r9
.text:00000001401C4A7D call KiConvertToGuiThread
检查 eax
的值是否高于地址 [r10+rdi+10h]
处的值,其中
eax
=System Call Index
rdi
:KiSystemServiceStart()
中先前提取的Table Identifier
。值为0x20h
或0x00h
r10
包含KeServiceDescriptorTable
的地址
System Call Index
与 KeServiceDescriptorTable
中的内容进行了比较。
如果系统调用与 GUI 函数相关,则有 [KeServiceDescriptorTable+20h+10h]
,否则 [KeServiceDescriptorTable+00h+10h]
。
KeServiceDescriptorTable
为:
0: kd> dps nt!KeServiceDescriptorTable
fffff801`54e0b880 fffff801`54caf4c0 nt!KiServiceTable
fffff801`54e0b888 00000000`00000000
fffff801`54e0b890 00000000`000001cf <- [KeServiceDescriptorTable+00h+10h]
fffff801`54e0b898 fffff801`54cafc00 nt!KiArgumentTable
fffff801`54e0b8a0 00000000`00000000
fffff801`54e0b8a8 00000000`00000000
fffff801`54e0b8b0 00000000`00000000
fffff801`54e0b8b8 00000000`00000000
fffff801`54e0b8c0 fffff801`54a6f780 nt!KiBreakpointTrap
fffff801`54e0b8c8 fffff801`54a6fa80 nt!KiOverflowTrap
fffff801`54e0b8d0 fffff801`54a74100 nt!KiRaiseSecurityCheckFailure
fffff801`54e0b8d8 fffff801`54a74440 nt!KiRaiseAssertion
fffff801`54e0b8e0 fffff801`54a74780 nt!KiDebugServiceTrap
fffff801`54e0b8e8 fffff801`54a751c0 nt!KiSystemCall64
fffff801`54e0b8f0 fffff801`54a74d00 nt!KiSystemCall32
fffff801`54e0b8f8 00000000`00000000
对于 GUI 函数(表标识符位于 20h
),公式为 [KeServiceDescriptorTable+20h+10h]
-> [54e0b880+20h+10h]
-> [54e0b8b0]
。这个地址的值为 0h
。
但是,如果我们的系统调用是本机函数,则表标识符将为 00h
和公式 [KeServiceDescriptorTable+00h+10h]
-> [54e0b880+00h+10h]
-> [54e0b890]
。我们可以看到这个地址的值为 1CFh
。这个值是SYSTEM_SERVICE_TABLE.ServiceLimit
,表示 ServiceTable
中的元素个数。
typedef struct tag_SYSTEM_SERVICE_TABLE {
PULONG ServiceTable;
PULONG_PTR CounterTable;
ULONG_PTR ServiceLimit;
PBYTE ArgumentTable;
} SYSTEM_SERVICE_TABLE;
SYSTEM_SERVICE_TABLE
的 ServiceTable
项是 relative value address
(RVA) 的数组。该数组的每个元素都链接到一个内核例程函数。
所以,这个检查用来检查 System Call Index
是否有效。
如果是 NtUserSetMenu()
这样的 GUI 函数。它的 Sytem Call Index
是 496h
!因此检查将是 496h > 0h
并且我们的 System Call Index
将被视为无效(也称为跳转)!
如果是 NtQueryVirtualMemory()
,不是 GUI 函数。所以在这种情况下,检查将是 if 23h (the System call Index of the function) > 1CFh
。由于 23h
低于 1CFh
,因此不会进行跳转,并且将在此函数中继续执行。
但是,在 NtUserSetMenu()
的情况下, 496h > 00h
。所以这里跳到 loc_1401C4A65
。
假设我们处于 NtUserSetMenu()
的情况,并且这是该线程第一次处理 GUI 函数
.text:00000001401C4A65 loc_1401C4A65: ; CODE XREF: KiSystemCall64+373↑j
.text:00000001401C4A65 cmp edi, 20h ; ' '
.text:00000001401C4A68 jnz short loc_1401C4AC5
.text:00000001401C4A6A mov [rbp-80h], eax
.text:00000001401C4A6D mov [rbp-78h], rcx
.text:00000001401C4A71 mov [rbp-70h], rdx
.text:00000001401C4A75 mov [rbp-68h], r8
.text:00000001401C4A79 mov [rbp-60h], r9
.text:00000001401C4A7D call KiConvertToGuiThread
.text:00000001401C4A82 or eax, eax
.text:00000001401C4A84 mov eax, [rbp-80h]
.text:00000001401C4A87 mov rcx, [rbp-78h]
.text:00000001401C4A8B mov rdx, [rbp-70h]
.text:00000001401C4A8F mov r8, [rbp-68h]
.text:00000001401C4A93 mov r9, [rbp-60h]
.text:00000001401C4A97 mov [rbx+90h], rsp
.text:00000001401C4A9E jz KiSystemServiceRepeat
.text:00000001401C4AA4 lea rdi, xmmword_1405439A0
.text:00000001401C4AAB mov esi, [rdi+10h]
.text:00000001401C4AAE mov rdi, [rdi]
.text:00000001401C4AB1 cmp eax, esi
.text:00000001401C4AB3 jnb short loc_1401C4AC5
.text:00000001401C4AB5 lea rdi, [rdi+rsi*4]
.text:00000001401C4AB9 movsx eax, byte ptr [rdi+rax]
.text:00000001401C4ABD or eax, eax
.text:00000001401C4ABF jle KiSystemServiceExit
如果 edi
是 20h
,这意味着我们正在处理一个GUI函数,我们需要通过函数 KiConvertGuiThread
将当前线程转换为GUI线程,然后返回到 KiSystemServiceRepeat
但如果 edi
不是 20h
,则意味着我们的系统调用不是来自 GUI 函数,并且 System Call Index
超出了 ServiceTable
.因此,在最后一种情况下,例程基本上退出系统调用处理工作流程
处于 NtUserSetMenu()
的情况,当前的线程将转换为 GUI 线程,然后我们回到 KiSystemServiceRepeat()
的开头
用 test dword ptr [rbx+78h], 80h
检查的 GuiThread
标志将被设置,跳转并执行下面的指令
.text:00000001401C4504 KiSystemServiceRepeat: ; CODE XREF: KiSystemCall64+8DE↓j
.text:00000001401C4504 lea r10, KeServiceDescriptorTable
.text:00000001401C450B lea r11, KeServiceDescriptorTableShadow
.text:00000001401C4512 test dword ptr [rbx+78h], 80h
.text:00000001401C4519 jz short loc_1401C452E
.text:00000001401C451B test dword ptr [rbx+78h], 200000h
.text:00000001401C4522 jz short loc_1401C452B
.text:00000001401C4524 lea r11, KeServiceDescriptorTableFilter
0x200000是RestrictedGuiThread
,如果设置了此标志,则 r10
的值将是 KeServiceDescriptorTableFilter
的地址,否则它将是 KeServiceDescriptorTableShadow
因此,从这里看来,对于Native
函数,将使用 KeServiceDescriptorTable
,对于 GUI 函数,它将使用 KeServiceDescriptorTableFilter
或 KeServiceDescriptorTableShadow
在 Windows 10 之前,仅存在两个 Service Descriptor Table
。 KeServiceDescriptorTable
和 KeServiceDescriptorTableShadow
。
自 Windows 10 起,引入了 KeServiceDescriptorTableFilter
表。
由于内核处理的 GUI 函数通常是漏洞利用研究的目标,因此 Microsoft 决定引入这个新表来减少攻击面。这里有一篇 Google Project Zero 的文章:
https://googleprojectzero.blogspot.com/2016/11/breaking-chain.html
关于这张表的信息很少。在 Windows Internals Part 1
一书中可以找到的内容:
Filter Win32k System Call: This filters access to the Win32k kernel-mode subsystem driver only to certain API allowing simple GUI and Direct X access, mitigating many of the possible attacks, without completely disabling availability of the GUI/GDI Services.
过滤 Win32k 系统调用:这会过滤对 Win32k 内核模式子系统驱动程序的访问,仅允许某些 API 允许简单的 GUI 和 Direct X 访问,从而减轻许多可能的攻击,而不会完全禁用 GUI/GDI 服务的可用性。
This [filtering] is set through an internal process creation attribute flag, which can define one out of three possible sets of Win32k filters that are enabled. However, because the filter sets are hard-coded, this mitigation is reserved for Microsoft internal usage.
此[过滤]是通过内部进程创建属性标志设置的,该标志可以定义启用的三组可能的 Win32k 过滤器中的一组。但是,由于筛选器集是硬编码的,因此此缓解措施保留供 Microsoft 内部使用。
继续,如果是 GUI 函数,则会跳转到loc_1401C4A65
但是这次是:
.text:00000001401C452E loc_1401C452E: ; CODE XREF: KiSystemCall64+359↑j
.text:00000001401C452E cmp eax, [r10+rdi+10h]
.text:00000001401C4533 jnb loc_1401C4A65
.text:00000001401C4539 mov r10, [r10+rdi]
.text:00000001401C453D movsxd r11, dword ptr [r10+rax*4]
.text:00000001401C4541 mov rax, r11
.text:00000001401C4544 sar r11, 4
.text:00000001401C4548 add r10, r11
.text:00000001401C454B cmp edi, 20h ; ' '
.text:00000001401C454E jnz short loc_1401C45A0
.text:00000001401C4550 mov r11, [rbx+_KTHREAD.Teb]
根据标志 RestrictedGuiThread
的值, r10
的值现在是 KeServiceDescriptorTableFilter
或 KeServiceDescriptorTableShadow
的地址
为了简化,我们会说在这种情况下,我们的进程不受 Win32k filters
保护,因此使用 KeServiceDescriptorTableShadow
再看一下KeServiceDescriptorTableShadow
:
0: kd> dps nt!KeServiceDescriptorTableShadow
fffff801`54df4980 fffff801`54caf4c0 nt!KiServiceTable
fffff801`54df4988 00000000`00000000
fffff801`54df4990 00000000`000001cf
fffff801`54df4998 fffff801`54cafc00 nt!KiArgumentTable
fffff801`54df49a0 ffffb34b`e904b000
fffff801`54df49a8 00000000`00000000
fffff801`54df49b0 00000000`000004da <- [KeServiceDescriptorTableShadow+20h+10h]
fffff801`54df49b8 ffffb34b`e904c84c
fffff801`54df49c0 00000000`00111311
fffff801`54df49c8 00000000`00000000
fffff801`54df49d0 ffffffff`80000018
fffff801`54df49d8 00000000`00000000
fffff801`54df49e0 00000000`00000000
fffff801`54df49e8 00000000`00000000
fffff801`54df49f0 00000000`00000000
fffff801`54df49f8 00000000`00000000
[r10+rdi+10h]
-> [KeServiceDescriptorTableShadow+20h+10h]
-> [54df4980+20h+10h]
-> [54df49b0]
。我们可以看到这个地址的值为 4DAh
NtUserSetMenu
的syscall number
是 0x496 < 0x4da
其中
r10
=KeServiceDescriptorTableShadow
或者KeServiceDescriptorTable
rdi
=TI
rax
=syscall number
mov r10, [r10+rdi]
// For NtQueryVirtualMemory() -> [KeServiceDescriptorTable+00h]
// For NtUserSetMenu() -> [KeServiceDescriptorTableShadow+20h]
如果是NtQueryVirtualMemory
的话,r10 就是 KiServiceTable
的地址:
0: kd> dps nt!KeServiceDescriptorTable
fffff801`54e0b880 fffff801`54caf4c0 nt!KiServiceTable <- [KeServiceDescriptorTable+00h]
fffff801`54e0b888 00000000`00000000
fffff801`54e0b890 00000000`000001cf
fffff801`54e0b898 fffff801`54cafc00 nt!KiArgumentTable
如果是NtUserSetMenu
的话,r10 就是 W32pServiceTable
的地址:
0: kd> dps nt!KeServiceDescriptorTableShadow
fffff801`54df4980 fffff801`54caf4c0 nt!KiServiceTable
fffff801`54df4988 00000000`00000000
fffff801`54df4990 00000000`000001cf
fffff801`54df4998 fffff801`54cafc00 nt!KiArgumentTable
fffff801`54df49a0 ffffb34b`e904b000 win32k!W32pServiceTable <- [KeServiceDescriptorTableShadow+20h]
fffff801`54df49a8 00000000`00000000
fffff801`54df49b0 00000000`000004da
fffff801`54df49b8 ffffb34b`e904c84c win32k!W32pArgumentTable
fffff801`54df49c0 00000000`00111311
fffff801`54df49c8 00000000`00000000
fffff801`54df49d0 ffffffff`80000018
在你想要加载的那个模块, .reload /f xxx.dll 。 .reload
符号 nt!
表示该地址位于模块 ntoskrnl.exe
中。 win32k!
表示该地址位于驱动程序 win32k.sys
中。
在这里我们可以明确地看到 GUI 函数和 Native 函数根本不在内核的同一部分。
继续:
movsxd r11, dword ptr [r10+rax*4]
// For NtQueryVirtualMemory() -> [nt!KiServiceTable + 23h*4h]
// For NtUserSetMenu() -> [win32k!W32pServiceTable + 496h*4h]
这里 r11
将存储 Service Table
中的值。 Service Table
也称为 System Service Dispatch Table
(SSDT)。
movsxd
是一个mov
,它允许在将较小的寄存器复制到 64 位寄存器时保留符号扩展。这是通过用符号扩展填充额外的位来完成的
Service Table
是与内核例程相关的 RVA
数组
对于NtQueryVirtualMemory
:
0: kd> dd /c1 nt!KiServiceTable + 4*0x23 L1
fffff801`54caf54c 02950d02
对于NtUserSetMenu
:
1: kd> dd /c1 W32pServiceTable + 4*0x496 L1
ffffb34b`e904c258 ff9a8ca0
乘4是因为 数组中的每一项都是 4 个字节长
继续:
mov rax, r11
sar r11, 4 // 右移4位
RVA保存到rax
.
对于 NtQueryVirtualMemory()
的情况,在 RVA
02950d02
上向右算术移位 4 位将得到 02950d0
对于 NtUserSetMenu()
的情况,在 RVA
ff9a8ca0h
上向右移动 4 位将得到 0xFFF9A8CA
右移4位是因为 SSDT
中检索到的数据实际上包含2个东西。
最右面 4 位是使用堆栈传递的参数数量,其余的是我们的 RVA。因此,通过右移 4 位可以检索 RVA
的实际值。
NtQueryVirtualMemory
:02953402
,两个参数NtUserSetMenu
:ff9a8ca0
,0个参数
但是 NtQueryVirtualMemory
的参数其实是6个:
NtQueryVirtualMemory(
HANDLE ProcessHandle,
PVOID BaseAddress,
MEMORY_INFORMATION_CLASS MemoryInformationClass,
PVOID Buffer,
ULONG Length,
PULONG ResultLength
);
在 32 位中,函数的所有参数都是使用堆栈传递的。在 Windows 64 位系统中,前 4 个参数按以下顺序使用以下寄存器传递:
RCX
;RDX
;R8
;R9
.
NtUserSetMenu()
使用 3 个参数,因此将使用 RCX
、 RDX
和 R8
寄存器传递它们。这里不会涉及堆栈,因此 ff9a8ca0
中使用堆栈传递 0 个参数。
NtQueryVirtualMemory()
使用 6 个参数,前 4 个参数将使用 RCX
、 RDX
、 R8
和 R9
中的 2 个参数使用堆栈传递
将 RVA
添加到 System Service Dispatch Table
的地址中。
add r10, r11 // r10 = SSDT, r11 = RVA
来看看这个地址有什么。
对于 NtQueryVirtualMemory()
这里有问题,,,得改改()
1: kd> u nt!KiServiceTable + 00295340 L1
nt!MmQueryVirtualMemory+0x240:
fffff801`54f44800 2440 and al,40h
对于NtUserSetMenu()
:
1: kd> u W32pServiceTable + (-65736) L1
win32k!NtUserSetMenu:
ffffb34b`e8fe58ca 48ff2587730500 jmp qword ptr [win32k!_imp_NtUserSetMenu (ffffb34b`e903cc58)]
在内核中找到了函数的地址!!!
继续来看NtUserSetMenu
,但这次是在 KeServiceDescriptorTableFilter
表中
1: kd> .reload /f win32k.sys
1: kd> dd /c1 win32k!W32pServiceTableFilter+4*0x496 L1
ffffb34b`e904df88 ffd2b100
1: kd> ? (ffd2b100>>>4)
Evaluate expression: 268249872 = 00000000`0ffd2b10
1: kd> dd /c1 win32k!W32pServiceTableFilter+ffffffff`fffd2b10 L1
ffffb34b`e901f840 48ec8348
1: kd> u win32k!W32pServiceTableFilter+ffffffff`fffd2b10 L1
win32k!stub_UserSetMenu:
ffffb34b`e901f840 4883ec48 sub rsp,48h
这里的函数名称不同。该函数称为 stub_UserSetMenu
而不是 NtUserSetMenu
可以在 Windows Internals Part 2
书中找到答案。
The only material difference between the Filter entries is that they point to system calls in Win32k.sys with names like stub_UserGetThreadState, while the real array [SSDT not filtered] points to NtUserGetThreadState. The former stubs will check if Win32k.sys filtering enabled for this system call, based, in part, on the filter set that’s been loaded for the process.
Filter 条目之间唯一的实质性区别是它们指向 Win32k.sys 中的系统调用,其名称类似于 Stub_UserGetThreadState,而实际数组 [SSDT 未过滤] 指向 NtUserGetThreadState。前一个存根将检查是否为此系统调用启用了 Win32k.sys 过滤,部分基于已为进程加载的过滤器集。
Based on this determination, they will either fail the call and return STATUS_INVALID_SYSTEM_SERVICE if the filter set prohibits it or end up calling the original function (such as NtUserGetThreadState), with potential telemetry if auditing is enabled.
基于此确定,如果过滤器集禁止调用,它们将导致调用失败并返回 STATUS_INVALID_SYSTEM_SERVICE,或者最终调用原始函数(例如 NtUserGetThreadState),如果启用了审核,则可能会进行遥测。
KiSystemServiceCopyEnd()
之后执行KiSystemServiceGdiTebAccess
.text:00000001401C4557 KiSystemServiceGdiTebAccess: ; DATA XREF: KiSystemServiceHandler+D↑o
.text:00000001401C4557 cmp dword ptr [r11+1740h], 0
.text:00000001401C455F jz short loc_1401C45A0
.text:00000001401C4561 mov [rbp-50h], rax
.text:00000001401C4565 mov [rbp-48h], rcx
.text:00000001401C4569 mov [rbp-40h], rdx
.text:00000001401C456D mov rbx, r8
.text:00000001401C4570 mov rdi, r9
.text:00000001401C4573 mov rsi, r10
.text:00000001401C4576 mov ecx, 7
.text:00000001401C457B xor edx, edx
.text:00000001401C457D xor r8, r8
.text:00000001401C4580 xor r9, r9
.text:00000001401C4583 call PsInvokeWin32Callout
.text:00000001401C4588 mov rax, [rbp-50h]
.text:00000001401C458C mov rcx, [rbp-48h]
.text:00000001401C4590 mov rdx, [rbp-40h]
.text:00000001401C4594 mov r8, rbx
.text:00000001401C4597 mov r9, rdi
.text:00000001401C459A mov r10, rsi
.text:00000001401C459D nop dword ptr [rax]
.text:00000001401C45A0
.text:00000001401C45A0 loc_1401C45A0: ; CODE XREF: KiSystemCall64+38E↑j
.text:00000001401C45A0 ; KiSystemCall64+39F↑j
.text:00000001401C45A0 and eax, 0Fh
.text:00000001401C45A3 jz KiSystemServiceCopyEnd
再之后是KiSystemServiceCopyEnd
.text:00000001401C4660 KiSystemServiceCopyEnd: ; CODE XREF: KiSystemCall64+3E3↑j
.text:00000001401C4660 ; DATA XREF: KiSystemServiceHandler+27↑o ...
.text:00000001401C4660 test cs:KiDynamicTraceMask, 1
.text:00000001401C466A jnz loc_1401C4B03
.text:00000001401C4670 test dword ptr cs:PerfGlobalGroupMask+8, 40h
.text:00000001401C467A jnz loc_1401C4B77
.text:00000001401C4680 mov rax, r10
.text:00000001401C4683 call rax
这里有个 call rax
,rax
来自r10
,r10
保存的是内核函数的具体地址,至此,syscall
的流程就走完了
**Security Note**: 安全注意事项:
At this point you may think “Ok, so I just have to make a malicious driver and place hooks on the `LSTAR` register or in the `SSDT` to hijack the kernel workflow”. Well, there was a time when this was possible. Actually, it was a way for EDR or AV to perform analysis (like today with hooks in DLLs).
此时您可能会想“好吧,所以我只需制作一个恶意驱动程序并在 `LSTAR` 寄存器或 `SSDT` 寄存器或 `SSDT` 中放置挂钩来劫持内核工作流程”。嗯,曾经有一段时间这是可能的。实际上,这是 EDR 或 AV 执行分析的一种方式(就像今天在 DLL 中使用钩子一样)。
However, it’s not possible anymore. To prevent this kind of change, Microsoft invented the `Kernel Patch Protection` (KPP) also known as `Patch Guard` (PG). With `KPP`, any attempt to patch the `SSDT` or `LSTAR` will lead to a a `BSOD` (blue screen of death).
然而,这已经不可能了。为了防止这种变化,微软发明了 `Kernel Patch Protection` (KPP)也称为 `Patch Guard` (PG)。对于 `KPP` ,任何修补 `SSDT` 或 `LSTAR` 的尝试都将导致 `BSOD` (蓝屏死机)。
Security Note:
此时可能会想:好吧,所以我只需制作一个恶意驱动程序并在 LSTAR
寄存器或 SSDT
寄存器或 SSDT
中放置挂钩来劫持内核工作流程”。嗯,曾经有一段时间这是可能的。实际上,这是 EDR 或 AV 执行分析的一种方式(就像今天在 DLL 中使用钩子一样)。
然而,这已经不可能了。为了防止这种变化,微软发明了 Kernel Patch Protection
(KPP)也称为 Patch Guard
(PG)。对于 KPP
,任何修补 SSDT
或 LSTAR
的尝试都将导致 BSOD
(蓝屏死机)。
ref:
https://www.codeproject.com/Articles/1191465/The-Quest-for-the-SSDTs