Chernobyl
Learning
基于异常的反调试实践

前言

最近在学习软件调试相关的内容,顺便举一反三找找反调试以及反反调试的相关方法。逛着逛着找到了这个网站:2种基于异常机制的反调试方法,虽然一看就知道不知道从哪转载的,而且搜索引擎有很多这篇文章一模一样的重复结果,但是代码倒是挺有趣的,研究玩玩。

第一题

第一段代码是基于IsDebugPresent()类似实现的反调试,代码如下

#include <windows.h>

int AntiDebugInSEH(EXCEPTION_POINTERS* ExceptionInfo)
{
    DWORD state = 0x00;

    __asm
    {
        mov eax,dword ptr fs:[30h];
        movzx eax, byte ptr ds:[eax+2h];
        mov state,eax;
    }

    ExceptionInfo->ContextRecord->Eax = state;
    ExceptionInfo->ContextRecord->Eip = 0x06+(DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress;
    return EXCEPTION_CONTINUE_EXECUTION;
}

int main()
{
    int i=0;
    __try
    {
        __asm
        {
            xor eax,eax;
            mov dword ptr [eax],0; //触发异常
        }
        __asm
        {
                    //异常返回后,eax的值为AntiDebugInSEH中ExceptionInfo->ContextRecord->Eax设定的值                    
                    mov i,eax
        }
        if(i==1)
        {}
        else
        {
            MessageBox(NULL,"","",0);
        }
        i++;
    }
    __except(AntiDebugInSEH(GetExceptionInformation()))
    {

    }
}

程序的流程为通过主动引发异常跳转入异常处理函数,在异常处理函数内部查看fs:[30h]+2h的值,如果为1则不显示任何结果,为0则弹出一个空窗口。在着手这个程序前,先来看看IsDebugPresent究竟是何方神圣。

IsDebugPresent()

VC中有一个用于检测调试状态的APIIsDebugPresent(),当程序处于被调试状态时,该API返回True。出于对软件的保护,我们在程序的变量初始化前就调用该API检测调试情况。代码如下:

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>

int main()
{
    if (IsDebuggerPresent())//检测是否处于被调试状态
    {
        std::cout << "this program can not run in debugger!";
        getchar();
        return E_FAIL;
    }

    CONTEXT thread_context;//上下文结构体
    HANDLE hThread = GetCurrentThread();//获取当前进程的句柄
    DWORD test_var = 0;

    thread_context.ContextFlags = CONTEXT_DEBUG_REGISTERS | CONTEXT_FULL;//设置线程上下文的类型
    if (!GetThreadContext(hThread, &thread_context))//通过句柄获取进程的上下文
    {
        std::cout << "Failed to get thread context.";
        return E_FAIL;
    }

    thread_context.Dr0 = (DWORD)& test_var;//将DR0设置为test_var的地址
    thread_context.Dr7 = 0xF0001;//F代表4字节读写访问断点,01代表启用DR0断点
    if (!SetThreadContext(hThread, &thread_context))//将线程上下文应用于句柄所指向的线程
    {
        std::cout << "Failed to set thread context";
        return E_FAIL;
    }

    test_var = 1;//修改test_var的值,触发断点
    GetThreadContext(hThread, &thread_context);//获取修改后的线程上下文
    printf("DR6:%x",thread_context.Dr6);
    getchar();
    return S_OK;
}

上述代码在VS2017编译后,直接在IDE里执行会报告检测到调试器——即使没有下任何断点。

如果不在调试环境下运行,由于后续会触发调试异常,没有调试器处理,双击执行程序会导致闪退,在命令行运行会导致程序死锁。有没有方法绕过这个API来继续程序的执行呢?

原理

用户态调用的IsDebugPresent会转换到内核态中的IsDebugPresent,其汇编代码如下:

    KERNELBASE!IsDebuggerPresent:
76d0b940 64a130000000 mov     eax, dword ptr fs:[00000030h] fs:0053:00000030=0035b000
76d0b946 0fb64002     movzx   eax, byte ptr [eax+2]
76d0b94a c3           ret     
76d0b94b cc           int     3
76d0b94c cc           int     3
76d0b94d cc           int     3
76d0b94e cc           int     3
76d0b94f cc           int     3

该API首先会将fs寄存器偏移0x30的地址传入EAX寄存器中,然后提取EAX所指向地址偏移2字节的值传入eax中,最后返回。为什么检测调试指令要读取一个固定地址的值?实际上,进程的PEB常存储于fs寄存器加上特定偏移量的地址中。IsDebugPresent监视的是进程PEB结构体中BeingDebugged域的值,当该位为1时,代表进程正处于被调试状态。下面是Windbg中peb的结构体说明:

0:000> dt _PEB
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   +0x003 IsProtectedProcess : Pos 1, 1 Bit
   +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit
   +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit
   +0x003 IsPackagedProcess : Pos 4, 1 Bit
......

下面来看看fs:[30]对应地址存储的信息

0:000> dg fs
                                  P Si Gr Pr Lo
Sel    Base     Limit     Type    l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0053 0091f000 00000fff Data RW Ac 3 Bg By P  Nl 000004f3

0:000> dd 0091f000+0x30
0091f030  0091c000 00000000 00000000 00000000

0:000> dt _PEB 0091c000
ntdll!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x4 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
 ......

PEB是一个庞大的结构体,可以看到,PEB基址偏移0x2处的域刚好就是BeingDebugged。至此,通过IsDebugPresent()反调试的原理已经被摸清,下面来看看怎么绕过。

绕过方式

  • 最简单的当然是修改寄存器的值。首先通过bp KERNELBASE!IsDebuggerPresent在内核API调用前让程序停下来,在ret执行前使用r @eax=0修改EAX的值——大功告成。

  • 复杂但究其根源的方法是修改程序的PEB,将调试位置0。比较方便的方法为先通过dg获取fs寄存器指向的基址,然后通过eb指令将对应位置置0。

0:000> dg fs
                                P Si Gr Pr Lo
Sel    Base     Limit     Type    l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0053 00e05000 00000fff Data RW Ac 3 Bg By P  Nl 000004f3

0:000> eb poi(00e05000+0x30)+2 0

回到题目,可以发现,题目中以下语句

mov eax,dword ptr fs:[30h];
movzx eax, byte ptr ds:[eax+2h];
mov state,eax;

是不是就是读取_PEB中BeingDebugged字段的汇编语句,由此得出,绕过的方式与我实现的示例程序一样,修改对应字段的值就好。

第二题

首先,第二题在网站中并没有解答…网页上的原文是

这段代码,还是读者自己试下,我暂时没想到如果绕过检测的方法。

这么多重复的文章,抄来抄去的,居然连这句也一并抄了,就不会动动脑子解决掉吗

“我TM”的图片搜索ç"“æžœ

算了,挖挖看看。照例先上题目源码

#include <windows.h>

LONG WINAPI UnhandledExcept(EXCEPTION_POINTERS *pExpInfo)  
{
    if(pExpInfo->ExceptionRecord->ExceptionCode != EXCEPTION_BREAKPOINT)
    {
        return EXCEPTION_EXECUTE_HANDLER;
    }
    else
    {
        pExpInfo->ContextRecord->Eip = (DWORD)(pExpInfo->ContextRecord->Eip+1);
        pExpInfo->ContextRecord->Eax = 0x0000FEEE;
        return EXCEPTION_CONTINUE_EXECUTION;
    }
}

int main()
{
    HMODULE hMod = LoadLibrary("kernel32.dll");
    DWORD* funcAddr = (DWORD*)GetProcAddress(hMod,"ExitProcess");

    SetUnhandledExceptionFilter(UnhandledExcept);
    _asm int 3;
    __asm
    {
        cmp ax,0xFEEE;
        jz next;
        push 0;
        mov eax,funcAddr;
        call eax;
next:
    }
    {
        MessageBox(NULL,"","",MB_OK);
    }
}

嗯,通过断点异常来让调试器捕获,于是无法执行程序中内置的异常处理流程,导致程序直接退出。看起来就像是先有鸡还是先有蛋的的悖论,连接上了调试器断点肯定得断,不连接就无法获取程序的信息。但是INT 3断点异常是陷阱,改个EIP就能过去,为了硬核一点,我对程序进行了小小的修改,将异常改为除0异常,如果不执行程序内部的异常处理,调试器将永远卡在一个地方,改EIP也木得用。

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
DWORD flag = 0;
void __stdcall succ()
{
    MessageBox(NULL, "NoDebug", "Good", MB_OK);
}

void __stdcall fail()
{
    MessageBox(NULL, "Debug!", "Oops", MB_OK);
}

LONG WINAPI UnhandledExcept(EXCEPTION_POINTERS *pExpInfo)
{

    pExpInfo->ContextRecord->Eip = (DWORD)(pExpInfo->ContextRecord->Eip +0xB);
    pExpInfo->ContextRecord->Eax = 0x0000FEEE;
    return EXCEPTION_CONTINUE_EXECUTION;
}

int main()
{
    HMODULE hMod = LoadLibrary("kernel32.dll");
    DWORD* funcAddr = (DWORD*)GetProcAddress(hMod, "ExitProcess");
    SetUnhandledExceptionFilter(UnhandledExcept);
    flag = 6 / flag;//引发除0异常
    __asm
    {
        cmp eax, 0x0000FEEE;
        jz next;
        mov eax, fail;
        call eax;
        push 0;
        mov eax, funcAddr;
        call eax;
    next:
        mov eax, succ;
        call eax;
    }
    return 0;
}

这货在VS2017中编译运行的情况是这样的

点击继续无法运行,一直卡在了异常报错处。在Windbg中也好不到哪去

0:000> g
(1f84.3ea0): Integer divide-by-zero - code c0000094 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000006 ebx=0048b000 ecx=a5e70000 edx=00000000 esi=0032fa08 edi=0032faf0
eip=003b19a5 esp=0032fa08 ebp=0032faf0 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
Breakpoints!main+0x75:
003b19a5 f73538a13b00    div     eax,dword ptr [Breakpoints!flag (003ba138)] ds:002b:003ba138=00000000

0:000> g
(1f84.3ea0): Integer divide-by-zero - code c0000094 (!!! second chance !!!)
eax=00000006 ebx=0048b000 ecx=a5e70000 edx=00000000 esi=0032fa08 edi=0032faf0
eip=003b19a5 esp=0032fa08 ebp=0032faf0 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
Breakpoints!main+0x75:
003b19a5 f73538a13b00    div     eax,dword ptr [Breakpoints!flag (003ba138)] ds:002b:003ba138=00000000

就像军训的“一二一”一样不停地循环报错。双击直接运行,嗯,温顺得不得了

成了,十分符合题目的要求,接下来开搞!

失败的尝试-1

首先联想到的是其底层实现可能与IsDebugPresent一样,通过检测PEB中的BeingDebugging位来判断是否将异常传递给调试器。那就改来试试

0:000> dg fs
                                  P Si Gr Pr Lo
Sel    Base     Limit     Type    l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0053 009a1000 00000fff Data RW Ac 3 Bg By P  Nl 000004f3

0:000> eb poi(009a1000+0x30)+2 0

0:000> g
(2668.1f30): Integer divide-by-zero - code c0000094 (first chance)

0:000> g
(2668.1f30): Integer divide-by-zero - code c0000094 (!!! second chance !!!)

死循环,看来系统的异常分发机制与该标志位无关。改下EIP强行跳转到下一条语句?

0:000> r @eip=003b19ab

0:000> g
(2668.1f30): Integer divide-by-zero - code c0000094 (first chance)
eip=003b19a5

0:000> g
(2668.1f30): Integer divide-by-zero - code c0000094 (!!! second chance !!!)
eip=003b19a5

不仅还是陷入了死循环,而且EIP还跳回去了。

失败的尝试-2

搜索了一番与反调试相关的文章教程,发现了一篇挺不错的文章[How to debug UnhandleExceptionHandler!],刚好适用于这种情况。文章的第一步

1. put a bp on kernel32!UnhandledExceptionFilter

然而我打开windbg,得到的是这样的结果

0:000> bp kernel32!UnhandledExceptionFilter
Couldn't resolve error at 'kernel32!UnhandledExceptionFilter'

“纳尼”的图片搜索ç"“æžœ

是不是我没下符号表…是不是没加载kernel32的模块…吓得我赶紧lm看看

0:000> lm
start    end        module name
003a0000 003c0000   Breakpoints C (private pdb symbols)  F:\OneDrive\cpp\Breakpoints\Debug\Breakpoints.pdb
0f0e0000 0f0fb000   VCRUNTIME140D   (deferred)             
0f740000 0f8b4000   ucrtbased   (deferred)             
74aa0000 74b80000   KERNEL32   (pdb symbols)  

0:000> x kernel32!unhandled*
74ad5f00          KERNEL32!UnhandledExceptionFilterStub (<no parameter info>)

程序加载了模块,符号表下号好了,但是只有KERNEL32!UnhandledExceptionFilterStub而不是kernel32!UnhandledExceptionFilter。这两个API就特么是雷锋跟雷峰塔的关系,死马当活马医,下个断试试

0:000> bp kernel32!UnhandledExceptionFilterStub

0:000> g
(2668.1f30): Integer divide-by-zero - code c0000094 (first chance)

0:000> g
(2668.1f30): Integer divide-by-zero - code c0000094 (!!! second chance !!!)

并没有什么卵用,虽然名称中包含了Filter,但是异常分发并不走这条路。搜索枯肠,终于在user32翻到个user32!UnhandledExceptionFilter。虽然是用户态但是黑猫白猫能抓到老鼠就是好猫嘛,名称一样肯定能断下来的flag

0:000> bp user32!UnhandledExceptionFilter

0:000> g
(2668.1f30): Integer divide-by-zero - code c0000094 (first chance)

 0:000> g
(2668.1f30): Integer divide-by-zero - code c0000094 (!!! second chance !!!)

“WTF”的图片搜索ç"“æžœ

还是死循环…用户态不断的吗,分发直接走的内核态?

失败的尝试-3

是win10你逼我的

windbg中有一条call指令可以直接调用某个地址的方法,格式为.call API(Param1, Param2,..)。如果在引发异常的时候直接调用异常处理API能不能绕过呢?不过异常处理函数的原型为LONG WINAPI UnhandledExcept(EXCEPTION_POINTERS *pExpInfo),参数需要一个指向异常信息的EXCEPTION_POINTERS指针。其原型为

typedef struct _EXCEPTION_POINTERS {
  PEXCEPTION_RECORD ExceptionRecord;
  PCONTEXT          ContextRecord;
} EXCEPTION_POINTERS, *PEX

在内存里搜搜看

0:000> .excr
Unable to get exception context, HRESULT 0x8000FFFF

0:000> .exr -1
ExceptionAddress: 003b19a5 (Breakpoints!main+0x00000075)
ExceptionCode: c0000094 (Integer divide-by-zero)
ExceptionFlags: 00000000
NumberParameters: 0

0:000> s -d esp L1000 0xc0000094
00b3f948  c0000094 00000000 00000000 003ba57c  ............|.;.
00b3f9b4  c0000094 753fa0e0 56fb63e5 2ab7db1f  ......?u.c.V...*

0:000> .exr 00b3f948
ExceptionAddress: 003ba57c (Breakpoints!__dyn_tls_dtor_callback)
ExceptionCode: c0000094 (Integer divide-by-zero)
ExceptionFlags: 00000000
NumberParameters: 3909000

0:000> .exr 00b3f9b4
ExceptionAddress: 2ab7db1f
ExceptionCode: c0000094 (Integer divide-by-zero)
ExceptionFlags: 753fa0e0
NumberParameters: 0

在内存中能搜索到结构体的第一个元素ExceptionRecord,但是指向其的指针却没有任何踪迹,总不能按每4个字节全部解引用一遍看看内容吧。于是我突然灵机一动

“灵稽一动”的图片搜索ç"“æžœ

传个0作为参数,手动跳过流程,改改寄存器变量值,岂不美哉

0:000> .call Breakpoints!UnhandledExcept(0)
Thread is set up for call, 'g' will execute.
WARNING: This can have serious side-effects,
including deadlocks and corruption of the debuggee.

0:000> t
(2668.1f30): Integer divide-by-zero - code c0000094 (first chance)

0:000> t
(2668.1f30): Integer divide-by-zero - code c0000094 (!!! second chance !!!)

不行,无法跳转,EIP已经被锁死了,只能从异常分发机制下手,而非对寄存器或执行顺序进行魔改。

“这可咋整啊”的图片搜索ç"“æžœ

但是要么没有一模一样的API,长得像的,用户态的又断不下来,脑壳疼,切换到WIN7试试。

WIN7

一如既往地中断到div指令前,还是一样的错误,还是熟悉的味道

(244.5c0): Integer divide-by-zero - code c0000094 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000006 ebx=7ffdf000 ecx=0026f67c edx=00000000 esi=0026f940 edi=0026fa28
eip=013d19a5 esp=0026f940 ebp=0026fa28 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
*** WARNING: Unable to verify checksum for Breakpoints.exe
Breakpoints!main+0x75:
013d19a5 f73538a13d01    div     eax,dword ptr [Breakpoints!flag (013da138)] ds:0023:013da138=00000000

首先还是老样子,搜索巨硬提供的系统模块Kernel32的符号表中有没有UnHandledExceptionFilter这个妖孽

0:000> x kernel32!Unhandle*
770eed38 kernel32!UnhandledExceptionFilter = <no type information>

“我TM”的图片搜索ç"“æžœ

这尼玛…最新版win10巨硬不给符号表,换到win7老实得不得了。珍惜生命,远离最新版系统

那接下来可就一马平川了,先给kernel32!UnhandledExceptionFilter下个断点,然后看看情况

0:000> bu kernel32!UnhandledExceptionFilter
0:000> g
Breakpoint 0 hit
eax=770eed38 ebx=00000000 ecx=0026f30f edx=777f70b4 esi=0026f408 edi=00000000
eip=770eed38 esp=0026f3dc ebp=0026faf4 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
kernel32!UnhandledExceptionFilter:
770eed38 6a5c            push    5Ch

嗯,看来确实进入了系统分发结构化异常的流程,接下来看看kernel32!UnhandledExceptionFilter会调用哪些方法来确定调试器的存在

kernel32!UnhandledExceptionFilter:
770eed38 6a5c            push    5Ch
770eed3a 68f0ee0e77      push    offset kernel32!BaseReleaseProcessExePath+0x13b8 (770eeef0)
770eed3f e82cd0feff      call    kernel32!_SEH_prolog4 (770dbd70)
770eed44 c745e006000000  mov     dword ptr [ebp-20h],6
770eed4b 33f6            xor     esi,esi
770eed4d 8975e4          mov     dword ptr [ebp-1Ch],esi
770eed50 8975dc          mov     dword ptr [ebp-24h],esi
770eed53 8975d8          mov     dword ptr [ebp-28h],esi
770eed56 8b5d08          mov     ebx,dword ptr [ebp+8]
770eed59 8b03            mov     eax,dword ptr [ebx]
770eed5b f6400410        test    byte ptr [eax+4],10h
770eed5f 0f8538e0ffff    jne     kernel32!UnhandledExceptionFilter+0x29 (770ecd9d)
770eed65 c745d401000000  mov     dword ptr [ebp-2Ch],1
770eed6c 8138090400c0    cmp     dword ptr [eax],0C0000409h
770eed72 0f8473e0ffff    je      kernel32!UnhandledExceptionFilter+0x3f (770ecdeb)
770eed78 53              push    ebx
770eed79 e843020000      call    kernel32!CheckForReadOnlyResourceFilter (770eefc1)
770eed7e 83f8ff          cmp     eax,0FFFFFFFFh
770eed81 0f841de0ffff    je      kernel32!UnhandledExceptionFilter+0x91 (770ecda4)
770eed87 e8ad010000      call    kernel32!BasepIsDebugPortPresent (770eef39)
770eed8c 85c0            test    eax,eax
770eed8e 0f8509e0ffff    jne     kernel32!UnhandledExceptionFilter+0x29 (770ecd9d)
......

该API调用了kernel32!BasepIsDebugPortPresent来确定调试器的端口,动态分析得出调用完该API后,EAX=1,通过test判断非0后经过jne跳转到调试器的处理例程中。在test指令执行前将EAX清空可以实现调试器隐藏。

那么这个API又是如何实现调试器情况监视呢?跟进去看看

kernel32!BasepIsDebugPortPresent:
770eef39 8bff            mov     edi,edi
770eef3b 55              push    ebp
770eef3c 8bec            mov     ebp,esp
770eef3e 51              push    ecx
770eef3f 8365fc00        and     dword ptr [ebp-4],0
770eef43 6a00            push    0
770eef45 6a04            push    4
770eef47 8d45fc          lea     eax,[ebp-4]
770eef4a 50              push    eax
770eef4b 6a07            push    7
770eef4d 6aff            push    0FFFFFFFFh
770eef4f ff154c150977    call    dword ptr [kernel32!_imp__NtQueryInformationProcess (7709154c)]
.....

原来在异常分发的时候,调试器监测的底层实现是ntdll!NtQueryInformationProcess。MSDN上关于该API的介绍为:

__kernel_entry NTSTATUS NtQueryInformationProcess(
  IN HANDLE           ProcessHandle,
  IN PROCESSINFOCLASS ProcessInformationClass,
  OUT PVOID           ProcessInformation,
  IN ULONG            ProcessInformationLength,
  OUT PULONG          ReturnLength
);

Parameters

ProcessHandle

A handle to the process for which information is to be retrieved.

ProcessInformationClass

The type of process information to be retrieved. This parameter can be one of the following values from the PROCESSINFOCLASS enumeration.

Value Meaning
ProcessBasicInformation 0 Retrieves a pointer to a PEB structure that can be used to determine whether the specified process is being debugged, and a unique value used by the system to identify the specified process.Use the CheckRemoteDebuggerPresent and GetProcessId functions to obtain this information.
ProcessDebugPort 7 Retrieves a DWORD_PTR value that is the port number of the debugger for the process. A nonzero value indicates that the process is being run under the control of a ring 3 debugger.Use the CheckRemoteDebuggerPresent or IsDebuggerPresent function.

第一个参数为进程的句柄,第二个参数是选定查询信息的类型,第三个参数是指向返回查询结果的指针,第四个参数是信息的长度,第五个参数是返回信息的长度。看看传入的参数信息:

0:000> kb
ChildEBP RetAddr  Args to Child              
001af6f4 770eef55 ffffffff 00000007 001af710 ntdll!NtQueryInformationProcess

0:000> dd 001af6f4+8
001af6fc  ffffffff 00000007 001af710 00000004
001af70c  00000000 00000000 001af798 770eed8c

第一个参数FFFFFFFF是一个伪句柄,只适用于线程内部使用,指向其自身。第二个参数7根据表格意为查询调试端口信息,返回地址为001af710。通过查看EPROCESS结构体发现其中有DEBUG_PORT位域,推测与其有关。

0:000> .process
Implicit process is now 7ffd4000

0:000> dt _EPROCESS 7ffd4000
ntdll!_EPROCESS
  ......
   +0x0ec DebugPort        : (null) 
  .....

尝试在该位域下一个读写硬件断点,但是函数执行完毕后没有中断,看来与其无关

0:000> ba r4 7ffd4000++0x0ec

0:000> p
eax=000000ea ebx=001af7c8 ecx=001af71d edx=777f70b4 esi=00000000 edi=00000000
eip=777f604d esp=001af6f8 ebp=001af714 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!NtQueryInformationProcess+0x5:
777f604d ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)

