Windows平台常见反调试技术梳理(下)

admin 2023-12-01 13:11:32 AnQuanKeInfo 来源:ZONE.CI 全球网 0 阅读模式

 

0x01 反调试方法

软件及硬件断点

断点(breakpoint)是调试器提供的主要工具。我们可以使用断点在特定位置中断程序执行流程。断点有两种类型:

1、软件断点

2、硬件断点

如果没有断点支持,我们很难逆向分析目标软件。常用的反逆向分析技巧都会检测断点是否存在,因此我们也有对应的反调试方法。

软件断点

在IA-32架构中,有一条特殊的指令:带有0xCC操作码(opcode)的int 3h,这条指令可以用来调用调试句柄。当CPU执行这条指令时,就会产生中断,将控制权交给调试器。为了获得控制权,调试器需要将int 3h指令注入代码中。为了检测断点是否存在,我们可以计算函数的校验和。示例代码如下:

DWORD CalcFuncCrc(PUCHAR funcBegin, PUCHAR funcEnd)
{
    DWORD crc = 0;
    for (; funcBegin < funcEnd; ++funcBegin)
    {
        crc += *funcBegin;
    }
    return crc;
}
#pragma auto_inline(off)
VOID DebuggeeFunction()
{
    int calc = 0;
    calc += 2;
    calc <<= 8;
    calc -= 3;
}
VOID DebuggeeFunctionEnd()
{
};
#pragma auto_inline(on)
DWORD g_origCrc = 0x2bd0;
int main()
{
    DWORD crc = CalcFuncCrc((PUCHAR)DebuggeeFunction, (PUCHAR)DebuggeeFunctionEnd);
    if (g_origCrc != crc)
    {
        std::cout << "Stop debugging program!" << std::endl;
        exit(-1);
    }
    return 0;
}

需要注意的是,以上代码只有在设置/INCREMENTAL:NO链接器选项时才能生效,否则当获取函数地址来计算校验和时,我们会得到相对跳转地址:

DebuggeeFunction:
013C16DB  jmp         DebuggeeFunction (013C4950h)

g_origCrc全局变量中包含CalcFuncCrc函数已计算出的crc。为了检测函数尾部,我们使用了stub函数(桩函数)技巧。由于函数代码顺序存放,DebuggeeFunction函数的尾部就是DebuggeeFunctionEnd函数的头部。我们还使用了#pragma auto_inline(off)指令来阻止编译器在中间嵌入函数。

如何绕过

绕过软件断点检测并没有通用的方法。如果想完成该任务,我们应当找到计算校验和的代码,将返回值替换为其他常量值,也要修改存储函数校验和的所有变量的值。

硬件断点

在x86架构中,开发者在检查和调试代码时会用到一些调试寄存器。这些寄存器可以让我们中断程序执行流,在读写内存时将控制权交给调试器。调试寄存器属于特权资源,程序只有在实模式(real mode)或者安全模式(safe mode)下且特权级CPL=0时才能使用这些寄存器。调试寄存器总共有8个,分别为DR0DR7

  • DR0DR3:断点寄存器
  • DR4DR5:保留
  • DR6:调试状态
  • DR7:调试控制

DR0DR3包含断点的线性地址。系统会在物理地址转换之前比较这些地址。每个断点都在DR7寄存器中单独描述。DR6寄存器用来表示哪个断点处于激活状态。DR7通过访问模式来定义断点激活模式,分别为:读取(read)、写入(write)或者执行(execute)。基于硬件断点的检查示例如下所示:

CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (GetThreadContext(GetCurrentThread(), &ctx))
{
    if (ctx.Dr0 != 0 || ctx.Dr1 != 0 || ctx.Dr2 != 0 || ctx.Dr3 != 0)
    {
        std::cout << "Stop debugging program!" << std::endl;
        exit(-1);
    }
}

我们也可以通过SetThreadContext函数来重设硬件断点。重设硬件断点的代码如下所示:

CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(GetCurrentThread(), &ctx);

