第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()
");

    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...

");
}

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

⑦ 内存校验和

原文地址:https://www.cnblogs.com/Rev-omi/p/13721336.html