SSDT表(System Service Descriptor Table)是Windows操作系统内核中的关键组成部分,负责存储系统服务调用的相关信息。具体而言,SSDT表包含了系统调用的函数地址以及其他与系统服务相关的信息。每个系统调用对应SSDT表中的一个表项,其中存储了相应系统服务的函数地址。SSDT表在64位和32位系统上可能有不同的结构,但通常以数组形式存在。

对于系统调用的监控、分析或修改等高级操作,常需要内核枚举SSDT表基址。这一操作通常通过内核模块实现,涉及技术手段如逆向工程和Hooking。

看一款闭源ARK工具的枚举效果:

直接步入正题,首先SSDT表中文为系统服务描述符表,SSDT表的作用是把应用层与内核联系起来起到桥梁的作用,枚举SSDT表也是反内核工具最基本的功能,通常在64位系统中要想找到SSDT表,需要先找到KeServiceDescriptorTable这个函数,由于该函数没有被导出,所以只能动态的查找它的地址,庆幸的是我们可以通过查找msr(c0000082)这个特殊的寄存器来替代查找KeServiceDescriptorTable这一步,在新版系统中查找SSDT可以归纳为如下这几个步骤。

  • rdmsr c0000082 -> KiSystemCall64Shadow -> KiSystemServiceUser -> SSDT

首先第一步通过rdmsr C0000082 MSR寄存器得到KiSystemCall64Shadow的函数地址,计算KiSystemCall64ShadowKiSystemServiceUser偏移量,如下图所示。

  • 得到相对偏移6ed53180(KiSystemCall64Shadow) - 6ebd2a82(KiSystemServiceUser) = 1806FE
  • 也就是说 6ed53180(rdmsr) - 1806FE = KiSystemServiceUser

如上当我们找到了KiSystemServiceUser的地址以后,在KiSystemServiceUser向下搜索可找到KiSystemServiceRepeat里面就是我们要找的SSDT表基址。

其中fffff8036ef8c880则是SSDT表的基地址,紧随其后的fffff8036ef74a80则是SSSDT表的基地址。

那么如果将这个过程通过代码的方式来实现,我们还需要使用《内核枚举IoTimer定时器》中所使用的特征码定位技术,如下我们查找这段特征。

  1. #include <ntifs.h>
  2. #pragma intrinsic(__readmsr)
  3. ULONGLONG ssdt_address = 0;
  4. // 获取 KeServiceDescriptorTable 首地址
  5. ULONGLONG GetLySharkCOMKeServiceDescriptorTable()
  6. {
  7. // 设置起始位置
  8. PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082) - 0x1806FE;
  9. // 设置结束位置
  10. PUCHAR EndSearchAddress = StartSearchAddress + 0x100000;
  11. DbgPrint("[LyShark Search] 扫描起始地址: %p --> 扫描结束地址: %p \n", StartSearchAddress, EndSearchAddress);
  12. PUCHAR ByteCode = NULL;
  13. UCHAR OpCodeA = 0, OpCodeB = 0, OpCodeC = 0;
  14. ULONGLONG addr = 0;
  15. ULONG templong = 0;
  16. for (ByteCode = StartSearchAddress; ByteCode < EndSearchAddress; ByteCode++)
  17. {
  18. // 使用MmIsAddressValid()函数检查地址是否有页面错误
  19. if (MmIsAddressValid(ByteCode) && MmIsAddressValid(ByteCode + 1) && MmIsAddressValid(ByteCode + 2))
  20. {
  21. OpCodeA = *ByteCode;
  22. OpCodeB = *(ByteCode + 1);
  23. OpCodeC = *(ByteCode + 2);
  24. // 对比特征值 寻找 nt!KeServiceDescriptorTable 函数地址
  25. /*
  26. nt!KiSystemServiceRepeat:
  27. fffff803`6ebd2b94 4c8d15e59c3b00 lea r10,[nt!KeServiceDescriptorTable (fffff803`6ef8c880)]
  28. fffff803`6ebd2b9b 4c8d1dde1e3a00 lea r11,[nt!KeServiceDescriptorTableShadow (fffff803`6ef74a80)]
  29. fffff803`6ebd2ba2 f7437880000000 test dword ptr [rbx+78h],80h
  30. fffff803`6ebd2ba9 7413 je nt!KiSystemServiceRepeat+0x2a (fffff803`6ebd2bbe) Branch
  31. */
  32. if (OpCodeA == 0x4c && OpCodeB == 0x8d && OpCodeC == 0x15)
  33. {
  34. // 获取高位地址fffff802
  35. memcpy(&templong, ByteCode + 3, 4);
  36. // 与低位64da4880地址相加得到完整地址
  37. addr = (ULONGLONG)templong + (ULONGLONG)ByteCode + 7;
  38. return addr;
  39. }
  40. }
  41. }
  42. return 0;
  43. }
  44. VOID UnDriver(PDRIVER_OBJECT driver)
  45. {
  46. DbgPrint(("驱动程序卸载成功! \n"));
  47. }
  48. NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
  49. {
  50. DbgPrint("hello lyshark");
  51. ssdt_address = GetLySharkCOMKeServiceDescriptorTable();
  52. DbgPrint("[LyShark] SSDT = %p \n", ssdt_address);
  53. DriverObject->DriverUnload = UnDriver;
  54. return STATUS_SUCCESS;
  55. }

