动态反调试技术(一):基于SEH的反调试技术

动态反调试技术

动态反调试技术较于静态反调试技术破解难度更大。

基于SEH的反调试技术

Windows用户态异常处理流程:

在这里插入图片描述

基于SEH反调试的基本原理:

在这里插入图片描述

基本例子

现在通过一个基本例子来让大家感受一下SEH反调试技术:

#include "Windows.h"

bool g_isDebugged = false;

void CheckDebugger()
{
	__try
	{
        DebugBreak();

		g_isDebugged = true;
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		g_isDebugged = false;
	}
}

int main(int argc, char const *argv[])
{
	CheckDebugger();

	if (g_isDebugged == true)
	{
		::MessageBoxW(NULL, L"Debugged :(", L"INFO", MB_OK);
	}
	else
	{
		::MessageBoxW(NULL, L"Good Job!!!", L"INFO", MB_OK);
	}

	return 0;
}

基本原理

在 64 位 Windows 中,当异常发生时,事件分发的优先级顺序是:

  1. 内核调试器(如果有,如 KD/WinDbg 内核调试)
  2. 用户态调试器(通过 DebugActiveProcess 附加,如 x64dbg/x64dbg)
  3. VEH(向量化异常处理程序)
  4. SEH(结构化异常处理,即 __try/__except

调试器会首先收到 EXCEPTION_DEBUG_EVENT 事件,并决定:

  • DBG_CONTINUE:将异常传递给程序(程序自己的 SEH 会处理)。
  • DBG_EXCEPTION_NOT_HANDLED:也表示传递给程序。
  • 或者,调试器可以修改线程上下文(例如改变 RIP 跳过故障指令)然后继续执行。

反调试的核心就是检测调试器是否在这个链条上,以及它的行为是否符合预期


具体技术实现

技术1:异常流劫持检测(基础但有效)

这是最直接的方法,检测调试器是否“吞掉”了异常。

BOOL IsDebuggedBySEH() {
    __try {
        // 触发一个硬件异常
        RaiseException(0x12345678, 0, 0, NULL);
        // 如果执行到这里,说明异常被调试器处理了(调试器修改了RIP跳过RaiseException)
        return TRUE;
    }
    __except(EXCEPTION_EXECUTE_HANDLER) {
        // 正常流程:异常被本程序的 SEH 捕获
        return FALSE;
    }
}

变种:使用硬件异常(更难以被完美模拟)

__try {
    // 触发特权指令异常(#UD)
    __asm { ud2 }  // 0F 0B,触发无效操作码异常
    return TRUE;
}
__except(EXCEPTION_EXECUTE_HANDLER) {
    return FALSE;
}

变种:访问违例 + 内存属性检测

__try {
    // 尝试写入只读内存
    DWORD oldProtect;
    VirtualProtect((LPVOID)&IsDebuggedBySEH, 4, PAGE_READONLY, &oldProtect);
    *(DWORD*)&IsDebuggedBySEH = 0xDEADBEEF;  // 触发访问违例
    VirtualProtect((LPVOID)&IsDebuggedBySEH, 4, oldProtect, &oldProtect);
    return TRUE;  // 如果执行到这里,说明调试器跳过了写入指令
}
__except(EXCEPTION_EXECUTE_HANDLER) {
    return FALSE;
}
技术2:VEH 优先级检测(高级)

利用 VEH 在 SEH 之前被调用的特性,检测调试器是否破坏了调用链。

// 全局计数器
volatile LONG g_VehCallCount = 0;

LONG NTAPI MyVectoredHandler(PEXCEPTION_POINTERS pExc) {
    InterlockedIncrement(&g_VehCallCount);
    return EXCEPTION_CONTINUE_SEARCH;  // 继续传递给 SEH
}

BOOL IsDebuggedByVEH() {
    PVOID hVeh = AddVectoredExceptionHandler(1, MyVectoredHandler);
    g_VehCallCount = 0;
    
    __try {
        RaiseException(0x55555555, 0, 0, NULL);
    }
    __except(EXCEPTION_EXECUTE_HANDLER) {
        // 正常应进入这里
    }
    
    RemoveVectoredExceptionHandler(hVeh);
    
    // 关键检测:VEH 必须被调用过一次
    return (g_VehCallCount == 0);  // 如果为0,说明调试器截胡了异常,VEH没被调用
}
技术3:基于 Unwind 信息的隐藏陷阱(非常隐蔽)

64 位 SEH 严重依赖 .pdata 节中的 UNWIND_INFO。可以在 UNWIND_CODE 中设置陷阱。

原理:在 UNWIND_CODE 中插入罕见的、自定义的展开操作码(虽然标准操作码有限,但可以通过不常见的组合),或者依赖展开过程中的特定副作用。

// 方法:创建一个“复杂”的栈帧,然后故意破坏它
__declspec(noinline) void DeceptiveFunction() {
    // 分配不常见的栈大小,使用特定的寄存器保存模式
    __asm {
        push r12
        push r13
        push r14
        sub  rsp, 0x1234  // 不常见的分配大小
    }
    
    __try {
        __asm { int 3 }  // 触发断点异常
    }
    __except(EXCEPTION_EXECUTE_HANDLER) {
        // 正常的展开应该能正确处理 r12-r14 的恢复和 rsp 调整
    }
    
    __asm {
        add  rsp, 0x1234
        pop  r14
        pop  r13
        pop  r12
    }
}

// 检测:在 except 块中检查寄存器是否被正确恢复
// 如果调试器单步执行或修改了上下文,可能导致栈不平衡或寄存器错误
技术4:SEH 作为代码混淆/控制流混淆

将关键逻辑隐藏在 __except 块中,使静态分析者难以发现真正的控制流。

// 正常代码看起来是“死胡同”
BOOL CheckLicense() {
    BOOL isValid = FALSE;
    
    __try {
        // 故意触发异常
        *(int*)0 = 1;
        
        // 逆向者看到这里可能会认为前面的代码不会执行
        // 但实际上异常会跳转到 except 块
        isValid = SomeComplexLicenseCheck();  // 这行永远不会在无调试时执行
    }
    __except(EXCEPTION_EXECUTE_HANDLER) {
        // 真正的验证逻辑在这里
        isValid = RealLicenseCheck();
        
        // 可以进一步嵌套异常
        __try {
            if (isValid) {
                *(int*)0x1 = 1;  // 再次触发异常,跳到内层处理
            }
        }
        __except(EXCEPTION_EXECUTE_HANDLER) {
            // 第二层真实逻辑
            DecryptAndExecute();
        }
    }
    
    return isValid;
}
技术5:检测异常处理的时间差异(时序攻击)

调试器处理异常会有显著的时间延迟。

LARGE_INTEGER freq, start, end;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start);

