IzayoiShiki
反调试技术整理

反调试技术整理

反调试技术整理

Anti-Debug: Exceptions (checkpoint.com)

IsDebuggerPresent()

检查peb中的BeingDebugged参数,当返回值为1时表明正在被调试

检查段寄存器中的fs:[0x30] (32位)GS:[0x60] (64位)也是一样的效果

IsDebuggerPresent - CTF Wiki (ctf-wiki.org)

CheckRemoteDebuggerPresent()

1
2
3
4
BOOL WINAPI CheckRemoteDebuggerPresent(
_In_ HANDLE hProcess,
_Inout_ PBOOL pbDebuggerPresent
);

如果调试器存在,pbDebuggerPresent的值会被设置为0xffffffff

CheckRemoteDebuggerPresent通过调用NtQueryInformationProcess来实现对调试器的检测,当参数设置为7时,NtQueryInformationProcess会返回远程调试器的端口

CheckRemoteDebuggerPresent - CTF Wiki (ctf-wiki.org)

OutputDebugString

在有调试器存在和没有调试器存在时,OutputDebugString函数表现会有所不同。最明显的不同是, 如果有调试器存在,
其后的GetLastError()的返回值为零

异常捕获与处理

程序通过引发或捕获异常,将异常丢给调试器进行处理,从而完成反调试,在没有调试器的时候程序自身需要拥有处理异常的能力

Windows异常

Windows异常处理机制 - 知乎 (zhihu.com)

VEH和SEH

VEH和SEH_seh veh-CSDN博客

VEH

1.CPU捕获异常
2.通过KiDispatchException进行分发(3环异常将EIP修改为KiUserExceptionDispatcher)
3.KiUserExceptionDispatcher调用RtlDispatchException
4.RtlDispatchException查找VEH处理函数链表,并调用相关处理函数
5.代码返回到ZwContinue再次进入0环
6.线程再次返回3环后,从修正的位置开始执行

SEH

1.FS:[0]指向SEH链表的第一个成员

2.SEH的异常处理函数必须在当前线程的堆栈中

3.只有当VEH中的异常处理函数不存在或者不处理才会到SEH链表中查找

异常捕获

硬件异常

1.检测到异常

2.查找IDT表,执行中断处理函数_KiTrap00

_KiTrap00函数:1)保存现场,将寄存器堆栈信息等保存到_trap_frame

2)调用CommonDispatchException函数(分析参数EAX,EBX中的值)

3.CommonDispatchException函数:该函数构造一个_EXCEPTION_RECORD结构体并赋值

1
2
3
4
5
6
7
8
9
10
type struct_EXCEPTION_RECORD
{
DWORD ExceptionCode; //异常代码
DWORD ExceptionFlags; //异常状态
struct_EXCEPTION_RECORD* ExceptionRecord; //下一个异常
PVOID ExceptionAddress; //异常发生地址
DWORD NumberParameters; //附加参数指针
ULONG_PTR ExceptionInformation
[EXCEPTION_MAXIMUM_PARAMETERS]; //附加参数个数
}

4.调用KiDispatchException

软件异常

1.检测到throw关键字

2.转入CxxThrowException

3.调用KERNEL32.DLL RaiseException,填充_EXCEPTION_RECORD结构体

4.转入NTDLL.DLL RtIRaiseException()

5.转入NT NtRaiseException

6.转入NT KiRaiseExceptionExceptionCode最高位清零

7.调用KiDispatchException

异常处理

内核异常

1.尝试传递给内核调试器

2.失败,调用RtIDispatchException传递给SEH

RtIDispatchException函数:遍历异常链表,调用异常处理函数,如果成功处理返回1,失败则调用下一个,直到最后也没成功处理即返回0

image-20241118223532698

3.传递给内核调试器

4.终止Windows运行(BSOD)

用户异常

1.尝试传递给内核调试器

2.失败,则传入KiUserExceptionDispatcher

3.调用RtIDispatchException查找并执行异常处理函数

4.由VEH链表查找至SEH链表

5.如果未处理,则传递给内核调试器

6.失败,终止进程

try与__try

C++ 异常捕获 try 和 __try的区别-CSDN博客

CloseHandle()

反调试 - CloseHanlde,NtClose_closehandle 检测异常-CSDN博客

调用CloseHandle释放一个无效句柄,这个异常会被一个异常处理程序缓存,即被调试器缓存,如果没有被调试,那么函数返回FALSE,GetLastError = ERROR_INVALID_HANDLE:0xC0000008;但如果被调试,那么系统将抛出异常 C0000008H

断点植入(int 2dh, int 3h)

提前植入一个断点,再植入VEH异常处理例程,如果正在被调试的话会抛出中断异常

2d断点会跳过下一个字节

Interrupt 3 - CTF Wiki (ctf-wiki.org)

Page-Guard

分配被保护的内存区,用PAGE_GUARD属性修饰,填入ret指令,如果程序尝试访问保护页中的地址,系统会引发 STATUS_GUARD_PAGE_VIOLATION (0x80000001) 异常

