【翻译】Windows 反调试参考翻译

[1] 说明
这篇文章分类并提供了几种在基于WINDOWS NT的操作系统中使用的反调试技术.反调试技术是程序用来检测自身是否运行于调试器之下的各种方式之一.它们被用于商业执行保护,加壳以及恶意软件, 为了阻止或减慢逆向工程过程.我们假定程序是在ring3级调试器,例如Windows平台的OllyDbg下分析.本文的目标是逆向工程人员和恶意软件分析人员.注意有一点我们只单纯的涉及一些基本的反调试和反跟踪技术.特殊的调试器检测,例如窗口或进程枚举,注册表扫描等等. 在此我将不再多说.
[2] 反调试和反跟踪技术
- 探索内存差异
(1) 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中一些控制堆处理流程的标志将被设置:
***_HEAP_ENABLE_TAIL_CHECK, ***_HEAP_ENABLE_FREE_CHECK 和***_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字段,但是如果堆表现的行为和进程未被调试时不一致将会是个大问题. 这是个强有力的反调试,因为进程的堆有很多并且它们的块能单独被***_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) 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 
;...
- 探索系统差异
(1) 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 
...
(2) kernel32!CheckRemoteDebuggerPresent
这个API接受两个参数: 一个是进程句柄, 另一个是DWORD指针. 调用成功如果进程正在被调试则DWORD 指针值设置为1. ,在系统内部这个API通过设置参数ProcessDebugPort (7)调用ntdll!NtQueryInformationProcess ProcessInformationClass 
例子: 
push offset isdebugged 
push -1 
call CheckRemoteDebuggerPresent 
test eax, eax 
jne @DebuggerDetected 
...
(3) UnhandledExceptionFilter
当发生异常时Windows XP SP>=2, Windows 2003, and 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 
;...
(4) 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 
;...
(5) 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: 
;...
(6) Self-debugging(自我调试)
进程能通过调试自身检测出是否被调试. 例如创建一个新进程,然后在父进程中调用kernel32!DebugActiveProcess(pid).
这个API轮流调用 ntdll!DbgUiDebugActiveProcess ,而ntdll!DbgUiDebugActiveProcess将调用syscall ZwDebugActiveProcess. 如果进程正在被调试则syscall 失败. 注意检索父进程ID能通过toolhelp32 APIs (在父进程PROCESSENTRY32结构中的th32ParentProcessID字段).
(7) Kernel-mode timers(内核模式定时器)
kernel32!QueryPerformanceCounter 是一个有效的反调试. 这个API调用ntdll!NtQueryPerformanceCounter ,而ntdll!NtQueryPerformanceCounter是对 ZwQueryPerformanceCounter 包装的syscall.
再一次, 没有容易的办法绕过这个反调试.
(8) User-mode timers(用户模式定时器)
kernel32!GetTickCount 这个API返回自从系统启动以来的毫秒数. 非常有意思的就是它并没有使用内核相关的服务来完成任务. 每个用户模式进程拥有映射到自己地址空间的计数器. 对于8Gb 用户模式空间, 返回值如下:
d[0x7FFE0000] * d[0x7FFE0004] / (2^24)
(9) kernel32!OutputDebugStringA
这个反调试非常的原始, 我只在用ReCrypt v0.80加壳的文件中遇到过一次. 这个伎俩包括使用一个有效的ASCII字符串调用 OutputDebugStringA. 如果程序正处于被调试, 返回值将会是作为参数传递进去的字符串的地址. 在正常条件下, 返回值为1.
例子: 
xor eax, eax 
push offset szHello 
call OutputDebugStringA 
cmp eax, 1 
jne @DebuggerDetected 
...
(10) 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 
;...
- CPU anti-debug
(1) 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 
;...
(2) "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 
;...
(3) 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 
;...
(4) 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
(5) Popf and the trap flag(Popf和陷阱标志)
位于标志寄存器中的陷阱标志, 控制程序的跟踪. 如果标志被设置, 执行一个指令将会生产一个单步异常. 陷阱标志能被处理为了阻碍跟踪者. 例如, 下面这的指令序列将会设置陷阱标志:
pushf 
mov dword [esp], 0x100 
popf
如果程序被跟踪,将不会没有实际效果在标志寄存器上, 因此调试器将会处理异常,认为这是来自合法的跟踪. 异常处理器不会被执行到. 绕过这个反跟踪伎俩只要简单的跳过pushf指令
(6) 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)然后运行程序(为了避免使用陷阱标志).
(7) 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
(8) Context modification(上下文改变)
由于调试寄存器使用, 上下文也能够以一种不方便的方式改变程序的执行流程. 调试器很容易被混淆! 
注意另一个syscall, NtContinue能用于在当前线程中加载一个新的上下文 (例如,这个 syscall 被用于异常处理器的管理).
- 未归类的反调试
(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 异常将会产生.
如果你创建自己的跟踪或者调试工具, 可以使用硬件断点避免这个问题.
[3]总结
知道恶意软件或者保护壳使用的反调试和反跟踪技术对于逆向工程者是非常有用的知识. 程序总是有办法检测到在调试器中运行--- 同样适用于虚拟或模拟环境, 但是既然ring3 级调试器是已知最常见使用的分析工具, 了解常见的反调试技巧,以及怎么样通过它们事实证明是很有用的.
[4]链接
MSDN 
Portable Executable Tutorial, Matt Pietrek 
Syscall Reference, The Metasploit Project 
Undocumented Functions for MS Windows NT/2K 
Intel Manuals 
- Common exception codes - Microsoft Windows SDK, ntdll.h 
- Status codes list (including common exception codes) - Microsoft Windows DDK, ntstatus.h 
- Context Structures documentation - Microsoft Windows SDK, ntdll.h
[5] Data reference
- CONTEXT structure for IA32 processors 
struct CONTEXT_IA32 

// ContextFlags must be set to the appropriate CONTEXT_* flag 
// before calling (Set|Get)ThreadContext 
DWORD ContextFlags; 
// CONTEXT_DEBUG_REGISTERS (not included in CONTEXT_FULL) 
DWORD Dr0; // 04h 
DWORD Dr1; // 08h 
DWORD Dr2; // 0Ch 
DWORD Dr3; // 10h 
DWORD Dr6; // 14h 
DWORD Dr7; // 18h
// CONTEXT_FLOATING_POINT 
FLOATING_SAVE_AREA FloatSave;
// CONTEXT_SEGMENTS 
DWORD SegGs; // 88h 
DWORD SegFs; // 90h 
DWORD SegEs; // 94h 
DWORD SegDs; // 98h
// CONTEXT_INTEGER 
DWORD Edi; // 9Ch 
DWORD Esi; // A0h 
DWORD Ebx; // A4h 
DWORD Edx; // A8h 
DWORD Ecx; // ACh 
DWORD Eax; // B0h
// CONTEXT_CONTROL 
DWORD Ebp; // B4h 
DWORD Eip; // B8h 
DWORD SegCs; // BCh (must be sanitized) 
DWORD EFlags; // C0h 
DWORD Esp; // C4h 
DWORD SegSs; // C8h
// CONTEXT_EXTENDED_REGISTERS (processor-specific) 
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; 
};
- Process Environment Block structure (from The Wine Project) 
struct PEB 

