Win32下常见反调试技术

1 探索内存差异

跟到的一个IceSword用户层程序中的一个反调试代码:
(跟Kernel32!IsDebuggerPresent函数的实现方法一致)
mov     eax, dword ptr fs:[18]     当前进程TEB->Ptr32 _NT_TIB
mov     eax, dword ptr [eax+30]     eax+30h是peb的地址

movzx   eax, byte ptr [eax+2]     eax+2 是peb结构中uchar beingdebug的标志的地址


ntdll!_NT_TIB
   +0x000 ExceptionList    : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 StackBase        : Ptr32 Void
   +0x008 StackLimit       : Ptr32 Void
   +0x00c SubSystemTib     : Ptr32 Void
   +0x010 FiberData        : Ptr32 Void
   +0x010 Version          : Uint4B
   +0x014 ArbitraryUserPointer : Ptr32 Void
   +0x018 Self             : Ptr32 _NT_TIB
  +0x01c EnvironmentPointer : Ptr32 Void
   +0x020 ClientId         : _CLIENT_ID
  +0x028 ActiveRpcHandle  : Ptr32 Void
   +0x02c ThreadLocalStoragePointer : Ptr32 Void
   +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
   +0x034 LastErrorValue   : Uint4B

ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar


kernel32!IsDebuggerPresent
如果进程正在被调试IsDebuggerPresent 返回值为 1, 否则为0. 这个API 简单的读取 PEB!BeingDebugged 字节标志(在PEB结构中偏移量为2).
设置PEB!BeingDebugged为0就可以轻松绕过它.
例子:
call IsDebuggerPresent
test eax, eax
jne @DebuggerDetected
...

2.    PEB!IsDebugged
这个字段指向进程环境块中的第二个字节.当进程被调试时由系统设置.这个字节可以重置为0而对程序的执行没有任何影响(这仅是一个通知标志).
  例子:
  mov eax, fs:[30h]
  mov eax, byte [eax+2]
  test eax, eax
  jne @DebuggerDetected
  ...

3. PEB!NtGlobalFlags

当进程创建的时候,系统设置一些标志,它们将定义各种API在该程序中的行为.这些标志能从PEB中偏移量为0x68的DWORD类型字段读取到. 不同标志的默认值设置依赖于进程是否被调试.如果进程正被调试, 在ntdll中一些控制堆处理流程的标志将被设置:
FLG_HEAP_ENABLE_TAIL_CHECK, FLG_HEAP_ENABLE_FREE_CHECK 和FLG_HEAP_VALIDATE_PARAMETERS.
这个反调试能被重置NtGlobalFlags 字段通过
例子:
mov eax, fs:[30h]
mov eax, [eax+68h]
and eax, 0x70
test eax, eax
jne @DebuggerDetected
  ...
    
4.    Heap flags
    
 如先前所解释的NtGlobalFlags 通知尤其是堆流程怎么表现。尽管很容易修改PEB字段,但是如果堆表现的行为和进程未被调试时不一致将会是个大问题. 这是个强有力的反调试,因为进程的堆有很多并且它们的块能单独被FLG_HEAP_*标志(例如块尾)影响. 堆头也一样能被影响.例如,检查 在堆头的ForceFlags (偏移量为 0x10) 标志能发现是否有调试器.
这有两种方式可以轻松的绕过这个反调试:

创建一个非调试进程, 然后再附加调试器(一种容易的解决方案就是创建一个挂起进程, 运行到入口点, 补丁到一个无限循环中, 恢复进程, 附加调试器, 还原入口点).

强制被调试进程的NtGlobalFlags标志,通过注册表键"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options", 创建一个键名为进程名没有值的子键,在此子键下面把字符串GlobalFlags设为空值.
  例子:
  mov eax, fs:[30h]
  mov eax, [eax+18h] ;process heap
  mov eax, [eax+10h] ;heap flags
  test eax, eax
  jne @DebuggerDetected
  ...

5. 探索系统差异
NtQueryInformationProcess
ntdll!NtQueryInformationProcess 是对ZwQueryInformationProcess 进行包装的syscall. 函数原型如下:

NTSYSAPI NTSTATUS NTAPI NtQueryInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESS_INFORMATION_CLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength
);

当把 ProcessInformationClass 设为7 (ProcessDebugPort常量)时调用, 如果进程被调试系统将会设置 ProcessInformation 为-1.
这也是个强有力的反调试, 并且没有容易的办法绕过它. 但是, 如果程序被跟踪, 当syscall返回的时候ProcessInformation能够被修改.
另外一个解决方案就是使用系统级驱动用钩子钩住ZwNtQueryInformationProcess syscall.
绕过NtQueryInformationProcess 将会使许多反调试技术 (例如CheckRemoteDebuggerPresent 或者UnhandledExceptionFilter)也失效.

例子:
push 0
push 4
push offset isdebugged
push 7 ;ProcessDebugPort
push -1
call NtQueryInformationProcess
test eax, eax
jne @ExitError
cmp isdebugged, 0
jne @DebuggerDetected
...

6. kernel32!CheckRemoteDebuggerPresent

这个API接受两个参数: 一个是进程句柄, 另一个是DWORD指针. 调用成功如果进程正在被调试则DWORD 指针值设置为1. ,在系统内部这个API通过设置参数ProcessDebugPort (7)调用ntdll!NtQueryInformationProcess ProcessInformationClass
例子:
push offset isdebugged
push -1
call CheckRemoteDebuggerPresent
test eax, eax
jne @DebuggerDetected
...

7. UnhandledExceptionFilter
当发生异常时Windows XP SP2, Windows XP SP3,Windows 2003,Windows Vista等操作系统处理异常的方式如下:

- 如果有向量化异常则传递控制给每个进程的向量化异常处理器.
- 如果异常未被处理, 传递控制给每个线程的SEH 处理器, 在产生异常的进程中指向 FS:[0]. SEH 是链起来的,如果前面的未处理则后面的将被轮流调用.
- 如果异常没有被任何处理器处理, 最终SEH处理器 (由系统设置)将会调用 kernel32!UnhandledExceptionFilter. 这个函数将会怎么做取决于进程是否被调试
- 如果未被调试, 它会调用用户级过滤器 (通过kernel32!SetUnhandledExceptionFilter设置).
- 如果被调试, 程序将被终止.

在UnhandledExceptionFilter中是使用ntdll!NtQueryInformationProcess检测调试器的.

例子:
push @not_debugged
call SetUnhandledExceptionFilter
xor eax, eax
mov eax, dword [eax] ; trigger exception
;program terminated if debugged
;...
@not_debugged:
;process the exception
;continue the execution
;...

8. NtSetInformationThread
ntdll!NtSetInformationThread 是对 ZwSetInformationThread syscall包装. 函数原型如下:
NTSYSAPI NTSTATUS NTAPI NtSetInformationThread(
IN HANDLE ThreadHandle,
IN THREAD_INFORMATION_CLASS ThreadInformationClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength
);

当把ThreadInformationClass 设置为0x11 (ThreadHideFromDebugger常量)调用, 线程将会从调试器在分离出去.

类似于 ZwQueryInformationProcess, 绕过这个反调试要么修改ZwSetInformationThread 参数在它被调用之前, 要么直接使用内核驱动钩子钩住这个syscall.
例子:
push 0
push 0
push 11h ;ThreadHideFromDebugger
push -2
call NtSetInformationThread
;thread detached if debugged
;...

9. kernel32!CloseHandle 和NtClose

APIs让ZwClose syscall (例如CloseHandle, 非直接地) 的使用者能用于检测调试器. 当进程被调试, 使用一个无效的句柄调用 ZwClose 将会产生一个STATUS_INVALID_HANDLE (0xC0000008) 异常.
As with all anti-debugs that rely on information made directly available from the kernel (therefore involving a syscall), 唯一合适绕过"CloseHandle"这个反调试的办法就是要么在它被调用之前从ring3修改syscall 数据 , 要么安装内核钩子.
这个反调试尽管非常的强悍,但还没见广泛用于恶意软件.