创建 Guard 页面 - Win32 apps | Microsoft Learn

引发EXCEPTION_EXECUTE_HANDLER错误

EFLAGS

使用EFLAGS写入异常标志,然后监视异常

将EFLAGS与0X100进行或运算,利用VEH检测调试器

EFLAGS寄存器(标志寄存器) - Reverse-xiaoyu - 博客园 (cnblogs.com)

UnhandledExceptionFilter()

引发并不处理异常,诱导调试器去处理

反调试 - SetUnhandledExceptionFilter-CSDN博客

内存扫描与监视

硬件断点寄存器监视

利用GetThreadContext获取上下文,检查硬件断点寄存器的值是否为0

软断点、硬件断点和内存断点(逆向基础知识) - 知乎 (zhihu.com)

MEM_WRITE_WATCH

利用MEM_WRITE_WATCH检查申请的内存块中的写入,访问情况

VirtualAlloc 函数 (memoryapi.h) - Win32 apps | Microsoft Learn

LFH低碎片堆

堆碎片是一种状态,其中可用内存被分解为较小的非连续块。 当堆被碎片化时,即使堆中的可用内存总量足以满足请求,内存分配也可能会失败,因为没有一个内存块足够大。 低碎片堆 (LFH) 有助于减少堆碎片

调试工具无法启用LFH,会导致空间开辟错误,因此可以判断是否正在调试

低碎片堆 - Win32 apps | Microsoft Learn

反HOOK

通过对比DLL中函数的地址判断是否被hook

NtGlobalFlag

在 32 位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处, 64 位机器则是在偏移0xBC位置. 该字段的默认值为 0. 当调试器正在运行时, 该字段会被设置为一个特定的值. 尽管该值并不能十分可信地表明某个调试器真的有在运行

有调试器时会有以下标志位

1
2
3
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

NtGlobalFlag - CTF Wiki (ctf-wiki.org)

NtQueryInformationProcess

ProcessInformationClass设置为7,可以检查调试器

NtQueryInformationProcess 函数 (winternl.h) - Win32 apps | Microsoft Learn

NtQuerySystemInformation

当第一个参数传入 0x23 (SystemInterruptInformation) 时,会返回一个 SYSTEM_KERNEL_DEBUGGER_INFORMATION 结构,里面的成员KdKdDebuggerEnableKdDebuggerNotPresent 标志系统是否启用内核调试

反调试 - r3 使用 NtQuerySystemInformation 获取 KdKdDebuggerEnable 和 KdDebuggerNotPresent_r3 hook ntquerysysteminformation-CSDN博客

查询调试对象

利用NtQueryObject检查所有调试对象,从而禁止调试

Windows软件调试学习笔记(一)—— 调试对象_createdebugobject-CSDN博客

ZwSetInformationThread

将第二个参数设为0x11,可以将进程隐藏

ZwSetInformationThread - CTF Wiki (ctf-wiki.org)

NtYeildExecution

这个函数可以让任何就绪的线程暂停执行,等待下一个线程调度。
当前线程放弃剩余时间,让给其他线程执行。如果没有其他准备好的线程,该函数返回false,否则返回true。
当前线程如果被调试,那么调试器线程若处于单步状态,随时等待继续运行,则被调试线程执行NtYieldExecution时,调试器线程会恢复执行。
此时NtYieldExecution返回true,该线程则认为自身被调试了

父进程检查

如果启动进程不是cmd.exe或explorer.exe,则返回警告

Heap Flags

Heap Flags - CTF Wiki (ctf-wiki.org)

HeapFlag

大于2说明正在被调试

HeapForceFlag

大于0说明正在被调试

Job Object

为了共享权限,调试器和被调试进程会放在同一job object中,通过检查进程同一object中的所有进程,可以枚举出调试器

SeDebugPrivileges

一般进程是默认禁用SeDebugPrivilege权限的,但是调试器会开启此权限,通过调试器开启的进程也会获得该权限,通过检查进程是否启用SeDebugPrivilege权限可以间接检测调试器

在 C++ 中启用和禁用特权 - Win32 apps | Microsoft Learn

KUSER_SHARD_DATA

用户空间和内核空间有一块共享空间KUSER_SHARD_DATA,可以通过检查其中的内核调试检查位KdDebuggerEnabled来获取内核调试状态

KUSER_SHARED_DATA (ntddk.h) - Windows drivers | Microsoft Learn

0xCC

Debug版本下未被初始化的区域会被设置为0xCC,字符为“烫”,通过检查关键区域的字节码,确认其中是否有0xCC,可以检测到是否处于调试状态

TLS_Callback

TLS在main函数之前执行

[原创]TLS回调函数(Note)-软件逆向-看雪-安全社区|安全招聘|kanxue.com

WUDF

WUDFPlatfrom.dll中有一个函数类似于IsDebuggerPresent,可以用于检测调试器

Author:IzayoiShiki
Link:http://izayoishiki.github.io/2024/11/18/反调试技术整理/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可