对象句柄

通过使用内核对象句柄检测调试器存在,某些接受内核对象句柄作为其参数的WinAPI函数在调试下可能会表现不同,或者会由于调试器的实现而产生副作用。此外,在调试开始时,操作系统会创建特定的内核对象。


1. OpenProcess()

csrss.exe进程上使用kernel32!OpenProcess()函数,可以检测到某些调试器。只有当该进程的用户是管理员组的成员并且具有调试权限时,调用才会成功。


C / C ++代码

  1. typedef DWORD (WINAPI *TCsrGetProcessId)(VOID);
  2. bool Check()
  3. {
  4. HMODULE hNtdll = LoadLibraryA("ntdll.dll");
  5. if (!hNtdll)
  6. return false;
  7. TCsrGetProcessId pfnCsrGetProcessId = (TCsrGetProcessId)GetProcAddress(hNtdll, "CsrGetProcessId");
  8. if (!pfnCsrGetProcessId)
  9. return false;
  10. HANDLE hCsr = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pfnCsrGetProcessId());
  11. if (hCsr != NULL)
  12. {
  13. CloseHandle(hCsr);
  14. return true;
  15. }
  16. else
  17. return false;
  18. }

2. CreateFile()

CREATE_PROCESS_DEBUG_EVENT事件发生时,调试文件的句柄存储在CREATE_PROCESS_DEBUG_INFO结构。因此,调试器可以从该文件中读取调试信息。如果调试器未关闭此句柄,则不会以独占方式打开文件。一些调试器可能会忘记关闭句柄。

此技巧使用kernel32!CreateFileW()(或kernel32!CreateFileA())以独占方式打开当前进程的文件。如果调用失败,我们可以认为当前进程正在调试器的存在下运行。


C / C ++代码

  1. bool Check()
  2. {
  3. CHAR szFileName[MAX_PATH];
  4. if (0 == GetModuleFileNameA(NULL, szFileName, sizeof(szFileName)))
  5. return false;
  6. return INVALID_HANDLE_VALUE == CreateFileA(szFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);
  7. }

3. CloseHandle()

如果进程在调试器下运行,并且无效句柄传递给ntdll!NtClose()kernel32!CloseHandle()函数,则将引发EXCEPTION_INVALID_HANDLE0xC0000008)异常。异常可以由异常处理程序缓存。如果将控件传递给异常处理程序,则表明存在调试器。


C / C ++代码

  1. bool Check()
  2. {
  3. __try
  4. {
  5. CloseHandle((HANDLE)0xDEADBEEF);
  6. return false;
  7. }
  8. __except (EXCEPTION_INVALID_HANDLE == GetExceptionCode()
  9. ? EXCEPTION_EXECUTE_HANDLER
  10. : EXCEPTION_CONTINUE_SEARCH)
  11. {
  12. return true;
  13. }
  14. }

4. LoadLibrary()

当使用kernel32!LoadLibraryW()(或kernel32!LoadLibraryA())函数将文件加载到进程内存时,将发生LOAD_DLL_DEBUG_EVENT事件。加载文件的句柄将存储在LOAD_DLL_DEBUG_INFO结构中。因此,调试器可以从该文件中读取调试信息。如果调试器未关闭此句柄,则不会以独占方式打开文件。一些调试器可能会忘记关闭句柄。

为了检查调试器是否存在,我们可以使用kernel32!LoadLibraryA()加载任何文件,并尝试使用kernel32!CreateFileA()专门打开它。如果kernel32!CreateFileA()调用失败,则表明存在调试器。


