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 寄存器中

图示:

Pasted-image-20230629170115

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

Pasted-image-20230630163830

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 = FirstArgumenteax = 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

Pasted-image-20230630170058

这里以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

先加载了KeServiceDescriptorTableKeServiceDescriptorTableShadow,统称为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

图示:

Pasted-image-20230630171806

之后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
  • rdiKiSystemServiceStart() 中先前提取的 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 函数,它将使用 KeServiceDescriptorTableFilterKeServiceDescriptorTableShadow

在 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

Pasted-image-20230702142244

但是这次是:

.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

NtUserSetMenusyscall number0x496 < 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 (S​​SDT)。

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 个字节长

Pasted-image-20230702203238

继续:

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 的实际值。

  • NtQueryVirtualMemory02953402,两个参数
  • NtUserSetMenuff9a8ca0,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)]

在内核中找到了函数的地址!!!

Pasted-image-20230702211312

继续来看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 raxrax来自r10r10保存的是内核函数的具体地址,至此,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 ,任何修补 SSDTLSTAR 的尝试都将导致 BSOD (蓝屏死机)。

ref:

https://alice.climent-pommeret.red/posts/a-syscall-journey-in-the-windows-kernel/#the-execution-of-the-kernel-routine-via-kisystemservicecopyend

https://www.codeproject.com/Articles/1191465/The-Quest-for-the-SSDTs

https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/glimpse-into-ssdt-in-windows-x64-kernel

0%