这样所有DRx寄存器都会被清零。

如何绕过

如果我们观察GetThreadContext函数,可以看到其中会调用NtGetContextThread函数。

0:000> u KERNELBASE!GetThreadContext L6
KERNELBASE!GetThreadContext:
7538d580 8bff            mov     edi,edi
7538d582 55              push    ebp
7538d583 8bec            mov     ebp,esp
7538d585 ff750c          push    dword ptr [ebp+0Ch]
7538d588 ff7508          push    dword ptr [ebp+8]
7538d58b ff1504683975    call    dword ptr [KERNELBASE!_imp__NtGetContextThread (75396804)]

为了让代码读取Dr0Dr7寄存器的值为0,我们需要重设CONTEXT结构中ContextFlags字段的CONTEXT_DEBUG_REGISTERS标志,然后在NtGetContextThread函数调用后恢复该值。至于GetThreadContext函数,该函数调用的是NtSetContextThread。绕过硬件断点检查并重置相关字段的代码如下所示:

typedef NTSTATUS(NTAPI *pfnNtGetContextThread)(
    _In_  HANDLE             ThreadHandle,
    _Out_ PCONTEXT           pContext
    );
typedef NTSTATUS(NTAPI *pfnNtSetContextThread)(
    _In_ HANDLE              ThreadHandle,
    _In_ PCONTEXT            pContext
    );
pfnNtGetContextThread g_origNtGetContextThread = NULL;
pfnNtSetContextThread g_origNtSetContextThread = NULL;
NTSTATUS NTAPI HookNtGetContextThread(
    _In_  HANDLE              ThreadHandle,
    _Out_ PCONTEXT            pContext)
{
    DWORD backupContextFlags = pContext->ContextFlags;
    pContext->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS;
    NTSTATUS status = g_origNtGetContextThread(ThreadHandle, pContext);
    pContext->ContextFlags = backupContextFlags;
    return status;
}
NTSTATUS NTAPI HookNtSetContextThread(
    _In_ HANDLE              ThreadHandle,
    _In_ PCONTEXT            pContext)
{
    DWORD backupContextFlags = pContext->ContextFlags;
    pContext->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS;
    NTSTATUS status = g_origNtSetContextThread(ThreadHandle, pContext);   
    pContext->ContextFlags = backupContextFlags;
    return status;
}
void HookThreadContext()
{
  HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
  g_origNtGetContextThread = (pfnNtGetContextThread)GetProcAddress(hNtDll, "NtGetContextThread");
  g_origNtSetContextThread = (pfnNtSetContextThread)GetProcAddress(hNtDll, "NtSetContextThread");
  Mhook_SetHook((PVOID*)&g_origNtGetContextThread, HookNtGetContextThread);
  Mhook_SetHook((PVOID*)&g_origNtSetContextThread, HookNtSetContextThread);
}

SEH

SEH(Structured Exception Handling)是操作系统向应用程序提供的一种机制,使应用程序可以接受关于异常情况的通知(如除以0、引用不存在的指针或者执行受限指令)。这种机制可以让我们在应用内部处理异常,无需操作系统介入。如果异常没有被处理,就会导致程序异常终止。开发者通常会在栈中找到指向SEH的指针,也就是SEH帧(SEH frame)。当前SEH帧地址位于FS选择器(x64系统上是GS选择器)相对地址offset 0处,该地址指向的是ntdll!_EXCEPTION_REGISTRATION_RECORD结构:

0:000> dt ntdll!_EXCEPTION_REGISTRATION_RECORD
   +0x000 Next             : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 Handler          : Ptr32 _EXCEPTION_DISPOSITION

当出现异常时,控制权将交给当前的SEH处理函数(handler)。根据所处的具体情况,这个SEH处理函数应当返回如下某个_EXCEPTION_DISPOSITION枚举值:

typedef enum _EXCEPTION_DISPOSITION {
    ExceptionContinueExecution,
    ExceptionContinueSearch,
    ExceptionNestedException,
    ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

如果处理函数返回的是ExceptionContinueSearch,系统会从触发异常的指令处继续执行。如果处理函数不知道如何处理异常,就会返回ExceptionContinueSearch,然后系统会移到处理链中的下一个处理函数。我们可以在WinDbg调试器中使用!exchain命令浏览当前的异常处理链:

0:000> !exchain
00a5f3bc: AntiDebug!_except_handler4+0 (008b7530)
  CRT scope  0, filter: AntiDebug!SehInternals+67 (00883d67)
                func:   AntiDebug!SehInternals+6d (00883d6d)
00a5f814: AntiDebug!__scrt_stub_for_is_c_termination_complete+164b (008bc16b)
00a5f87c: AntiDebug!_except_handler4+0 (008b7530)
  CRT scope  0, filter: AntiDebug!__scrt_common_main_seh+1b0 (008b7c60)
                func:   AntiDebug!__scrt_common_main_seh+1cb (008b7c7b)
00a5f8e8: ntdll!_except_handler4+0 (775674a0)
  CRT scope  0, filter: ntdll!__RtlUserThreadStart+54386 (7757f076)
                func:   ntdll!__RtlUserThreadStart+543cd (7757f0bd)
00a5f900: ntdll!FinalExceptionHandlerPad4+0 (77510213)

链中最后一个处理程序是系统分配的默默人处理程序。如果之前的所有处理程序都无法处理异常,那么系统处理程序就会访问注册表,获取如下键值:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\AeDebug

根据AeDebug表项的值,应用程序会被终止,或者控制权会交给调试器。调试器路径位于Debugger表项中(REG_SZ)。

当创建新进程时,系统会将主SEH帧(primary SEH frame)加入其中。主SEH帧的处理程序同样由系统定义。主SEH帧大多数情况下位于分配给进程的内存栈的开头处。SEH处理程序函数原型如下所示:

typedef EXCEPTION_DISPOSITION (*PEXCEPTION_ROUTINE) (
    __in struct _EXCEPTION_RECORD *ExceptionRecord,
    __in PVOID EstablisherFrame,
    __inout struct _CONTEXT *ContextRecord,
    __inout PVOID DispatcherContext
    );

如果应用程序正在被调试,那么在生成int 3h中断后,调试器将会拦截控制权。否则,控制权就会交给SEH处理函数。基于SEH帧的反调试代码如下所示:

BOOL g_isDebuggerPresent = TRUE;
EXCEPTION_DISPOSITION ExceptionRoutine(
    PEXCEPTION_RECORD ExceptionRecord,
    PVOID             EstablisherFrame,
    PCONTEXT          ContextRecord,
    PVOID             DispatcherContext)
{
    g_isDebuggerPresent = FALSE;
    ContextRecord->Eip += 1;
    return ExceptionContinueExecution;
}
int main()
{
    __asm
    {
        // set SEH handler
        push ExceptionRoutine
        push dword ptr fs:[0]
        mov  dword ptr fs:[0], esp
        // generate interrupt
        int  3h
        // return original SEH handler
        mov  eax, [esp]
        mov  dword ptr fs:[0], eax
        add  esp, 8
    }
    if (g_isDebuggerPresent)
    {
        std::cout << "Stop debugging program!" << std::endl;
        exit(-1);
    }
    return 0
}

如上代码中设置了SEH处理函数,指向该处理函数的指针位于处理链的开头,然后生成int 3h中断。如果应用程序没有被调试,控制权会转给SEH处理程序,g_isDebuggerPresent的值会被设置为FALSEContextRecord->Eip += 1这一行代码会修改执行流中的下一条指令地址,这样就会执行在int 3h后的下一条指令。然后代码返回原始的SEH处理函数,清空栈,并且检查是否存在调试器。

如何绕过

虽然绕过SEH检查并没有通用的方法,但逆向分析人员还是可以使用某些技术来减轻工作量。我们来观察下关于SEH处理程序的调用栈:

0:000> kn
 # ChildEBP RetAddr  
00 0059f06c 775100b1 AntiDebug!ExceptionRoutine 
01 0059f090 77510083 ntdll!ExecuteHandler2+0x26
02 0059f158 775107ff ntdll!ExecuteHandler+0x24
03 0059f158 003b11a5 ntdll!KiUserExceptionDispatcher+0xf
04 0059fa90 003d7f4e AntiDebug!main+0xb5
05 0059faa4 003d7d9a AntiDebug!invoke_main+0x1e
06 0059fafc 003d7c2d AntiDebug!__scrt_common_main_seh+0x15a 
07 0059fb04 003d7f68 AntiDebug!__scrt_common_main+0xd 
08 0059fb0c 753e7c04 AntiDebug!mainCRTStartup+0x8
09 0059fb20 7752ad1f KERNEL32!BaseThreadInitThunk+0x24
0a 0059fb68 7752acea ntdll!__RtlUserThreadStart+0x2f
0b 0059fb78 00000000 ntdll!_RtlUserThreadStart+0x1b

可以看到该调用来自于ntdll!ExecuteHandler2。这个函数是任何SEH处理函数的调用起点。我们可以在调用指令处设置断点:

0:000> u ntdll!ExecuteHandler2+24 L3
ntdll!ExecuteHandler2+0x24:
775100af ffd1            call    ecx
775100b1 648b2500000000  mov     esp,dword ptr fs:[0]
775100b8 648f0500000000  pop     dword ptr fs:[0]
0:000> bp 775100af

设置断点后,我们应该分析被调用的每个SEH处理函数的代码。如果反调试技术涉及到对SEH处理函数的多次调用,那么逆向人员应该花精力绕过这些函数。

VEH

VEH(Vectored Exception Handler)是从Windows XP引入的一种机制,也是SEH的变种。VEH和SEH并不相互依赖,可以同时工作。当添加了新的VEH处理函数时,SEH链并不会受到影响,因为VEH处理函数存在于未导出的ntdll!LdrpVectorHandlerList变量中。VEH和SEH机制非常相似,唯一的区别在于系统使用已公开的函数来设置并删除VEH处理函数。添加并删除VEH处理函数的函数原型以及VEH处理函数本身的原型如下所示:

PVOID WINAPI AddVectoredExceptionHandler(
    ULONG                       FirstHandler,
    PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
ULONG WINAPI RemoveVectoredExceptionHandler(
    PVOID Handler
);
LONG CALLBACK VectoredHandler(
    PEXCEPTION_POINTERS ExceptionInfo
);
The _EXCEPTION_POINTERS structure looks like this:  
typedef struct _EXCEPTION_POINTERS {
  PEXCEPTION_RECORD ExceptionRecord;
  PCONTEXT          ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

在处理函数中收到控制权后,系统会收集当前进程上下文并通过ContextRecord参数进行传递。使用VEH的反调试代码如下所示:

LONG CALLBACK ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
    PCONTEXT ctx = ExceptionInfo->ContextRecord;
    if (ctx->Dr0 != 0 || ctx->Dr1 != 0 || ctx->Dr2 != 0 || ctx->Dr3 != 0)
    {
        std::cout << "Stop debugging program!" << std::endl;
        exit(-1);
    }
    ctx->Eip += 2;
    return EXCEPTION_CONTINUE_EXECUTION;
}
int main()
{
    AddVectoredExceptionHandler(0, ExceptionHandler);
    __asm int 1h;
    return 0;
}

这里我们设置了一个VEH处理函数并生成中断(int 1h不是必需操作)。当产生中断时,会出现异常,控制权会转给VEH处理函数。如果设置了硬件断点,系统就会终止程序执行。如果没有硬件断点,EIP寄存器的值就会加2,以便在int 1h指令后继续执行。

如何绕过

来观察下涉及到VEH处理函数的调用栈:

0:000> kn
 # ChildEBP RetAddr  
00 001cf21c 774d6822 AntiDebug!ExceptionHandler 
01 001cf26c 7753d151 ntdll!RtlpCallVectoredHandlers+0xba
02 001cf304 775107ff ntdll!RtlDispatchException+0x72
03 001cf304 00bf4a69 ntdll!KiUserExceptionDispatcher+0xf
04 001cfc1c 00c2680e AntiDebug!main+0x59 
05 001cfc30 00c2665a AntiDebug!invoke_main+0x1e 
06 001cfc88 00c264ed AntiDebug!__scrt_common_main_seh+0x15a 
07 001cfc90 00c26828 AntiDebug!__scrt_common_main+0xd 
08 001cfc98 753e7c04 AntiDebug!mainCRTStartup+0x8 
09 001cfcac 7752ad1f KERNEL32!BaseThreadInitThunk+0x24
0a 001cfcf4 7752acea ntdll!__RtlUserThreadStart+0x2f
0b 001cfd04 00000000 ntdll!_RtlUserThreadStart+0x1b

如上所示,控制权会从main+0x59转移到ntdll!KiUserExceptionDispatcher。来看下main+0x59中负责该操作的具体指令:

0:000> u main+59 L1
AntiDebug!main+0x59
00bf4a69 cd02            int     1

生成中断的指令如上所示。KiUserExceptionDispatcher函数是系统从内核模式到用户模式的一个回调函数,函数原型如下所示:

VOID NTAPI KiUserExceptionDispatcher(
    PEXCEPTION_RECORD pExcptRec, 
    PCONTEXT ContextFrame
);

我们可以通过KiUserExceptionDispatcher函数hook来绕过硬件断点检测,如下所示:

typedef  VOID (NTAPI *pfnKiUserExceptionDispatcher)(
    PEXCEPTION_RECORD pExcptRec,
    PCONTEXT ContextFrame
    );
pfnKiUserExceptionDispatcher g_origKiUserExceptionDispatcher = NULL;
VOID NTAPI HandleKiUserExceptionDispatcher(PEXCEPTION_RECORD pExcptRec, PCONTEXT ContextFrame)
{
    if (ContextFrame && (CONTEXT_DEBUG_REGISTERS & ContextFrame->ContextFlags))
    {
        ContextFrame->Dr0 = 0;
        ContextFrame->Dr1 = 0;
        ContextFrame->Dr2 = 0;
        ContextFrame->Dr3 = 0;
        ContextFrame->Dr6 = 0;
        ContextFrame->Dr7 = 0;
        ContextFrame->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS;
    }
}
__declspec(naked) VOID NTAPI HookKiUserExceptionDispatcher() 
// Params: PEXCEPTION_RECORD pExcptRec, PCONTEXT ContextFrame
{
    __asm
    {
        mov eax, [esp + 4]
        mov ecx, [esp]
        push eax
        push ecx
        call HandleKiUserExceptionDispatcher
        jmp g_origKiUserExceptionDispatcher
    }
}
int main()
{
    HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
    g_origKiUserExceptionDispatcher = (pfnKiUserExceptionDispatcher)GetProcAddress(hNtDll, "KiUserExceptionDispatcher");
    Mhook_SetHook((PVOID*)&g_origKiUserExceptionDispatcher, HookKiUserExceptionDispatcher);
    return 0;
}

在上述代码中,DRx寄存器的值会在HookKiUserExceptionDispatcher函数中重置,也就是说,会在调用VEH处理函数前重置。

NtSetInformationThread:从调试器中隐藏线程

在Windows 2000中,出现了传递给NtSetInformationThread函数的一个新的线程信息类:ThreadHideFromDebugger。这是微软在研究如何防御逆向工程时在Windows中引入的第一个反调试技术,并且这种技术也非常强大。如果某个线程设置了该标志,那么该线程就会停止发送关于调试事件的通知。这些事件包括断点信息以及关于程序完成的通知信息。该标志的值存放于_ETHREAD结构的HideFromDebugger字段中。

1: kd> dt _ETHREAD HideFromDebugger 86bfada8
ntdll!_ETHREAD
   +0x248 HideFromDebugger : 0y1

设置ThreadHideFromDebugger的代码如下所示:

typedef NTSTATUS (NTAPI *pfnNtSetInformationThread)(
    _In_ HANDLE ThreadHandle,
    _In_ ULONG  ThreadInformationClass,
    _In_ PVOID  ThreadInformation,
    _In_ ULONG  ThreadInformationLength
    );
const ULONG ThreadHideFromDebugger = 0x11;
void HideFromDebugger()
{
    HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
    pfnNtSetInformationThread NtSetInformationThread = (pfnNtSetInformationThread)
        GetProcAddress(hNtDll, "NtSetInformationThread");
    NTSTATUS status = NtSetInformationThread(GetCurrentThread(), 
        ThreadHideFromDebugger, NULL, 0);
}

如何绕过

为了阻止应用程序向调试器隐藏线程信息,我们需要hook NtSetInformationThread函数调用。hook代码如下所示:

pfnNtSetInformationThread g_origNtSetInformationThread = NULL;
NTSTATUS NTAPI HookNtSetInformationThread(
    _In_ HANDLE ThreadHandle,
    _In_ ULONG  ThreadInformationClass,
    _In_ PVOID  ThreadInformation,
    _In_ ULONG  ThreadInformationLength
    )
{
    if (ThreadInformationClass == ThreadHideFromDebugger && 
        ThreadInformation == 0 && ThreadInformationLength == 0)
    {
        return STATUS_SUCCESS;
    }
    return g_origNtSetInformationThread(ThreadHandle, 
        ThreadInformationClass, ThreadInformation, ThreadInformationLength
}

void SetHook()
{
    HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
    if (NULL != hNtDll)
    {
        g_origNtSetInformationThread = (pfnNtSetInformationThread)GetProcAddress(hNtDll, "NtSetInformationThread");
        if (NULL != g_origNtSetInformationThread)
        {
            Mhook_SetHook((PVOID*)&g_origNtSetInformationThread, HookNtSetInformationThread);
        }
    }
}

在被hook的函数中,如果正确调用的话就会返回STATUS_SUCCESS,并且不会将控制权交给原始的NtSetInformationThread函数。

NtCreateThreadEx

Windows从Vista开始引入了NtCreateThreadEx函数,函数原型如下所示:

NTSTATUS NTAPI NtCreateThreadEx (
    _Out_    PHANDLE              ThreadHandle,
    _In_     ACCESS_MASK          DesiredAccess,
    _In_opt_ POBJECT_ATTRIBUTES   ObjectAttributes,
    _In_     HANDLE               ProcessHandle,
    _In_     PVOID                StartRoutine,
    _In_opt_ PVOID                Argument,
    _In_     ULONG                CreateFlags,
    _In_opt_ ULONG_PTR            ZeroBits,
    _In_opt_ SIZE_T               StackSize,
    _In_opt_ SIZE_T               MaximumStackSize,
    _In_opt_ PVOID                AttributeList
);

其中最有趣的参数是CreateFlags,该参数可以使用如下标志:

#define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001
#define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002
#define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004
#define THREAD_CREATE_FLAGS_HAS_SECURITY_DESCRIPTOR 0x00000010
#define THREAD_CREATE_FLAGS_ACCESS_CHECK_IN_TARGET 0x00000020
#define THREAD_CREATE_FLAGS_INITIAL_THREAD 0x00000080

如果新线程设置了THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志,那么在创建时就可以向调试器隐藏该线程信息,这与NtSetInformationThread函数设置的ThreadHideFromDebugger相同。负责安全任务的代码可以在设置THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志的线程中执行。

如何绕过

我们可以hook NtCreateThreadEx函数来绕过这种技术,在该函数中重置THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER

句柄跟踪

从Windows XP开始,Windows系统就具备跟踪内核对象句柄的机制。当跟踪模式启动后,与句柄所有操作都会被保存到循环缓冲区中,并且当使用不存在的句柄时(比如使用CloseHandle函数关闭该句柄),那么就会出现EXCEPTION_INVALID_HANDLE异常。如果进程没有通过调试器启动,那么CloseHandle函数会返回FALSE。基于CloseHandle的反调试技术代码如下所示:

EXCEPTION_DISPOSITION ExceptionRoutine(
    PEXCEPTION_RECORD ExceptionRecord,
    PVOID             EstablisherFrame,
    PCONTEXT          ContextRecord,
    PVOID             DispatcherContext)
{
    if (EXCEPTION_INVALID_HANDLE == ExceptionRecord->ExceptionCode)
    {
        std::cout << "Stop debugging program!" << std::endl;
        exit(-1);
    }
    return ExceptionContinueExecution;
}
int main()
{
    __asm
    {
        // set SEH handler
        push ExceptionRoutine
        push dword ptr fs : [0]
        mov  dword ptr fs : [0], esp
    }
    CloseHandle((HANDLE)0xBAAD);
    __asm
    {
        // return original SEH handler
        mov  eax, [esp]
        mov  dword ptr fs : [0], eax
        add  esp, 8
    }
    return 0
}

篡改堆栈段

当修改ss堆栈段(stack segment register)寄存器时,调试器会跳过指令跟踪。在如下示例中,调试器会立即移到xor edx, edx指令,而上一条指令仍会被执行。

这里大家可以拓展阅读“How to Reverse Engineer Software (Windows) in a Right Way”这篇文章。

调试信息

从Windows 10开始,Windows修改了OutputDebugString函数的实现,改成带有特定参数的RaiseException调用。因此,现在调试输出异常必须由调试器来处理。

我们可以使用两种异常类型来检测是否存在调试器,分别为DBG_PRINTEXCEPTION_C0x40010006)以及DBG_PRINTEXCEPTION_W0x4001000A)。

#define DBG_PRINTEXCEPTION_WIDE_C 0x4001000A
WCHAR * outputString = L"Any text";
ULONG_PTR args[4] = {0};
args[0] = (ULONG_PTR)wcslen(outputString) + 1;
args[1] = (ULONG_PTR)outputString;
__try
{
    RaiseException(DBG_PRINTEXCEPTION_WIDE_C, 0, 4, args);
    printf("Debugger detected");
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
    printf("Debugger NOT detected");
}

因此,如果异常没有被处理,就意味着没有附加调试器。

DBG_PRINTEXCEPTION_W用于宽字符输出,DBG_PRINTEXCEPTION_C用于ansi字符。这意味着在使用DBG_PRINTEXCEPTION_C的情况下,arg[0]会保存strlen()的结果,而args[1]在指向ansi字符串(char *)。

 

0x02 总结

本文描述了一系列反逆向工程技术,特别是反调试方法。我们从最简单的技术开始,也介绍了相应的绕过方法。本文并没有覆盖所有技术,还有一些技术大家可以自己进一步研究,比如:

  • 自调试进程
  • 使用FindWindow函数的调试器检测技术
  • 基于时间计算的检测技术(参考这篇文章
  • NtQueryObject
  • BlockInput
  • NtSetDebugFilterState
  • 自修改代码

虽然我们主要关注的是反调试保护方法,还有其他一些反逆向分析方法,包括反转储(anti-dumping)以及混淆技术。

这里我们要再次强调一下,即使最优秀的反逆向分析技术也无法完全避免软件被逆向分析。反调试技术的主要任务是使逆向分析人员操作起来更为复杂,尽可能提高反逆向分析的难度。

 

0x03 参考资料

https://msdn.microsoft.com/library

http://www.infosecinstitute.com/

http://pferrie.tripod.com/

http://www.openrce.org/articles/

http://www.nynaeve.net/

http://stackoverflow.com/

http://x86.renejeschke.de/

weinxin
版权声明
本站原创文章转载请注明文章出处及链接,谢谢合作!
评论:0   参与:  0