__try {
    __asm { int 3 }  // 断点异常
}
__except(EXCEPTION_EXECUTE_HANDLER) {
    QueryPerformanceCounter(&end);
    
    // 计算耗时(以微秒计)
    LONGLONG elapsed = (end.QuadPart - start.QuadPart) * 1000000 / freq.QuadPart;
    
    // 正常异常处理应在几微秒内,调试器介入需要几十到几百微秒
    if (elapsed > 50) {  // 阈值根据实际情况调整
        return TRUE;  // 检测到调试器
    }
}
return FALSE;
技术6:破坏 .pdata 完整性检测(自校验)

64 位 SEH 依赖 .pdata 的完整性。可以检测 .pdata 是否被调试器修改。

BOOL CheckPdataIntegrity() {
    // 1. 获取 .pdata 节的内存范围
    HMODULE hMod = GetModuleHandle(NULL);
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hMod + ((PIMAGE_DOS_HEADER)hMod)->e_lfanew);
    
    // 遍历节表找到 .pdata
    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
    for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
        if (memcmp(pSection->Name, ".pdata", 6) == 0) {
            // 2. 计算 CRC32 或哈希
            DWORD hash = CalculateCRC32((BYTE*)hMod + pSection->VirtualAddress, pSection->Misc.VirtualSize);
            
            // 3. 与预计算的正确值比较
            if (hash != EXPECTED_PDATA_HASH) {
                return TRUE;  // 被修改,可能被调试器打补丁
            }
            break;
        }
        pSection++;
    }
    return FALSE;
}

在检测到进程被调试之后,我们可以将代码跳转到垃圾代码来干扰逆向分析人员的跟踪。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值