例子:
push offset @not_debugged
push dword fs:[0]
mov fs:[0], esp
push 1234h ;invalid handle
call CloseHandle
; if fall here, process is debugged
;...
@not_debugged:
;...

10. Self-debugging(自我调试)

进程能通过调试自身检测出是否被调试. 例如创建一个新进程,然后在父进程中调用kernel32!DebugActiveProcess(pid).

这个API轮流调用 ntdll!DbgUiDebugActiveProcess ,而ntdll!DbgUiDebugActiveProcess将调用syscall ZwDebugActiveProcess. 如果进程正在被调试则syscall 失败. 注意检索父进程ID能通过toolhelp32 APIs (在父进程PROCESSENTRY32结构中的th32ParentProcessID字段).

11. Kernel-mode timers(内核模式定时器)

  kernel32!QueryPerformanceCounter是个有效的反调试。该API调用ntdll!NtQueryPerformanceCounter,而ntdll!NtQueryPerformanceCounter是对 ZwQueryPerformanceCounter 包装的syscall.

  没有找到比较容易的办法绕过这种反调试.

12. User-mode timers(用户模式定时器)

kernel32!GetTickCount 这个API返回自从系统启动以来的毫秒数. 非常有意思的就是它并没有使用内核相关的服务来完成任务. 每个用户模式进程拥有映射到自己地址空间的计数器. 对于8Gb 用户模式空间, 返回值如下:

d[0x7FFE0000] * d[0x7FFE0004] / (2^24)

13. kernel32!OutputDebugStringA

这个反调试非常的原始, 我只在用ReCrypt v0.80加壳的文件中遇到过一次. 这个伎俩包括使用一个有效的ASCII字符串调用 OutputDebugStringA. 如果程序正处于被调试, 返回值将会是作为参数传递进去的字符串的地址. 在正常条件下, 返回值为1.

例子:
xor eax, eax
push offset szHello
call OutputDebugStringA
cmp eax, 1
jne @DebuggerDetected
...

14. Ctrl-C

当控制台程序被调试, Ctrl-C信号将会抛出EXCEPTION_CTL_C异常, 反之信号处理器将会被直接调用如果未被调试.

例子:
push offset exhandler
push 1
call RtlAddVectoredExceptionHandler
push 1
push sighandler
call SetConsoleCtrlHandler
push 0
push CTRL_C_EVENT
call GenerateConsoleCtrlEvent
push 10000
call Sleep
push 0
call ExitProcess
exhandler:
;check if EXCEPTION_CTL_C, if it is,
;debugger detected, should exit process
;...
sighandler:
;continue
;...

15. CPU anti-debug
Rogue Int3(流氓的Int3)
愚弄脆弱的调试器这可是个经典的反调试.它包含插入INT3 机器码在有效的指令序列中间. 当INT3 被执行到, 如果程序未被调试, 控制将会传递给受保护的异常处理器程序执行继续.

由于INT3指令被调试器用于设置软件断点, 插入INT3机器码能够愚弄调试器使其认为这是一个自己的断点. 因此控制不会传递给异常处理器, 当然程序的结果将会改变. 调试器应该跟踪在哪里自己设置了软件断点来避免它.

类似地, 注意 INT3 可以编码为0xCD, 0x03.

例子:
push offset @handler
push dword fs:[0]
mov fs:[0], esp
;...
db 0CCh
;if fall here, debugged
;...
@handler:
;continue execution
;...

16. "Ice" Breakpoint
这个所谓的"Ice breakpoint" 是Intel 未公开的指令之一, 机器码 0xF1. 它被用于检查跟踪程序.
执行这个指令将产生单步异常. 因此 ,如果程序已经被跟踪, 调试器将会以为它是通过设置标志寄存器中的单步标志位生成的正常异常. 相关的异常处理器将不会被执行到, 自然程序也就不会按预期的执行了.
绕过这个伎俩很容易: 运行跳过这个指令, 而不是单步到它上面. 异常将会生成, 既然程序未被跟踪 , 调试器应该知道传递控制给异常处理器.
例子:
push offset @handler
push dword fs:[0]
mov fs:[0], esp
;...
db 0F1h
;if fall here, traced
;...
@handler:
;continue execution
;...

