
近期有一个项目中需要遍历SSDT与ShadowSSDT,按照网上常规的readmsr(0xc0000082)方式获取这两张表的起始地址,Windows 7到Windows 10均无问题,但是发现Windows 11下无法正常获取。网上搜索没有任何相关的讨论,于是便简单研究了一下,有了此文。
常规获取方式
网上常规的获取方式为使用readmsr指令获取寄存器0xC0000082的内容,google搜索排名第一的是一篇推文参考1:

对应的C代码大致如下:
- PKSERVICE_TABLE_DESCRIPTOR GetKeServiceDescriptorTableAddress(SEARCH_TABLE_TYPE SearchType)
- {
- PULONG_PTR KiSystemCall64 = NULL;
- PUCHAR CurrentAddr = NULL;
- KiSystemCall64 = (PVOID)__readmsr(0xC0000082);
- for (CurrentAddr = KiSystemCall64; CurrentAddr <= KiSystemCall64 + 1024; ++CurrentAddr)
- {
- switch (SearchType)
- {
- case KeServiceDescriptorTableType:
- if (0x4c == *CurrentAddr && 0x8d == *(PUCHAR)(CurrentAddr + 1) && 0x15 == *(PUCHAR)(CurrentAddr + 2))
- {
- UHALF_PTR Offset = *(PUHALF_PTR)(CurrentAddr + 3);
- ULONG_PTR Base = CurrentAddr + 7;
- return X64_ADDRESS_ADD(Base, Offset);
- }
- break;
- case KeServiceDescriptorTableShadowType:
- if (0x4c == *CurrentAddr && 0x8d == *(PUCHAR)(CurrentAddr + 1) && 0x1d == *(PUCHAR)(CurrentAddr + 2))
- {
- UHALF_PTR Offset = *(PUHALF_PTR)(CurrentAddr + 3);
- ULONG_PTR Base = CurrentAddr + 7;
- return X64_ADDRESS_ADD(Base, Offset);
- }
- break;
- }
- }
- }
如本文一开始所述,这段代码在Windows 10以下的系统上是没问题的, 可以正确获取到KeServiceDescriptor与KeServiceDescriptorShaodow表的地址,但是在Windows 11下无法正常工作。
问题探究
我们先看一下,Windows 11下readmsr指令获取到的值是什么:
- 0: kd> vertarget
- Windows 10 Kernel Version 22000 MP (2 procs) Free x64
- Product: WinNt, suite: TerminalServer SingleUserTS
- Edition build lab: 22000.1.amd64fre.co_release.210604-1628
- Machine Name:
- Kernel base = 0xfffff803`14a00000 PsLoadedModuleList = 0xfffff803`15629710
- Debug session time: Mon Oct 31 12:30:14.915 2022 (UTC + 8:00)
- System Uptime: 0 days 0:17:39.229
- 0: kd> rdmsr 0xc0000082
- msr[c0000082] = fffff803`154b4180
- 0: kd> ln fffff803`154b4180
- Browse module
- Set bu breakpoint
- (fffff803`154b4180) nt!KiSystemCall64Shadow | (fffff803`154b5060) nt!_guard_retpoline_icall_handler
- Exact matches:
所以可以确定两点:
- Windows 11虽然叫11,但是依然使用的是Windows 10的内核,换皮操作系统实锤
- Windows 11下MSR[0xC0000082]寄存器中存储的是KiSystemCall64Shadow,而Windows 11之前这里一直都是存放的KiSystemCall64
上面说的第二点,即为在Windows 11下无法通过MSR[0xC0000082]获取KeServiceDescriptor或KeServiceDescriptorShaodow表首地址的直接原因,那么我们真的就无法稳定获取上述两张表的地址了吗?非也~
问题解决
让我们先来看一下几个关键数据结构的基本信息:
- 0: kd> rdmsr 0xc0000082
- msr[c0000082] = fffff803`154b4180
- 0: kd> ln fffff803`154b4180
- Browse module
- Set bu breakpoint
- (fffff803`154b4180) nt!KiSystemCall64Shadow | (fffff803`154b5060) nt!_guard_retpoline_icall_handler
- Exact matches:
- 0: kd> x nt!KiSystemCall64
- fffff803`14e24bc0 nt!KiSystemCall64 (KiSystemCall64)
- 0: kd> uf nt!KiSystemCall64Shadow
- Flow analysis was incomplete, some code may be missing
- nt!KiSystemServiceUser:
- fffff803`14e24e1a c645ab02 mov byte ptr [rbp-55h],2
- fffff803`14e24e1e 65488b1c2588010000 mov rbx,qword ptr gs:[188h]
- fffff803`14e24e27 0f0d8b90000000 prefetchw [rbx+90h]
- fffff803`14e24e2e 0fae5dac stmxcsr dword ptr [rbp-54h]
- ......
- nt!KiSystemServiceRepeat:
- fffff803`14e24ef4 4c8d15c5c99d00 lea r10,[nt!KeServiceDescriptorTable (fffff803`158018c0)]
- fffff803`14e24efb 4c8d1dfe208e00 lea r11,[nt!KeServiceDescriptorTableShadow (fffff803`15707000)]
- fffff803`14e24f02 f7437880000000 test dword ptr [rbx+78h],80h
- ......
- nt!KiSystemCall64Shadow+0x24c:
- fffff803`154b43cc 0faee8 lfence
- nt!KiSystemCall64Shadow+0x24f:
- fffff803`154b43cf 65c604255308000000 mov byte ptr gs:[853h],0
- fffff803`154b43d8 e93d0a97ff jmp nt!KiSystemServiceUser (fffff803`14e24e1a) Branch
- 0: kd> uf nt!KiSystemCall64
- Flow analysis was incomplete, some code may be missing
- nt!KiSystemCall64:
- fffff803`14e24bc0 0f01f8 swapgs
- fffff803`14e24bc3 654889242510000000 mov qword ptr gs:[10h],rsp
- fffff803`14e24bcc 65488b2425a8010000 mov rsp,qword ptr gs:[1A8h]
- ......
- fffff803`14e24ef4 4c8d15c5c99d00 lea r10,[nt!KeServiceDescriptorTable (fffff803`158018c0)]
- fffff803`14e24efb 4c8d1dfe208e00 lea r11,[nt!KeServiceDescriptorTableShadow (fffff803`15707000)]
- fffff803`14e24f02 f7437880000000 test dword ptr [rbx+78h],80h
- fffff803`14e24f09 7413 je nt!KiSystemServiceRepeat+0x2a (fffff803`14e24f1e) Branch
- ......
- fffff803`154b3e19 0f01f8 swapgs
- fffff803`154b3e1c 480f07 sysretq
可以看到,在KiSystemCall64Shadow函数的最后,出现了一个非常不常见的指令:lfence。而通过查询该指令得知,其OP Code是一个固定值:0x0faee8,如下图参考2。

而在lfence之后的不远处紧跟一条je跳转指令,该指令跳转到:nt!KiSystemServiceUser (fffff803`14e24e1a)。
通过windbg输出,我们可以知道第二个重要信息:nt!KiSystemCall64的地址范围为fffff803`14e24bc0至fffff803`154b3e1c。
OK,到此我们发现KiSystemCall64Shadow最后的跳转,正好会跳转到KiSystemCall64开始的不远处继续执行,而传统获取KeServiceDescriptorTable和KeServiceDescriptorTableShadow的方法,其实就是在KiSystemCall64函数内搜索特征码。
因此,我们可以根据lfence指令定位下面的je跳转,计算出跳转地址后,即可以利用传统搜索KiSystemCall64函数的方式获取我们需要的表结构起始地址了。
Coding Time
废话不多说, 稍微改造一下本文一开始的代码:
- PKSERVICE_TABLE_DESCRIPTOR GetKeServiceDescriptorTableAddress(SEARCH_TABLE_TYPE SearchType)
- {
- PULONG_PTR KiSystemCall64 = NULL;
- PUCHAR CurrentAddr = NULL;
- KiSystemCall64 = (PVOID)__readmsr(0xC0000082);
- for (CurrentAddr = KiSystemCall64; CurrentAddr <= KiSystemCall64 + 1024; ++CurrentAddr)
- {
- switch (SearchType)
- {
- case KeServiceDescriptorTableType:
- if (0x4c == *CurrentAddr && 0x8d == *(PUCHAR)(CurrentAddr + 1) && 0x15 == *(PUCHAR)(CurrentAddr + 2))
- {
- UHALF_PTR Offset = *(PUHALF_PTR)(CurrentAddr + 3);
- ULONG_PTR Base = CurrentAddr + 7;
- return X64_ADDRESS_ADD(Base, Offset);
- }
- break;
- case KeServiceDescriptorTableShadowType:
- if (0x4c == *CurrentAddr && 0x8d == *(PUCHAR)(CurrentAddr + 1) && 0x1d == *(PUCHAR)(CurrentAddr + 2))
- {
- UHALF_PTR Offset = *(PUHALF_PTR)(CurrentAddr + 3);
- ULONG_PTR Base = CurrentAddr + 7;
- return X64_ADDRESS_ADD(Base, Offset);
- }
- break;
- }
- // Windows 11, msr[0xC0000082] = nt!KiSystemCall64Shadow
- // We need search back
- // fffff803`154b43c8 4883c408 add rsp,8
- // fffff803`154b43cc 0faee8 lfence <-- all of these bytes
- // fffff803`154b43cf 65c604255308000000 mov byte ptr gs:[853h],0 <-- last byte
- // fffff803`154b43d8 e93d0a97ff jmp nt!KiSystemServiceUser (fffff803`14e24e1a) Branch <-- first 1 byte
- if (0x00e8ae0f == (UHALF_PTR)((*(PUHALF_PTR)CurrentAddr) & 0x00FFFFFF))
- {
- PUCHAR j = NULL;
- for (j = CurrentAddr; j < CurrentAddr + 100; ++j)
- {
- if (0xe9 == *j && 0x00 == *(j - 1))
- {
- UHALF_PTR Offset = *(PUHALF_PTR)(j + 1);
- CurrentAddr = X64_ADDRESS_ADD((ULONG_PTR)j, Offset);
- KiSystemCall64 = CurrentAddr;
- break;
- }
- }
- }
- }
- }
验证一下结果,重载ntoskrnl.exe,然后验证重载前后的SSDT表是否一致:

没有问题, 打完收工~