Watch & Learn

Debugwar Blog

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

SSDT & ShadowSSDT的遍历与Hook恢复

2022-11-15 07:59:43

上一篇博文讨论了Windows 11下获取SSDT与ShadowSSDT表地址的问题,这篇博文我们来聊一下这两张表的遍历和Hook检测的问题。

SSDT与ShadowSSDT表的结构

两张表使用了同一数据结构,只是名字不同而已,具体结构C语言表示如下:

  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;  

其中_KSERVICE_TABLE_DESCRIPTOR对应nt!KeServiceDescriptorTablent!KeServiceDescriptorTableShadow。而nt!KiServiceTable一般指向_KSYSTEM_SERVICE_TABLE的首地址,即ServiceTableBase

这么说还是太抽象了,来看几个实际的例子。

  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;  

由于我们要查看ShadowSSDT,因此上面先用.process切换到了fffffa80060a7b30这个进程上下文(有SessionID的程序),不然看到的nt!KeServiceDescriptorTableShadow可能不是有效数据。

上文中,我们根据表结构,已经可以得到ServiceTableBase地址了,那么如何获取这个Base下各个函数的实际地址呢?

计算表中各个函数地址

如果此时你是x64的环境,而且你迫不及待的去看ServiceTableBase处内容的话,你会发现和真实的函数地址对不上,产生这个现象的原因本文后面会提到。现在让我们先从最简单的情况开始讨论。

x86环境

x86环境下情况最为简单,你会发现ServiceTableBase处的地址就是对应函数的地址,无论是静态看还是动态看:


调试器中观察相同位置的数值及符号如下:


由于基址不同,以NtAccessCheck函数为例。静态环境的值为0x00471b6c, 加载在基址0x00400000处。动态环境的值为0x820d5b6c,加载在基址0x82064000处。

两个环境的值分别换算成RVA,可知:
  1. 0x00471b6c - 0x00400000 = 0x820d5b6c - 0x08206400 = 0x71b6c  
虽然静态与动态环境下KiServiceTable具体的值不一样(这是由于重定位造成的),但是通过上述RVA计算可知,其内存处的值是可以对上的。最终我们可以得出结论,x86下 KiServiceTable直接存放的就是目标函数地址。

x64环境

x64下微软做了调整,静态环境下,在低版本内核中(Windows7、8、8.1)和x86一样直接存放函数地址。而高版本内核(Windows 10以上),KiServiceTable中将不再直接存放函数地址,而且数据长度也由原来的4字节变成了8字节。

调整较大的是动态环境,当用调试器进行内核调试时,会发现无论高版本还是低版本,KiServiceTable处的值无论如何也和静态文件中的不一样,这主要是因为内核会对KiServiceTable处的值做“压缩”,压缩的动作由nt!KeCompactServiceTable函数完成的,后文中我们会有讨论。

下面我们分开看一下高低版本各环境的情况。

低版本内核

下图是Windows 7 x64下KiServiceTable的截图,可见依然是存储的函数地址,此时的加载基址为0x00000001`40000000


然后通过调试器观察系统跑起来后KiServiceTable处的动态值:


如果此时我们尝试用上一节中的方法将动静态地址分别转换成RVA,会发现无论如何也得不出相等的数值,这也就是本节开头所说:由于动态地址处的值经过nt!KeCompactServiceTable函数的处理,因此已经不能计算出相同的RVA。

高版本内核

如果此时你打开高版本内核(比如Windows 10 x64),定位到KiServiceTable处,你就会发现事情并不简单。此时的IDA甚至不能“正确”的识别SSDT表中的函数,而且数据上看上去也是每4 bytes为一组,和低版本中每8 bytes为一组的情况大相径庭:


此处我就不截调试状态下KiServiceTable地址处的值了,你只需要知道和低版本的情况一致:也无法对的上,即可。

那么,我们如何得出正确函数的地址呢?

KeCompactServiceTable函数

上文中说到x64下静态与动态KiServiceTable处数值无法对应的问题,导致这一问题的最终原因是系统在启动阶段使用nt!KeCompactServiceTable函数对该地址处的值进行了“压缩”处理,接下来我们看一下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. }  

将上述逻辑更精炼一点描述如下:

  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. }  

需要注意的一点是:在Windows 10 x64及以上的系统版本中, 有2处重要差别:
  1. KiServiceTable处的数据由8字节长度变成了4字节
  2. 引入了内核加载基址参与计算
具体的KeCompactServiceTable反汇编代码我就不贴了,有兴趣的可以逆一下。总之逆向完成之后,上面代码的第13行在Windows 10 x64下应该为(可能有轻微不一样,但是计算结果应该是等价的):
  1. FunctionCookie = (UHALF_PTR)*(PUHALF_PTR)(ServiceTableBase + Index * 4) \  
  2.     - (UHALF_PTR)ServiceTableBase + (UHALF_PTR)NewImageBase;  

真实函数地址计算

首先是总体的公式,x64系统下,无论高低,所有版本SSDT每个表项对应函数的计算公式都为:

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

注意UHALF_PTR在x64下是4字节长度,计算的时候别把高位也带上了。下面我们以Windows 7 x64和Windows 10 x64为例来手动算一下地址。

Windows 7 x64

首先是Windows 7 x64, 回收一张上文中的截图:


按照上面的计算公式(注意数据类型是64位下的UHALF_PTR,也就是4个字节):
  • KiServiceTableBase = 0x40071B00
  • FunctionCookie      = 0x40482190 - 0x40071B00 = 0x410690
  • FunctionAddress     = 0x1`00000000 + 0x40071B00 + 0x410690 = 0x140482190
结果即为NtMapUserPhysicalPagesScatter函数的地址。

Windows 10 x64

再来看一下Windows 10 x64下的计算,继续回收上文截图:


这里需要注意,Windows 10 x64上的每个表项变为了4个字节而不是Windows 7 x64上的8个字节:
  • KiServiceTableBase = 0x402EE800
  • FunctionCookie      = 0x000E54B4 - 0x402EE800 + 0x40000000 = 0xFFDF6CB4
  • FunctionAddress    = 0x1`00000000 + 0x402EE800 + 0xFFDF6CB4 = 0x1400e54b4
我们看一下Windows 10 x64下0x1400e54b4地址处是什么:


上图中左右两个函数的高位地址不同,是因为真正在内存中加载的内核经过了地址重定位的原因。

Hook检测

其实知道了真正地址的计算方法,只需要按照如下思路计算出原始地址处的值,然后再和现内核同一偏移处的值比较,值不一样即被Hook过:
  1. 重载一份内核
  2. 按照nt!KeCompactServiceTable函数的逻辑,修正第1步中KiServiceTable各表项的值
  3. 将重载内核的KiServiceTable各表项与现内核同偏移处的值比较
  4. 不一样的即为被Hook过的函数
这里就不上具体代码了, 给一份验证过后的截图:

参考

  1. Windows 11 SSDT & ShadowSSDT地址获取问题
Catalog
  • SSDT与ShadowSSDT表的结构
  • 计算表中各个函数地址
  • x86环境
  • x64环境
  • 低版本内核
  • 高版本内核
  • KeCompactServiceTable函数
  • 真实函数地址计算
  • Windows 7 x64
  • Windows 10 x64
  • Hook检测
  • 参考
  • CopyRight(c) 2020 - 2025 Debugwar.com

    Designed by Hacksign