64位系统上获取SSDT表地址以及从中获取指定SSDT函数的地址

DemonGan

发布日期: 2019-02-25 23:13:46 浏览量: 1014
评分:
star star star star star star star star star star_border
*转载请注明来自write-bug.com

背景

SSDT 全称为 System Services Descriptor Table,即系统服务描述符表。SSDT 表的作用就是把 ring3 的 WIN32 API 函数和 ring0 的内核 API 函数联系起来。对于ring3下的一些API,最终会对应于 ntdll.dll 里一个 Ntxxx 函数,例如 CreateFile,最终调用到 ntdll.dll 里的 NtCreateFile 这个函数。NtCreateFile最终将系统服务号放入EAX,然后 CALL 系统的服务分发函数 KiSystemService,进入到内核当中。从 ring3 到 ring0,最终在 ring0 当中通过传入的 EAX 得到对应的同名系统服务的内核地址,这样就完成了一次系统服务的调用。SSDT 并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等。

SSDT 通过修改此表的函数地址可以对常用 Windows 函数进行 HOOK,从而实现对一些核心的系统动作进行过滤、监控的目的。一些HIPS、防毒软件、系统监控、注册表监控软件往往会采用此接口来实现自己的监控模块。

本质上,其实 SSDT 就是一个用来保存 Windows 系统服务地址的数组而已 。

32 位系统和 64 位上,获取 SSDT 表的方式并不相同,获取 SSDT 表中的函数地址也不相同。现在,我就分别对其进行极讲解介绍,并形成文档。本文主要讲解的是 64 位系统下,编程实现获取 SSDT 表的地址,以及获取 SSDT 表函数对应的内核地址。

实现原理

获取 SSDT 表的地址

在 64 位系统中,SSDT 表并没有在内核 Ntoskrnl.exe 中导出,所以,我们不能像 32 位那样直接获取导出符号 KeServiceDescriptorTable。所以,必须要使用其它方法获取。

我们通过使用 WinDbg 在 Win7 x64、Win8.1 x64 等 64 位系统上逆向内核中的 KiSystemCall64 内核函数,逆向代码如下:

  1. // Win7 x64
  2. nt!KiSystemServiceRepeat:
  3. fffff800`03e8d772 4c8d15c7202300 lea r10,[nt!KeServiceDescriptorTable (fffff800`040bf840)]
  4. fffff800`03e8d779 4c8d1d00212300 lea r11,[nt!KeServiceDescriptorTableShadow (fffff800`040bf880)]
  5. …(略)
  1. // Win8.1 x64
  2. nt!KiSystemServiceRepeat:
  3. fffff800`11b6c752 4c8d1567531f00 lea r10,[nt!KeServiceDescriptorTable (fffff800`11d61ac0)]
  4. fffff800`11b6c759 4c8d1da0531f00 lea r11,[nt!KeServiceDescriptorTableShadow (fffff800`11d61b00)]
  5. …(略)

我们从从上面的代码可以知道,KiSystemCall64 内核函数中,有调用到内核的 KeServiceDescriptorTable 以及 KeServiceDescriptorTableShadow。我们可以根据特征码 4c8d15 搜索内存方式,获取 KeServiceDescriptorTable 的偏移 offset,再计算出 KeServiceDescriptorTable 的地址,计算公式为:

  1. KeServiceDescriptorTable地址 = 特征码4c8d15地址 + 7 + offset

注意,offset 可能为正,也可能为负,所以应该用有符号 4 字节数据类型来保存。

对于 KiSystemCall64 内核函数地址的获取,虽然 Ntoskrnl.exe 也没有导出内核函数 KiSystemCall64,但是,我们可以根据下面代码获取:

  1. __readmsr(0xC0000082)

直接通过读取指定的 msr 得出。msr 的中文全称是就是“特别模块寄存器”(model specific register),它控制 CPU 的工作环境和标示 CPU 的工作状态等信息(例如倍频、最大 TDP、 危险警报温度),它能够读取,也能够写入,但是无论读取还是写入,都只能在 ring 0 下进行。我们通过读取 0xC0000082 寄存器,能够得到 KiSystemCall64 的地址,然后从 KiSystemCall64 的地址开始,往下搜索特征码。

获取 SSDT 表函数地址

在 64 位下,SSDT 表的结构为:

  1. #pragma pack(1)
  2. typedef struct _SERVICE_DESCIPTOR_TABLE
  3. {
  4. PULONG ServiceTableBase; // SSDT基址
  5. PVOID ServiceCounterTableBase; // SSDT中服务被调用次数计数器
  6. ULONGLONG NumberOfService; // SSDT服务个数
  7. PVOID ParamTableBase; // 系统服务参数表基址
  8. }SSDTEntry, *PSSDTEntry;
  9. #pragma pack()

和 32 位上不同的就是第 3 个成员 SSDT 服务个数 NumberOfService,由 4 字节变成了 8 字节。

