学海无涯哟
部分参考
https://www.cnblogs.com/iBinary/p/10990674.html
https://whitebird0.github.io/post/%E5%8F%8D%E8%B0%83%E8%AF%95%E6%8A%80%E6%9C%AF
https://blog.csdn.net/qq_32400847/article/details/52798050
https://blog.csdn.net/weixin_45551083/article/details/106949220
https://www.cnblogs.com/revercc/p/13718778.html
https://www.cnblogs.com/revercc/p/13721197.html
https://www.52pojie.cn/thread-1523805-1-1.html
https://blog.csdn.net/weixin_44604541/article/details/124981176
https://cloud.tencent.com/developer/article/1471373
https://www.52pojie.cn/thread-1523805-1-1.html
https://www.cnblogs.com/youyaoqi/p/15518314.html
https://www.freebuf.com/articles/others-articles/181085.html
https://www.cnblogs.com/Sna1lGo/p/14732048.html
https://anti-debug.checkpoint.com/techniques/misc.html
https://www.chinapyg.com/thread-140342-1-1.html
调试标志
- 系统表中的特殊标志(驻留在进程内存中并由操作系统设置)可用于显示进程调试状态。使用特定的 API 函数或者检查内存中的系统表能够获取这些标志的状态。
- 最常见的反调试技术。
Win32 API
IsDebuggerPresent()
- 函数 kernel32.IsDebuggerPresent() 确定当前进程是否被用户模式调试器(如 OllyDbg或 x64dbg)调试。通常,该函数只检查进程环境块(PEB)的 BeingDebugging 标志。
如果正在调试进程,则可以使用以下代码终止该进程: 汇编:
call IsDebuggerPresent test al, al jne being_debugged ... being_debugged: push 1 call ExitProcess
c 语言:
if (IsDebuggerPresent()) ExitProcess(-1);
CheckRemoteDebuggerPresent()
- 函数 kernel32.CheckRemoteDebuggerPresent() 检查调试器(在同一台计算机上的不同进程中)是否附加到当前进程。
c 语言:
BOOL bDebuggerPresent; if (TRUE == CheckRemoteDebuggerPresent(GetCurrentProcess(), &bDebuggerPresent) && TRUE == bDebuggerPresent) // 函数执行成功且返回值 bDebuggerPresent 不为 0 ExitProcess(-1);
x86 汇编:
lea eax, [bDebuggerPresent] push eax push -1 ; GetCurrentProcess() call CheckRemoteDebuggerPresent cmp [bDebuggerPresent], 1 jz being_debugged ... being_debugged: push -1 call ExitProcess
x64 汇编:
lea rdx, [bDebuggerPresent] mov rcx, -1 ; GetCurrentProcess() call CheckRemoteDebuggerPresent cmp [bDebuggerPresent], 1 jz being_debugged ... being_debugged: mov ecx, -1 call ExitProcess
NtQueryInformationProcess()
- 函数 ntdll.NtQueryInformationProcess() 可以从进程中检索不同类型的信息。它接受 ProcessInformationClass 参数,该参数指定要获取的信息并定义 ProcessInformation 参数的输出类型。
ProcessDebugPort
- 使用 ntdll.NtQueryInformationProcess() 检索进程的调试器的端口号。如果正在调试进程,它会检索出一个等于 0xFFFFFFFF(十进制 -1)的 DWORD 值。有关 ProcessDebugPort 的官方说明 ntQueryInformationProcess 函数 (winternl.h) - Win32 应用 |Microsoft 学习
c 语言:
typedef NTSTATUS (NTAPI *TNtQueryInformationProcess)( IN HANDLE ProcessHandle, IN PROCESSINFOCLASS ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength ); HMODULE hNtdll = LoadLibraryA("ntdll.dll"); // 加载动态链接库 if (hNtdll) { auto pfnNtQueryInformationProcess = (TNtQueryInformationProcess)GetProcAddress( hNtdll, "NtQueryInformationProcess"); // 获取 NtQueryInformationProcess 函数地址 if (pfnNtQueryInformationProcess) { DWORD dwProcessDebugPort, dwReturned; NTSTATUS status = pfnNtQueryInformationProcess( GetCurrentProcess(), ProcessDebugPort, // 数字 7 &dwProcessDebugPort, sizeof(DWORD), &dwReturned); // NT_SUCCESS 是 Windows 内核中的一个宏定义,用于表示操作是否成功。在 Windows 内核编程中,许多函数和方法返回一个 NTSTATUS 值来指示操作的结果。NT_SUCCESS 宏用于检查 NTSTATUS 值是否代表成功。 if (NT_SUCCESS(status) && (-1 == dwProcessDebugPort)) ExitProcess(-1); } }
x86 汇编:
lea eax, [dwReturned] push eax ; ReturnLength push 4 ; ProcessInformationLength lea ecx, [dwProcessDebugPort] push ecx ; ProcessInformation push 7 ; ProcessInformationClass,这里是 ProcessDebugPort,也就是数字 7 push -1 ; ProcessHandle call NtQueryInformationProcess inc dword ptr [dwProcessDebugPort] jz being_debugged ... being_debugged: push -1 call ExitProcess
x64 汇编:
lea rcx, [dwReturned] push rcx ; ReturnLength mov r9d, 4 ; ProcessInformationLength lea r8, [dwProcessDebugPort] ; ProcessInformation mov edx, 7 ; ProcessInformationClass mov rcx, -1 ; ProcessHandle call NtQueryInformationProcess cmp dword ptr [dwProcessDebugPort], -1 jz being_debugged ... being_debugged: mov ecx, -1 call ExitProcess
ProcessDebugFlags
- 一个名为 EPROCESS 的内核结构表示一个进程对象,它包含字段 NoDebugInherit。可以使用未记录的类 ProcessDebugFlags(0x1f)检索此字段的返回值。如果返回值为 0,则表示存在调试器。也可以在 www.vergiliusproject.com 搜索 _EPROCESS 可以查看这个结构具体信息。
c 语言:
typedef NTSTATUS(NTAPI *TNtQueryInformationProcess)( IN HANDLE ProcessHandle, IN DWORD ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength ); HMODULE hNtdll = LoadLibraryA("ntdll.dll"); if (hNtdll) { auto pfnNtQueryInformationProcess = (TNtQueryInformationProcess)GetProcAddress( hNtdll, "NtQueryInformationProcess"); if (pfnNtQueryInformationProcess) { DWORD dwProcessDebugFlags, dwReturned; const DWORD ProcessDebugFlags = 0x1f; NTSTATUS status = pfnNtQueryInformationProcess( GetCurrentProcess(), ProcessDebugFlags, &dwProcessDebugFlags, sizeof(DWORD), &dwReturned); if (NT_SUCCESS(status) && (0 == dwProcessDebugFlags)) ExitProcess(-1); } }
x86 汇编:
lea eax, [dwReturned] push eax ; ReturnLength push 4 ; ProcessInformationLength lea ecx, [dwProcessDebugFlags] push ecx ; ProcessInformation push 1Fh ; ProcessInformationClass push -1 ; ProcessHandle call NtQueryInformationProcess cmp dword ptr [dwProcessDebugFlags], 0 jz being_debugged ... being_debugged: push -1 call ExitProcess
x64 汇编:
lea rcx, [dwReturned] push rcx ; ReturnLength mov r9d, 4 ; ProcessInformationLength lea r8, [dwProcessDebugFlags] ; ProcessInformation mov edx, 1Fh ; ProcessInformationClass mov rcx, -1 ; ProcessHandle call NtQueryInformationProcess cmp dword ptr [dwProcessDebugFlags], 0 jz being_debugged ... being_debugged: mov ecx, -1 call ExitProcess
ProcessDebugObjectHandle
- 调试开始时,将创建一个名为“调试对象(debug object)”的内核对象。可以使用未记录的 ProcessDebugObjectHandle(0x1e)类查询此句柄的值。
c 语言:
typedef NTSTATUS(NTAPI * TNtQueryInformationProcess)( IN HANDLE ProcessHandle, IN DWORD ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength ); HMODULE hNtdll = LoadLibraryA("ntdll.dll"); if (hNtdll) { auto pfnNtQueryInformationProcess = (TNtQueryInformationProcess)GetProcAddress( hNtdll, "NtQueryInformationProcess"); if (pfnNtQueryInformationProcess) { DWORD dwReturned; HANDLE hProcessDebugObject = 0; const DWORD ProcessDebugObjectHandle = 0x1e; NTSTATUS status = pfnNtQueryInformationProcess( GetCurrentProcess(), ProcessDebugObjectHandle, &hProcessDebugObject, sizeof(HANDLE), &dwReturned); if (NT_SUCCESS(status) && (0 != hProcessDebugObject)) ExitProcess(-1); } }
x86 汇编:
lea eax, [dwReturned] push eax ; ReturnLength push 4 ; ProcessInformationLength lea ecx, [hProcessDebugObject] push ecx ; ProcessInformation push 1Eh ; ProcessInformationClass push -1 ; ProcessHandle call NtQueryInformationProcess cmp dword ptr [hProcessDebugObject], 0 jnz being_debugged ... being_debugged: push -1 call ExitProcess
x64 汇编:
lea rcx, [dwReturned] push rcx ; ReturnLength mov r9d, 4 ; ProcessInformationLength lea r8, [hProcessDebugObject] ; ProcessInformation mov edx, 1Eh ; ProcessInformationClass mov rcx, -1 ; ProcessHandle call NtQueryInformationProcess cmp dword ptr [hProcessDebugObject], 0 jnz being_debugged ... being_debugged: mov ecx, -1 call ExitProcess
RtlQueryProcessHeapInformation()
- ntdll.RtlQueryProcessHeapInformation() 函数可用于从当前进程的进程内存中读取堆标志。
c 语言:
bool Check() { ntdll::PDEBUG_BUFFER pDebugBuffer = ntdll::RtlCreateQueryDebugBuffer(0, FALSE); // 创建调试缓冲区,用于记录调试信息。 if(!SUCCEEDED(ntdll::RtlQueryProcessHeapInformation((ntdll::PRTL_DEBUG_INFORMATION)pDebugBuffer))) return false; ULONG dwFlags = ((ntdll::PRTL_PROCESS_HEAPS)pDebugBuffer->HeapInformation)->Heaps[0].Flags; return dwFlags & ~HEAP_GROWABLE; }
RtlQueryProcessDebugInformation()
- ntdll.RtlQueryProcessDebugInformation() 函数可用于从被请求进程的进程内存中读取某些字段,包括堆标志寄存器。
c 语言:
bool Check() { ntdll::PDEBUG_BUFFER pDebugBuffer = ntdll::RtlCreateQueryDebugBuffer(0, FALSE); if (!SUCCEEDED(ntdll::RtlQueryProcessDebugInformation(GetCurrentProcessId(), ntdll::PDI_HEAPS | ntdll::PDI_HEAP_BLOCKS, pDebugBuffer))) return false; ULONG dwFlags = ((ntdll::PRTL_PROCESS_HEAPS)pDebugBuffer->HeapInformation)->Heaps[0].Flags; return dwFlags & ~HEAP_GROWABLE; }
NtQuerySystemInformation()
- ntdll.NtQuerySystemInformation() 函数接受要查询的信息类别作为参数。包括 SystemKernelDebuggerInformation(0x23)类在内的大多数类都没有被记录下来。SystemKernelDebuggerInformation 类返回两个标志寄存器的值。al 中的 KdDebuggerEnabled,和 ah 中的 KdDebuggerNotPresent。因此,如果内核调试器存在,ah 中的返回值为零。
c 语言:
enum { SystemKernelDebuggerInformation = 0x23 }; typedef struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION { BOOLEAN DebuggerEnabled; BOOLEAN DebuggerNotPresent; } SYSTEM_KERNEL_DEBUGGER_INFORMATION, *PSYSTEM_KERNEL_DEBUGGER_INFORMATION; bool Check() { NTSTATUS status; SYSTEM_KERNEL_DEBUGGER_INFORMATION SystemInfo; status = NtQuerySystemInformation( (SYSTEM_INFORMATION_CLASS)SystemKernelDebuggerInformation, &SystemInfo, sizeof(SystemInfo), NULL); return SUCCEEDED(status) ? (SystemInfo.DebuggerEnabled && !SystemInfo.DebuggerNotPresent) : false; }
解决方案
- 对于 IsDebuggerPresent()。将进程环境块(PEB)的 BeingDebugged 标志设置为 0。 更多信息请参见 BeingDebugged 标志寄存器反制措施。
- 对于 CheckRemoteDebuggerPresent() 和 NtQueryInformationProcess():
由于 CheckRemoteDebuggerPresent() 调用 NtQueryInformationProcess(),唯一的方法是拦截 NtQueryInformationProcess() 并在返回缓冲区设置以下值。
- 如果是 ProcessDebugPort 查询,则为 0(或除 -1 外的任何值)。
- 如果是 ProcessDebugFlags 查询,则为非零值。
- 在 ProcessDebugObjectHandle 查询的情况下为 0。
反制用 RtlQueryProcessHeapInformation()、RtlQueryProcessDebugInformation() 和 NtQuerySystemInformation() 函数反调试的唯一方法是拦截它们并修改返回值:
手动检查
- 不使用调试 API 函数,手工检查进程的内存。
PEB.BeingDebugged 标志
- 这个方法不需要调用 IsDebuggerPresent() 检查 PEB 的 BeingDebugged 标志寄存器的另一种方法。
32 位进程:
mov eax, fs:[30h] cmp byte ptr [eax+2], 0 jne being_debugged
64 位进程:
mov rax, gs:[60h] cmp byte ptr [rax+2], 0 jne being_debugged
WOW64 进程:
mov eax, fs:[30h] cmp byte ptr [eax+1002h], 0
WOW64(Windows 32-bit on Windows 64-bit)是一种技术,用于在64位操作系统上运行32位应用程序。它提供了一个兼容层,使得32位应用程序可以在64位操作系统上运行,同时仍然能够访问系统资源和功能。和 64 位进程区别如下:
- 运行环境:WOW64 进程是 32 位应用程序在 64 位操作系统上的运行环境,它的二进制代码是 32 位的;而 64 位进程是原生 64 位应用程序,它的二进制代码是 64 位的。
- 内存使用:WOW64 进程只能访问 32 位地址空间,最多可使用 4GB(实际上因为操作系统本身和其他限制,可用内存可能更少),并且受到 32 位进程的内存限制;而 64 位进程可以访问更大的地址空间,通常是16EB(Exabyte,即十亿千字节)。
- 寄存器和指令集:WOW64 进程使用 32 位寄存器和指令集,与传统 32 位应用程序相同;而 64 位进程使用 64 位寄存器和指令集,具有更高的计算能力和扩展性。
- 兼容性:WOW64 进程可以运行 32 位的应用程序和组件,但可能无法直接运行一些 64 位特定的应用程序或组件;而 64 位进程可以运行 64 位特定的应用程序和组件,并且通常能够更好地利用系统资源。
c 语言:
#ifndef _WIN64 PPEB pPeb = (PPEB)__readfsdword(0x30); #else PPEB pPeb = (PPEB)__readgsqword(0x60); #endif // _WIN64 if (pPeb->BeingDebugged) goto being_debugged;
NtGlobalFlag
进程环境块的 NtGlobalFlag 字段(32位 Windows 的 0x68 偏移,64 位 Windows 的 0xBC)默认为 0。附加一个调试器并不改变 NtGlobalFlag 的值。但是,如果进程是由调试器创建的,则将设置以下标志寄存器:
- FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
- FLG_HEAP_ENABLE_FREE_CHECK (0x20)
- FLG_HEAP_VALIDATE_PARAMETERS (0x40)
32 位进程:
mov eax, fs:[30h]
mov al, [eax+68h]
and al, 70h
cmp al, 70h ; 如果进程被调试这个成员通常值为0x70(代表上述三个标志被设置)
jz being_debugged
64 位进程:
mov rax, gs:[60h]
mov al, [rax+BCh]
and al, 70h
cmp al, 70h
jz being_debugged
WOW64 进程:
mov eax, fs:[30h]
mov al, [eax+10BCh]
and al, 70h
cmp al, 70h
jz being_debugged
c 语言:
#define FLG_HEAP_ENABLE_TAIL_CHECK 0x10
#define FLG_HEAP_ENABLE_FREE_CHECK 0x20
#define FLG_HEAP_VALIDATE_PARAMETERS 0x40
#define NT_GLOBAL_FLAG_DEBUGGED (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS)
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0x68);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0xBC);
#endif // _WIN64
if (dwNtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED)
goto being_debugged;
堆标志寄存器
- 堆包含两个字段,它们会受到调试器存在的影响。具体如何影响,取决于 Windows 的版本。这些字段是标志寄存器 (Flags) 和强标志寄存器(ForceFlags)。
- 标志寄存器和强标志寄存器的值通常分别设置为 HEAP_GROWABLE 和 0。
当调试器出现时,在 Windows NT、Windows 2000 和 32 位 Windows XP 上,标志寄存器字段被设置为这些标志寄存器的组合。
- HEAP_GROWABLE (2)
- HEAP_TAIL_CHECKING_ENABLED (0x20)
- HEAP_FREE_CHECKING_ENABLED (0x40)
- HEAP_SKIP_VALIDATION_CHECKS (0x10000000)
- HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)
在 64 位的 Windows XP 和 Windows Vista 及更高版本中,如果有调试器存在,Flags 字段会被设置为这些标志寄存器组合:
当调试器出现时,ForceFlags 字段被设置为这些标志寄存器的组合:
c 语言:
bool Check()
{
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
PVOID pHeapBase = !m_bIsWow64
? (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x18))
: (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x1030));
DWORD dwHeapFlagsOffset = IsWindowsVistaOrGreater()
? 0x40
: 0x0C;
DWORD dwHeapForceFlagsOffset = IsWindowsVistaOrGreater()
? 0x44
: 0x10;
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
PVOID pHeapBase = (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x30));
DWORD dwHeapFlagsOffset = IsWindowsVistaOrGreater()
? 0x70
: 0x14;
DWORD dwHeapForceFlagsOffset = IsWindowsVistaOrGreater()
? 0x74
: 0x18;
#endif // _WIN64
PDWORD pdwHeapFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapFlagsOffset);
PDWORD pdwHeapForceFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapForceFlagsOffset);
return (*pdwHeapFlags & ~HEAP_GROWABLE) || (*pdwHeapForceFlags != 0);
}
堆保护
- 如果在 NtGlobalFlag 中设置了 HEAP_TAIL_CHECKING_ENABLED 标志寄存器,序列 0xABABABAB 将被附加在所分配的堆块的末端(在 32 位 Windows 中为 2 次,在 64 位 Windows 中为 4 次)。
- 如果在 NtGlobalFlag 中设置了 HEAP_FREE_CHECKING_ENABLED 标志寄存器,如果需要额外的字节来填充空的空间,直到下一个内存块,那么将附加序列 0xFEEEFEEE。
c 语言:
bool Check() { PROCESS_HEAP_ENTRY HeapEntry = { 0 }; do { if (!HeapWalk(GetProcessHeap(), &HeapEntry)) return false; } while (HeapEntry.wFlags != PROCESS_HEAP_ENTRY_BUSY); PVOID pOverlapped = (PBYTE)HeapEntry.lpData + HeapEntry.cbData; return ((DWORD)(*(PDWORD)pOverlapped) == 0xABABABAB); }
检查 KUSER_SHARED_DATA 结构
- 这种技术最初被描述为 TitanHide 的一个问题,TitanHie 是一个内核驱动程序,用于隐藏调试器以防止检测。结构 KUSER_SHARED_DATA 及其字段的详细文档可在此处获得。
以下是该问题的作者在帖子中关于结构特征及其适当字段的内容:“0x7ffe02d4 实际上是 0x7ffe0000+0x2d4。0x7ffe0000 是 KUSER_SHARED_DATA 结构的固定用户模式地址,该结构包含在用户模式和内核之间共享的数据(尽管用户模式不能对其进行写访问)。该结构具有一些有趣的属性:
- 它的地址是固定的,自推出以来一直存在于所有 Windows 版本中
- 其用户模式地址在 32 位和 64 位模式中相同
- 所有的偏移量和大小都是严格固定的,并且只添加新字段来代替未使用的填充空间
c 语言:
bool check_kuser_shared_data_structure()
{
unsigned char b = *(unsigned char*)0x7ffe02d4;
return ((b & 0x01) || (b & 0x02));
}
解决方案
PEB.BeingDebugged 标志
- 将 BeingDebugged 标志设置为 0。这可以通过 DLL 注入来完成。如果您使用 OllyDbg 或 x32/64dbg 作为调试器,您可以选择各种反调试插件,如 ScyllaHide。
c 语言:
#ifndef _WIN64 PPEB pPeb = (PPEB)__readfsdword(0x30); #else PPEB pPeb = (PPEB)__readgsqword(0x60); #endif // _WIN64 pPeb->BeingDebugged = 0;
NtGlobalFlag
- 将 NtGlobalFlag 设置为 0。这可以通过 DLL 注入来完成。如果您使用 OllyDbg 或 x32/64dbg 作为调试器,您可以选择各种反调试插件,如 ScyllaHide。
c 语言:
#ifndef _WIN64 PPEB pPeb = (PPEB)__readfsdword(0x30); *(PDWORD)((PBYTE)pPeb + 0x68) = 0; #else PPEB pPeb = (PPEB)__readgsqword(0x60); *(PDWORD)((PBYTE)pPeb + 0xBC); = 0; #endif // _WIN64
堆标志
- 将堆标志设置为 0。这可以通过 DLL 注入来完成。如果您使用 OllyDbg 或 x32/64dbg 作为调试器,您可以选择各种反调试插件,如 ScyllaHide。
c 语言:
#ifndef _WIN64 PPEB pPeb = (PPEB)__readfsdword(0x30); PVOID pHeapBase = !m_bIsWow64 ? (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x18)) : (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x1030)); DWORD dwHeapFlagsOffset = IsWindowsVistaOrGreater() ? 0x40 : 0x0C; DWORD dwHeapForceFlagsOffset = IsWindowsVistaOrGreater() ? 0x44 : 0x10; #else PPEB pPeb = (PPEB)__readgsqword(0x60); PVOID pHeapBase = (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x30)); DWORD dwHeapFlagsOffset = IsWindowsVistaOrGreater() ? 0x70 : 0x14; DWORD dwHeapForceFlagsOffset = IsWindowsVistaOrGreater() ? 0x74 : 0x18; #endif // _WIN64 *(PDWORD)((PBYTE)pHeapBase + dwHeapFlagsOffset) = HEAP_GROWABLE; *(PDWORD)((PBYTE)pHeapBase + dwHeapForceFlagsOffset) = 0;
堆保护
- 手动修补 32 位的堆后 12 字节和 64 位中的堆后 20 个字节。Hook 掉 kernel32.HeapAlloc(),并在分配后对堆进行修补。
c 语言:
#ifndef _WIN64 SIZE_T nBytesToPatch = 12; #else SIZE_T nBytesToPatch = 20; #endif // _WIN64 SIZE_T nDwordsToPatch = nBytesToPatch / sizeof(DWORD); PVOID pHeapEnd = (PBYTE)HeapEntry.lpData + HeapEntry.cbData; for (SIZE_T offset = 0; offset < nDwordsToPatch; offset++) *((PDWORD)pHeapEnd + offset) = 0;
KUSER_SHARED_DATA
- 关于可能的缓解措施,请在描述该技术时查看链接(TitanHide的问题),并在此处查看修补 kdcom.dll 的代码草案。
对象句柄
- 以下一组技术表示使用内核对象句柄来检测调试器是否存在的检查。某些接受内核对象句柄作为其参数的 WinAPI 函数 在调试下的行为可能会有所不同,或者由于调试器的实现而导致副作用。 此外,在调试开始时,操作系统会创建特定的内核目标。
OpenProcess()
- 可以通过在 csrss.exe 进程上使用 kernel32.OpenProcess() 函数来检测某些调试器。只有当进程的用户是 administrators 组的成员并且具有调试权限时,调用才会成功。
c 语言:
typedef DWORD (WINAPI *TCsrGetProcessId)(VOID); bool Check() { HMODULE hNtdll = LoadLibraryA("ntdll.dll"); if (!hNtdll) return false; TCsrGetProcessId pfnCsrGetProcessId = (TCsrGetProcessId)GetProcAddress(hNtdll, "CsrGetProcessId"); if (!pfnCsrGetProcessId) return false; // 使用 pfnCsrGetProcessId 函数指针调用 CsrGetProcessId 函数,获取当前进程 ID,并打开具有 PROCESS_ALL_ACCESS 权限的进程句柄,将其保存在变量 hCsr 中。 HANDLE hCsr = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pfnCsrGetProcessId()); if (hCsr != NULL) { CloseHandle(hCsr); return true; } else return false; }
CreateFile()
- 当 CREATE_PROCESS_DEBUG_EVENT 事件发生时,调试文件的句柄存储在 CREATE_PPROCESS_DEBUG_INFO 结构中。因此,调试器可以从该文件中读取调试信息。如果调试器未关闭此句柄,则不会以独占访问权限打开文件。有些调试器可能会忘记关闭句柄。
- 此技巧使用 kernel32.CreateFileW()(或 kernel32.CreateFileA() )以独占方式打开当前进程的文件。如果调用失败,我们可以认为当前进程是在调试器存在的情况下运行的。
c 语言:
bool Check() { CHAR szFileName[MAX_PATH]; if (0 == GetModuleFileNameA(NULL, szFileName, sizeof(szFileName))) return false; return INVALID_HANDLE_VALUE == CreateFileA(szFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0); }
CloseHandle()
- 如果进程在调试器下运行,并且向 ntdll.NtClose() 或 kernel32.CloseHandle() 函数传递了无效句柄,则将引发 EXCEPTION_invalid_handle(0xC0000008)异常。异常可以由异常处理程序缓存。如果将控件传递给异常处理程序,则表示存在调试器。
c 语言:
bool Check() { __try { CloseHandle((HANDLE)0xDEADBEEF); return false; } __except (EXCEPTION_INVALID_HANDLE == GetExceptionCode() ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return true; } }
LoadLibrary()
- 当使用 kernel32.LoadLibraryW()(或 kernel32.LoadLibraryA() )函数将文件加载到进程内存时,会发生 LOAD_DLL_DEBUG_EVENT 事件。加载文件的句柄将存储在 LOAD_DLL_DEBUG_INFO 结构中。因此,调试器可以从该文件中读取调试信息。如果调试器未关闭此句柄,则不会以独占访问权限打开文件。有些调试器可能会忘记关闭句柄。
- 为了检查调试器的存在,我们可以使用 kernel32.LoadLibraryA() 加载任何文件,并尝试使用 kernel32.CreateFileA()以独占方式打开它。如果 kernel32.CreateFileA() 调用失败,则表示存在调试器。
c 语言:
bool Check() { CHAR szBuffer[] = { "C:\\Windows\\System32\\calc.exe" }; LoadLibraryA(szBuffer); return INVALID_HANDLE_VALUE == CreateFileA(szBuffer, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); }
NtQueryObject()
- 调试会话开始时,会创建一个名为“调试对象(debug object)”的内核对象,并将句柄与之关联。使用 ntdll.NtQueryObject() 函数,可以查询现有对象的列表,并检查与任何现有调试对象关联的句柄数。
- 然而,这种技术不能确定当前进程是否正在调试中。它只显示从系统启动后,调试器是否在系统上运行。
c 语言:
typedef struct _OBJECT_TYPE_INFORMATION { UNICODE_STRING TypeName; ULONG TotalNumberOfHandles; ULONG TotalNumberOfObjects; } OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION; typedef struct _OBJECT_ALL_INFORMATION { ULONG NumberOfObjects; OBJECT_TYPE_INFORMATION ObjectTypeInformation[1]; } OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION; typedef NTSTATUS (WINAPI *TNtQueryObject)( HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength ); enum { ObjectAllTypesInformation = 3 }; #define STATUS_INFO_LENGTH_MISMATCH 0xC0000004 bool Check() { bool bDebugged = false; NTSTATUS status; LPVOID pMem = nullptr; ULONG dwMemSize; POBJECT_ALL_INFORMATION pObjectAllInfo; PBYTE pObjInfoLocation; HMODULE hNtdll; TNtQueryObject pfnNtQueryObject; hNtdll = LoadLibraryA("ntdll.dll"); if (!hNtdll) return false; pfnNtQueryObject = (TNtQueryObject)GetProcAddress(hNtdll, "NtQueryObject"); if (!pfnNtQueryObject) return false; status = pfnNtQueryObject( NULL, (OBJECT_INFORMATION_CLASS)ObjectAllTypesInformation, &dwMemSize, sizeof(dwMemSize), &dwMemSize); if (STATUS_INFO_LENGTH_MISMATCH != status) goto NtQueryObject_Cleanup; pMem = VirtualAlloc(NULL, dwMemSize, MEM_COMMIT, PAGE_READWRITE); if (!pMem) goto NtQueryObject_Cleanup; status = pfnNtQueryObject( (HANDLE)-1, (OBJECT_INFORMATION_CLASS)ObjectAllTypesInformation, pMem, dwMemSize, &dwMemSize); if (!SUCCEEDED(status)) goto NtQueryObject_Cleanup; pObjectAllInfo = (POBJECT_ALL_INFORMATION)pMem; pObjInfoLocation = (PBYTE)pObjectAllInfo->ObjectTypeInformation; for(UINT i = 0; i < pObjectAllInfo->NumberOfObjects; i++) { POBJECT_TYPE_INFORMATION pObjectTypeInfo = (POBJECT_TYPE_INFORMATION)pObjInfoLocation; if (wcscmp(L"DebugObject", pObjectTypeInfo->TypeName.Buffer) == 0) { if (pObjectTypeInfo->TotalNumberOfObjects > 0) bDebugged = true; break; } // Get the address of the current entries // string so we can find the end pObjInfoLocation = (PBYTE)pObjectTypeInfo->TypeName.Buffer; // Add the size pObjInfoLocation += pObjectTypeInfo->TypeName.Length; // Skip the trailing null and alignment bytes ULONG tmp = ((ULONG)pObjInfoLocation) & -4; // Not pretty but it works pObjInfoLocation = ((PBYTE)tmp) + sizeof(DWORD); } NtQueryObject_Cleanup: if (pMem) VirtualFree(pMem, 0, MEM_RELEASE); return bDebugged; }
解决方案
- 减轻这些检查的最简单方法是手动跟踪程序直到反调试函数,然后跳过它(例如,用 NOP 指令进行修改、更改指令指针、在反调试检测后更改零标志)。
如果编写反反调试解决方案,则需要 hook 列出的函数,并分析函数的参数是否是在反调试,如果在反调试则更改返回值:
- ntdll.OpenProcess:如果第三个参数是 csrss.exe 的句柄,则返回 NULL。
- ntdll.NtClose:检查是否可以使用 ntdll.NtQueryObject() 检索有关输入句柄的任何信息,如果句柄无效,则不引发异常。
- ntdll.NtQueryObject:如果 ObjectAllTypesInformation 类被查询,从结果中过滤“调试对象(debug objects)”。
在没有 hook 的情况下使用以下技术进行反反调试:
异常
- 以下方法会故意导致异常,以验证在没有调试器的情况下运行的进程是否不具有典型的进一步行为。
UnhandledExceptionFilter()
- 如果发生异常并且未注册异常处理程序(或者已注册但未处理此类异常),则将调用 kernel32.UnhandledExceptionFilter() 函数。可以使用 kernel32.SetUnhandledExceptionFilter() 注册自定义的未处理异常筛选器。但是,如果程序在调试器下运行,则不会调用自定义筛选器,并且异常将传递给调试器。因此,如果注册了未处理的异常筛选器并将控制传递给它,则该进程没有使用调试器运行。
x86 汇编(FASM)
include 'win32ax.inc' .code start: jmp begin not_debugged: invoke MessageBox,HWND_DESKTOP,"Not Debugged","",MB_OK invoke ExitProcess,0 begin: invoke SetUnhandledExceptionFilter, not_debugged int 3 jmp being_debugged being_debugged: invoke MessageBox,HWND_DESKTOP,"Debugged","",MB_OK invoke ExitProcess,0 .end start
c 语言:
LONG UnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo) { PCONTEXT ctx = pExceptionInfo->ContextRecord; ctx->Eip += 3; // Skip \xCC\xEB\x?? return EXCEPTION_CONTINUE_EXECUTION; } bool Check() { bool bDebugged = true; SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)UnhandledExceptionFilter); __asm { int 3 // CC jmp near being_debugged // EB ?? } bDebugged = false; being_debugged: return bDebugged; }
RaiseException()
- DBC_CONTROL_C 或 DBG_RIPEVENT 等异常不会传递给当前进程的异常处理程序,而是由调试器使用。这使我们可以注册一个异常处理程序,使用 kernel32.RaiseException() 函数引发这些异常,并检查控件是否传递给了我们的处理程序。如果未调用异常处理程序,则进程可能正在调试中。
c 语言:
bool Check() { __try { RaiseException(DBG_CONTROL_C, 0, 0, NULL); return true; } __except(DBG_CONTROL_C == GetExceptionCode() ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return false; } }
Hiding Control Flow with Exception Handlers
- 这种方法不检查是否存在调试器,但有助于在异常处理程序序列中隐藏程序的控制流。
- 我们可以注册一个异常处理程序(结构化或向量化),它会引发另一个异常,并将其传递给引发下一个异常的下一个处理程序,依此类推。最后,处理程序的序列应该会导致我们想要隐藏的过程。
- 使用结构化异常处理程序:
c 语言:
#include <Windows.h> void MaliciousEntry() { // ... } void Trampoline2() { __try { __asm int 3; } __except (EXCEPTION_EXECUTE_HANDLER) { MaliciousEntry(); } } void Trampoline1() { __try { __asm int 3; } __except (EXCEPTION_EXECUTE_HANDLER) { Trampoline2(); } } int main(void) { __try { __asm int 3; } __except (EXCEPTION_EXECUTE_HANDLER) {} { Trampoline1(); } return 0; }
- 使用向量化异常处理程序
c 语言:
#include <Windows.h> PVOID g_pLastVeh = nullptr; void MaliciousEntry() { // ... } LONG WINAPI ExeptionHandler2(PEXCEPTION_POINTERS pExceptionInfo) { MaliciousEntry(); ExitProcess(0); } LONG WINAPI ExeptionHandler1(PEXCEPTION_POINTERS pExceptionInfo) { if (g_pLastVeh) { RemoveVectoredExceptionHandler(g_pLastVeh); g_pLastVeh = AddVectoredExceptionHandler(TRUE, ExeptionHandler2); if (g_pLastVeh) __asm int 3; } ExitProcess(0); } int main(void) { g_pLastVeh = AddVectoredExceptionHandler(TRUE, ExeptionHandler1); if (g_pLastVeh) __asm int 3; return 0; }
解决方案
调试期间:
- 对于调试器检测检查:只需用 NOP 填充相应的反调试函数即可。
- 对于控制流隐藏:您必须手动跟踪程序直到有效负载。
时间反调试
- 当在调试器中跟踪进程时,指令执行之间会有巨大的延迟。可以使用几种方法计算代码某些部分之间的“本地”延迟,并将其与实际延迟进行比较。
RDPMC/RDTSC
- 这些指令要求在 CR4 寄存器中设置标志 PCE。
- RDPMC 指令只能在内核模式下使用。
c 语言:
bool IsDebugged(DWORD64 qwNativeElapsed) { ULARGE_INTEGER Start, End; __asm { xor ecx, ecx rdpmc mov Start.LowPart, eax mov Start.HighPart, edx } // ... some work __asm { xor ecx, ecx rdpmc mov End.LowPart, eax mov End.HighPart, edx } return (End.QuadPart - Start.QuadPart) > qwNativeElapsed; }
- RDTSC是用户模式指令。
c 语言:
bool IsDebugged(DWORD64 qwNativeElapsed) { ULARGE_INTEGER Start, End; __asm { xor ecx, ecx rdtsc mov Start.LowPart, eax mov Start.HighPart, edx } // ... some work __asm { xor ecx, ecx rdtsc mov End.LowPart, eax mov End.HighPart, edx } return (End.QuadPart - Start.QuadPart) > qwNativeElapsed; }
GetLocalTime()
c 语言:
bool IsDebugged(DWORD64 qwNativeElapsed) { SYSTEMTIME stStart, stEnd; FILETIME ftStart, ftEnd; ULARGE_INTEGER uiStart, uiEnd; GetLocalTime(&stStart); // ... some work GetLocalTime(&stEnd); if (!SystemTimeToFileTime(&stStart, &ftStart)) return false; if (!SystemTimeToFileTime(&stEnd, &ftEnd)) return false; uiStart.LowPart = ftStart.dwLowDateTime; uiStart.HighPart = ftStart.dwHighDateTime; uiEnd.LowPart = ftEnd.dwLowDateTime; uiEnd.HighPart = ftEnd.dwHighDateTime; return (uiEnd.QuadPart - uiStart.QuadPart) > qwNativeElapsed; }
GetSystemTime()
c 语言:
bool IsDebugged(DWORD64 qwNativeElapsed) { SYSTEMTIME stStart, stEnd; FILETIME ftStart, ftEnd; ULARGE_INTEGER uiStart, uiEnd; GetSystemTime(&stStart); // ... some work GetSystemTime(&stEnd); if (!SystemTimeToFileTime(&stStart, &ftStart)) return false; if (!SystemTimeToFileTime(&stEnd, &ftEnd)) return false; uiStart.LowPart = ftStart.dwLowDateTime; uiStart.HighPart = ftStart.dwHighDateTime; uiEnd.LowPart = ftEnd.dwLowDateTime; uiEnd.HighPart = ftEnd.dwHighDateTime; return (uiEnd.QuadPart - uiStart.QuadPart) > qwNativeElapsed; }
GetTickCount()
c 语言:
bool IsDebugged(DWORD dwNativeElapsed) { DWORD dwStart = GetTickCount(); // ... some work return (GetTickCount() - dwStart) > dwNativeElapsed; }
ZwGetTickCount() / KiGetTickCount()
- 这两个函数都只能在内核模式下使用。
- 就像用户模式 GetTickCount() 或 GetSystemTime() 一样,内核模式 ZwGetTickCount() 从 KUSER_SHARED_DATA 页面读取。此页面以 只读 方式映射到虚拟地址的用户模式范围,并以 读写 方式映射到内核范围。系统时钟刻度更新系统时间,系统时间直接存储在此页面中。
- ZwGetTickCount() 的使用方法与GetTickCount() 相同。使用 KiGetTickCount() 比调用 ZwGetTickCount() 快,但比直接从 KUSER_SHARED_DATA 页面读取稍慢。
c 语言:
bool IsDebugged(DWORD64 qwNativeElapsed) { ULARGE_INTEGER Start, End; __asm { int 2ah mov Start.LowPart, eax mov Start.HighPart, edx } // ... some work __asm { int 2ah mov End.LowPart, eax mov End.HighPart, edx } return (End.QuadPart - Start.QuadPart) > qwNativeElapsed; }
QueryPerformanceCounter()
c 语言:
bool IsDebugged(DWORD64 qwNativeElapsed) { LARGE_INTEGER liStart, liEnd; QueryPerformanceCounter(&liStart); // ... some work QueryPerformanceCounter(&liEnd); return (liEnd.QuadPart - liStart.QuadPart) > qwNativeElapsed; }
timeGetTime()
c 语言:
bool IsDebugged(DWORD dwNativeElapsed) { DWORD dwStart = timeGetTime(); // ... some work return (timeGetTime() - dwStart) > dwNativeElapsed; }
解决方案
- 调试期间:只需使用 NOP 填充时间检查函数,并将这些检查的结果设置为适当的值。
- 对于反反调试解决方案的开发:没有必要对它做任何事情,因为所有的定时检查都不是很可靠。您仍然可以挂接计时功能并加快通话间隔时间。
进程内存
- 进程可以检查自己的内存,以检测调试器是否存在或干扰调试器。
- 本部分包括进程内存和检查线程上下文、搜索断点和函数补丁作为反调试方法。
断点
- 进程内存并在代码中搜索软件断点,或检查 CPU 调试寄存器以确定是否设置了硬件断点。
软件断点(INT3)
- 识别 0xCC 字节的某些函数的机器代码,该字节代表 INT 3 汇编指令。
- 此方法可能会产生许多误报情况,因此应谨慎使用。
c 语言:
bool CheckForSpecificByte(BYTE cByte, PVOID pMemory, SIZE_T nMemorySize = 0) { PBYTE pBytes = (PBYTE)pMemory; for (SIZE_T i = 0; ; i++) { // Break on RET (0xC3) if we don't know the function's size if (((nMemorySize > 0) && (i >= nMemorySize)) || ((nMemorySize == 0) && (pBytes[i] == 0xC3))) break; if (pBytes[i] == cByte) return true; } return false; } bool IsDebugged() { PVOID functionsToCheck[] = { &Function1, &Function2, &Function3, }; for (auto funcAddr : functionsToCheck) { if (CheckForSpecificByte(0xCC, funcAddr)) return true; } return false; }
防跨步(Anti-Step-Over)
- 调试器允许单步执行函数调用。在这种情况下,调试器在调用之后的指令上隐式设置软件断点(即被调用函数的返回地址)。
- 为了检测是否有人试图单步执行该函数,我们可以检查返回地址处内存的第一个字节。如果软件断点 (0xCC) 位于返回地址,我们可以使用其他指令(例如 NOP)对其进行修补。它很可能会破坏代码并使进程崩溃。另一方面,我们可以用一些有意义的代码而不是 NOP 来修补返回地址,并更改程序的控制流。
直接修改内存
- 调用此函数后,可以从函数内部检查是否存在软件断点。我们可以在返回地址读取一个字节,如果该字节等于 0xCC (INT 3),则可以用 0x90 (NOP) 重写。该过程可能会崩溃,因为我们损坏了返回地址的指令。但是,如果您知道函数调用后面的指令,则可以使用此指令的第一个字节重写断点。
c 语言:
#include <intrin.h> #pragma intrinsic(_ReturnAddress) void foo() { // ... PVOID pRetAddress = _ReturnAddress(); if (*(PBYTE)pRetAddress == 0xCC) // int 3 { DWORD dwOldProtect; if (VirtualProtect(pRetAddress, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { *(PBYTE)pRetAddress = 0x90; // nop VirtualProtect(pRetAddress, 1, dwOldProtect, &dwOldProtect); } } // ... }
ReadFile()
- 该方法使用 kernel32.ReadFile() 函数在返回地址处修补代码。
- 这个方法是读取当前进程的可执行文件,并将返回地址作为输出缓冲区传递给 kernel32.ReadFile()。返回地址处的字节将使用“M”字符(PE 映像的第一个字节)进行修补,进程可能会崩溃。
c 语言:
#include <intrin.h> #pragma intrinsic(_ReturnAddress) void foo() { // ... PVOID pRetAddress = _ReturnAddress(); if (*(PBYTE)pRetAddress == 0xCC) // int 3 { DWORD dwOldProtect, dwRead; CHAR szFilePath[MAX_PATH]; HANDLE hFile; if (VirtualProtect(pRetAddress, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { if (GetModuleFileNameA(NULL, szFilePath, MAX_PATH)) { hFile = CreateFileA(szFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (INVALID_HANDLE_VALUE != hFile) ReadFile(hFile, pRetAddress, 1, &dwRead, NULL); } VirtualProtect(pRetAddress, 1, dwOldProtect, &dwOldProtect); } } // ... }
WriteProcessMemory()
- 此方法使用 kernel32.WriteProcessMemory() 函数,用于修补返回地址处的代码。
c 语言:
#include <intrin.h> #pragma intrinsic(_ReturnAddress) void foo() { // ... BYTE Patch = 0x90; PVOID pRetAddress = _ReturnAddress(); if (*(PBYTE)pRetAddress == 0xCC) { DWORD dwOldProtect; if (VirtualProtect(pRetAddress, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { WriteProcessMemory(GetCurrentProcess(), pRetAddress, &Patch, 1, NULL); VirtualProtect(pRetAddress, 1, dwOldProtect, &dwOldProtect); } } // ... }
Toolhelp32ReadProcessMemory()
- 函数 kernel32.Toolhelp32ReadProcessMemory() 允许操作者读取其他进程的内存。且它可用于检查防跨步情况。
c 语言:
#include <TlHelp32.h> bool foo() { // .. PVOID pRetAddress = _ReturnAddress(); BYTE uByte; if (FALSE != Toolhelp32ReadProcessMemory(GetCurrentProcessId(), _ReturnAddress(), &uByte, sizeof(BYTE), NULL)) { if (uByte == 0xCC) ExitProcess(0); } // .. }
内存断点
- 内存断点是通过使用保护页来实现的(至少在 OllyDbg 和 ImmunityDebugger 中是这样)。保护页为内存页访问提供一次触发警报。执行保护页时,会引发异常 STATUS_guard_page_VIOLATION。
- 可以通过在 kernel32.VirtualAlloc()、kernel32.VirtualAllocEx()、kernel32.VirtualProtect()和 kernel32.Virtual ProtectEx() 函数中设置 page_guard 页面保护修饰符来创建保护页。
- 然而,我们可以使用调试器实现内存断点的方式来检查程序是否在调试器下执行。我们可以分配一个只包含一个字节 0xC3 的可执行缓冲区,0xC3 代表 RET 指令。然后,我们将该缓冲区标记为保护页,将存在调试器的情况下处理的地址推送到堆栈,然后跳到分配的缓冲区。指令 RET 将被执行,如果调试器(OllyDbg 或 ImmunityDebugger)存在,我们将获得我们推送到堆栈的地址。如果在没有调试器的情况下执行程序,我们将使用异常处理程序。
c 语言:
bool IsDebugged() { DWORD dwOldProtect = 0; SYSTEM_INFO SysInfo = { 0 }; GetSystemInfo(&SysInfo); PVOID pPage = VirtualAlloc(NULL, SysInfo.dwPageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (NULL == pPage) return false; PBYTE pMem = (PBYTE)pPage; *pMem = 0xC3; // Make the page a guard page if (!VirtualProtect(pPage, SysInfo.dwPageSize, PAGE_EXECUTE_READWRITE | PAGE_GUARD, &dwOldProtect)) return false; __try { __asm { mov eax, pPage push mem_bp_being_debugged jmp eax } } __except(EXCEPTION_EXECUTE_HANDLER) { VirtualFree(pPage, NULL, MEM_RELEASE); return false; } mem_bp_being_debugged: VirtualFree(pPage, NULL, MEM_RELEASE); return true; }
硬件断点
- 调试寄存器 DR0、DR1、DR2 和 DR3 可以从线程上下文中检索。 如果它们包含非零值,则可能意味着进程在调试器下执行,并且设置了硬件断点。
c 语言:
bool IsDebugged() { CONTEXT ctx; ZeroMemory(&ctx, sizeof(CONTEXT)); ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; if(!GetThreadContext(GetCurrentThread(), &ctx)) return false; return ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3; }
其它内存检查
- 本部分包含直接检查或操作正在运行的进程的虚拟内存以检测或阻止调试的技术。
NtQueryVirtualMemory()
- 代码所在的进程的内存页在所有进程之间共享,直到写入页为止。之后,操作系统会复制此页面并将其映射到进程虚拟内存,以便此页面不再“共享”。
- 因此,我们可以查询当前进程的工作集,并检查带有代码的页面的工作集块的 Shared 和 ShareCount 字段。如果代码中存在软件断点,则不得设置这些字段。
NTDLL 声明
namespace ntdll { //... #define STATUS_INFO_LENGTH_MISMATCH 0xC0000004 // ... typedef enum _MEMORY_INFORMATION_CLASS { MemoryBasicInformation, MemoryWorkingSetList, } MEMORY_INFORMATION_CLASS; // ... typedef union _PSAPI_WORKING_SET_BLOCK { ULONG Flags; struct { ULONG Protection :5; ULONG ShareCount :3; ULONG Shared :1; ULONG Reserved :3; ULONG VirtualPage:20; }; } PSAPI_WORKING_SET_BLOCK, *PPSAPI_WORKING_SET_BLOCK; typedef struct _MEMORY_WORKING_SET_LIST { ULONG NumberOfPages; PSAPI_WORKING_SET_BLOCK WorkingSetList[1]; } MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST; // ... }
c 语言:
bool IsDebugged() { #ifndef _WIN64 NTSTATUS status; PBYTE pMem = nullptr; DWORD dwMemSize = 0; do { dwMemSize += 0x1000; pMem = (PBYTE)_malloca(dwMemSize); if (!pMem) return false; memset(pMem, 0, dwMemSize); status = ntdll::NtQueryVirtualMemory( GetCurrentProcess(), NULL, ntdll::MemoryWorkingSetList, pMem, dwMemSize, NULL); } while (status == STATUS_INFO_LENGTH_MISMATCH); ntdll::PMEMORY_WORKING_SET_LIST pWorkingSet = (ntdll::PMEMORY_WORKING_SET_LIST)pMem; for (ULONG i = 0; i < pWorkingSet->NumberOfPages; i++) { DWORD dwAddr = pWorkingSet->WorkingSetList[i].VirtualPage << 0x0C; DWORD dwEIP = 0; __asm { push eax call $+5 pop eax mov dwEIP, eax pop eax } if (dwAddr == (dwEIP & 0xFFFFF000)) return (pWorkingSet->WorkingSetList[i].Shared == 0) || (pWorkingSet->WorkingSetList[i].ShareCount == 0); } #endif // _WIN64 return false; }
检测函数补丁(Detecting a function patch)
- 检测调试器的一种常见方法是调用 kernel32.IsDebuggerPresent() 中。反反调试很简单,例如更改 EAX 寄存器中的结果或修改 kernel32.IsDebuggerPresent() 函数的代码。
- 因此,我们可以验证 kernel32.IsDebuggerPresent() 是否被修改,而不是检查进程内存中的断点。我们可以读取这个函数的第一个字节,并将它们与其他进程中相同函数的这些字节进行比较。即使启用了ASLR,Windows 库也会在所有进程中加载到相同的基地址。基址只有在重新启动后才会更改,但对于所有进程,它们在会话期间都将保持不变。
c 语言:
bool IsDebuggerPresent() { HMODULE hKernel32 = GetModuleHandleA("kernel32.dll"); if (!hKernel32) return false; FARPROC pIsDebuggerPresent = GetProcAddress(hKernel32, "IsDebuggerPresent"); if (!pIsDebuggerPresent) return false; HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (INVALID_HANDLE_VALUE == hSnapshot) return false; PROCESSENTRY32W ProcessEntry; ProcessEntry.dwSize = sizeof(PROCESSENTRY32W); if (!Process32FirstW(hSnapshot, &ProcessEntry)) return false; bool bDebuggerPresent = false; HANDLE hProcess = NULL; DWORD dwFuncBytes = 0; const DWORD dwCurrentPID = GetCurrentProcessId(); do { __try { if (dwCurrentPID == ProcessEntry.th32ProcessID) continue; hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessEntry.th32ProcessID); if (NULL == hProcess) continue; if (!ReadProcessMemory(hProcess, pIsDebuggerPresent, &dwFuncBytes, sizeof(DWORD), NULL)) continue; if (dwFuncBytes != *(PDWORD)pIsDebuggerPresent) { bDebuggerPresent = true; break; } } __finally { if (hProcess) CloseHandle(hProcess); } } while (Process32NextW(hSnapshot, &ProcessEntry)); if (hSnapshot) CloseHandle(hSnapshot); return bDebuggerPresent; }
修改 ntdll.DbgBreakPoint()
函数 ntdll.DbgBreakPoint() 具有以下代码:
cc int3 ; DbgBreakPoint c3 ret
- 当调试器附加到正在运行的进程时会调用该函数。该函数可以截获异常,所以允许调试器获得控制权。如果我们删除 ntdll.DbgBreakPoint() 中的断点,调试器将不会中断,线程将退出。
c 语言:
void Patch_DbgBreakPoint() { HMODULE hNtdll = GetModuleHandleA("ntdll.dll"); if (!hNtdll) return; FARPROC pDbgBreakPoint = GetProcAddress(hNtdll, "DbgBreakPoint"); if (!pDbgBreakPoint) return; DWORD dwOldProtect; if (!VirtualProtect(pDbgBreakPoint, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect)) return; *(PBYTE)pDbgBreakPoint = (BYTE)0xC3; // ret }
修改 ntdll.DbgUiRemoteBreakin()
- 当调试器调用 kernel32.DebugActiveProcess() 时,调试器会相应地调用 ntdll.DbgUiRemoteBreakin()。为了防止调试器附加到进程,我们可以修补 ntdll.DbgUiRemoteBreakin() 代码以调用 kernel32.TerminateProcess()。
- 在下面的示例中,我们使用以下代码修改 ntdll.DbgUiRemoteBreakin():
6A 00 push 0
68 FF FF FF FF push -1 ; GetCurrentProcess() result
B8 XX XX XX XX mov eax, kernel32!TreminateProcess
FF D0 call eax
- 因此,一旦我们尝试将调试器附加到应用程序,应用程序就会自行终止。
c 语言:
#pragma pack(push, 1) struct DbgUiRemoteBreakinPatch { WORD push_0; BYTE push; DWORD CurrentPorcessHandle; BYTE mov_eax; DWORD TerminateProcess; WORD call_eax; }; #pragma pack(pop) void Patch_DbgUiRemoteBreakin() { HMODULE hNtdll = GetModuleHandleA("ntdll.dll"); if (!hNtdll) return; FARPROC pDbgUiRemoteBreakin = GetProcAddress(hNtdll, "DbgUiRemoteBreakin"); if (!pDbgUiRemoteBreakin) return; HMODULE hKernel32 = GetModuleHandleA("kernel32.dll"); if (!hKernel32) return; FARPROC pTerminateProcess = GetProcAddress(hKernel32, "TerminateProcess"); if (!pTerminateProcess) return; DbgUiRemoteBreakinPatch patch = { 0 }; patch.push_0 = '\x6A\x00'; patch.push = '\x68'; patch.CurrentPorcessHandle = 0xFFFFFFFF; patch.mov_eax = '\xB8'; patch.TerminateProcess = (DWORD)pTerminateProcess; patch.call_eax = '\xFF\xD0'; DWORD dwOldProtect; if (!VirtualProtect(pDbgUiRemoteBreakin, sizeof(DbgUiRemoteBreakinPatch), PAGE_READWRITE, &dwOldProtect)) return; ::memcpy_s(pDbgUiRemoteBreakin, sizeof(DbgUiRemoteBreakinPatch), &patch, sizeof(DbgUiRemoteBreakinPatch)); VirtualProtect(pDbgUiRemoteBreakin, sizeof(DbgUiRemoteBreakinPatch), dwOldProtect, &dwOldProtect); }
执行代码校验和
- 验证代码校验和是检测软件断点、调试器的跨步、函数的内联挂钩或数据修改的可靠方法。
下面的示例显示了如何验证函数的校验和。
PVOID g_pFuncAddr; DWORD g_dwFuncSize; DWORD g_dwOriginalChecksum; static void VeryImportantFunction() { // ... } static DWORD WINAPI ThreadFuncCRC32(LPVOID lpThreadParameter) { while (true) { if (CRC32((PBYTE)g_pFuncAddr, g_dwFuncSize) != g_dwOriginalChecksum) ExitProcess(0); Sleep(10000); } return 0; } size_t DetectFunctionSize(PVOID pFunc) { PBYTE pMem = (PBYTE)pFunc; size_t nFuncSize = 0; do { ++nFuncSize; } while (*(pMem++) != 0xC3); return nFuncSize; } int main() { g_pFuncAddr = (PVOID)&VeryImportantFunction; g_dwFuncSize = DetectFunctionSize(g_pFuncAddr); g_dwOriginalChecksum = CRC32((PBYTE)g_pFuncAddr, g_dwFuncSize); HANDLE hChecksumThread = CreateThread(NULL, NULL, ThreadFuncCRC32, NULL, NULL, NULL); // ... return 0; }
解决方案
调试期间:
- 对于反跨步技巧:进入执行跨步检查的函数并执行它直到结束(OllyDbg/x32/x64dbg 中的 Ctrl+F9)。
- 过所有“内存”反调试技巧(包括防跨步)的最佳方法是找到准确的检查函数并用 NOP 进行修补,或者设置允许应用程序进一步执行的返回值。
对于反调试工具开发:
断点扫描:
汇编指令
- 以下技术通过检查 CPU 执行特定指令时调试器的行为来检测调试器是否存在。
INT 3
- 指令 INT3 是用作软件断点的中断。如果不存在调试器,则在 INT3 指令后,将生成异常EXCEPTION_BREAKPOINT (0x80000003) 并调用异常处理程序。如果调试器存在,则不会将控制权授予异常处理程序。
c 语言:
bool IsDebugged() { __try { __asm int 3; return true; } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } }
- 除了 INT3 指令的缩写形式(0xCC 操作码)之外,该指令还有一种长形式:CD 03 操作码。
- 当发生异常 EXCEPTION_BREAKPOINT 时,Windows 会将 EIP 寄存器递减到 0xCC 操作码的假定位置,并将控件传递给异常处理程序。对于 INT3 指令的长格式,EIP 将指向指令的中间(即 0x03 字节)。因此,如果我们想在 INT3 指令之后继续执行,则应在异常处理程序中编辑 EIP(否则我们很可能会 EXCEPTION_ACCESS_VIOLATION 异常)。如果没有,我们可以忽略指令指针修改。
c 语言:
bool g_bDebugged = false; int filter(unsigned int code, struct _EXCEPTION_POINTERS *ep) { g_bDebugged = code != EXCEPTION_BREAKPOINT; // 比较 code 与 EXCEPTION_BREAKPOINT的值来判断是否发生了调试断点异常。如果 code 不等于 EXCEPTION_BREAKPOINT,将 g_bDebugged 设置为 true,表示不处于调试状态;否则 g_bDebugged 设置为 false,表示处于调试状态。 return EXCEPTION_EXECUTE_HANDLER; // 返回 EXCEPTION_EXECUTE_HANDLER,表示继续执行异常处理程序。 } bool IsDebugged() { __try { __asm __emit(0xCD); __asm __emit(0x03); } __except (filter(GetExceptionCode(), GetExceptionInformation())) { return g_bDebugged; } }
INT 2D
- 执行 INT2D 指令就像 INT3 指令一样,也会引发异常 EXCEPTION_BREAKPOINT。但对于 INT2D,Windows 使用 EIP 寄存器作为异常地址,然后递增 EIP 寄存器值。执行 INT2D 时,Windows 还会检查 EAX 寄存器的值。如果在所有版本的 Windows 上为 1、3 或 4,或者在 Vista+ 上为 5,则异常地址将增加一。
- 该指令可能会给一些调试器带来问题,因为在 EIP 异常地址加一之后,INT2D 指令后面的字节将被跳过,并且可能会从损坏的指令继续执行。
- 在本例中,我们将一个字节的 NOP 指令放在 INT2D 之后,以便在任何情况下跳过它。如果在没有调试器的情况下执行程序,则控制将传递给异常处理程序。
c 语言:
bool IsDebugged() { __try { __asm xor eax, eax; __asm int 0x2d; __asm nop; return true; } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } }
DebugBreak
- 正如 DebugBreak 文档中所写,“DebugBreak 会导致当前进程中发生断点异常。这允许调用线程向调试器发出信号以处理异常”。
- 如果在没有调试器的情况下执行程序,则控件将传递给异常处理程序。否则,调试器将截获执行。
c 语言:
bool IsDebugged() { __try { DebugBreak(); } __except(EXCEPTION_BREAKPOINT) { return false; } return true; }
ICE
- ICE 是英特尔未记录的指令之一。它的操作码是 0xF1。它可用于检测程序是否被跟踪。
- 如果执行 ICE 指令,将引发 EXCEPTION_SINGLE_STEP (0x80000004) 异常。
- 但是,如果已跟踪程序,则调试器会将此异常视为通过执行在 Flags 寄存器中设置了 SingleStep 位的指令而生成的正常异常。因此,在调试器下,不会调用异常处理程序,并且在 ICE 指令之后继续执行。
c 语言:
bool IsDebugged() { __try { __asm __emit 0xF1; return true; } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } }
堆栈段寄存器(Stack Segment Register)
这是一个技巧,可用于检测是否正在跟踪程序。 诀窍包括跟踪以下程序集指令序列:
push ss pop ss pushf
- 通过此代码在调试器中单步执行后,将设置陷阱标志。通常,它不可见,因为调试器在传递每个调试器事件后清除陷阱标志。但是,如果我们之前将 EFLAGS 保存到堆栈中,我们将能够检查是否设置了 Trap Flag。
c 语言
bool IsDebugged() { bool bTraced = false; __asm { push ss pop ss pushf test byte ptr [esp+1], 1 jz movss_not_being_debugged } bTraced = true; movss_not_being_debugged: // restore stack __asm popf; return bTraced; }
指令计数(Instruction Counting)
- 此技术滥用了某些调试器处理EXCEPTION_SINGLE_STEP异常的方式。
- 这个技巧的想法是按照一些预定义的顺序(例如 NOPs 的序列)为每条指令设置硬件断点。执行带有硬件断点的指令会引发 EXCEPTION_SINGLE_STEP 异常,该异常可由向量异常处理程序捕获。在异常处理程序中,我们递增一个寄存器,该寄存器扮演指令计数器(在本例中为 EAX)和指令指针 EIP,以将控制传递给序列中的下一条指令。因此,每次将控件传递给序列中的下一条指令时,都会引发异常并递增计数器。序列完成后,我们检查计数器,如果它不等于序列的长度,我们将其视为正在调试程序。
c 语言:
#include "hwbrk.h" static LONG WINAPI InstructionCountingExeptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) { pExceptionInfo->ContextRecord->Eax += 1; pExceptionInfo->ContextRecord->Eip += 1; return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; } __declspec(naked) DWORD WINAPI InstructionCountingFunc(LPVOID lpThreadParameter) { __asm { xor eax, eax nop nop nop nop cmp al, 4 jne being_debugged } ExitThread(FALSE); being_debugged: ExitThread(TRUE); } bool IsDebugged() { PVOID hVeh = nullptr; HANDLE hThread = nullptr; bool bDebugged = false; __try { hVeh = AddVectoredExceptionHandler(TRUE, InstructionCountingExeptionHandler); if (!hVeh) __leave; hThread = CreateThread(0, 0, InstructionCountingFunc, NULL, CREATE_SUSPENDED, 0); if (!hThread) __leave; PVOID pThreadAddr = &InstructionCountingFunc; // Fix thread entry address if it is a JMP stub (E9 XX XX XX XX) if (*(PBYTE)pThreadAddr == 0xE9) pThreadAddr = (PVOID)((DWORD)pThreadAddr + 5 + *(PDWORD)((PBYTE)pThreadAddr + 1)); for (auto i = 0; i < m_nInstructionCount; i++) m_hHwBps[i] = SetHardwareBreakpoint( hThread, HWBRK_TYPE_CODE, HWBRK_SIZE_1, (PVOID)((DWORD)pThreadAddr + 2 + i)); ResumeThread(hThread); WaitForSingleObject(hThread, INFINITE); DWORD dwThreadExitCode; if (TRUE == GetExitCodeThread(hThread, &dwThreadExitCode)) bDebugged = (TRUE == dwThreadExitCode); } __finally { if (hThread) CloseHandle(hThread); for (int i = 0; i < 4; i++) { if (m_hHwBps[i]) RemoveHardwareBreakpoint(m_hHwBps[i]); } if (hVeh) RemoveVectoredExceptionHandler(hVeh); } return bDebugged; }
POPF and Trap Flag
- 这是另一个技巧,可以指示是否正在跟踪程序。
- Flags 寄存器中有一个 Trap Flag。设置陷阱标志时,将引发异常 SINGLE_STEP。但是,如果我们跟踪代码,调试器将清除陷阱标志,因此我们不会看到异常。
c 语言:
bool IsDebugged() { __try { __asm { pushfd mov dword ptr [esp], 0x100 popfd nop } return true; } __except(GetExceptionCode() == EXCEPTION_SINGLE_STEP ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_EXECUTION) { return false; } }
指令前缀(Instruction Prefixes)
- 此技巧仅适用于某些调试器。它滥用了这些调试器处理指令前缀的方式。
- 如果我们在 OllyDbg 中执行以下代码,在单步执行到第一个字节 F3 后,我们将立即到达 try 块的末尾。调试器只是跳过前缀,并将控制权交给 INT1 指令。
- 如果我们在没有调试器的情况下运行相同的代码,则会引发异常,我们将进入 except 块。
c 语言:
bool IsDebugged() { __try { // 0xF3 0x64 disassembles as PREFIX REP: __asm __emit 0xF3 __asm __emit 0x64 // One byte INT 1 __asm __emit 0xF1 return true; } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } }
解决方案
调试期间:
-
- 缓解以下所有检查的最佳方法是使用 NOP 指令修补它们。
- 关于反跟踪技术:我们可以简单地在代码中设置一个断点,而不是修改代码,该断点在检查之后运行程序直到这个断点。
- 对于反反调试工具开发:无缓解措施。
交互式检查
- 下面的技术让正在运行的进程管理一个用户接口,或者与它的父进程交互以发现对一个被调试的进程来说是固有的不一致的地方
自我调试
至少有三个函数可以用来作为调试器附加到一个正在运行的进程上:
- kernel32!DebugActiveProcess()
- ntdll!DbgUiDebugActiveProcess()
- ntdll!NtDebugActiveProcess()
c 语言:
#define EVENT_SELFDBG_EVENT_NAME L"SelfDebugging"
bool IsDebugged()
{
WCHAR wszFilePath[MAX_PATH], wszCmdLine[MAX_PATH];
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
HANDLE hDbgEvent;
hDbgEvent = CreateEventW(NULL, FALSE, FALSE, EVENT_SELFDBG_EVENT_NAME);
if (!hDbgEvent)
return false;
if (!GetModuleFileNameW(NULL, wszFilePath, _countof(wszFilePath)))
return false;
swprintf_s(wszCmdLine, L"%s %d", wszFilePath, GetCurrentProcessId());
if (CreateProcessW(NULL, wszCmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return WAIT_OBJECT_0 == WaitForSingleObject(hDbgEvent, 0);
}
return false;
}
bool EnableDebugPrivilege()
{
bool bResult = false;
HANDLE hToken = NULL;
DWORD ec = 0;
do
{
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
break;
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid))
break;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if( !AdjustTokenPrivileges( hToken, FALSE, &tp, sizeof(tp), NULL, NULL))
break;
bResult = true;
}
while (0);
if (hToken)
CloseHandle(hToken);
return bResult;
}
int main(int argc, char **argv)
{
if (argc < 2)
{
if (IsDebugged())
ExitProcess(0);
}
else
{
DWORD dwParentPid = atoi(argv[1]);
HANDLE hEvent = OpenEventW(EVENT_MODIFY_STATE, FALSE, EVENT_SELFDBG_EVENT_NAME);
if (hEvent && EnableDebugPrivilege())
{
if (FALSE == DebugActiveProcess(dwParentPid))
SetEvent(hEvent);
else
DebugActiveProcessStop(dwParentPid);
}
ExitProcess(0);
}
// ...
return 0;
}
GenerateConsoleCtrlEvent()
- 当用户按下 Ctrl+C 或 Ctrl+Break 并且控制台窗口处于焦点中时,Windows 会检查是否有这个事件的处理程序。所有的控制台进程都有一个默认的处理函数,调用 kernel32.ExitProcess() 函数。然而,我们可以为这些事件注册一个自定义的处理程序,它忽略了 Ctrl+C 或 Ctrl+Break 信号。
- 然而,如果一个控制台进程正在被调试,而 CTRL+C 信号没有被禁用,系统会产生一个 DBG_CONTROL_C 异常。通常这个异常会被调试器拦截,但是如果我们注册一个异常处理程序,我们将能够检查 DBG_CONTROL_C 是否被引发。如果我们在自己的异常处理程序中拦截了 DBG_CONTROL_C 异常,它可能表明该进程正在被调试。
c 语言:
bool g_bDebugged{ false }; std::atomic<bool> g_bCtlCCatched{ false }; static LONG WINAPI CtrlEventExeptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { if (pExceptionInfo->ExceptionRecord->ExceptionCode == DBG_CONTROL_C) { g_bDebugged = true; g_bCtlCCatched.store(true); } return EXCEPTION_CONTINUE_EXECUTION; } static BOOL WINAPI CtrlHandler(DWORD fdwCtrlType) { switch (fdwCtrlType) { case CTRL_C_EVENT: g_bCtlCCatched.store(true); return TRUE; default: return FALSE; } } bool IsDebugged() { PVOID hVeh = nullptr; BOOL bCtrlHadnlerSet = FALSE; __try { hVeh = AddVectoredExceptionHandler(TRUE, CtrlEventExeptionHandler); if (!hVeh) __leave; bCtrlHadnlerSet = SetConsoleCtrlHandler(CtrlHandler, TRUE); if (!bCtrlHadnlerSet) __leave; GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); while (!g_bCtlCCatched.load()) ; } __finally { if (bCtrlHadnlerSet) SetConsoleCtrlHandler(CtrlHandler, FALSE); if (hVeh) RemoveVectoredExceptionHandler(hVeh); } return g_bDebugged; }
BlockInput()
- 函数 user32.BlockInput() 可以阻止所有的鼠标和键盘事件,这是禁用调试器的一个相当有效的方法。在 Windows Vista 和更高版本中,这个调用需要管理员权限。
- 我们还可以检测是否 Hook user32.BlockInput() 和其他反调试调用的工具存在。该函数只允许阻断输入一次。第二次调用将返回 FALSE。如果该函数无论输入多少都返回 TRUE,可能表明存在一些拦截方案。
c 语言:
bool IsHooked () { BOOL bFirstResult = FALSE, bSecondResult = FALSE; __try { bFirstResult = BlockInput(TRUE); bSecondResult = BlockInput(TRUE); } __finally { BlockInput(FALSE); } return bFirstResult && bSecondResult; }
NtSetInformationThread()
- 函数 ntdll.NtSetInformationThread() 可以用来从调试器中隐藏一个线程。借助未记录的值 THREAD_INFORMATION_CLASS::ThreadHideFromDebugger (0x11) 的帮助下实现。这是由一个外部进程使用的,但任何线程都可以在自己身上使用它。
- 线程从调试器中隐藏后,它将继续运行,但调试器不会收到与此线程相关的事件。这个线程可以进行反调试检查,如代码校验、调试标志验证等。
- 然而,如果在隐藏的线程中有一个断点,或者我们把主线程从调试器中隐藏起来,进程就会崩溃,调试器就会被卡住。
- 在这个例子中,我们从调试器中隐藏了当前线程。这意味着,如果我们在调试器中跟踪这段代码,或者把断点放在这个线程的任何指令上,一旦 ntdll.NtSetInformationThread() 被调用,调试就会被卡住。
c 语言:
#define NtCurrentThread ((HANDLE)-2) bool AntiDebug() { NTSTATUS status = ntdll::NtSetInformationThread( NtCurrentThread, ntdll::THREAD_INFORMATION_CLASS::ThreadHideFromDebugger, NULL, 0); return status >= 0; }
EnumWindows() 和 SuspendThread()
- 这个技术的想法是暂停父进程的自有线程。
- 首先,我们需要验证父进程是否是一个调试器。这可以通过列举屏幕上的所有顶级窗口来实现(使用 user32.EnumWindows() 或 user32.EnumThreadWindows()),搜索进程 ID 为父进程 ID 的窗口(使用 user32.GetWindowThreadProcessId()),并检查此窗口的标题(通过 user32.GetWindowTextW())。如果父进程的窗口标题看起来像调试器的标题,我们可以用 kernel32.SuspendThread() 或 ntdll.NtSuspendThread() 暂停拥有的线程。
c 语言:
DWORD g_dwDebuggerProcessId = -1; BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) { DWORD dwProcessId = *(PDWORD)lParam; DWORD dwWindowProcessId; GetWindowThreadProcessId(hwnd, &dwWindowProcessId); if (dwProcessId == dwWindowProcessId) { std::wstring wsWindowTitle{ string_heper::ToLower(std::wstring(GetWindowTextLengthW(hwnd) + 1, L'\0')) }; GetWindowTextW(hwnd, &wsWindowTitle[0], wsWindowTitle.size()); if (string_heper::FindSubstringW(wsWindowTitle, L"dbg") || string_heper::FindSubstringW(wsWindowTitle, L"debugger")) { g_dwDebuggerProcessId = dwProcessId; return FALSE; } return FALSE; } return TRUE; } bool IsDebuggerProcess(DWORD dwProcessId) const { EnumWindows(EnumWindowsProc, reinterpret_cast<LPARAM>(&dwProcessId)); return g_dwDebuggerProcessId == dwProcessId; } bool SuspendDebuggerThread() { THREADENTRY32 ThreadEntry = { 0 }; ThreadEntry.dwSize = sizeof(THREADENTRY32); DWORD dwParentProcessId = process_helper::GetParentProcessId(GetCurrentProcessId()); if (-1 == dwParentProcessId) return false; HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwParentProcessId); if(Thread32First(hSnapshot, &ThreadEntry)) { do { if ((ThreadEntry.th32OwnerProcessID == dwParentProcessId) && IsDebuggerProcess(dwParentProcessId)) { HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, ThreadEntry.th32ThreadID); if (hThread) SuspendThread(hThread); break; } } while(Thread32Next(hSnapshot, &ThreadEntry)); } if (hSnapshot) CloseHandle(hSnapshot); return false; }
SwitchDesktop()
- Windows 支持每个会话有多个桌面。可以选择一个不同的活动桌面,其效果是隐藏了之前活动桌面的窗口,而且没有明显的方法可以切换回旧的桌面。
- 此外,来自被调试进程桌面的鼠标和键盘事件将不再被传递给调试器,因为它们的来源不再被共享。这显然使调试变得不可能。
c 语言:
BOOL Switch() { HDESK hNewDesktop = CreateDesktopA( m_pcszNewDesktopName, NULL, NULL, 0, DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_SWITCHDESKTOP, NULL); if (!hNewDesktop) return FALSE; return SwitchDesktop(hNewDesktop); }
OutputDebugString()
- 这种技术已经被废弃了,因为它只适用于早于 Vista 的 Windows 版本。然而,这项技术非常有名,不能在此不提。
- 这个想法很简单。如果调试器不存在,而 kernel32.OutputDebugString 被调用,那么就会发生错误。
c 语言:
bool IsDebugged() { if (IsWindowsVistaOrGreater()) return false; DWORD dwLastError = GetLastError(); OutputDebugString(L"AntiDebug_OutputDebugString"); return GetLastError() != dwLastError; }
解决方案
- 在调试过程中,最好跳过可疑的函数调用(例如用NOP填充)。
如果你写一个反调试方案,以下所有的函数都可以被拦截。
- kernel32.DebugActiveProcess
- ntdll.DbgUiDebugActiveProcess
- ntdll.NtDebugActiveProcess
- kernel32.GenerateConsoleCtrlEvent()
- user32.NtUserBlockInput
- ntdll.NtSetInformationThread
- user32.NtUserBuildHwndList(用于过滤EnumWindows输出)。
- kernel32.SuspendThread
- user32.SwitchDesktop
- kernel32.OutputDebugStringW
杂项
FindWindow()
- 这种技术包括系统中窗口类的简单枚举,并将它们与调试器的已知窗口类进行比较。
可以使用以下函数:
- user32!FindWindowW()
- user32!FindWindowA()
- user32!FindWindowExW()
- user32!FindWindowExA()
c 语言:
const std::vector<std::string> vWindowClasses = {
"OLLYDBG",
"WinDbgFrameClass", // WinDbg
"ID", // Immunity Debugger
"Zeta Debugger",
"Rock Debugger",
"ObsidianGUI",
};
bool IsDebugged()
{
for (auto &sWndClass : vWindowClasses)
{
if (NULL != FindWindowA(sWndClass.c_str(), NULL))
return true;
}
return false;
}
父进程检查
- 通常情况下,一个用户模式的进程是通过双击一个文件图标来执行的。如果该进程以这种方式执行,其父进程将是 shell 进程("explorer.exe")。
- 下面两种方法的主要思路是比较父进程的PID和 "explorer.exe "的PID。
NtQueryInformationProcess()
- 这个方法包括使用 user32.GetShellWindow() 获得 shell 进程窗口句柄,通过调用 user32.GetWindowThreadProcessId() 获得其进程 ID。
- 然后,通过调用 ntdll.NtQueryInformationProcess() 与 ProcessBasicInformation 类,可以从 PROCESS_BASIC_INFORMATION 结构中获得父进程 ID。
c 语言:
bool IsDebugged() { HWND hExplorerWnd = GetShellWindow(); if (!hExplorerWnd) return false; DWORD dwExplorerProcessId; GetWindowThreadProcessId(hExplorerWnd, &dwExplorerProcessId); ntdll::PROCESS_BASIC_INFORMATION ProcessInfo; NTSTATUS status = ntdll::NtQueryInformationProcess( GetCurrentProcess(), ntdll::PROCESS_INFORMATION_CLASS::ProcessBasicInformation, &ProcessInfo, sizeof(ProcessInfo), NULL); if (!NT_SUCCESS(status)) return false; return (DWORD)ProcessInfo.InheritedFromUniqueProcessId != dwExplorerProcessId; }
CreateToolhelp32Snapshot()
- 父进程 ID 和父进程名称可以通过 kernel32.CreateToolhelp32Snapshot()和 kernel32.Process32Next() 函数获得。
- c 语言:
DWORD GetParentProcessId(DWORD dwCurrentProcessId)
{
DWORD dwParentProcessId = -1;
PROCESSENTRY32W ProcessEntry = { 0 };
ProcessEntry.dwSize = sizeof(PROCESSENTRY32W);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(Process32FirstW(hSnapshot, &ProcessEntry))
{
do
{
if (ProcessEntry.th32ProcessID == dwCurrentProcessId)
{
dwParentProcessId = ProcessEntry.th32ParentProcessID;
break;
}
} while(Process32NextW(hSnapshot, &ProcessEntry));
}
CloseHandle(hSnapshot);
return dwParentProcessId;
}
bool IsDebugged()
{
bool bDebugged = false;
DWORD dwParentProcessId = GetParentProcessId(GetCurrentProcessId());
PROCESSENTRY32 ProcessEntry = { 0 };
ProcessEntry.dwSize = sizeof(PROCESSENTRY32W);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(Process32First(hSnapshot, &ProcessEntry))
{
do
{
if ((ProcessEntry.th32ProcessID == dwParentProcessId) &&
(strcmp(ProcessEntry.szExeFile, "explorer.exe")))
{
bDebugged = true;
break;
}
} while(Process32Next(hSnapshot, &ProcessEntry));
}
CloseHandle(hSnapshot);
return bDebugged;
}
选择器
- 选择器的值可能看起来很稳定,但实际上在某些情况下是不稳定的,而且还取决于Windows的版本。例如,一个选择器的值可以在一个线程中被设置,但它可能不会保持这个值很久。某些事件可能会导致选择器的值被改回其默认值。一个这样的事件是一个异常。在调试器的上下文中,单步异常仍然是一个异常,它可能导致一些意外的行为。
x86:
xor eax, eax push fs pop ds l1: xchg [eax], cl xchg [eax], cl
- 在 64 位版本的 Windows 中,单步执行此代码将导致 l1 处的访问冲突异常,因为即使在到达 l1 之前,DS 选择器也将恢复到默认值。在 32 位版本的 Windows 上,DS 选择器不会恢复其值,除非发生非调试异常。如果使用 SS 选择器,行为中的特定版本差异会进一步扩大。在 64 位版本的 Windows 上,SS 选择器将恢复到默认值,就像 DS 选择器一样。然而,在 32 位版本的 Windows 上,即使发生异常,SS 选择器值也不会被恢复。
x64:
xor eax, eax push offset l2 push d fs:[eax] mov fs:[eax], esp push fs pop ss xchg [eax], cl xchg [eax], cl l1: int 3 ;force exception to occur l2: ;looks like it would be reached ;if an exception occurs ...
- 那么当 "int 3 "指令在 l1 处到达并发生断点异常时,l2 处的异常处理程序就不会像预期的那样被调用。相反,进程被简单地终止了。
这种技术的一个变种是通过简单地检查赋值是否成功来检测单步事件。
push 3 pop gs mov ax, gs cmp al, 3 jne being_debugged
- FS 和 GS选择器是特殊情况。对于某些值,它们会受到单步事件的影响,即使是在 32 位版本的 Windows上。但是,对于 FS 选择器(从技术上讲,是 GS 选择器),如果它被设置为从 0 到 3 的值,那么在 32 位 Windows 版本上它将不能恢复到它的默认值。相反,它将被设置为零(GS 选择器以同样的方式受到影响,但 GS 选择器的默认值为零)。在 64 位版本的 Windows 上,它(它们)将恢复到它(它们)的默认值。
- 此代码也容易受到线程切换事件引起的竞态条件的影响。当线程切换事件发生时,它的行为就像一个异常,并将导致选择器值被更改,对于FS选择器来说,这意味着它将被设置为零。
这种技术的一种变体可以通过有意地等待线程切换事件的发生来解决这个问题。
push 3 pop gs l1: mov ax, gs cmp al, 3 je l1
- 但是,这段代码容易受到它首先试图检测到的问题的影响,因为它没有检查原始的赋值是否成功。当然,可以将这两个代码片段组合起来以产生所需的效果,方法是等待线程切换事件发生,然后在应该存在的时间窗口内执行赋值,直到下一个事件发生。Ferrie
c 语言:
bool IsTraced() { __asm { push 3 pop gs __asm SeclectorsLbl: mov ax, gs cmp al, 3 je SeclectorsLbl push 3 pop gs mov ax, gs cmp al, 3 jne Selectors_Debugged } return false; Selectors_Debugged: return true; }
DbgPrint()
- 调试函数如 ntdll.DbgPrint()、kernel32.OutputDebugStringW() 导致 DBG_PRINTEXCEPTION_C (0x40010006)异常。如果程序使用附加的调试器执行,则调试器将处理此异常。但是如果没有调试器,并且注册了异常处理程序,异常处理程序将捕获该异常。
c 语言:
bool IsDebugged() { __try { RaiseException(DBG_PRINTEXCEPTION_C, 0, 0, 0); } __except(GetExceptionCode() == DBG_PRINTEXCEPTION_C) { return false; } return true; }
DbgSetDebugFilterState()
- 函数 ntdll.DbgSetDebugFilterState() 和 ntdll.NtSetDebugFilterState() 只设置一个标志寄存器,如果内核模式的调试器存在,将被检查。因此,如果一个内核调试器被连接到系统上,这些函数将成功。然而,这些函数也可能因为一些用户模式的调试器引起的副作用而成功。这些功能需要管理员的权限。
c 语言:
bool IsDebugged() { return NT_SUCCESS(ntdll::NtSetDebugFilterState(0, 0, TRUE)); }
NtYieldExecution() / SwitchToThread()
- 这种方法其实并不可靠,因为它只显示当前进程中是否有一个高优先级的线程。然而,它可以作为一种反跟踪技术。
- 当一个应用程序在调试器中被追踪,并且执行了一个单步,上下文不能被切换到其他线程。这意味着 ntdll.NtYieldExecution() 返回 STATUS_NO_YIELD_PERFORMED(0x40000024),这导致 kernel32.SwitchToThread() 返回 0。
- 使用这种技术的策略是,如果 kernel32.SwitchToThread() 返回 0,或者 ntdll.NtYieldExecution() 返回 STATUS_NO_YIELD_PERFORMED,则有一个循环修改一些计数器。这可能是一个解密字符串的循环,或者其他一些应该在调试器中手动分析的循环。如果计数器在离开循环后有预期值(预期即如果所有 kernel32.SwitchToThread() 返回 0 的值),我们认为调试器是存在的。
- 在下面的例子中,我们定义了一个一字节的计数器(初始化为 0),如果 kernel32.SwitchToThread 返回 0,它将向左移动一位。如果它移动了 8 次,那么计数器的值将变成 0,调试器被认为是存在的。
c 语言:
bool IsDebugged() { BYTE ucCounter = 1; for (int i = 0; i < 8; i++) { Sleep(0x0F); ucCounter <<= (1 - SwitchToThread()); } return ucCounter == 0; }
解决方案
- 调试期间:用 NOP 来填补反调试或反跟踪的检查。
对于反调试绕过工具的开发:
- 对于 FindWindow():拦截 user32.NtUserFindWowEx() 函数。在拦截中,调用原始的 user32.NtUserFindWowEx() 函数。如果它是从被调试的进程中调用的,并且父进程看起来很可疑,那么从拦截中返回 unsuccessfully(不成功)。
用于父进程检查:拦截 ntdll.NtQuerySystemInformation() 函数。如果 SystemInformationClass 是以下值之一:
- SystemProcessInformation
- SystemSessionProcessInformation
- SystemExtendedProcessInformation
和进程名称看起来很可疑,那么拦截必须修改进程名称。