如上代码中所提及的步骤我想不需要再做解释了,这段代码运行后即可输出SSDT表的基址。

如上通过调用GetLySharkCOMKeServiceDescriptorTable()得到SSDT地址以后我们就需要对该地址进行解密操作。

得到ServiceTableBase的地址后,就能得到每个服务函数的地址。但这个表存放的并不是SSDT函数的完整地址,而是其相对于ServiceTableBase[Index]>>4的数据,每个数据占四个字节,所以计算指定Index函数完整地址的公式是;

  • 在x86平台上: FuncAddress = KeServiceDescriptorTable + 4 * Index
  • 在x64平台上:FuncAddress = [KeServiceDescriptorTable+4*Index]>>4 + KeServiceDescriptorTable

如下汇编代码就是一段解密代码,代码中rcx寄存器传入SSDT的下标,而rdx寄存器则是传入SSDT表基址。

  1. 48:8BC1 | mov rax,rcx | rcx=index
  2. 4C:8D12 | lea r10,qword ptr ds:[rdx] | rdx=ssdt
  3. 8BF8 | mov edi,eax |
  4. C1EF 07 | shr edi,7 |
  5. 83E7 20 | and edi,20 |
  6. 4E:8B1417 | mov r10,qword ptr ds:[rdi+r10] |
  7. 4D:631C82 | movsxd r11,dword ptr ds:[r10+rax*4] |
  8. 49:8BC3 | mov rax,r11 |
  9. 49:C1FB 04 | sar r11,4 |
  10. 4D:03D3 | add r10,r11 |
  11. 49:8BC2 | mov rax,r10 |
  12. C3 | ret |

