Watch & Learn

Debugwar Blog

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

Traversal and Hook Recovery of SSDT & ShadowSSDT

2022-11-15 @ UTC+0

The previous blog post discussed the issue of obtaining SSDT and ShadowSSDT table addresses under Windows 11. In this blog post, we will talk about the traversal of these two tables and the issue of Hook detection. 

Structure of SSDT and ShadowSSDT Tables

Both tables use the same data structure, just with different names. The specific structure is represented in C language as follows:

  1. typedef struct _KSYSTEM_SERVICE_TABLE {  
  2.     PUCHAR ServiceTableBase;  
  3.     PULONG Count;  
  4.     ULONG_PTR TableSize;  
  5.     PUCHAR ArgumentTable;  
  6. } KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;  
  7.   
  8. typedef struct _KSERVICE_TABLE_DESCRIPTOR  
  9. {  
  10.     KSYSTEM_SERVICE_TABLE   ntoskrnl;  
  11.     KSYSTEM_SERVICE_TABLE   win32k;  
  12.     KSYSTEM_SERVICE_TABLE   notUsed1;  
  13.     KSYSTEM_SERVICE_TABLE   notUsed2;  
  14. } KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;  

Here, _KSERVICE_TABLE_DESCRIPTOR corresponds to nt!KeServiceDescriptorTable and nt!KeServiceDescriptorTableShadow. And nt!KiServiceTable generally points to the starting address of _KSYSTEM_SERVICE_TABLE, that is, ServiceTableBase.