和 32 位系统不同的是,ServiceTableBase 中存放的并不是 SSDT 函数的完整地址。而是存放的是 ServiceTableBase[SSDT函数索引号]>>4 的偏移地址。那么,64 位下计算 SSDT 函数地址的完整公式为:

  1. ULONG ulOffset = (ServiceTableBase + SSDT函数索引号*4) >> 4;
  2. PVOID pSSDTFuncAddr = (PUCHAR)ServiceTableBase + ulOffset;
  3. // 或者
  4. ULONG ulOffset = ServiceTableBase[SSDT函数索引号] >> 4;
  5. PVOID pSSDTFuncAddr = (PUCHAR)ServiceTableBase + ulOffset;

SSDT 函数索引号可以从 ntdll.dll 文件中获取,当 ring3 级 API 函数最终进入 ring0 级的时候,它会先将 SSDT函数索引号 mov 给 eax 寄存器。所以,我们获取 ntdll.dll 导出函数的地址,从中获取 SSDT 函数索引号。具体的实现过程分析过程,可以参考我写的《内核内存映射文件之获取SSDT函数索引号》这篇文章。

编码实现

获取SSDT函数索引号

  1. // 从 ntdll.dll 中获取 SSDT 函数索引号
  2. ULONG GetSSDTFunctionIndex(UNICODE_STRING ustrDllFileName, PCHAR pszFunctionName)
  3. {
  4. ULONG ulFunctionIndex = 0;
  5. NTSTATUS status = STATUS_SUCCESS;
  6. HANDLE hFile = NULL;
  7. HANDLE hSection = NULL;
  8. PVOID pBaseAddress = NULL;
  9. // 内存映射文件
  10. status = DllFileMap(ustrDllFileName, &hFile, &hSection, &pBaseAddress);
  11. if (!NT_SUCCESS(status))
  12. {
  13. KdPrint(("DllFileMap Error!\n"));
  14. return ulFunctionIndex;
  15. }
  16. // 根据导出表获取导出函数地址, 从而获取 SSDT 函数索引号
  17. ulFunctionIndex = GetIndexFromExportTable(pBaseAddress, pszFunctionName);
  18. // 释放
  19. ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
  20. ZwClose(hSection);
  21. ZwClose(hFile);
  22. return ulFunctionIndex;
  23. }
  24. // 内存映射文件
  25. NTSTATUS DllFileMap(UNICODE_STRING ustrDllFileName, HANDLE *phFile, HANDLE *phSection, PVOID *ppBaseAddress)
  26. {
  27. NTSTATUS status = STATUS_SUCCESS;
  28. HANDLE hFile = NULL;
  29. HANDLE hSection = NULL;
  30. OBJECT_ATTRIBUTES objectAttributes = { 0 };
  31. IO_STATUS_BLOCK iosb = { 0 };
  32. PVOID pBaseAddress = NULL;
  33. SIZE_T viewSize = 0;
  34. // 打开 DLL 文件, 并获取文件句柄
  35. InitializeObjectAttributes(&objectAttributes, &ustrDllFileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
  36. status = ZwOpenFile(&hFile, GENERIC_READ, &objectAttributes, &iosb,
  37. FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
  38. if (!NT_SUCCESS(status))
  39. {
  40. KdPrint(("ZwOpenFile Error! [error code: 0x%X]", status));
  41. return status;
  42. }
  43. // 创建一个节对象, 以 PE 结构中的 SectionALignment 大小对齐映射文件
  44. status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile);
  45. if (!NT_SUCCESS(status))
  46. {
  47. ZwClose(hFile);
  48. KdPrint(("ZwCreateSection Error! [error code: 0x%X]", status));
  49. return status;
  50. }
  51. // 映射到内存
  52. status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &pBaseAddress, 0, 1024, 0, &viewSize, ViewShare, MEM_TOP_DOWN, PAGE_READWRITE);
  53. if (!NT_SUCCESS(status))
  54. {
  55. ZwClose(hSection);
  56. ZwClose(hFile);
  57. KdPrint(("ZwMapViewOfSection Error! [error code: 0x%X]", status));
  58. return status;
  59. }
  60. // 返回数据
  61. *phFile = hFile;
  62. *phSection = hSection;
  63. *ppBaseAddress = pBaseAddress;
  64. return status;
  65. }
  66. // 根据导出表获取导出函数地址, 从而获取 SSDT 函数索引号
  67. ULONG GetIndexFromExportTable(PVOID pBaseAddress, PCHAR pszFunctionName)
  68. {
  69. ULONG ulFunctionIndex = 0;
  70. // Dos Header
  71. PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
  72. // NT Header
  73. PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
  74. // Export Table
  75. PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
  76. // 有名称的导出函数个数
  77. ULONG ulNumberOfNames = pExportTable->NumberOfNames;
  78. // 导出函数名称地址表
  79. PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
  80. PCHAR lpName = NULL;
  81. // 开始遍历导出表
  82. for (ULONG i = 0; i < ulNumberOfNames; i++)
  83. {
  84. lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);
  85. // 判断是否查找的函数
  86. if (0 == _strnicmp(pszFunctionName, lpName, strlen(pszFunctionName)))
  87. {
  88. // 获取导出函数地址
  89. USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
  90. ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
  91. PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);
  92. // 获取 SSDT 函数 Index
  93. #ifdef _WIN64
  94. ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4);
  95. #else
  96. ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 1);
  97. #endif
  98. break;
  99. }
  100. }
  101. return ulFunctionIndex;
  102. }