BOOLEAN InheritedAddressSpace; // 00 
BOOLEAN ReadImageFileExecOptions; // 01 
BOOLEAN BeingDebugged; // 02 
BOOLEAN SpareBool; // 03 
HANDLE Mutant; // 04 
HMODULE ImageBaseAddress; // 08 
PPEB_LDR_DATA LdrData; // 0c 
RTL_UPROCESS_PARAMETERS *ProcessParameters; // 10 
PVOID SubSystemData; // 14 
HANDLE ProcessHeap; // 18 
PRTL_CRITICAL_SECTION FastPebLock; // 1c 
PVOID /*PPEBLOCKROUTI*/ FastPebLockRoutine; // 20 
PVOID /*PPEBLOCKROUTI*/ FastPebUnlockRoutine; // 24 
ULONG EnvironmentUpdateCount; // 28 
PVOID KernelCallbackTable; // 2c 
PVOID EventLogSection; // 30 
PVOID EventLog; // 34 
PVOID /*PPEB_FREE_BLO*/ FreeList; // 38 
ULONG TlsExpansionCounter; // 3c 
PRTL_BITMAP TlsBitmap; // 40 
ULONG TlsBitmapBits[2]; // 44 
PVOID ReadOnlySharedMemoryBase; // 4c 
PVOID ReadOnlySharedMemoryHeap; // 50 
PVOID *ReadOnlyStaticServerData; // 54 
PVOID AnsiCodePageData; // 58 
PVOID OemCodePageData; // 5c 
PVOID UnicodeCaseTableData; // 60 
ULONG NumberOfProcessors; // 64 
ULONG NtGlobalFlag; // 68 
BYTE Spare2[4]; // 6c 
LARGE_INTEGER CriticalSectionTimeout; // 70 
ULONG HeapSegmentReserve; // 78 
ULONG HeapSegmentCommit; // 7c 
ULONG HeapDeCommitTotalFreeTh; // 80 
ULONG HeapDeCommitFreeBlockTh; // 84 
ULONG NumberOfHeaps; // 88 
ULONG MaximumNumberOfHeaps; // 8c 
PVOID *ProcessHeaps; // 90 
PVOID GdiSharedHandleTable; // 94 
PVOID ProcessStarterHelper; // 98 
PVOID GdiDCAttributeList; // 9c 
PVOID LoaderLock; // a0 
ULONG OSMajorVersion; // a4 
ULONG OSMinorVersion; // a8 
ULONG OSBuildNumber; // ac 
ULONG OSPlatformId; // b0 
ULONG ImageSubSystem; // b4 
ULONG ImageSubSystemMajorVersion; // b8 
ULONG ImageSubSystemMinorVersion; // bc 
ULONG ImageProcessAffinityMask; // c0 
ULONG GdiHandleBuffer[34]; // c4 
ULONG PostProcessInitRoutine; // 14c 
PRTL_BITMAP TlsExpansionBitmap; // 150 
ULONG TlsExpansionBitmapBits[32]; // 154 
ULONG SessionId; // 1d4 
};
- Thread Environment Block structure (from The Wine Project) 
struct TEB 

