[参考]反调试技术整理
反调试技术整理
前言
由于技术复杂,并且Windows存在大量未公开的内核参数、函数等,这些技术可能会过时。
反调试技术整理
-
PEB表、
IsDebuggerPresent
检查FS:[0x30]
(32位)GS:[0x60]
(64位),等同于IsDebuggerPresent()
BOOL IsDebuggerPresent();
-
CheckRemoteDebuggerPresent()/NtQueryInformationProcess
CheckRemoteDebuggerPresent()
调用NtQueryInformationProcess
,__kernel_entry NTSTATUS NtQueryInformationProcess( [in] HANDLE ProcessHandle, [in] PROCESSINFOCLASS ProcessInformationClass, [out] PVOID ProcessInformation, [in] ULONG ProcessInformationLength, [out, optional] PULONG ReturnLength );
调用此API,传入参数
ProcessInformationClass = 7
将返回一个句柄指向调试器参见CheckRemoteDebuggerPresent - CTF Wiki (ctf-wiki.org)、NtQueryInformationProcess - CTF Wiki (ctf-wiki.org)
-
异常捕获和处理
-
Windows异常处理流程简述
-
硬件异常:
-
CPU转储当前现场
-
CPU根据IDT查找异常处理例程(
KiTrapXX
)参见CPU和软件模拟异常的执行流程_鬼手56的博客-CSDN博客
使用IDA打开ntkrnlpa.idb
文件,在其中查找_IDT
可以看到对应的处理表 -
异常处理例程处理异常,完成异常信息封装
-
调用
CommonDispatchException
,完善EXCEPTION_RECORD
结构typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
-
将结构传递给
KiDispatchException
VOID KiDispatchException( [in] PEXCEPTION_RECORD ExceptionRecord, [in] PKEXCEPTION_FRAME ExceptionFrame, [in] PKTRAP_FRAME TrapFrame, [in] KPROCESSOR_MODE PreviousMode, [in] BOOLEAN FirstChance );
-
-
软件异常:
- 发生
throw
关键字 - 转入
_CxxThrowException
extern "C" void __stdcall _CxxThrowException( void* pExceptionObject _ThrowInfo* pThrowInfo );
- 通过
KERNEL32.DLL!RaiseException
填充EXCEPTION_RECORD
结构体void RaiseException( [in] DWORD dwExceptionCode, [in] DWORD dwExceptionFlags, [in] DWORD nNumberOfArguments, [in] const ULONG_PTR *lpArguments );
- 转入
NTDLL.DLL!RtlRaiseException
- 转入内核
NtRaiseException
- 转入内核
KiRaiseException
,Exception Code
最高位置零 - 传递到分发函数
KiDispatchException
- 发生
-
内核异常
- 尝试传递给内核调试器
- 失败,利用
RtlDispatchException
传递至SEHBOOLEAN RtlDispatchException( [in] PEXCEPTION_RECORD ExceptionRecord, [in] PCONTEXT ContextRecord );
- 传递给内核调试器
- 终止Windows运行(BSoD)
-
用户异常
-
传递给内核调试器
-
失败或者不存在内核调试器:填充上下文
CONTEXT
,从KiExceptionDispatch
转入KeUserExceptionDispather
typedef struct _CONTEXT { DWORD64 P1Home; DWORD64 P2Home; DWORD64 P3Home; DWORD64 P4Home; DWORD64 P5Home; DWORD64 P6Home; DWORD ContextFlags; DWORD MxCsr; WORD SegCs; WORD SegDs; WORD SegEs; WORD SegFs; WORD SegGs; WORD SegSs; DWORD EFlags; DWORD64 Dr0; DWORD64 Dr1; DWORD64 Dr2; DWORD64 Dr3; DWORD64 Dr6; DWORD64 Dr7; DWORD64 Rax; DWORD64 Rcx; DWORD64 Rdx; DWORD64 Rbx; DWORD64 Rsp; DWORD64 Rbp; DWORD64 Rsi; DWORD64 Rdi; DWORD64 R8; DWORD64 R9; DWORD64 R10; DWORD64 R11; DWORD64 R12; DWORD64 R13; DWORD64 R14; DWORD64 R15; DWORD64 Rip; union { XMM_SAVE_AREA32 FltSave; NEON128 Q[16]; ULONGLONG D[32]; struct { M128A Header[2]; M128A Legacy[8]; M128A Xmm0; M128A Xmm1; M128A Xmm2; M128A Xmm3; M128A Xmm4; M128A Xmm5; M128A Xmm6; M128A Xmm7; M128A Xmm8; M128A Xmm9; M128A Xmm10; M128A Xmm11; M128A Xmm12; M128A Xmm13; M128A Xmm14; M128A Xmm15; } DUMMYSTRUCTNAME; DWORD S[32]; } DUMMYUNIONNAME; M128A VectorRegister[26]; DWORD64 VectorControl; DWORD64 DebugControl; DWORD64 LastBranchToRip; DWORD64 LastBranchFromRip; DWORD64 LastExceptionToRip; DWORD64 LastExceptionFromRip; } CONTEXT, *PCONTEXT;
-
内核从
KiUserExceptionDispatcher
获得控制,调用RtlDispatchException
查找并调用异常处理函数:VEH→SEH,从fs:[0]
开始。VOID KiUserExceptionDispatcher ( [in] PEXCEPTION_RECORD ExceptionRecord, [in] PCONTEXT ContextRecord );
-
失败或者未处理:进入内核再次处理
-
如果传递给内核调试器失败或者未处理,终止进程
-
-
-
-
CloseHandle()
如果一个进程在调试器下运行,并且一个无效的句柄被传递给ntdll!NtClose()
或kernel32!CloseHandle()
函数,那么将引发EXCEPTION_INVALID_HANDLE:0xC0000008
异常。这个异常可以被一个异常处理程序缓存起来。如果控制被传递给异常处理程序,表明有一个调试器存在。 -
植入中断(包括
int 3
和int 2dh
)
int 2dh
可以用于检测包括RING0,RING3
在内的所有调试器。通过提前植入一个断点,并附带植入一个VEH异常解决例程,可以用于判断调试器的存在与否。
另外,附加调试器的程序在运行完int 2dh
后,会跳过此指令之后的一个字节. -
利用
STATUS_GUARD_PAGE_VIOLATION
异常
分配被保护的内存区,利用属性PAGE_GUARD
标记分配的内存区。向其中填入ret
指令。当存在OD调试器解释时将返回到上一个入栈地址,而直接执行将产生STATUS_GUARD_PAGE_VIOLATION
错误(数据执行保护) -
(Windows XP/2000)使用宏
OutputDebugString
void OutputDebugStringA( [in, optional] LPCSTR lpOutputString );
此宏在没有调试器附加时,执行将产生一个
LastError
。检查此错误的值在执行前后是否发生改变,可以知道是否有调试器寄生。 -
引发
EXCEPTION_EXECUTE_HANDLER
错误
触发方法:关闭一个不存在的句柄CloseHandle(0x99999999ULL);
-
使用
EFLAGS
写入异常标志,然后监视异常
直接将EFLAGS
与0x100
或运算,利用VEH检查调试器的存在 -
利用
UnhandledExceptionFilter()
在除去try…catch块的同时接管错误/*Global*/ BOOL bIsBeinDbg = TRUE; //... //示例 LPTOP_LEVEL_EXCEPTION_FILTER Top = SetUnhandledExceptionFilter(myUnhandledExcepFilter); //在myUnhandledExcepFilter里更改全局变量bIsBeinDbg的值为FALSE RaiseException(EXCEPTION_FLT_DIVIDE_BY_ZERO, 0, 0, NULL);//在此处产生任意错误 SetUnhandledExceptionFilter(Top); return bIsBeinDbg;
-
-
内存扫描与监视
-
硬件断点寄存器检查
- 提示:对于API
ZeroMemory
,部分编译器会将此宏直接优化掉。因此建议使用SecureZeroMemory
- 利用
GetThreadContext
获取程序运行上下文,检查DrN
寄存器的值是否为0利用MEM_WRITE_WATCH
监察申请的内存块的访问、写入情况。尤其适合对反调试部分的字节码进行动态写入之后检查是否被修改。BOOL GetThreadContext( [in] HANDLE hThread, [in, out] LPCONTEXT lpContext );
- 提示:对于API
-
利用
MEM_WRITE_WATCH
监察申请的内存块的访问、写入情况。尤其适合对反调试部分的字节码进行动态写入之后检查是否被修改。
在分配内存时指定LPVOID VirtualAlloc( [in, optional] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flAllocationType, [in] DWORD flProtect );
使用
VirtualAlloc
时指定参数[in] flAllocationType
为MEM_WRITE_WATCH = 0x00200000
需要检查时,使用
GetWriteWatch
函数,其中参数[in] lpBaseAddress
填入上文申请的内存地址参见[GetWriteWatch function (memoryapi.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/me moryapi/nf-memoryapi-getwritewatch)
UINT GetWriteWatch( [in] DWORD dwFlags, [in] PVOID lpBaseAddress, [in] SIZE_T dwRegionSize, [out] PVOID *lpAddresses, [in, out] ULONG_PTR *lpdwCount, [out] LPDWORD lpdwGranularity );
-
-
LFH:低碎片堆
LFH 不是单独的堆。 而是应用程序可以为其堆启用的策略。 启用 LFH 后,系统会在某些预先确定的大小中分配内存。 当应用程序从启用了 LFH 的堆请求内存分配时,系统会分配足够大以包含所请求大小的最小内存块。 无论是否启用 LFH,系统都不会将 LFH 用于大于 16 KB 的分配。
-
当多次申请、释放堆内存之后,可能出现堆碎片。堆碎片的产生可能导致无法一次性分配大量连续的内存,尽管这个大小的内存在数值上是空闲的。
-
参见/windows 堆分析 - 合天网安实验室/《软件调试》
在调试模式下,不存在此堆。因此可以通过探测此堆的地址,间接判断调试器存在与否。
探测方法很简单,只需要查看
nt!_HEAP.FrontEndHeap
对于从Windows10开始的操作系统,此值的偏移经常变动,因此不便于通过硬编码的方式查找
BOOL LowFragmentationHeap(VOID) { PINT_PTR FrontEndHeap = NULL; HANDLE hHeap = GetProcessHeap(); if (IsWindowsVista() || IsWindows7()) { #if defined (ENV64BIT) FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0x178); #elif defined(ENV32BIT) FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0xd4); #endif } if (IsWindows8or8PointOne()) { #if defined (ENV64BIT) FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0x170); #elif defined(ENV32BIT) FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0xd0); #endif } // In Windows 10. the offset changes very often. // Ignoring it from now. if (FrontEndHeap && *FrontEndHeap == NULL) { return TRUE; } return FALSE; }
-
-
反HOOK
- 检查某DLL内部的函数的地址(利用
GetProcAddress
),并于DLL的地址比较。如果被HOOK,那么函数的地址就将位于对应DLL的地址空间之外。
判断地址空间利用lpBaseOfDll
PE属性和SizeOfImage
属性 - 要检查函数是否被HOOK,可以传入错误的参数,例如传入错误句柄或者错误大小,观察函数对错误参数的处理
- 检查某DLL内部的函数的地址(利用
-
NtGlobalFlag
检查FLG_HEAP_ENABLE_TAIL_CHECK/FLG_HEAP_ENABLE_FREE_CHECK/FLG_HEAP_VALIDATE_PARAMETERS
的值。
在 32 位机器上,NtGlobalFlag
字段位于PEB
(进程环境块)0x68
的偏移处, 64 位机器则是在偏移0xBC
位置. 该字段的默认值为 0. 当调试器正在运行时, 该字段会被设置为一个特定的值. 尽管该值并不能十分可信地表明某个调试器真的有在运行, 但该字段常出于该目的而被使用.
简单示例:__declspec(naked) BOOL DetectDebuggerUsingNtGlobalFlag32() { __asm { push ebp; mov ebp, esp; pushad; mov eax, fs:[30h]; //从此处获取PEB表 mov al, [eax + 68h]; and al, 70h; cmp al, 70h; je being_debugged; popad; mov eax, 0; jmp being_debugged + 6 being_debugged: popad; mov eax, 1; leave; retn; } }
-
NtQueryInformationProcess
__kernel_entry NTSTATUS NtQueryInformationProcess( [in] HANDLE ProcessHandle, [in] PROCESSINFOCLASS ProcessInformationClass, [out] PVOID ProcessInformation, [in] ULONG ProcessInformationLength, [out, optional] PULONG ReturnLength );
NtQueryInformationProcess function (winternl.h) - Win32 apps | Microsoft Docs
函数
ntdll!NtQueryInformationProcess()
可以从一个进程中检索不同种类的信息。它接受一个ProcessInformationClass
参数,该参数指定了ProcessInformation
参数的输出类型。typedef enum _PROCESSINFOCLASS { ProcessBasicInformation = 0, ProcessDebugPort = 7, ProcessWow64Information = 26, ProcessImageFileName = 27, ProcessBreakOnTermination = 29, ProcessDebugObjectHandle = 30, //undocumented, 0x1e ProcessDebugFlags = 31 //undocumented, 0x1f } PROCESSINFOCLASS;
-
传递
ProcessDebugPort
时,如果进程正在被调试,API会检索到一个等于0xFFFFFFFF(十进制-1)的DWORD值。 -
传递
ProcessDebugFlags
,将返回一个EPROCESS
内核结构//这是一个简略的`EPROCESS`结构,请访问参见来查看完整的结构 //undocumented typedef struct _EPROCESS { //... // 调试端口 PVOID DebugPort; // 异常端口 PVOID ExceptionPort; //... // 指向3环PEB(进程环境块),包含了进程地址空间中的堆和系统模块等信息 PPEB Peb; //... union { // 包含了进程的标志位,反映了进程当前状态和配置,上面那一大堆宏就是了 ULONG Flags; // 字段只能由 PS_SET_BITS 和其他互锁宏设置。 最好通过位定义来读取字段,因此很容易找到引用 struct { ULONG CreateReported : 1; ULONG NoDebugInherit : 1; // 调试器 //... } } } EPROCESS, *PEPROCESS;
-
传递
ProcessDebugObjectHandle
,获取调试对象句柄
-
-
通过
NtQuerySystemInformation
尝试获取调试器句柄__kernel_entry NTSTATUS NtQuerySystemInformation( [in] SYSTEM_INFORMATION_CLASS SystemInformationClass, [in, out] PVOID SystemInformation, [in] ULONG SystemInformationLength, [out, optional] PULONG ReturnLength );
参数
SYSTEM_INFORMATION_CLASS
结构如下typedef enum _SYSTEM_INFORMATION_CLASS { SystemBasicInformation = 0, //... SystemKernelDebuggerInformation = 35, //undocumented, 0x23 //... SystemPolicyInformation = 134, } SYSTEM_INFORMATION_CLASS;
ntdll!NtQuerySystemInformation()
函数接受要查询的信息类别作为参数,然而该参数大多数类都没有被记录下来,包括SystemKernelDebuggerInformation(0x23)
类,它从Windows NT开始就存在了。SystemKernelDebuggerInformation
返回两个标志寄存器的值:al
中的KdDebuggerEnabled
,和ah
中的KdDebuggerNotPresent
。因此,如果内核调试器存在,ah
中的返回值为零。 -
查询调试对象
参见调试篇——调试对象与调试事件
DbgUiConnectToDbg
函数会创建调试对象,并把它放到TEB
的DbgSsReserved[1]
成员,也就是该偏移0xF24
位置。利用NtQueryObject
检查所有调试对象,可以广泛的禁止系统调试,但是容易误伤。__kernel_entry NTSYSCALLAPI NTSTATUS NtQueryObject( [in, optional] HANDLE Handle, [in] OBJECT_INFORMATION_CLASS ObjectInformationClass, [out, optional] PVOID ObjectInformation, [in] ULONG ObjectInformationLength, [out, optional] PULONG ReturnLength );
-
利用
NtSetInformationThread
将线程从调试器中隐藏。__kernel_entry NTSYSCALLAPI NTSTATUS NtSetInformationThread( [in] HANDLE ThreadHandle, [in] THREADINFOCLASS ThreadInformationClass, [in] PVOID ThreadInformation, [in] ULONG ThreadInformationLength );
关于
ThreadInformationClass
的枚举量,请查阅THREADINFOCLASS (geoffchappell.com)typedef enum _THREADINFOCLASS { ThreadBasicInformation = 0, //... ThreadHideFromDebugger = 17, //... MaxThreadInfoClass = 51, } THREADINFOCLASS;
//示例 NtSetInformationThread(handle.get(), ThreadHideFromDebugger, nullptr, 0);
参见ZwSetInformationThread - CTF Wiki (ctf-wiki.org)
反制[原创]调试陷阱ThreadHideFromDebugger的另一种对抗方法-软件逆向-看雪论坛-安全社区|安全招聘|bbs.pediy.com
-
NtYieldExecution
这个函数可以让任何就绪的线程暂停执行,等待下一个线程调度。线程放弃剩余时间,让给其他线程执行。如果没有其他准备好的线程,该函数返回false,否则返回true。当前线程如果被调试,那么调试器线程若处于单步状态,随时等待继续运行,则被调试线程执行NtYieldExecution
时,调试器线程会恢复执行。此时NtYieldExecution返回true,该线程则认为自身被调试了。
此方法并不准确,因为检测到的行为可能是由上层应用程序触发。因此会设置一个计数器。 -
父进程检查
如果启动进程不是cmd.exe
、explorer.exe
,则返回警告 -
HeapFlags
和HeapForceFlags
检查几个位,注意这些位的值的大小。如果HeapFlags
的值大于 2 说明程序处于调试状态;如果HeapForceFlags
的值大于 0 则说明处于调试状态。- Flags 字段:
- 在 32 位 Windows NT, Windows 2000 和 Windows XP 中,
Flags
位于堆的0x0C
偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x40
偏移处. - 在 64 位 Windows XP 中,
Flags
字段位于堆的0x14
偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x70
偏移处.
- 在 32 位 Windows NT, Windows 2000 和 Windows XP 中,
- ForceFlags 字段:
- 在 32 位 Windows NT, Windows 2000 和 Windows XP 中,
ForceFlags
位于堆的0x10
偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x44
偏移处. - 在 64 位 Windows XP 中,
ForceFlags
字段位于堆的0x18
偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x74
偏移处.
- 在 32 位 Windows NT, Windows 2000 和 Windows XP 中,
- Flags 字段:
-
检查作业容器
Job Object
Windows 提供一个作业对象,它允许我们将进程组合在一起并创建一个"沙箱"来限制进程能做什么.可以将作业想象成一个进程容器.但是,只包含一个进程的作业同样有用,因为这样可以对进程施加平时不能施加的限制.
检查同在一个作业对象中的其他进程。 -
SeDebugPrivileges
默认情况下进程是没有SeDebugPrivilege权限的,但是当进程通过调试器启动时,由于调试器本身启动了SeDebugPrivilege权限,所以我们可以检测进程的SeDebugPrivilege权限来间接判断是否存在调试器,而对SeDebugPrivilege权限的判断可以用能否打开csrss.exe进程来判断。 -
KUSER_SHARED_DATA
参见KUSER_SHARED_DATA (ntddk.h) - Windows drivers | Microsoft Docs
//这是一个简略的`EPROCESS`结构,请访问参见来查看完整的结构 typedef struct _KUSER_SHARED_DATA { ULONG TickCountLowDeprecated; //... BOOLEAN KdDebuggerEnabled; //位置: 0x2D4 //... ULONG64 UserPointerAuthMask; } KUSER_SHARED_DATA, *PKUSER_SHARED_DATA;
用户空间和内核空间其实有一块共享区域
KUSER_SHARED_DATA
,大小为 4 KB。它们的内存地址虽然不一样,但是它们都是有同一块物理内存映射出来的,其中存在内核调试检查位KdDebuggerEnabled
可以获取内核调试状态。
此内存块的地址为0xFFDF0000
(x86)、0xFFFFF78000000000
(x64),对应的要检查的位在0x2d4
处BOOL SharedUserData_KernelDebugger() { const ULONG_PTR UserSharedData = 0x7FFE0000; const UCHAR KdDebuggerEnabledByte = *(UCHAR*)(UserSharedData + 0x2D4); // 0x2D4 = the offset of the field // Extract the flags. // The meaning of these is the same as in NtQuerySystemInformation(SystemKernelDebuggerInformation). // Normally if a debugger is attached, KdDebuggerEnabled is true, KdDebuggerNotPresent is false and the byte is 0x3. const BOOLEAN KdDebuggerEnabled = (KdDebuggerEnabledByte & 0x1) == 0x1; const BOOLEAN KdDebuggerNotPresent = (KdDebuggerEnabledByte & 0x2) == 0; if (KdDebuggerEnabled || !KdDebuggerNotPresent) return TRUE; return FALSE; }
-
扫描关键位置字节码,排除
0xCC
-
利用TLS进行反调试
TLS在执行主函数前执行 -
WUDF驱动框架动态链接库
从"C:\Windows\System32\WUDFPlatform.dll"
中导出几个函数,其中存在一个与IsDebuggerPresent
相类似的函数WudfIsAnyDebuggerPresent
等
参考资料
作者发布、转载的任何文章中所涉及的技术、思路、工具仅供以安全目的的学习交流,并严格遵守《中华人民共和国网络安全法》、《中华人民共和国数据安全法》等网络安全法律法规。
任何人不得将技术用于非法用途、盈利用途。否则作者不对未许可的用途承担任何后果。
本文遵守CC BY-NC-SA 3.0协议,您可以在任何媒介以任何形式复制、发行本作品,或者修改、转换或以本作品为基础进行创作
您必须给出适当的署名,提供指向本文的链接,同时标明是否(对原文)作了修改。您可以用任何合理的方式来署名,但是不得以任何方式暗示作者为您或您的使用背书。
同时,本文不得用于商业目的。混合、转换、基于本作品进行创作,必须基于同一协议(CC BY-NC-SA 3.0)分发。
如有问题, 可发送邮件咨询.