Watch & Learn

Debugwar Blog

Step in or Step over, this is a problem ...

Windows 11 SSDT & ShadowSSDT Address Acquisition Problem

2022-10-31 @ UTC+0

Recently, a project required traversing SSDT and ShadowSSDT. The conventional way to obtain the starting addresses of these two tables is to use the readmsr(0xc0000082) method found online. This works fine from Windows 7 to Windows 10, but it was found that it does not work properly under Windows 11. There were no relevant discussions found online, so a simple study was conducted, resulting in this article.

Conventional Acquisition Method

The conventional method found online is to use the readmsr instruction to get the content of register 0xC0000082. The first result from a Google search is a tweet (reference 1):


The corresponding C code is roughly as follows:

  1. PKSERVICE_TABLE_DESCRIPTOR GetKeServiceDescriptorTableAddress(SEARCH_TABLE_TYPE SearchType)  
  2. {  
  3.     PULONG_PTR KiSystemCall64 = NULL;  
  4.     PUCHAR CurrentAddr = NULL;  
  5.   
  6.     KiSystemCall64 = (PVOID)__readmsr(0xC0000082);  
  7.     for (CurrentAddr = KiSystemCall64; CurrentAddr <= KiSystemCall64 + 1024; ++CurrentAddr)  
  8.     {  
  9.         switch (SearchType)  
  10.         {  
  11.           case KeServiceDescriptorTableType:  
  12.             if (0x4c == *CurrentAddr && 0x8d == *(PUCHAR)(CurrentAddr + 1) && 0x15 == *(PUCHAR)(CurrentAddr + 2))  
  13.             {  
  14.               UHALF_PTR Offset = *(PUHALF_PTR)(CurrentAddr + 3);  
  15.               ULONG_PTR Base = CurrentAddr + 7;  
  16.               return X64_ADDRESS_ADD(Base, Offset);  
  17.             }  
  18.           break;  
  19.           case KeServiceDescriptorTableShadowType:  
  20.             if (0x4c == *CurrentAddr && 0x8d == *(PUCHAR)(CurrentAddr + 1) && 0x1d == *(PUCHAR)(CurrentAddr + 2))  
  21.             {  
  22.               UHALF_PTR Offset = *(PUHALF_PTR)(CurrentAddr + 3);  
  23.               ULONG_PTR Base = CurrentAddr + 7;  
  24.               return X64_ADDRESS_ADD(Base, Offset);  
  25.             }  
  26.           break;  
  27.         }  
  28.     }  
  29. }  

As mentioned at the beginning of this article, this code works fine on systems below Windows 10. It can correctly obtain the addresses of KeServiceDescriptor and KeServiceDescriptorShadow tables, but it does not work properly under Windows 11.

Problem Exploration

Let’s first look at what value the readmsr instruction gets under Windows 11:

  1. 0: kd> vertarget  
  2. Windows 10 Kernel Version 22000 MP (2 procs) Free x64  
  3. Product: WinNt, suite: TerminalServer SingleUserTS  
  4. Edition build lab: 22000.1.amd64fre.co_release.210604-1628  
  5. Machine Name:  
  6. Kernel base = 0xfffff803`14a00000 PsLoadedModuleList = 0xfffff803`15629710  
  7. Debug session time: Mon Oct 31 12:30:14.915 2022 (UTC + 8:00)  
  8. System Uptime: 0 days 0:17:39.229  
  9. 0: kd> rdmsr 0xc0000082  
  10. msr[c0000082] = fffff803`154b4180  
  11. 0: kd> ln fffff803`154b4180  
  12. Browse module  
  13. Set bu breakpoint  
  14.   
  15. (fffff803`154b4180)   nt!KiSystemCall64Shadow   |  (fffff803`154b5060)   nt!_guard_retpoline_icall_handler  
  16. Exact matches:  

So we can confirm two points: 
  1. Although Windows 11 is called 11, it still uses the Windows 10 kernel, confirming that it is a re-skinned operating system.
  2. Under Windows 11, the MSR[0xC0000082] register stores KiSystemCall64Shadow, while before Windows 11, it always stored KiSystemCall64. 