NT_TIB Tib; // 000 Info block 
PVOID EnvironmentPointer; // 01c 
CLIENT_ID ClientId; // 020 PID,TID 
PVOID ActiveRpcHandle; // 028 
PVOID ThreadLocalStoragePointer; // 02c 
PEB *Peb; // 030 
DWORD LastErrorValue; // 034 
ULONG CountOfOwnedCriticalSections; // 038 
PVOID CsrClientThread; // 03c 
PVOID Win32ThreadInfo; // 040 
ULONG Win32ClientInfo[0x1f]; // 044 
PVOID WOW32Reserved; // 0c0 
ULONG CurrentLocale; // 0c4 
ULONG FpSoftwareStatusRegister; // 0c8 
PVOID SystemReserved1[54]; // 0cc 
PVOID Spare1; // 1a4 
LONG ExceptionCode; // 1a8 
BYTE SpareBytes1[40]; // 1ac 
PVOID SystemReserved2[10]; // 1d4 
DWORD num_async_io; // 1fc 
ULONG_PTR dpmi_vif; // 200 
DWORD vm86_pending; // 204 
DWORD pad6[309]; // 208 
ULONG gdiRgn; // 6dc 
ULONG gdiPen; // 6e0 
ULONG gdiBrush; // 6e4 
CLIENT_ID RealClientId; // 6e8 
HANDLE GdiCachedProcessHandle; // 6f0 
ULONG GdiClientPID; // 6f4 
ULONG GdiClientTID; // 6f8 
PVOID GdiThreadLocaleInfo; // 6fc 
PVOID UserReserved[5]; // 700 
PVOID glDispachTable[280]; // 714 
ULONG glReserved1[26]; // b74 
PVOID glReserved2; // bdc 
PVOID glSectionInfo; // be0 
PVOID glSection; // be4 
PVOID glTable; // be8 
PVOID glCurrentRC; // bec 
PVOID glContext; // bf0 
ULONG LastStatusValue; // bf4 
UNICODE_STRING StaticUnicodeString; // bf8 
WCHAR StaticUnicodeBuffer[261]; // c00 
PVOID DeallocationStack; // e0c 
PVOID TlsSlots[64]; // e10 
LIST_ENTRY TlsLinks; // f10 
PVOID Vdm; // f18 
PVOID ReservedForNtRpc; // f1c 
PVOID DbgSsReserved[2]; // f20 
ULONG HardErrorDisabled; // f28 
PVOID Instrumentation[16]; // f2c 
PVOID WinSockData; // f6c 
ULONG GdiBatchCount; // f70 
ULONG Spare2; // f74 
ULONG Spare3; // f78 
ULONG Spare4; // f7c 
PVOID ReservedForOle; // f80 
ULONG WaitingOnLoaderLock; // f84 
PVOID Reserved5[3]; // f88 
PVOID *TlsExpansionSlots; // f94 
};
- NtGlobalFlags 
***_STOP_ON_EXCEPTION 0x00000001 
***_SHOW_LDR_SNAPS 0x00000002 
***_DEBUG_INITIAL_COMMAND 0x00000004 
***_STOP_ON_HUNG_GUI 0x00000008 
***_HEAP_ENABLE_TAIL_CHECK 0x00000010 
***_HEAP_ENABLE_FREE_CHECK 0x00000020 
***_HEAP_VALIDATE_PARAMETERS 0x00000040 
***_HEAP_VALIDATE_ALL 0x00000080 
***_POOL_ENABLE_TAIL_CHECK 0x00000100 
***_POOL_ENABLE_FREE_CHECK 0x00000200 
***_POOL_ENABLE_TAGGING 0x00000400 
***_HEAP_ENABLE_TAGGING 0x00000800 
***_USER_STACK_TRACE_DB 0x00001000 
***_KERNEL_STACK_TRACE_DB 0x00002000 
***_MAINTAIN_OBJECT_TYPELIST 0x00004000 
***_HEAP_ENABLE_TAG_BY_DLL 0x00008000 
***_IGNORE_DEBUG_PRIV 0x00010000 
***_ENABLE_CSRDEBUG 0x00020000 
***_ENABLE_KDEBUG_SYMBOL_LOAD 0x00040000 
***_DISABLE_PAGE_KERNEL_STACKS 0x00080000 
***_HEAP_ENABLE_CALL_TRACING 0x00100000 
***_HEAP_DISABLE_COALESCING 0x00200000 
***_VALID_BITS 0x003FFFFF 
***_ENABLE_CLOSE_EXCEPTION 0x00400000 
***_ENABLE_EXCEPTION_LOGGING 0x00800000 
***_ENABLE_HANDLE_TYPE_TAGGING 0x01000000 
***_HEAP_PAGE_ALLOCS 0x02000000 
***_DEBUG_WINLOGON 0x04000000 
***_ENABLE_DBGPRINT_BUFFERING 0x08000000 
***_EARLY_CRITICAL_SECTION_EVT 0x10000000 
***_DISABLE_DLL_VERIFICATION 0x80000000
==================================================================================
一、反调试技术
反调试技术是一种常见的反检测技术,因为恶意软件总是企图监视自己的代码以检测是否自己正在被调试。为做到这一点,恶意软件可以检查自己代码是否被设置了断点,或者直接通过系统调用来检测调试器。
1.断点
为了检测其代码是否被设置断点,恶意软件可以查找指令操作码0xcc(调试器会使用该指令在断点处取得恶意软件的控制权),它会引起一个SIGTRAP。如果恶意软件代码本身建立了一个单独的处理程序的话,恶意软件也可以设置伪断点。用这种方法恶意软件可以在被设置断点的情况下继续执行其指令。
恶意软件也可以设法覆盖断点,例如有的病毒采用了反向解密循环来覆盖病毒中的断点。相反,还有的病毒则使用汉明码自我纠正自身的代码。汉明码使得程序可以检测并修改错误,但是在这里却使病毒能够检测并清除在它的代码中的断点。
2.计算校验和
恶意软件也可以计算自身的校验和,如果校验和发生变化,那么病毒会假定它正在被调试,并且其代码内部已被放置断点。VAMPiRE是一款抗反调试工具,可用来逃避断点的检测。VaMPiRE通过在内存中维护一张断点表来达到目的,该表记录已被设置的所有断点。该程序由一个页故障处理程序(PFH),一个通用保护故障处理程序(GPFH),一个单步处理程序和一个框架API组成。当一个断点被触发的时候,控制权要么传给PFH(处理设置在代码、数据或者内存映射I/O中的断点),要么传给GPFH(处理遗留的I/O断点)。单步处理程序用于存放断点,使断点可以多次使用。
3.检测调试器
在Linux系统上检测调试器有一个简单的方法,只要调用Ptrace即可,因为对于一个特定的进程而言无法连续地调用Ptrace两次以上。在Windows中,如果程序目前处于被调试状态的话,系统调用isDebuggerPresent将返回1,否则返回0。这个系统调用简单检查一个标志位,当调试器正在运行时该标志位被置1。直接通过进程环境块的第二个字节就可以完成这项检查,以下代码为大家展示的就是这种技术:
mov eax, fs:[30h]
move eax, byte [eax+2]
test eax, eax
jne @DdebuggerDetected
在上面的代码中,eax被设置为PEB(进程环境块),然后访问PEB的第二个字节,并将该字节的内容移入eax。通过查看eax是否为零,即可完成这项检测。如果为零,则不存在调试器;否则,说明存在一个调试器。
如果某个进程为提前运行的调试器所创建的,那么系统就会给ntdll.dll中的堆操作例程设置某些标志,这些标志分别是FLG_HEAP_ENABLE_TAIL_CHECK、FLG_HEAP_ENABLE_FREE_CHECK和FLG_HEAP_VALIDATE_PARAMETERS。我们可以通过下列代码来检查这些标志:
mov eax, fs:[30h]
mov eax, [eax+68h]
and eax, 0x70
test eax, eax
jne @DebuggerDetected
在上面的代码中,我们还是访问PEB,然后通过将PEB的地址加上偏移量68h到达堆操作例程所使用的这些标志的起始位置,通过检查这些标志就能知道是否存在调试器。
检查堆头部内诸如ForceFlags之类的标志也能检测是否有调试器在运行,如下所示:
mov eax, fs:[30h]
mov eax, [eax+18h] ;process heap
mov eax, [eax+10h] ;heap flags
test eax, eax
jne @DebuggerDetected
上面的代码向我们展示了如何通过PEB的偏移量来访问进程的堆及堆标志,通过检查这些内容,我们就能知道Force标志是否已经被当前运行的调试器提前设置为1了。
另一种检测调试器的方法是,使用NtQueryInformationProcess这个系统调用。我们可以将ProcessInformationClass设为7来调用该函数,这样会引用ProcessDebugPort,如果该进程正在被调试的话,该函数将返回-1。示例代码如下所示。
push 0push 4push offset isdebuggedpush 7 ;ProcessDebugPortpush -1call NtQueryInformationProcesstest eax, eaxjne @ExitErrorcmp isdebugged, 0jne @DebuggerDetected
在本例中,首先把NtQueryInformationProcess的参数压入堆栈。这些参数介绍如下:第一个是句柄(在本例中是0),第二个是进程信息的长度(在本例中为4字节),接下来是进程信息类别(在本例中是7,表示ProcessDebugPort),下一个是一个变量,用于返回是否存在调试器的信息。如果该值为非零值,那么说明该进程正运行在一个调试器下;否则,说明一切正常。最后一个参数是返回长度。使用这些参数调用NtQueryInformationProcess后的返回值位于isdebugged中。随后测试该返回值是否为0即可。
另外,还有其他一些检测调试器的方法,如检查设备列表是否含有调试器的名称,检查是否存在用于调试器的注册表键,以及通过扫描内存以检查其中是否含有调试器的代码等。
另一种非常类似于EPO的方法是,通知PE加载器通过PE头部中的线程局部存储器(TLS)表项来引用程序的入口点。这会导致首先执行TLS中的代码,而不是先去读取程序的入口点。因此,TLS在程序启动就可以完成反调试所需检测。从TLS启动时,使得病毒得以能够在调试器启动之前就开始运行,因为一些调试器是在程序的主入口点处切入的。
4.探测单步执行
恶意软件还能够通过检查单步执行来检测调试器。要想检测单步执行的话,我们可以把一个值放进堆栈指针,然后看看这个值是否还在那里。如果该值在那里,这意味着,代码正在被单步执行。当调试器单步执行一个进程时,当其取得控制时需要把某些指令压入栈,并在执行下一个指令之前将其出栈。所以,如果该值仍然在那里,就意味着其它正在运行的进程已经在使用堆栈。下面的示例代码展示了恶意软件是如何通过堆栈状态来检测单步执行的:
Mov bp,sp;选择堆栈指针
Push ax ;将ax压入堆栈
Pop ax ;从堆栈中选择该值
Cmp word ptr [bp -2],ax ;跟堆栈中的值进行比较
Jne debug ;如果不同,说明发现了调试器。
如上面的注释所述,一个值被压入堆栈然后又被弹出。如果存在调试器,那么堆栈指针�C2位置上的值就会跟刚才弹出堆栈的值有所不同,这时就可以采取适当的行动。
5.在运行时中检测速度衰减
通过观察程序在运行时是否减速,恶意代码也可以检测出调试器。如果程序在运行时速度显著放缓,那就很可能意味着代码正在单步执行。因此如果两次调用的时间戳相差甚远,那么恶意软件就需要采取相应的行动了。Linux跟踪工具包LTTng/LTTV通过观察减速问题来跟踪病毒。当LTTng/LTTV追踪程序时,它不需要在程序运行时添加断点或者从事任何分析。此外,它还是用了一种无锁的重入机制,这意味着它不会锁定任何Linux内核代码,即使这些内核代码是被跟踪的程序需要使用的部分也是如此,所以它不会导致被跟踪的程序的减速和等待。
6.指令预取
如果恶意代码篡改了指令序列中的下一条指令并且该新指令被执行了的话,那么说明一个调试器正在运行。这是指令预取所致:如果该新指令被预取,就意味着进程的执行过程中有其他程序的切入。否则,被预取和执行的应该是原来的指令。
7.自修改代码
恶意软件也可以让其他代码自行修改(自行修改其他代码),这样的一个例子是HDSpoof。这个恶意软件首先启动了一些异常处理例程,然后在运行过程中将其消除。这样一来,如果发生任何故障的话,运行中的进程会抛出一个异常,这时病毒将终止运行。此外,它在运行期间有时还会通过清除或者添加异常处理例程来篡改异常处理例程。在下面是HDSpoof清除全部异常处理例程(默认异常处理例程除外)的代码。
exception handlers before:
0x77f79bb8 ntdll.dll:executehandler2@20 + 0x003a0x0041adc9 hdspoof.exe+0x0001adc90x77e94809 __except_handler3
exception handlers after:
0x77e94809 __except_handler3
0x41b770: 8b44240c mov eax,dword ptr [esp+0xc]0x41b774: 33c9 xor ecx,ecx 0x41b776: 334804 xor ecx,dword ptr [eax+0x4]0x41b779: 334808 xor ecx,dword ptr [eax+0x8]0x41b77c: 33480c xor ecx,dword ptr [eax+0xc]0x41b77f: 334810 xor ecx,dword ptr [eax+0x10]0x41b782: 8b642408 mov esp,dword ptr [esp+0x8]0x41b786: 648f0500000000 pop dword ptr fs:[0x0]
下面是HDSpoof创建一个新的异常处理程序的代码。
0x41f52b: add dword ptr [esp],0x9ca
0x41f532: push dword ptr [dword ptr fs:[0x0]
0x41f539: mov dword ptr fs:[0x0],esp
8.覆盖调试程序信息
一些恶意软件使用各种技术来覆盖调试信息,这会导致调试器或者病毒本身的功能失常。通过钩住中断INT 1和INT 3(INT 3是调试器使用的操作码0xCC),恶意软件还可能致使调试器丢失其上下文。这对正常运行中的病毒来说毫无妨碍。另一种选择是钩住各种中断,并调用另外的中断来间接运行病毒代码。
下面是Tequila 病毒用来钩住INT 1的代码:
new_interrupt_one:
push bp
mov bp,sp
cs cmp b[0a],1 ;masm mod. needed
je 0506 ;masm mod. needed
cmp w[bp+4],09b4
ja 050b ;masm mod. needed
push ax
push es
les ax,[bp+2]
cs mov w[09a0],ax ;masm mod. needed
cs mov w[09a2],es ;masm mod. needed
cs mov b[0a],1
pop es
pop ax
and w[bp+6],0feff
pop bp
iret
一般情况下,当没有安装调试器的时候,钩子例程被设置为IRET。V2Px使用钩子来解密带有INT 1和INT 3的病毒体。在代码运行期间,会不断地用到INT 1和INT 3向量,有关计算是通过中断向量表来完成的。
一些病毒还会清空调试寄存器(DRn的内容。有两种方法达此目的,一是使用系统调用NtGetContextThread和NtSetContextThread。而是引起一个异常,修改线程上下文,然后用新的上下文恢复正常运行,如下所示:
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
上面的第一行代码将处理程序的偏移量压入堆栈,以确保当异常被抛出时它自己的处理程序能取得控制权。之后进行相应设置,包括用自己异或自己的方式将eax设为0,以将控制权传送给该处理程序。div eax 指令会引起异常,因为eax为0,所以AX将被除以零。该处理程序然后跳过除法指令,清空dr0-dr7,同样也把eax置0,表示异常将被处理,然后恢复运行。
9.解除调试器线程
我们可以通过系统调用NtSetInformationThread从调试器拆卸线程。为此,将ThreadInformationClass设为0x11(ThreadHideFromDebugger)来调用NtSetInformationThread,如果存在调试器的话,这会将程序的线程从调试器拆下来。以下代码就是一个例子:
push 0
push 0
push 11h ;ThreadHideFromDebugger
push -2
call NtSetInformationThread
在本例中,首先将NtSetInformationThread的参数压入堆栈,然后调用该函数来把程序的线程从调试器中去掉。这是因为这里的0用于线程的信息长度和线程信息,传递的-2用于线程句柄,传递的11h用于线程信息类别,这里的值表示ThreadHideFromDebugger。
10.解密
解密可以通过各种防止调试的方式来进行。有的解密依赖于特定的执行路径。如果这个执行路径没被沿用,比如由于在程序中的某个地方启动了一个调试器,那么解密算法使用的值就会出错,因此程序就无法正确进行自身的解密。HDSpoof使用的就是这种技术。
一些病毒使用堆栈来解密它们的代码,如果在这种病毒上使用调试器,就会引起解密失败,因为在调试的时候堆栈为INT 1所用。使用这种技术的一个例子是W95/SK病毒,它在堆栈中解密和构建其代码;另一个例子是Cascade病毒,它将堆栈指针寄存器作为一个解密密钥使用。代码如下所示:
lea si, Start ; position to decrypt
mov sp, 0682 ; length of encrypted body
Decrypt:
xor [si], si ; decryption key/counter 1
xor [si], sp ; decryption key/counter 2
inc si ; increment one counter
dec sp ; decrement the other
jnz Decrypt ; loop until all bytes are decrypted
Start: ; Virus body
对于Cascade病毒如何使用堆栈指针来解密病毒体,上面代码中的注释已经做了很好的说明。相反,Cryptor病毒将其密钥存储在键盘缓冲区中,这些密钥会被调试器破坏。Tequila使用解密器的代码作为解密钥,因此如果解密器被调试器修改后,那么该病毒就无法解密了。下面是Tequila用于解密的代码:
perform_encryption_decryption:
mov bx,0
mov si,0960
mov cx,0960
mov dl,b[si]
xor b[bx],dl
inc si
inc bx
cmp si,09a0
jb 0a61 ;masm mod. needed
mov si,0960
loop 0a52 ;masm mod. needed
ret
the_file_decrypting_routine:
push cs
pop ds
mov bx,4
mov si,0964
mov cx,0960
mov dl,b[si]
add b[bx],dl
inc si
inc bx
cmp si,09a4
jb 0a7e ;masm mod. needed
mov si,0964
loop 0a6f ;masm mod. needed
jmp 0390 ;masm mod. needed
人们正在研究可用于将来的新型反调试技术,其中一个项目的课题是关于多处器计算机的,因为当进行调试时,多处理器中的一个会处于闲置状态。这种新技术使用并行处理技术来解密代码。
二、逆转录病毒
逆转录病毒会设法禁用反病毒软件,比如可以通过携带一列进程名,并杀死正在运行的与表中同名的那些进程。许多逆转录病毒还把进程从启动列表中踢出去,这样该进程就无法在系统引导期间启动了。这种类型的恶意软件还会设法挤占反病毒软件的CPU时间,或者阻止反病毒软件连接到反病毒软件公司的服务器以使其无法更新病毒库。
三、混合技术
W32.Gobi病毒是一个多态逆转录病毒,它结合了EPO和其他一些反调试技术。该病毒还会在TCP端口666上打开一个后门。
Simile(又名Metaphor)是一个非常有名的复合型病毒,它含有大约14,000行汇编代码。这个病毒通过寻找API调用ExitProcess()来使用EPO,它还是一个多态病毒,因为它使用多态解密技术。它的90%代码都是用于多态解密,该病毒的主体和多态解密器在每次感染新文件时,都会放到一个半随机的地方。Simile的第一个有效载荷只在3月、6月、9月或12月份才会激活。在这些月份的17日变体A和B显示它们的消息。变体C在这些月份的第18日显示它的消息。变体A和B中的第二个有效载荷只有在五月14日激活,而变体C中的第二个有效载荷只在7月14日激活。
Ganda是一个使用EPO的逆转录病毒。它检查启动进程列表,并用一个return指令替换每个启动进程的第一个指令。这会使所有防病毒程序变得毫无用处。
四、小结
本文中,我们介绍了恶意软件用以阻碍对其进行逆向工程的若干反调试技术,同时介绍了逆转录病毒和各种反检测技术的组合。我们应该很好的理解这些技术,只有这样才能够更有效地对恶意软件进行动态检测和分析。

你可能感兴趣的:(windows,职场,翻译,调试,休闲)