获取SSDT表地址

  1. // 根据特征码, 从 KiSystemCall64 中获取 SSDT 地址
  2. PVOID GetSSDTAddress()
  3. {
  4. PVOID pServiceDescriptorTable = NULL;
  5. PVOID pKiSystemCall64 = NULL;
  6. UCHAR ulCode1 = 0;
  7. UCHAR ulCode2 = 0;
  8. UCHAR ulCode3 = 0;
  9. // 注意使用有符号整型
  10. LONG lOffset = 0;
  11. // 获取 KiSystemCall64 函数地址
  12. pKiSystemCall64 = (PVOID)__readmsr(0xC0000082);
  13. // 搜索特征码 4C8D15
  14. for (ULONG i = 0; i < 1024; i++)
  15. {
  16. // 获取内存数据
  17. ulCode1 = *((PUCHAR)((PUCHAR)pKiSystemCall64 + i));
  18. ulCode2 = *((PUCHAR)((PUCHAR)pKiSystemCall64 + i + 1));
  19. ulCode3 = *((PUCHAR)((PUCHAR)pKiSystemCall64 + i + 2));
  20. // 判断
  21. if (0x4C == ulCode1 &&
  22. 0x8D == ulCode2 &&
  23. 0x15 == ulCode3)
  24. {
  25. // 获取偏移
  26. lOffset = *((PLONG)((PUCHAR)pKiSystemCall64 + i + 3));
  27. // 根据偏移计算地址
  28. pServiceDescriptorTable = (PVOID)(((PUCHAR)pKiSystemCall64 + i) + 7 + lOffset);
  29. break;
  30. }
  31. }
  32. return pServiceDescriptorTable;
  33. }

获取SSDT函数地址

  1. // 获取 SSDT 函数地址
  2. PVOID GetSSDTFunction(PCHAR pszFunctionName)
  3. {
  4. UNICODE_STRING ustrDllFileName;
  5. ULONG ulSSDTFunctionIndex = 0;
  6. PVOID pFunctionAddress = NULL;
  7. PSSDTEntry pServiceDescriptorTable = NULL;
  8. ULONG ulOffset = 0;
  9. RtlInitUnicodeString(&ustrDllFileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
  10. // 从 ntdll.dll 中获取 SSDT 函数索引号
  11. ulSSDTFunctionIndex = GetSSDTFunctionIndex(ustrDllFileName, pszFunctionName);
  12. // 根据特征码, 从 KiSystemCall64 中获取 SSDT 地址
  13. pServiceDescriptorTable = GetSSDTAddress();
  14. // 根据索引号, 从SSDT表中获取对应函数偏移地址并计算出函数地址
  15. ulOffset = pServiceDescriptorTable->ServiceTableBase[ulSSDTFunctionIndex] >> 4;
  16. pFunctionAddress = (PVOID)((PUCHAR)pServiceDescriptorTable->ServiceTableBase + ulOffset);
  17. // 显示
  18. DbgPrint("[%s][SSDT Addr:0x%p][Index:%d][Address:0x%p]\n", pszFunctionName, pServiceDescriptorTable, ulSSDTFunctionIndex, pFunctionAddress);
  19. return pFunctionAddress;
  20. }

程序测试

在 Win7 64 位系统下,驱动程序正常执行:

在 Win10 64 位系统下,驱动程序正常执行:

总结

64 位下的 SSDT 表不再由 Ntoskrnl.exe 导出,我们需要从 KiSystemCall64 函数中扫描内存,计算出 KeServiceDescriptorTable 的地址。而且,SSDT 结构体的数据类型和含义也有些变化。ServiceTableBase 中并不存储完整的 SSDT 函数地址,这点要注意。

对于 SSDT 函数索引号的获取,可以从 ntdll.dll 的导出函数中获取,因为 ntdll.dll 导出函数的开头,总是将 SSDT 函数索引号 mov 到 eax 寄存器,所以,我们可以直接根据 ntdll.dll 的导出函数地址,获取 SSDT 函数索引号。

参考

参考自《Windows黑客编程技术详解》一书

上传的附件 cloud_download SSDTFunction_64_Test.7z ( 10.03kb, 6次下载 )

发送私信

这一切都不是我的,但总有一天,会是我的

73
文章数
67
评论数
最近文章
eject