This is still too abstract, let’s look at a few practical examples.

  1. kd> .process /i fffffa80060a7b30  
  2. You need to continue execution (press 'g' <enter>) for the context  
  3. to be switched. When the debugger breaks in again, you will be in  
  4. the new process context.  
  5. kd> g  
  6. Break instruction exception - code 80000003 (first chance)  
  7. nt!RtlpBreakWithStatusInstruction:  
  8. fffff800`03cd1490 cc              int     3  
  9. kd> !process fffffa80060a7b30 0  
  10. PROCESS fffffa80060a7b30  
  11.     SessionId: 0  Cid: 0138    Peb: 7fffffde000  ParentCid: 0130  
  12.     DirBase: 9e3fa000  ObjectTable: fffff8a006353010  HandleCount: 408.  
  13.     Image: csrss.exe  
  14.   
  15. kd> dps nt!KeServiceDescriptorTable L10  
  16.                                                                                                    // typedef struct _KSERVICE_TABLE_DESCRIPTOR {  
  17. fffff800`03f0a840  fffff800`03cda300 nt!KiServiceTable             //        KSYSTEM_SERVICE_TABLE   ntoskrnl.ServiceTableBase;  
  18. fffff800`03f0a848  00000000`00000000                                     //        KSYSTEM_SERVICE_TABLE   ntoskrnl.Count;  
  19. fffff800`03f0a850  00000000`00000191                                     //        KSYSTEM_SERVICE_TABLE   ntoskrnl.TableSize;  
  20. fffff800`03f0a858  fffff800`03cdaf8c nt!KiArgumentTable           //        KSYSTEM_SERVICE_TABLE   ntoskrnl.ArgumentTable;  
  21. fffff800`03f0a860  00000000`00000000                                     //        KSYSTEM_SERVICE_TABLE   win32k.ServiceTableBase;  
  22. fffff800`03f0a868  00000000`00000000                                     //        KSYSTEM_SERVICE_TABLE   win32k.Count;  
  23. fffff800`03f0a870  00000000`00000000                                     //        KSYSTEM_SERVICE_TABLE   win32k.TableSize;  
  24. fffff800`03f0a878  00000000`00000000                                     //        KSYSTEM_SERVICE_TABLE   win32k.ArgumentTable;  
  25. fffff800`03f0a880  fffff800`03cda300 nt!KiServiceTable             //        KSYSTEM_SERVICE_TABLE   notUsed1.ServiceTableBase;  
  26. fffff800`03f0a888  00000000`00000000                                     //        KSYSTEM_SERVICE_TABLE   notUsed1.Count;  
  27. fffff800`03f0a890  00000000`00000191                                     //        KSYSTEM_SERVICE_TABLE   notUsed1.TableSize;  
  28. fffff800`03f0a898  fffff800`03cdaf8c nt!KiArgumentTable           //        KSYSTEM_SERVICE_TABLE   notUsed1.ArgumentTable;  
  29. fffff800`03f0a8a0  fffff960`00161f00                                           //        KSYSTEM_SERVICE_TABLE   notUsed2.ServiceTableBase;  
  30. fffff800`03f0a8a8  00000000`00000000                                     //        KSYSTEM_SERVICE_TABLE   notUsed2.Count;  
  31. fffff800`03f0a8b0  00000000`0000033b                                     //        KSYSTEM_SERVICE_TABLE   notUsed2.TableSize;  
  32. fffff800`03f0a8b8  fffff960`00163c1c                                          //        KSYSTEM_SERVICE_TABLE   notUsed2.ArgumentTable;  
  33.                                                                                                    // } KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;  
  34.   
  35. kd> dps nt!KeServiceDescriptorTableShadow L10  
  36.                                                                                                    // typedef struct _KSERVICE_TABLE_DESCRIPTOR {  
  37. fffff800`03f0a880  fffff800`03cda300 nt!KiServiceTable             //        KSYSTEM_SERVICE_TABLE   ntoskrnl.ServiceTableBase;  
  38. fffff800`03f0a888  00000000`00000000                                     //        KSYSTEM_SERVICE_TABLE   ntoskrnl.Count;  
  39. fffff800`03f0a890  00000000`00000191                                     //        KSYSTEM_SERVICE_TABLE   ntoskrnl.TableSize;  
  40. fffff800`03f0a898  fffff800`03cdaf8c nt!KiArgumentTable           //        KSYSTEM_SERVICE_TABLE   ntoskrnl.ArgumentTable;  
  41. fffff800`03f0a8a0  fffff960`00161f00                                           //        KSYSTEM_SERVICE_TABLE   win32k.ServiceTableBase;  
  42. fffff800`03f0a8a8  00000000`00000000                                     //        KSYSTEM_SERVICE_TABLE   win32k.Count;  
  43. fffff800`03f0a8b0  00000000`0000033b                                     //        KSYSTEM_SERVICE_TABLE   win32k.TableSize;  
  44. fffff800`03f0a8b8  fffff960`00163c1c                                          //        KSYSTEM_SERVICE_TABLE   win32k.ArgumentTable;  
  45. fffff800`03f0a8c0  00000000`77c51206                                     //        KSYSTEM_SERVICE_TABLE   notUsed1.ServiceTableBase;  
  46. fffff800`03f0a8c8  00000000`00000000                                     //        KSYSTEM_SERVICE_TABLE   notUsed1.Count;  
  47. fffff800`03f0a8d0  fffff800`00a06428                                          //        KSYSTEM_SERVICE_TABLE   notUsed1.TableSize;  
  48. fffff800`03f0a8d8  fffff800`00a063d8                                          //        KSYSTEM_SERVICE_TABLE   notUsed1.ArgumentTable;  
  49. fffff800`03f0a8e0  00000000`00000002                                     //        KSYSTEM_SERVICE_TABLE   notUsed2.ServiceTableBase;  
  50. fffff800`03f0a8e8  00000000`00008600                                     //        KSYSTEM_SERVICE_TABLE   notUsed2.Count;  
  51. fffff800`03f0a8f0  00000000`000d79f0                                       //        KSYSTEM_SERVICE_TABLE   notUsed2.TableSize;  
  52. fffff800`03f0a8f8  00000000`00000000                                      //        KSYSTEM_SERVICE_TABLE   notUsed2.ArgumentTable;  
  53.                                                                                                    // } KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;  

Since we want to view ShadowSSDT, we first use .process to switch to the process context of fffffa80060a7b30 (a program with SessionID), otherwise the nt!KeServiceDescriptorTableShadow we see may not be valid data.

In the above text, based on the table structure, we can already get the ServiceTableBase address. So how do we get the actual address of each function under this Base? 

Calculating the Address of Each Function in the Table

If you are in an x64 environment and you can’t wait to look at the content at ServiceTableBase, you will find that it does not match the real function address. The reason for this phenomenon will be mentioned later in this article. Let’s start with the simplest situation. 

x86 Environment

The situation is simplest in the x86 environment. You will find that the address at ServiceTableBase is the address of the corresponding function, whether you look at it statically or dynamically:


Observe the same value and symbol in the debugger:


Due to different base addresses, take NtAccessCheck function as an example. The value in the static environment is 0x00471b6c, loaded at the base address 0x00400000. The value in the dynamic environment is 0x820d5b6c, loaded at the base address 0x82064000.

The values of the two environments are converted into RVA respectively, which can be known: 
  1. 0x00471b6c - 0x00400000 = 0x820d5b6c - 0x08206400 = 0x71b6c  
Although the specific values of KiServiceTable in the static and dynamic environments are different (this is caused by relocation), it can be known from the above RVA calculation that the values at their memory locations can be matched. In the end, we can conclude that KiServiceTable directly stores the target function address in the x86 environment. 

x64 Environment

Microsoft made adjustments in the x64 environment. In the static environment, in the lower version of the kernel (Windows7, 8, 8.1) and x86, the function address is directly stored. But in the higher version of the kernel (Windows 10 and above), KiServiceTable will no longer directly store the function address, and the data length has also changed from the original 4 bytes to 8 bytes.

The biggest adjustment is the dynamic environment. When using the debugger for kernel debugging, you will find that no matter the high version or the low version, the value at KiServiceTable is not the same as the static file anyway. This is mainly because the kernel will “compress” the value at KiServiceTable, and the compression action is completed by the nt!KeCompactServiceTable function, which we will discuss later in the text.

Next, let’s look at the situation of high and low versions separately.

Low Version Kernel

The following picture is a screenshot of KiServiceTable under Windows 7 x64, which shows that it is still storing the function address. The current loading base address is 0x00000001`40000000.


Then observe the dynamic value at KiServiceTable after the system is running:


If we try to convert the dynamic and static addresses into RVA using the method in the previous section, we will find that no matter what, we cannot get the same value. This is what was said at the beginning of this section: Since the value at the dynamic address has been processed by the nt!KeCompactServiceTable function, it can no longer calculate the same RVA. 

High Version Kernel

If you open a high version kernel (such as Windows 10 x64) and locate KiServiceTable, you will find that things are not simple. At this time, IDA can’t even “correctly” identify the functions in the SSDT table, and the data looks like every 4 bytes are a group, which is quite different from the situation where every 8 bytes are a group in the low version:


Here I won’t take a screenshot of the value at KiServiceTable in the debugging state, you just need to know that it is consistent with the situation in the low version: it can’t be matched, that’s it.

So, how do we get the correct function address?

KeCompactServiceTable Function

The above mentioned that the static and dynamic KiServiceTable values cannot correspond to each other in the x64 environment. The ultimate reason for this problem is that the system uses the nt!KeCompactServiceTable function to “compress” the value at this address during the startup phase. Next, let’s look at the decompiled code of this function under Windows 7 x64:

  1. unsigned int __fastcall KeCompactServiceTable(  
  2.         char *KiServiceTablePtr,  
  3.         char *ArgumentTable,  
  4.         unsigned int limit,  
  5.         boolean a4)  
  6. {  
  7.   size_t v5; // r10  
  8.   DWORD KiServiceTableBase; // ebx  
  9.   char *PKiServiceTablePtr; // rdx  
  10.   __int64 v8; // r11  
  11.   DWORD ValueOfServiceTable; // er8  
  12.   unsigned int result; // eax  
  13.   
  14.   v5 = limit;  
  15.   KiServiceTableBase = (unsigned int)KiServiceTablePtr;  
  16.   PKiServiceTablePtr = KiServiceTablePtr;  
  17.   if ( limit )  
  18.   {  
  19.     v8 = limit;  
  20.     do  
  21.     {  
  22.       ValueOfServiceTable = *(_DWORD *)PKiServiceTablePtr;  
  23.       PKiServiceTablePtr += 8;  
  24.       result = (unsigned __int8)*ArgumentTable++ >> 2;  
  25.       *(_DWORD *)KiServiceTablePtr = result | (16 * (ValueOfServiceTable - KiServiceTableBase));  
  26.       KiServiceTablePtr += 4;  
  27.       --v8;  
  28.     }  
  29.     while ( v8 );  
  30.   }  
  31.   if ( a4 == 1 )  
  32.     return (unsigned int)memmove(KiServiceTablePtr, PKiServiceTablePtr, v5);  
  33.   return result;  
  34. }  