18. Interrupt 2Dh(2Dh中断)
如果程序未被调试这个中断将会生产一个断点异常. 被调试并且未使用跟踪标志执行这个指令, 将不会有异常产生程序正常执行. 如果被调试并且指令被跟踪, 尾随的字节将被跳过并且执行继续. 因此, 使用 INT 2Dh 能作为一个强有力的反调试和反跟踪机制.
例子:
push offset @handler
push dword fs:[0]
mov fs:[0], esp
;...
db 02Dh
mov eax, 1 ;anti-tracing
;...
@handler:
;continue execution
;...

19, Timestamp counters (时间戳计数器)
存储自系统启动以来CPU时钟周期数的高精度计数,能使用 RDTSC指令查询. 经典的反调试包括在关键点测量时间差值, 常在异常处理器周围. 如果差值太大, 这就意味着程序正处于调试器控制之下 (在调试器中处理异常并且把控制返回给被调试者这是个很耗时的任务).
例子:
push offset handler
push dword ptr fs:[0]
mov fs:[0],esp
rdtsc
push eax
xor eax, eax
div eax ;trigger exception
rdtsc
sub eax, [esp] ;ticks delta
add esp, 4
pop fs:[0]
add esp, 4
cmp eax, 10000h ;threshold
jb @not_debugged
@debugged:
...
@not_debugged:
...
handler:
mov ecx, [esp+0Ch]
add dword ptr [ecx+0B8h], 2 ;skip div
xor eax, eax
ret

20. Popf and the trap flag(Popf和陷阱标志)
位于标志寄存器中的陷阱标志, 控制程序的跟踪. 如果标志被设置, 执行一个指令将会生产一个单步异常. 陷阱标志能被处理为了阻碍跟踪者. 例如, 下面这的指令序列将会设置陷阱标志:

pushf
mov dword [esp], 0x100
popf

如果程序被跟踪,将不会没有实际效果在标志寄存器上, 因此调试器将会处理异常,认为这是来自合法的跟踪. 异常处理器不会被执行到. 绕过这个反跟踪伎俩只要简单的跳过pushf指令

21. Stack Segment register(堆栈段寄存器)
这是个非常原始的反跟踪. 我在一个叫MarCrypt的壳中遇到. 我相信这并不广为人知, 更不用说使用了.
它包括跟踪过下面的指令序列:

push ss
pop ss
pushf
nop

当跟踪到pop ss上面, 下一个指令将会被执行并且调试器不会中断于其上, 因此停止在下面一个指令上 (在本例中是NOP ).
Marcrypt 以下面的方式使用这个反调试:

push ss
; junk
pop ss
pushf
; junk
pop eax
and eax, 0x100
or eax, eax
jnz @debugged
; carry on normal execution

这个伎俩就是这样的, 如果调试器跟踪过这些指令序列, popf 将会被隐式执行, 并且调试器不能够设置刚进堆栈的陷阱标志位. 保护程序检查陷阱标志如果发现则终止程序.
一个简单绕过这个反跟踪的办法就是断点在popf (我不明白这里上面的指令序列中并没有popf,是不是作者写错了应该是pushf)然后运行程序(为了避免使用陷阱标志).

22. Debug registers manipulation(调试寄存器处理)
调试寄存器 (DR0 到 DR7) 被用于设置硬件断点. 保护程序能处理它们来检测到硬件断点被设置(也就是说正在被调试), 重置它们或者设置为特殊的值来执行以后的代码检查. 一些壳例如tElock 使用调试寄存器来阻止逆向者使用它们.
从用户模式视图来看, 调试寄存器不能使用有特权的 'mov drx, ...' 指令来设置. 其它的方式存在:
- 一个异常产生,线程上下文改变 (它包括在异常发生之时的CPU寄存器), 然后恢复到使用新的上下文正常执行.
- 其它的方式就是使用NtGetContextThread和 NtSetContextThread syscalls (在kernel32中使用 GetThreadContext和SetThreadContext有效).