有了解密公式以后代码的编写就变得很容易,如下是读取SSDT的完整代码。

  1. #include <ntifs.h>
  2. #pragma intrinsic(__readmsr)
  3. typedef struct _SYSTEM_SERVICE_TABLE
  4. {
  5. PVOID ServiceTableBase;
  6. PVOID ServiceCounterTableBase;
  7. ULONGLONG NumberOfServices;
  8. PVOID ParamTableBase;
  9. } SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;
  10. ULONGLONG ssdt_base_aadress;
  11. PSYSTEM_SERVICE_TABLE KeServiceDescriptorTable;
  12. typedef UINT64(__fastcall *SCFN)(UINT64, UINT64);
  13. SCFN scfn;
  14. // 解密算法
  15. VOID DecodeSSDT()
  16. {
  17. UCHAR strShellCode[36] = "\x48\x8B\xC1\x4C\x8D\x12\x8B\xF8\xC1\xEF\x07\x83\xE7\x20\x4E\x8B\x14\x17\x4D\x63\x1C\x82\x49\x8B\xC3\x49\xC1\xFB\x04\x4D\x03\xD3\x49\x8B\xC2\xC3";
  18. /*
  19. 48:8BC1 | mov rax,rcx | rcx=index
  20. 4C:8D12 | lea r10,qword ptr ds:[rdx] | rdx=ssdt
  21. 8BF8 | mov edi,eax |
  22. C1EF 07 | shr edi,7 |
  23. 83E7 20 | and edi,20 |
  24. 4E:8B1417 | mov r10,qword ptr ds:[rdi+r10] |
  25. 4D:631C82 | movsxd r11,dword ptr ds:[r10+rax*4] |
  26. 49:8BC3 | mov rax,r11 |
  27. 49:C1FB 04 | sar r11,4 |
  28. 4D:03D3 | add r10,r11 |
  29. 49:8BC2 | mov rax,r10 |
  30. C3 | ret |
  31. */
  32. scfn = ExAllocatePool(NonPagedPool, 36);
  33. memcpy(scfn, strShellCode, 36);
  34. }
  35. // 获取 KeServiceDescriptorTable 首地址
  36. ULONGLONG GetKeServiceDescriptorTable()
  37. {
  38. // 设置起始位置
  39. PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082) - 0x1806FE;
  40. // 设置结束位置
  41. PUCHAR EndSearchAddress = StartSearchAddress + 0x8192;
  42. DbgPrint("扫描起始地址: %p --> 扫描结束地址: %p \n", StartSearchAddress, EndSearchAddress);
  43. PUCHAR ByteCode = NULL;
  44. UCHAR OpCodeA = 0, OpCodeB = 0, OpCodeC = 0;
  45. ULONGLONG addr = 0;
  46. ULONG templong = 0;
  47. for (ByteCode = StartSearchAddress; ByteCode < EndSearchAddress; ByteCode++)
  48. {
  49. // 使用MmIsAddressValid()函数检查地址是否有页面错误
  50. if (MmIsAddressValid(ByteCode) && MmIsAddressValid(ByteCode + 1) && MmIsAddressValid(ByteCode + 2))
  51. {
  52. OpCodeA = *ByteCode;
  53. OpCodeB = *(ByteCode + 1);
  54. OpCodeC = *(ByteCode + 2);
  55. // 对比特征值 寻找 nt!KeServiceDescriptorTable 函数地址
  56. // lyshark
  57. // 4c 8d 15 e5 9e 3b 00 lea r10,[nt!KeServiceDescriptorTable (fffff802`64da4880)]
  58. // 4c 8d 1d de 20 3a 00 lea r11,[nt!KeServiceDescriptorTableShadow (fffff802`64d8ca80)]
  59. if (OpCodeA == 0x4c && OpCodeB == 0x8d && OpCodeC == 0x15)
  60. {
  61. // 获取高位地址fffff802
  62. memcpy(&templong, ByteCode + 3, 4);
  63. // 与低位64da4880地址相加得到完整地址
  64. addr = (ULONGLONG)templong + (ULONGLONG)ByteCode + 7;
  65. return addr;
  66. }
  67. }
  68. }
  69. return 0;
  70. }
  71. // 得到函数相对偏移地址
  72. ULONG GetOffsetAddress(ULONGLONG FuncAddr)
  73. {
  74. ULONG dwtmp = 0;
  75. PULONG ServiceTableBase = NULL;
  76. if (KeServiceDescriptorTable == NULL)
  77. {
  78. KeServiceDescriptorTable = (PSYSTEM_SERVICE_TABLE)GetKeServiceDescriptorTable();
  79. }
  80. ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
  81. dwtmp = (ULONG)(FuncAddr - (ULONGLONG)ServiceTableBase);
  82. return dwtmp << 4;
  83. }
  84. // 根据序号得到函数地址
  85. ULONGLONG GetSSDTFunctionAddress(ULONGLONG NtApiIndex)
  86. {
  87. ULONGLONG ret = 0;
  88. if (ssdt_base_aadress == 0)
  89. {
  90. // 得到ssdt基地址
  91. ssdt_base_aadress = GetKeServiceDescriptorTable();
  92. }
  93. if (scfn == NULL)
  94. {
  95. DecodeSSDT();
  96. }
  97. ret = scfn(NtApiIndex, ssdt_base_aadress);
  98. return ret;
  99. }
  100. VOID UnDriver(PDRIVER_OBJECT driver)
  101. {
  102. DbgPrint(("驱动程序卸载成功! \n"));
  103. }
  104. NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
  105. {
  106. DbgPrint("hello lyshark \n");
  107. ULONGLONG ssdt_address = GetKeServiceDescriptorTable();
  108. DbgPrint("SSDT基地址 = %p \n", ssdt_address);
  109. // 根据序号得到函数地址
  110. ULONGLONG address = GetSSDTFunctionAddress(51);
  111. DbgPrint("[LyShark] NtOpenFile地址 = %p \n", address);
  112. // 得到相对SSDT的偏移量
  113. DbgPrint("函数相对偏移地址 = %p \n", GetOffsetAddress(address));
  114. DriverObject->DriverUnload = UnDriver;
  115. return STATUS_SUCCESS;
  116. }

运行后即可得到SSDT下标为51的函数也就是得到NtOpenFile的绝对地址和相对地址。

你也可以打开ARK工具,对比一下是否一致,如下图所示,LyShark的代码是没有任何问题的。