0:000> p
eax=000000ea ebx=001af7c8 ecx=001af71d edx=7ffe0300 esi=00000000 edi=00000000
eip=777f6052 esp=001af6f8 ebp=001af714 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!NtQueryInformationProcess+0xa:
777f6052 ff12            call    dword ptr [edx]      ds:0023:7ffe0300={ntdll!KiFastSystemCall (777f70b0)}

0:000> p
eax=00000000 ebx=001af7c8 ecx=001af6f4 edx=777f70b4 esi=00000000 edi=00000000
eip=777f6054 esp=001af6f8 ebp=001af714 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!NtQueryInformationProcess+0xc:
777f6054 c21400          ret     14h

函数执行完毕后,查看第三个参数指向的地址,非0,意味着系统监测到该程序处于ring3级别的调试器下,监测的方法…未知。

0:000> dd 001af710 L1
001af710  ffffffff

调用完毕后EAX = 0。如果在此时将EAX置为0x80000000,执行完test指令后,zf=1 sf=1, of=0,满足下一个jl的跳转条件,便可绕过调试器监测。

770eef4f ff154c150977    call    dword ptr [kernel32!_imp__NtQueryInformationProcess (7709154c)]
770eef55 85c0            test    eax,eax
770eef57 7c0a            jl      kernel32!BasepIsDebugPortPresent+0x2b (770eef63)
770eef59 837dfc00        cmp     dword ptr [ebp-4],0
770eef5d 0f85f7d20100    jne     kernel32!BasepIsDebugPortPresent+0x26 (7710c25a)
770eef63 33c0            xor     eax,eax
770eef65 c9              leave
770eef66 c3              ret
770eef67 90              nop