The above logic is described more concisely as follows:

  1. ULONG_PTR Index = 0, TableSize = 0, ServiceTableBase = 0, ArgumentTable = 0;  
  2.   
  3. TableSize = ServiceDescriptorTable->ntoskrnl.TableSize;  
  4. ServiceTableBase = ServiceDescriptorTable->ntoskrnl.ServiceTableBase;  
  5. ArgumentTable = ServiceDescriptorTable->ntoskrnl.ArgumentTable;  
  6.               
  7. for (Index = 0; Index < TableSize; ++Index)  
  8. {  
  9.     UHALF_PTR FunctionCookie = 0;  
  10.     PUHALF_PTR Pointer = ServiceTableBase + Index * 4;  
  11.     UINT8 ArgumentCookie = *(PUCHAR)(ArgumentTable + Index);  
  12.   
  13.     FunctionCookie = (UHALF_PTR)*(PUHALF_PTR)(ServiceTableBase + Index * 8) - (UHALF_PTR)ServiceTableBase;  
  14.     *Pointer = (16 * FunctionCookie) | (ArgumentCookie >> 2);  
  15. }  

One thing to note is: In the system version of Windows 10 x64 and above, there are 2 important differences:
  1. The original data at KiServiceTable has changed from 8 bytes to 4 bytes
  2. Introduced kernel loading base address for calculation
You can disassemble the code of KeCompactServiceTable by yourself. In short, after the reverse is completed, the 13th line of the above code should be (it may be slightly different, but the calculation result should be equivalent) under Windows 10 x64: 
  1. FunctionCookie = (UHALF_PTR)*(PUHALF_PTR)(ServiceTableBase + Index * 4) \  
  2.     - (UHALF_PTR)ServiceTableBase + (UHALF_PTR)NewImageBase;  

Real Function Address Calculation

x86

According to the analysis above, in the x86 environment, take out the value at the corresponding table, and directly add the ntoskrnl.exe module base address to get the original address of the table item, so the formula is:

FunctionAddress = NtoskrnlBase + *(PULONG_PTR)(ServiceTableBase + Index * 4)

x64

First of all, the overall formula, under the x64 system, regardless of the high and low, the calculation formula of the corresponding function of each table item in all versions of SSDT is:

FunctionAddress = HIWORD(KiServiceTableBase)<<32 + (UHALF_PTR)((UHALF_PTR)KiServiceTableBase + (UHALF_PTR)FunctionCookie)

Note that UHALF_PTR is 4 bytes long in x64, don’t bring the high bit when calculating. Next, let’s manually calculate the address with examples of Windows 7 x64 and Windows 10 x64. 
Windows 7 x64 Calculation Example
First of all, Windows 7 x64, recycle a screenshot from the above text:


According to the above calculation formula (note that the data type is UHALF_PTR under 64-bit, which is 4 bytes):
  • KiServiceTableBase = 0x40071B00
  • FunctionCookie      = 0x40482190 - 0x40071B00 = 0x410690
  • FunctionAddress     = 0x1`00000000 + 0x40071B00 + 0x410690 = 0x140482190
The result is the address of the NtMapUserPhysicalPagesScatter function. 
Windows 10 x64 Calculation Example
Let’s take a look at the calculation under Windows 10 x64, and continue to recycle the screenshot above:


Here you need to note that each table item on Windows 10 x64 has changed to 4 bytes instead of 8 bytes on Windows 7 x64: 
  • KiServiceTableBase = 0x402EE800
  • FunctionCookie      = 0x000E54B4 - 0x402EE800 + 0x40000000 = 0xFFDF6CB4
  • FunctionAddress    = 0x1`00000000 + 0x402EE800 + 0xFFDF6CB4 = 0x1400e54b4
Let’s see what is at the address 0x1400e54b4 under Windows 10 x64:


The high bit addresses of the two functions on the left and right of the above picture are different, because the real kernel loaded in memory has undergone address relocation. 

Hook Detection

In fact, knowing the calculation method of the real address, you only need to calculate the value at the original address according to the following ideas, and then compare it with the value at the same offset in the current kernel. If the value is different, it has been Hooked: 
  1. Reload a kernel
  2. According to the logic of the nt!KeCompactServiceTable function, correct the value of each table item in the first step of KiServiceTable
  3. Compare the reloaded kernel’s KiServiceTable table items with the values at the same offset in the current kernel
  4. The different ones are the functions that have been Hooked
Here I won’t post the specific code, just give a screenshot after verification:

Reference

  1. Windows 11 SSDT & ShadowSSDT Address Acquisition Problem
Catalog
Structure of SSDT and ShadowSSDT Tables
Calculating the Address of Each Function in the Table
x86 Environment
x64 Environment
Low Version Kernel
High Version Kernel
KeCompactServiceTable Function
Real Function Address Calculation
x86
x64
Windows 7 x64 Calculation Example
Windows 10 x64 Calculation Example
Hook Detection
Reference

CopyRight (c) 2020 - 2025 Debugwar.com

Designed by Hacksign