C / C ++代码

  1. bool Check()
  2. {
  3. CHAR szBuffer[] = { "C:\\Windows\\System32\\calc.exe" };
  4. LoadLibraryA(szBuffer);
  5. return INVALID_HANDLE_VALUE == CreateFileA(szBuffer, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
  6. }

5. NtQueryObject()

当调试会话开始时,将创建一个称为“调试对象”的内核对象,并将一个句柄与之关联。使用ntdll!NtQueryObject()函数,可以查询现有对象的列表,并检查与存在的任何调试对象关联的句柄数。

但是,该技术不能确定当前是否正在调试当前进程。它仅显示自系统启动以来调试器是否在系统上完全运行。


C / C ++代码

  1. typedef struct _OBJECT_TYPE_INFORMATION
  2. {
  3. UNICODE_STRING TypeName;
  4. ULONG TotalNumberOfHandles;
  5. ULONG TotalNumberOfObjects;
  6. } OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
  7. typedef struct _OBJECT_ALL_INFORMATION
  8. {
  9. ULONG NumberOfObjects;
  10. OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];
  11. } OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;
  12. typedef NTSTATUS (WINAPI *TNtQueryObject)(
  13. HANDLE Handle,
  14. OBJECT_INFORMATION_CLASS ObjectInformationClass,
  15. PVOID ObjectInformation,
  16. ULONG ObjectInformationLength,
  17. PULONG ReturnLength
  18. );
  19. enum { ObjectAllTypesInformation = 3 };
  20. #define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
  21. bool Check()
  22. {
  23. bool bDebugged = false;
  24. NTSTATUS status;
  25. LPVOID pMem = nullptr;
  26. ULONG dwMemSize;
  27. POBJECT_ALL_INFORMATION pObjectAllInfo;
  28. PBYTE pObjInfoLocation;
  29. HMODULE hNtdll;
  30. TNtQueryObject pfnNtQueryObject;
  31. hNtdll = LoadLibraryA("ntdll.dll");
  32. if (!hNtdll)
  33. return false;
  34. pfnNtQueryObject = (TNtQueryObject)GetProcAddress(hNtdll, "NtQueryObject");
  35. if (!pfnNtQueryObject)
  36. return false;
  37. status = pfnNtQueryObject(
  38. NULL,
  39. (OBJECT_INFORMATION_CLASS)ObjectAllTypesInformation,
  40. &dwMemSize, sizeof(dwMemSize), &dwMemSize);
  41. if (STATUS_INFO_LENGTH_MISMATCH != status)
  42. goto NtQueryObject_Cleanup;
  43. pMem = VirtualAlloc(NULL, dwMemSize, MEM_COMMIT, PAGE_READWRITE);
  44. if (!pMem)
  45. goto NtQueryObject_Cleanup;
  46. status = pfnNtQueryObject(
  47. (HANDLE)-1,
  48. (OBJECT_INFORMATION_CLASS)ObjectAllTypesInformation,
  49. pMem, dwMemSize, &dwMemSize);
  50. if (!SUCCEEDED(status))
  51. goto NtQueryObject_Cleanup;
  52. pObjectAllInfo = (POBJECT_ALL_INFORMATION)pMem;
  53. pObjInfoLocation = (PBYTE)pObjectAllInfo->ObjectTypeInformation;
  54. for(UINT i = 0; i < pObjectAllInfo->NumberOfObjects; i++)
  55. {
  56. POBJECT_TYPE_INFORMATION pObjectTypeInfo =
  57. (POBJECT_TYPE_INFORMATION)pObjInfoLocation;
  58. if (wcscmp(L"DebugObject", pObjectTypeInfo->TypeName.Buffer) == 0)
  59. {
  60. if (pObjectTypeInfo->TotalNumberOfObjects > 0)
  61. bDebugged = true;
  62. break;
  63. }
  64. // Get the address of the current entries
  65. // string so we can find the end
  66. pObjInfoLocation = (PBYTE)pObjectTypeInfo->TypeName.Buffer;
  67. // Add the size
  68. pObjInfoLocation += pObjectTypeInfo->TypeName.Length;
  69. // Skip the trailing null and alignment bytes
  70. ULONG tmp = ((ULONG)pObjInfoLocation) & -4;
  71. // Not pretty but it works
  72. pObjInfoLocation = ((PBYTE)tmp) + sizeof(DWORD);
  73. }
  74. NtQueryObject_Cleanup:
  75. if (pMem)
  76. VirtualFree(pMem, 0, MEM_RELEASE);
  77. return bDebugged;
  78. }

缓解措施

减轻这些检查的最简单方法是手动跟踪程序直到进行检查,然后将其跳过(例如,使用NOP修补或更改指令指针或在检查之后更改零标志)。

如果编写防反调试解决方案,则需要挂钩列出的函数并在分析它们的输入之后更改返回值:

  • ntdll!OpenProcess:如果第三个参数是crsss.exe的句柄,则返回NULL
  • ntdll!NtClose:您可以检查是否可以使用ntdll!NtQueryObject()检索有关输入句柄的任何信息,并且如果句柄无效,则不会引发异常。
  • ntdll!NtQueryObject:如果查询了ObjectAllTypesInformation类,则从结果中过滤调试对象。

以下技术应在不使用钩子的情况下进行处理:

  • ntdll!NtCreateFile:太通用而无法缓解。但是,如果您为特定的调试器编写插件,则可以确保关闭调试文件的句柄。
  • kernel32!LoadLibraryW/A:无缓解措施。