不作处理,接着跳转到程序的末尾,API通过清空EAX并自增来返回1这个状态,反汇编如下

7710c25a 33c0            xor     eax,eax
7710c25c 40              inc     eax
7710c25d c9              leave
7710c25e c3              ret

在返回前将EAX清空也是一个绕过的方法。

回到WIN10

至此本来打算告一段落了,突然想起,如果在win10下对ntdll!NtQueryInformationProcess下个断,会不会有意外收获?

0:000> bu ntdll!NtQueryInformationProcess

0:000> g
Breakpoint 3 hit
eax=011bf7d8 ebx=00000000 ecx=011bf7dc edx=011bf7e0 esi=011bf860 edi=011bfed8
eip=774d0610 esp=011bf7a8 ebp=011bf848 iopl=0         nv up ei ng nz na po cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000283
ntdll!NtQueryInformationProcess:
774d0610 b819000000      mov     eax,19h

嗯,一模一样…但为什么没有UnHandledExceptionFilter这个API,看了看调用堆栈明白了…全新的异常分发调用,走的是KERNELBASE!UnhandledExceptionFilter。难怪之前USER32的那个断点断不下来。依然是熟悉的味道,不过是前缀换成了KERNELBASE!

“我TM”的图片搜索ç"“æžœ

网络上东拼西凑的教程害skr人,不过在64位系统下找kernel32…..我蠢了

