第52章:动态反调试技术
异常
①SEH,以 DynAD_SEH.exe 程序为例
首先改变了 SEH 链,在 int 3 触发异常,此时注意栈中的 SEH 链,对对应的函数下断点即可暂停下来。
可以看到 EIP 被改变后,函数的执行流程即被改变。对 Contex(0xB6) 结构体中 EIP 指向的地址下断点。
此时直接就会显示 Not debugging ,而不是因为调试器处理了异常而导致程序退出。
② SetUnhandlerExceptionFilter
进程中发生异常时,如果 SEH 未处理或注册的 SEH 根本不存在,则会调用执行系统的 kernel32! UnhandlerExceptionFilter :
该函数内部会运行系统的最后一个异常处理器(Top Level Exception Filter OR Last Exception Filter).而函数的内部调用了ntdll ! NtQueryInformationProcess(ProcessDebugPort) .如果程序正常运行,则运行系统最后的异常处理函数,如果进程处于调试状态,则将异常派送给调试器。通过这个 API 可以修改系统最后的异常处理器。使用时只需要将新的 Top Level Exception Filter 作为该 API 的参数即可:
代码:
#include "stdio.h" #include "windows.h" #include "tchar.h" LPVOID g_pOrgFilter = 0; LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS pExcept) //注册的函数的声明以及实现 { SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)g_pOrgFilter); // 8900 MOV DWORD PTR DS:[EAX], EAX // FFE0 JMP EAX pExcept->ContextRecord->Eip += 4; return EXCEPTION_CONTINUE_EXECUTION; } void AD_SetUnhandledExceptionFilter() { printf("SEH : SetUnhandledExceptionFilter()\n"); g_pOrgFilter = (LPVOID)SetUnhandledExceptionFilter( //类型转换 (LPTOP_LEVEL_EXCEPTION_FILTER)ExceptionFilter); // 注册函数地址,而非调用函数 __asm { xor eax, eax; mov dword ptr [eax], eax //发生异常代码的首字节地址与 printf 刚好相差 4 字节 jmp eax } printf(" => Not debugging...\n\n"); } int _tmain(int argc, TCHAR* argv[]) { AD_SetUnhandledExceptionFilter(); return 0; }
在程序中看一下:
程序的流程是,先打印字符串,然后调用 SetHandledExceptionFilter() 注册新的 Top Level Exception Filter (当中包含异常处理代码),触发异常。异常触发后进入异常处理模块:
此时调用的异常处理函数并不是第一个 SEH 链上的函数:
因为并没有处理该异常,返回后继续调用后续的其它异常处理器:
进入 call ecx 后,会执行到 UnhandledExceptionFilter() ,表明 SEH 中没有函数处理了异常,将由系统异常处理函数处理:
在函数内部调用了 NtQueryInformationPorcess() API ,参数是 7(DebugPort)在后面比较时修改其值(FFFFFFFF => 00000000)即可:
修改值后,才会发生跳转,在执行 RtlDecodePointer() 后,会发现 Eax 的返回值就是 00401000:
此处就直接调用了:
再次通过 SetUnhandledExceptionFilter() ,设置最后一个异常函数,并且修改Contex => Eip(+4)即从 401252 -> 401056:
401052 是发生异常的地址:
然后函数退出,即执行完异常处理,回到 Contex => Eip 代码处(ZwContinue):
③ Timing Check
RDTSC 指令
x86 CPU 存在一个名为 TSC(Time Stamp Counter,时间戳计数器)的64位寄存器。CPU 对每个时钟周期计数,然后保存到 TSC. RDTSC 是一条指令,用于将 TSC 值读取到 EDX:EAX 寄存器中。
程序的核心非常简单,在两个 rdtsc 命令之间加入一个计数循环,时间多了就被判定为有调试器。
Ja 指令只有在 ZF 和 CF 全零时才会执行跳转,修改其中一个 Ja 指令就失效了。
④ 陷阱标志
在 x64 dbg 中,程序单步执行不会出现 Single Step 异常,但直接跑会遇到该异常。单步执行时,遇到 Jmp FFFFFFFF,会出现 Access_Violation 异常,但会转到本该处理 Single_Step 异常的异常处理函数处,导致程序仍然会执行到 Not Debugging 。
在 OD 中,程序程序单步执行不会出现 Single Step 异常,但直接跑会遇到该异常。单步执行时,遇到 Jmp FFFFFFFF,会直接跳到 FFFFFFFF,并将无法执行命令。
异触发后,程序处理该异常,将 Eip 的值修改,跳转到 Not Debugging 处。
⑤ INT 2D
该指令是内核模式中触发断点异常的指令,但也可以在用户模式下触发异常。
在程序中看一下:
使用单步执行指令,执行完 INT 2D 后,直接跳过后面的 nop 指令,并触发异常,红色框即是异常处理函数:
在 x64dbg 中无论使用哪种方式,都会在 401021 处触发异常,进入异常处理函数,并且都会忽略下一个字节的指令。
在 OD 中,步进指令会直接跑飞,但是不会触发异常。直接运行同样不会触发异常,并且二者都会忽略下一个字节的指令。
如果对 nop 指令下断点, x64dbg 会直接跳过,而 OD 会断在这个断点上。 为什么?
⑥ 检测 API 首字节 CC
⑦ 内存校验和