6.1 Windows驱动开发:内核枚举SSDT表基址的更多相关文章

  1. 驱动开发:Win10内核枚举SSDT表基址

    三年前面朝黄土背朝天的我,写了一篇如何在Windows 7系统下枚举内核SSDT表的文章<驱动开发:内核读取SSDT表基址>三年过去了我还是个单身狗,开个玩笑,微软的Windows 10系 ...

  2. Windows驱动开发-内核常用内存函数

    搞内存常用函数 C语言 内核 malloc ExAllocatePool memset RtlFillMemory memcpy RtlMoveMemory free ExFreePool

  3. Windows驱动开发(中间层)

    Windows驱动开发 一.前言 依据<Windows内核安全与驱动开发>及MSDN等网络质料进行学习开发. 二.初步环境 1.下载安装WDK7.1.0(WinDDK\7600.16385 ...

  4. [Windows驱动开发](一)序言

    笔者学习驱动编程是从两本书入门的.它们分别是<寒江独钓——内核安全编程>和<Windows驱动开发技术详解>.两本书分别从不同的角度介绍了驱动程序的制作方法. 在我理解,驱动程 ...

  5. windows驱动开发推荐书籍

    [作者] 猪头三 个人网站 :http://www.x86asm.com/ [序言] 很多人都对驱动开发有兴趣,但往往找不到正确的学习方式.当然这跟驱动开发的本土化资料少有关系.大多学的驱动开发资料都 ...

  6. windows 驱动开发入门——驱动中的数据结构

    最近在学习驱动编程方面的内容,在这将自己的一些心得分享出来,供大家参考,与大家共同进步,本人学习驱动主要是通过两本书--<独钓寒江 windows安全编程> 和 <windows驱动 ...

  7. Windows驱动——读书笔记《Windows驱动开发技术详解》

    =================================版权声明================================= 版权声明:原创文章 谢绝转载  请通过右侧公告中的“联系邮 ...

  8. Windows驱动开发-IRP的完成例程

    <Windows驱动开发技术详解 >331页, 在将IRP发送给底层驱动或其他驱动之前,可以对IRP设置一个完成例程,一旦底层驱动将IRP完成后,IRP完成例程立刻被处罚,通过设置完成例程 ...

  9. C++第三十八篇 -- 研究一下Windows驱动开发(二)--WDM式驱动的加载

    基于Windows驱动开发技术详解这本书 一.简单的INF文件剖析 INF文件是一个文本文件,由若干个节(Section)组成.每个节的名称用一个方括号指示,紧接着方括号后面的就是节内容.每一行就是一 ...

  10. C++第三十三篇 -- 研究一下Windows驱动开发(一)内部构造介绍

    因为工作原因,需要做一些与网卡有关的测试,其中涉及到了驱动这一块的知识,虽然程序可以运行,但是不搞清楚,心里总是不安,觉得没理解清楚.因此想看一下驱动开发.查了很多资料,看到有人推荐Windows驱动 ...

随机推荐

  1. RPC 框架性能测试,注意这 8 点就够了

    某天,二狗子写了一个 RPC 框架后,简单测了一下性能,发现超出 grpc 一大截.二狗子一高兴,忍不住找同事吹了一波.结果,同事亲测后对二狗子说框架性能也就这样.二狗子表示不服,跟同事一番唇枪舌剑后 ...

  2. 【Qt Libraries】QUrl 的基本使用方法

    参考博客: https://www.cnblogs.com/liushui-sky/p/10892097.html https://www.cnblogs.com/ShineLeBlog/p/1495 ...

  3. 2016年第七届蓝桥杯【C++省赛B组】

    第一题:煤球数目 有一堆煤球,堆成三角棱锥形.具体: 第一层放1个, 第二层3个(排列成三角形), 第三层6个(排列成三角形), 第四层10个(排列成三角形), .... 如果一共有100层,共有多少 ...

  4. php开发之文件下载的实现

    前言 php是网络安全学习里必不可少的一环,简单理解php的开发环节能更好的帮助我们去学习php以及其他语言的web漏洞原理 正文 在正常的开发中,文件下载的功能是必不可少,比如我们在论坛看到好看图片 ...

  5. python常见面试题讲解(五)质数因子

    题目描述 功能:输入一个正整数,按照从小到大的顺序输出它的所有质因子(重复的也要列举)(如180的质因子为2 2 3 3 5 ) 最后一个数后面也要有空格 输入描述: 输入一个long型整数 输出描述 ...

  6. java基础-集合-day14

    目录 1. 数据结构 算法 2. 本章的重点 集合 3. collections 4. list 5. 泛型 6. 泛型通配符 7. linkedList 8. 模拟linkedList源码 --面试 ...

  7. 概率图模型 · 蒙特卡洛采样 · MCMC | 非常好的教学视频

    https://www.bilibili.com/video/BV17D4y1o7J2?p=1 非常感谢!感觉学会一点了,应该能写作业了

  8. 使用docker compose 编排微服务发布

    本文为博主原创,未经允许不得转载: 目录: 1. compose 简介 2. compose 安装 3. 编写 docker-compose.yml 实现微服务发布 4. docker-compose ...

  9. 05-逻辑仿真工具VCS-执行过程

    Verilog Simulation Event Queue 主要了解VCS是如何处理交给它的代码的 Verilog的仿真事件队列,介绍VCS如何处理交给它的代码.VCS是Synopsys公司的,支持 ...

  10. Redis-主从复制-哨兵模式