大多数的保护者使用第一种, 非正式方式.
例子:
push offset handler
push dword ptr fs:[0]
mov fs:[0],esp
xor eax, eax
div eax ;generate exception
pop fs:[0]
add esp, 4
;continue execution
;...
handler:
mov ecx, [esp+0Ch] ;skip div
add dword ptr [ecx+0B8h], 2 ;skip div
mov dword ptr [ecx+04h], 0 ;clean dr0
mov dword ptr [ecx+08h], 0 ;clean dr1
mov dword ptr [ecx+0Ch], 0 ;clean dr2
mov dword ptr [ecx+10h], 0 ;clean dr3
mov dword ptr [ecx+14h], 0 ;clean dr6
mov dword ptr [ecx+18h], 0 ;clean dr7
xor eax, eax
ret

23. Context modification(上下文改变)

由于调试寄存器使用, 上下文也能够以一种不方便的方式改变程序的执行流程. 调试器很容易被混淆!
注意另一个syscall, NtContinue能用于在当前线程中加载一个新的上下文 (例如,这个 syscall 被用于异常处理器的管理).

24. 未归类的反调试
(1) TLS-callback(线程本地存储回调)
这个反调试在几年前还并不为人所知. 它包括指示PE 装载器程序的第一个入口点位于线程本地存储入口(在PE可选头部中的第10个目录入口数), 通过这样做, 程序的入口点将不会首先被执行. TLS入口能以一种隐蔽的方式执行反调试检查.
注意实际上, 这个技术并没有广泛使用.
即使稍老一点的的调试器 (包括 OllyDbg在内)也并没有注意到TLS, 对策是非常容易采纳的, 通过自定义插件的补丁工具.

(2) CC scanning(CC扫描)
壳最常见的一种保护方案就是循环对CC(这是INT3的机器码)扫描, 目的在于检测调试器设置的软件断点. 如果你想要避免这个麻烦, 可以使用硬件断点或者自定义一种软件断点. CLI (0xFA)(汇编语言中是清除中断指令) 就是替换INT3机器码很好的候选者. This instruction does have the requirements for the job: 在ring3级程序中执行它会产生一个特权指令异常, 并且只占用一字节空间.

(3) EntryPoint RVA set to 0(入口点RVA偏移量设置为0)
一些加壳的文件把入口点RVA设置为0, 这意味着它们将会开始执行 'MZ...' 对应于'dec ebx / pop edx ...'.
这本身并不是一个反调试技术, 但是如果使用软件断点中断在入口点就会变得让人苦恼.
如果创建一个挂起的进程, 然后设置 INT3 于 RVA 0, 将会擦除 MZ 值('M'). 这个魔术标志在进程创建时被检查, 但是当进程恢复(希望到达入口点)时它会再次在ntdll 中被检查. 哪样的话, 一个 INVALID_IMAGE_FORMAT 异常将会产生.
如果你创建自己的跟踪或者调试工具, 可以使用硬件断点避免这个问题


(4) Vista 反调试 (没有名字)
这个反调试特定于Windows Vista ,我通过比较在有和没有调试器时程序的内存转储发现的.我不能确定它的可靠性, 但是值得一提(测试于 Windows Vista 32位, SP0, 英文版本).
当进程被调试, 它的主线程TEB, 在偏移量0xBFC, 包括一个在系统dll中引用 的unicode字符串的指针. 而且, 字符串就跟在指针后面 (因此位于TEB中偏移量 0xC00). 如果进程未被调试, 指针为 NULL并且字符串没有出现.
例子:
call GetVersion
cmp al, 6
jne @NotVista
push offset _seh
push dword fs:[0]
mov fs:[0], esp
mov eax, fs:[18h] ; teb
add eax, 0BFCh
mov ebx, [eax] ; pointer to a unicode string
test ebx, ebx ; (ntdll.dll, gdi32.dll,...)
je @DebuggerNotFound
sub ebx, eax ; the unicode string follows the
sub ebx, 4 ; pointer
jne @DebuggerNotFound
;debugger detected if it reaches this point
;...




你可能感兴趣的:(逆向工程)