The second point mentioned above is the direct reason why we cannot obtain the starting address of KeServiceDescriptor or KeServiceDescriptorShadow table through MSR[0xC0000082] under Windows 11. So, can we really not stably obtain the addresses of these two tables? The answer is no ~

Problem Solution

Let’s first take a look at some basic information of a few key data structures:

  1. 0: kd> rdmsr 0xc0000082  
  2. msr[c0000082] = fffff803`154b4180  
  3. 0: kd> ln fffff803`154b4180  
  4. Browse module  
  5. Set bu breakpoint  
  6.   
  7. (fffff803`154b4180)   nt!KiSystemCall64Shadow   |  (fffff803`154b5060)   nt!_guard_retpoline_icall_handler  
  8. Exact matches:  
  9. 0: kd> x nt!KiSystemCall64  
  10. fffff803`14e24bc0 nt!KiSystemCall64 (KiSystemCall64)  
  11. 0: kd> uf nt!KiSystemCall64Shadow  
  12. Flow analysis was incomplete, some code may be missing  
  13. nt!KiSystemServiceUser:  
  14. fffff803`14e24e1a c645ab02        mov     byte ptr [rbp-55h],2  
  15. fffff803`14e24e1e 65488b1c2588010000 mov   rbx,qword ptr gs:[188h]  
  16. fffff803`14e24e27 0f0d8b90000000  prefetchw [rbx+90h]  
  17. fffff803`14e24e2e 0fae5dac        stmxcsr dword ptr [rbp-54h]  
  18. ......  
  19. nt!KiSystemServiceRepeat:  
  20. fffff803`14e24ef4 4c8d15c5c99d00  lea     r10,[nt!KeServiceDescriptorTable (fffff803`158018c0)]  
  21. fffff803`14e24efb 4c8d1dfe208e00  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff803`15707000)]  
  22. fffff803`14e24f02 f7437880000000  test    dword ptr [rbx+78h],80h  
  23. ......  
  24. nt!KiSystemCall64Shadow+0x24c:  
  25. fffff803`154b43cc 0faee8          lfence  
  26.   
  27. nt!KiSystemCall64Shadow+0x24f:  
  28. fffff803`154b43cf 65c604255308000000 mov   byte ptr gs:[853h],0  
  29. fffff803`154b43d8 e93d0a97ff      jmp     nt!KiSystemServiceUser (fffff803`14e24e1a)  Branch  
  30. 0: kd> uf nt!KiSystemCall64  
  31. Flow analysis was incomplete, some code may be missing  
  32. nt!KiSystemCall64:  
  33. fffff803`14e24bc0 0f01f8          swapgs  
  34. fffff803`14e24bc3 654889242510000000 mov   qword ptr gs:[10h],rsp  
  35. fffff803`14e24bcc 65488b2425a8010000 mov   rsp,qword ptr gs:[1A8h]  
  36. ......  
  37. fffff803`14e24ef4 4c8d15c5c99d00  lea     r10,[nt!KeServiceDescriptorTable (fffff803`158018c0)]  
  38. fffff803`14e24efb 4c8d1dfe208e00  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff803`15707000)]  
  39. fffff803`14e24f02 f7437880000000  test    dword ptr [rbx+78h],80h  
  40. fffff803`14e24f09 7413            je      nt!KiSystemServiceRepeat+0x2a (fffff803`14e24f1e)  Branch  
  41. ......  
  42. fffff803`154b3e19 0f01f8 swapgs
  43. fffff803`154b3e1c 480f07 sysretq

We can see that at the end of the KiSystemCall64Shadow function, there is a very uncommon instruction: lfence. By querying this instruction, we know that its OP Code is a fixed value: 0x0faee8, as shown in the following figure (reference 2).


Not far after lfence, there is a je instruction, which jumps to: nt!KiSystemServiceUser (fffff803`14e24e1a).

Through windbg output, we can know the second important information: the address range of nt!KiSystemCall64 is from fffff80314e24bc0 to fffff803154b3e1c.

OK, at this point, we find that the last jump of KiSystemCall64Shadow will jump to a place not far from the beginning of KiSystemCall64 to continue execution. The traditional method of obtaining KeServiceDescriptorTable and KeServiceDescriptorTableShadow is actually to search for feature codes within the KiSystemCall64 function.

Therefore, we can locate the je instruction below based on the lfence instruction, calculate the jump address, and then use the traditional method of searching the KiSystemCall64 function to obtain the starting address of the table structure we need. 

Coding Time

Without further ado, let’s slightly modify the code at the beginning of this article:

  1. PKSERVICE_TABLE_DESCRIPTOR GetKeServiceDescriptorTableAddress(SEARCH_TABLE_TYPE SearchType)  
  2. {  
  3.   PULONG_PTR KiSystemCall64 = NULL;  
  4.   PUCHAR CurrentAddr = NULL;  
  5.   
  6.   KiSystemCall64 = (PVOID)__readmsr(0xC0000082);  
  7.   for (CurrentAddr = KiSystemCall64; CurrentAddr <= KiSystemCall64 + 1024; ++CurrentAddr)  
  8.   {  
  9.     switch (SearchType)  
  10.     {  
  11.       case KeServiceDescriptorTableType:  
  12.         if (0x4c == *CurrentAddr && 0x8d == *(PUCHAR)(CurrentAddr + 1) && 0x15 == *(PUCHAR)(CurrentAddr + 2))  
  13.         {  
  14.           UHALF_PTR Offset = *(PUHALF_PTR)(CurrentAddr + 3);  
  15.           ULONG_PTR Base = CurrentAddr + 7;  
  16.           return X64_ADDRESS_ADD(Base, Offset);  
  17.         }  
  18.       break;  
  19.       case KeServiceDescriptorTableShadowType:  
  20.         if (0x4c == *CurrentAddr && 0x8d == *(PUCHAR)(CurrentAddr + 1) && 0x1d == *(PUCHAR)(CurrentAddr + 2))  
  21.         {  
  22.           UHALF_PTR Offset = *(PUHALF_PTR)(CurrentAddr + 3);  
  23.           ULONG_PTR Base = CurrentAddr + 7;  
  24.           return X64_ADDRESS_ADD(Base, Offset);  
  25.         }  
  26.       break;  
  27.     }  
  28.     // Windows 11, msr[0xC0000082] = nt!KiSystemCall64Shadow  
  29.     // We need search back  
  30.     //  fffff803`154b43c8 4883c408        add     rsp,8  
  31.     //  fffff803`154b43cc 0faee8          lfence  <-- all of these bytes  
  32.     //  fffff803`154b43cf 65c604255308000000 mov   byte ptr gs:[853h],0 <-- last byte  
  33.     //  fffff803`154b43d8 e93d0a97ff      jmp     nt!KiSystemServiceUser (fffff803`14e24e1a)  Branch <-- first 1 byte  
  34.     if (0x00e8ae0f == (UHALF_PTR)((*(PUHALF_PTR)CurrentAddr) & 0x00FFFFFF))  
  35.     {  
  36.       PUCHAR j = NULL;  
  37.       for (j = CurrentAddr; j < CurrentAddr + 100; ++j)  
  38.       {  
  39.         if (0xe9 == *j && 0x00 == *(j - 1))  
  40.         {  
  41.           UHALF_PTR Offset = *(PUHALF_PTR)(j + 1);  
  42.           CurrentAddr = X64_ADDRESS_ADD((ULONG_PTR)j, Offset);  
  43.           KiSystemCall64 = CurrentAddr; 
  44.           break; 
  45.         }  
  46.       }  
  47.     }  
  48.   }  
  49. }  

Verify the result, reload ntoskrnl.exe, and then verify whether the SSDT table is consistent before and after reloading:


No problem, job done~

Reference

  1. For the night is dark and full of shadows.
  2. LFENCE —— Load Fence
Catalog
Conventional Acquisition Method
Problem Exploration
Problem Solution
Coding Time
Reference

CopyRight (c) 2020 - 2025 Debugwar.com

Designed by Hacksign