0:000> kp
 # ChildEBP RetAddr  
00 003af0b0 754f9d74 ntdll!NtQueryInformationProcess
01 003af0d4 754fa1d1 KERNELBASE!BasepIsDebugPortPresent+0x1d
02 003af16c 77502fff KERNELBASE!UnhandledExceptionFilter+0xf1
03 003af9dc 774c65fd ntdll!__RtlUserThreadStart+0x3ca01
04 003af9ec 00000000 ntdll!_RtlUserThreadStart+0x1b

还是一样的配方,还是熟悉的味道,修改KERNELBASE!BasepIsDebugPortPresent的返回结果,大功告成。

总结

玩了一遍Windows调试器监视与异常分发的流程,熟悉了与调试/反调试相关的API。不过..由于网上资料质量参差不齐,走了太多的弯路,心情如图。

“我TM”的图片搜索ç"“æžœ

EOF
首页      技术      逆向与汇编      基于异常的反调试实践

Chernobyl

文章作者

发表评论

textsms
account_circle
email

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

  • Lixiong

    擦 十年前的文章有了一个backtrack

    6月前 回复

基于异常的反调试实践
前言 最近在学习软件调试相关的内容,顺便举一反三找找反调试以及反反调试的相关方法。逛着逛着找到了这个网站:2种基于异常机制的反调试方法,虽然一看就知道不知道从哪转载的,而且搜…
扫描二维码继续阅读
2019-02-03
%d 博主赞过: