【矛与盾】调戏调试器:反断点技术

时间:2022-11-18 03:29:14

标 题: 【矛与盾】调戏调试器:反断点技术
作 者: bxc
时 间: 2014-06-23,21:02:37
链 接: http://bbs.pediy.com/showthread.php?t=189359

首先声明,理解本文需要对用户态的调试器原理有所了解,否则可能有些内容会不理解。
先来个测试程序,本文以分析和理解此程序为主。
测试程序下载
无论是逆向分析、还是脱壳破解,都离不开调试器。而windows下面用户态调试器最常用的,那就是OllyDbg了。
现在就用OD载入并调试该程序,如下图所示。
【矛与盾】调戏调试器:反断点技术
OD载入程序后,就停在了OEP(原始入口点)。
可能有经验的朋友观察入口代码,就会发现,该程序的入口是典型的VC++。但是不要着急【矛与盾】调戏调试器:反断点技术,慢慢来。
现在F9运行程序,程序是控制台界面的。如下图所示:
【矛与盾】调戏调试器:反断点技术
提示下个断点,随便找个地方按F2下断点就行(不要下在INT 3处),我就下在OEP了。
【矛与盾】调戏调试器:反断点技术
刚下完断点,就被中断了,一般程序是不可能,因为入口点一般只调用一次。
现在F7单步一下试试,如下图:
【矛与盾】调戏调试器:反断点技术
单步一下,程序居然直接崩了,很奇怪吧。好了,就调试到这里。【矛与盾】调戏调试器:反断点技术现在看TRE.exe的源码来讲解。

代码:
#include <SDKDDKVer.h>
#include <Windows.h>
#include <stdio.h>
#include <tchar.h>
#include <process.h>

#pragma comment(linker, "/ENTRY:MyEntry")

#define ONLY_ASM __declspec(naked)

#ifdef __cplusplus
extern "C"{
#endif

#ifdef UNICODE 
  int wmainCRTStartup(
#else
  int mainCRTStartup(
#endif
    void);

#ifdef __cplusplus
}
#endif

#ifdef UNICODE
#define _tmainCRTStartup wmainCRTStartup
#else
#define _tmainCRTStartup mainCRTStartup
#endif // UNICODE


typedef VOID (NTAPI * Func_RtlRaiseException)(__in PEXCEPTION_RECORD ExceptionRecord);
typedef NTSTATUS (NTAPI * Func_NtRaiseException)(__in PEXCEPTION_RECORD ExceptionRecord, __in PCONTEXT ContextRecord, __in BOOLEAN FirstChance);

int _tmain(int argc, _TCHAR* argv[]);
DWORD WINAPI ProtectFunc(void * lParam);

DWORD tid = 0;
DWORD dwtmp0;
DWORD dwtmp1;
Func_RtlRaiseException f_rre;
Func_NtRaiseException f_nre;
DWORD dwtarr[12];

char fName[17] =
{
  0x82, 0xb8, 0x9e, 0xad, 0xa5, 0xbf, 0xa9, 0x89, 0xb4,
  0xaf, 0xa9, 0xbc, 0xb8, 0xa5, 0xa3, 0xa2, 0X00
};

PVOID GetSectionAddrByName(HMODULE hMod, PCHAR pSecName, DWORD * pSecSize)
{
  if (!hMod || !pSecName) return NULL;

  PIMAGE_DOS_HEADER pDos;
  PIMAGE_NT_HEADERS32 pNtHeader;
  PIMAGE_SECTION_HEADER pSection;
  WORD sNum;
  WORD i;

  pDos = (PIMAGE_DOS_HEADER)hMod;
  if (pDos->e_magic != IMAGE_DOS_SIGNATURE) return NULL;
  pNtHeader = (PIMAGE_NT_HEADERS32)((UINT)hMod + pDos->e_lfanew);
  if (pNtHeader->Signature != IMAGE_NT_SIGNATURE) return NULL;
  pSection = (PIMAGE_SECTION_HEADER)((UINT)pNtHeader + 0x18 + pNtHeader->FileHeader.SizeOfOptionalHeader);
  sNum = pNtHeader->FileHeader.NumberOfSections;

  for (i = 0; i < sNum; i++)
  {
    if (strcmp((PCHAR)pSection[i].Name, pSecName) == 0)
    {
      *pSecSize = pSection[i].Misc.VirtualSize;
      return (PVOID)((UINT)hMod + pSection[i].VirtualAddress);
    }
  }
  return NULL;
}


ONLY_ASM INT_PTR WINAPI GetFuncAddr(INT_PTR hMod, const PCHAR pName)
{
  _asm
  {
    push ebp
      mov ebp, esp
      sub esp, 0x10                 //为局部变量开辟空间
      push ebx
      push esi
      push edi
      mov ebx, [ebp + 0x08]
      mov eax, [ebx + 0x3c]         //dosheader->e_lfanew
      mov eax, [ebx + eax + 0x78]    //导出表地址
      test eax, eax                //判断导出表地址是否为空
      je ReturnNull
      add eax, ebx                  //加模块基址
      //取出输出表中一些有用的值   
      mov  ebx, [eax + 0x18]
      mov[ebp - 0x04], ebx
      mov  ebx, [eax + 0x1C]
      add  ebx, [ebp + 0x08]
      mov[ebp - 0x08], ebx
      mov  ebx, [eax + 0x20]
      add  ebx, [ebp + 0x08]
      mov[ebp - 0x0C], ebx
      mov  ebx, [eax + 0x24]
      add  ebx, [ebp + 0x08]
      mov[ebp - 0x10], ebx

      mov esi, [ebp + 0x0C]
      test esi, 0xFFFF0000
      jne Get_API_AddressByName
      mov eax, esi
      dec eax
      jmp Get_API_AddressByIndex

      //函数名取地址
    Get_API_AddressByName :
    xor eax, eax
      mov edi, [ebp - 0x0C]
      mov ecx, [ebp - 0x04]

    LoopNumberOfName :
                     mov esi, [ebp + 0x0C]
                     push eax
                     mov ebx, [edi]
                     add ebx, [ebp + 0x08]
                   Match_API :
                             mov al, byte ptr[ebx]
                             cmp al, [esi]
                             jnz Not_Match
                             or al, 0x00
                             jz Get_API_Index_Found
                             inc ebx
                             inc esi
                             jmp Match_API
                           Not_Match :
    pop eax
      inc eax
      add edi, 0x04
      loop LoopNumberOfName
      jmp ReturnNull

    Get_API_Index_Found :
    pop eax

    Get_API_AddressByIndex :
    mov ebx, [ebp - 0x10]
      movzx eax, word ptr[ebx + eax * 0x02]
      imul eax, 0x04
      add  eax, [ebp - 0x08]
      mov  eax, [eax]
      add  eax, [ebp + 0x08]
      jmp ReturnVal

    ReturnNull :
    xor eax, eax

    ReturnVal :
    pop edi
      pop esi
      pop ebx
      add esp, 0x10
      pop ebp
      retn 0x08
  }
}

void MyInit1()
{
  INT_PTR NtBase = 0;
  PCHAR pc = fName;

  __asm mov eax, fs:[0x30]
  __asm mov eax, [eax + 0x0C]
  __asm mov eax, [eax + 0x1C]
  __asm mov eax, [eax + 0x08]
  __asm mov NtBase, eax

  f_nre = (Func_NtRaiseException)GetFuncAddr(NtBase, fName);

  while (*pc)
  {
    *pc ^= 0xCC;
    pc++;
  }

  CreateThread(NULL, 4194304, ProtectFunc, MyInit1, 0, &tid);

  if (!tid || !f_nre)
  {
    printf("初始化失败!\n");
    ExitProcess(0);
  }
}

void MyInit()
{
  HMODULE hMod = GetModuleHandle(NULL);
  PCHAR pc = fName;
  PIMAGE_DOS_HEADER pDos;
  PIMAGE_NT_HEADERS32 pNtHeader;

  pDos = (PIMAGE_DOS_HEADER)hMod;

  while (*pc)
  {
    *pc ^= 0xCC;
    pc++;
  }

  MyInit1();
  if (pDos->e_magic != IMAGE_DOS_SIGNATURE) return;
  pNtHeader = (PIMAGE_NT_HEADERS32)((UINT)hMod + pDos->e_lfanew);
  if (pNtHeader->Signature != IMAGE_NT_SIGNATURE) return;
  return;
}

ONLY_ASM int CallCrt()
{
  __asm call _tmainCRTStartup
  /* 下面的指令就是混淆视线的 */
  __asm retn
  __asm nop
  __asm push ebp
  __asm mov ebp, esp
  __asm int 3
  __asm int 1
  __asm mov esp, ebp
  __asm pop ebp
  __asm retn
}

ONLY_ASM int MyEntry()
{
  __asm
  {
    call MyInit
      jmp CallCrt
      /* 下面的指令就是混淆视线的 */
      push ebp
      mov ebp, esp
      call IsDebuggerPresent
      push 1
      mov dwtmp0, eax
      call _tmain
      push dword ptr [ebp + 0x08]
      call ProtectFunc
      cmp dwtmp1, 0
      pop ecx
      pop ecx
      jnz JMP0
      push 0x1
      call _tmain
      pop ecx
    JMP0 :
    push 0xC0000409
      call ProtectFunc
      pop ecx
      pop ebp
      retn

      push    ebp
      mov     ebp, esp
      push    0x17
      call    IsDebuggerPresent
      test    eax, eax
      je      JMP0
      push    0x2
      pop     ecx
      mov     dwtarr[0], eax
      mov     dwtarr[1], ecx
      mov     dwtarr[2], edx
      mov     dwtarr[3], ebx
      mov     dwtarr[4], esi
      mov     dwtarr[5], edi
      mov     word ptr dwtarr[6], ss
      mov     word ptr dwtarr[7], cs
      mov     word ptr dwtarr[8], ds
      mov     word ptr dwtarr[9], es
      mov     word ptr dwtarr[10], fs
      mov     word ptr dwtarr[11], gs
      pushfd
      push    0x4
      call _tmain
      pop     eax
      imul    eax, eax, 0x0
      mov     dword ptr ss : [ebp + eax - 0x8], ecx
      push    0x4
      pop     eax
      mov     esp, ebp
      pop     ebp
      retn
  }
}

PBYTE WINAPI CompBytes(PBYTE pm0, PBYTE pm1, UINT mMax, PBYTE pOByte)
{
  UINT i;

  for (i = 0; i < mMax;i++)
  {
    if (pm0[i] != pm1[i])
    {
      if (pm0[i] == 0xCC)
      {
        *pOByte = pm1[i];
        return &pm0[i];
      }
    }
  }
  return NULL;
}

DWORD WINAPI ProtectFunc(void * lParam)
{
  PBYTE poMem;
  PBYTE oAddr;
  DWORD mSize;
  PBYTE nAddr;
  NTSTATUS ns;
  EXCEPTION_RECORD ER;
  CONTEXT ct;
  BYTE OByte;

  oAddr = (PBYTE)GetSectionAddrByName(GetModuleHandle(NULL), ".text", &mSize);
  poMem = (PBYTE)HeapAlloc(GetProcessHeap(), 0, mSize);
  
  if (!poMem)
  {
    printf("HeapAlloc失败!退出...\n");
    ExitProcess(0);
  }

  memcpy_s(poMem, mSize, oAddr, mSize);

  while (true)
  {
    nAddr = CompBytes(oAddr, poMem, mSize, &OByte);
    if (nAddr)
    {
      while (*nAddr != OByte)
      {
        /* 抛出假断点异常 */
        ER.ExceptionCode = EXCEPTION_BREAKPOINT;
        ER.ExceptionFlags = 0;
        ER.ExceptionRecord = NULL;
        ER.ExceptionAddress = nAddr;
        ER.NumberParameters = 1;
        ER.ExceptionInformation[0] = 0;

        ct.ContextFlags = CONTEXT_FULL;
        ct.Eip = DWORD(nAddr);
        ct.Dr0 = 0;
        ct.Dr1 = 0;
        ct.Dr2 = 0;
        ct.Dr3 = 0;
        ct.Dr6 = 0;
        ct.Dr7 = 0;
        ct.SegGs = 0x2B;
        ct.SegFs = 0x53;
        ct.SegEs = 0x2B;
        ct.SegDs = 0x2B;
        ct.SegCs = 0x23;
        ct.EFlags = 0x00000246;
        ct.SegSs = 0x2B;

        ns = f_nre(&ER, &ct, FALSE);
        Sleep(60);
      }
    }
    Sleep(80);
  }

  HeapFree(GetProcessHeap(), 0, poMem);
  return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
  printf("下个断点试试!:)\n");
  getchar();
  printf("按任意键退出!\n");
  getchar();
  return 0;
}
代码有些技术性的东西,可能会有点难理解。
首先,本程序入口不是默认的CRT入口。
程序入口是ONLY_ASM int MyEntry();函数。
由纯汇编写成。本函数整容成了CRT入口的样子:
call XXXXXXXX
jmp XXXXXXXX
后面的代码完全就是混淆视线用的。一般调试VC的程序,不会跟进CRT里面的。
第一个call就直接步过了,如果你在这里步过了,那可就错过精彩内容了。
第二个jmp才是跳转到真正的CRT入口。
看来下call MyInit都做了些什么吧。
代码:
void MyInit()
{
  HMODULE hMod = GetModuleHandle(NULL);
  PCHAR pc = fName;
  PIMAGE_DOS_HEADER pDos;
  PIMAGE_NT_HEADERS32 pNtHeader;

  pDos = (PIMAGE_DOS_HEADER)hMod;

  while (*pc)
  {
    *pc ^= 0xCC;
    pc++;
  }

  MyInit1();
  if (pDos->e_magic != IMAGE_DOS_SIGNATURE) return;
  pNtHeader = (PIMAGE_NT_HEADERS32)((UINT)hMod + pDos->e_lfanew);
  if (pNtHeader->Signature != IMAGE_NT_SIGNATURE) return;
  return;
}
这段代码里,只有2部分是有用的。
while和MyInit1。
while是解码字符串用的,因为需要动态获取API的地址,又不想被枚举出来字符串。所以我把每个字符都xor 0xCC了。
用的时候,在xor 0xCC解密出来。
解码完的字符串是"NtRaiseException"。
然后是调用MyInit1();,看下MyInit1()的代码:
代码:
void MyInit1()
{
  INT_PTR NtBase = 0;
  PCHAR pc = fName;

  __asm mov eax, fs:[0x30]
  __asm mov eax, [eax + 0x0C]
  __asm mov eax, [eax + 0x1C]
  __asm mov eax, [eax + 0x08]
  __asm mov NtBase, eax

  f_nre = (Func_NtRaiseException)GetFuncAddr(NtBase, fName);

  while (*pc)
  {
    *pc ^= 0xCC;
    pc++;
  }

  CreateThread(NULL, 4194304, ProtectFunc, MyInit1, 0, &tid);

  if (!tid || !f_nre)
  {
    printf("初始化失败!\n");
    ExitProcess(0);
  }
}
MyInit1函数里的代码都是有用的。
先看第一段,取TEB放到eax。然后取第一个模块的基址。
这里要补充一点,不论是xp还是win 8.1,不论32位还是64位系统,一个进程的第一个模块,一定是ntdll.dll。
32位和64位的差距是第二个模块,32位下第二个模块是kernel32.dll,64位是kernelbase.dll模块。
然后调用GetFuncAddr(GetFuncAddr是汇编写的,功能等于GetProcAddress),取ntdll模块中NtRaiseException的地址。
把取到的地址存到f_nre中,接着while再把字符串加密了,这个可以不要。
创建一个新线程,线程入口是ProctectFunc,线程参数是MyInit1的地址。这个可以没有,就是用来迷惑人的。
如果创建失败,就退出进程。
现在再看看在ProctectFunc里都做了什么:
代码:
DWORD WINAPI ProtectFunc(void * lParam)
{
  PBYTE poMem;
  PBYTE oAddr;
  DWORD mSize;
  PBYTE nAddr;
  NTSTATUS ns;
  EXCEPTION_RECORD ER;
  CONTEXT ct;
  BYTE OByte;

  oAddr = (PBYTE)GetSectionAddrByName(GetModuleHandle(NULL), ".text", &mSize);
  poMem = (PBYTE)HeapAlloc(GetProcessHeap(), 0, mSize);
  
  if (!poMem)
  {
    printf("HeapAlloc失败!退出...\n");
    ExitProcess(0);
  }

  memcpy_s(poMem, mSize, oAddr, mSize);

  while (true)
  {
    nAddr = CompBytes(oAddr, poMem, mSize, &OByte);
    if (nAddr)
    {
      while (*nAddr != OByte)
      {
        /* 抛出假断点异常 */
        ER.ExceptionCode = EXCEPTION_BREAKPOINT;
        ER.ExceptionFlags = 0;
        ER.ExceptionRecord = NULL;
        ER.ExceptionAddress = nAddr;
        ER.NumberParameters = 1;
        ER.ExceptionInformation[0] = 0;

        ct.ContextFlags = CONTEXT_FULL;
        ct.Eip = DWORD(nAddr);
        ct.Dr0 = 0;
        ct.Dr1 = 0;
        ct.Dr2 = 0;
        ct.Dr3 = 0;
        ct.Dr6 = 0;
        ct.Dr7 = 0;
        ct.SegGs = 0x2B;
        ct.SegFs = 0x53;
        ct.SegEs = 0x2B;
        ct.SegDs = 0x2B;
        ct.SegCs = 0x23;
        ct.EFlags = 0x00000246;
        ct.SegSs = 0x2B;

        ns = f_nre(&ER, &ct, FALSE);
        Sleep(60);
      }
    }
    Sleep(80);
  }

  HeapFree(GetProcessHeap(), 0, poMem);
  return 0;
}
首先取本模块的.text段的基址和段大小。
然后再进程默认堆上申请一块内存。内存大小是.text段的大小。
然后把.text段复制过去,之后开始死循环。
不停的比较2块内存块。当某字节被修改,并且那个字节是0xCC(int3的机器码),那就返回那个字节所在的地址。
然后就是重中之重,构造一个异常的结构,并且用NtRaiseException抛出。
这里你也许会问,为什么不用RaiseException抛异常?如果真那么简单,我想就不用写本文了。
先来稍微讲下抛出异常函数的结构:
代码:
void WINAPI RaiseException(
__in DWORD dwExceptionCode,
__in DWORD dwExceptionFlags,
__in DWORD nNumberOfArguments,
__in const ULONG_PTR *lpArguments);

VOID NTAPI RtlRaiseException (
__in PEXCEPTION_RECORD ExceptionRecord);

NTSTATUS NTAPI NtRaiseException (
__in PEXCEPTION_RECORD ExceptionRecord,
__in PCONTEXT ContextRecord,
__in BOOLEAN FirstChance);
微软只给开发者公开了第一个API,即RaiseException,
后两个函数都在ntdll中被导出,但是没有公开。
这后两个函数的结构好像是在MS泄露的NT源码中提到的。
我是在一个外国网站找到定义的: 链接
其中RaiseException是对RtlRaiseException的封装,
RtlRaiseException又是对NtRaiseException的封装。
如果调用RaiseException的话,用户只能配置简单的几个参数。
调用RtlRaiseException的话,用户只能配置EXCEPTION_RECORD结构。
调用NtRaiseException的话,才算是真正用户自定义的异常。能配置异常记录结构,还能配置线程上下文。
先看下RaiseException的反汇编代码:
【矛与盾】调戏调试器:反断点技术
再看看RtlRaiseException的反汇编代码:
【矛与盾】调戏调试器:反断点技术
我们如果调用ZwRaiseException(NtRaiseException和ZwRaiseException其实是一个函数),需要自己配置参数即可。
本来我的代码是调用RtlRaiseException抛出断点异常给调试器的,可惜没有骗过OD。
个人认为,OD是需要根据CONTEXT的Eip而不是EXCEPTION_RECORD的ExceptionAddress来判断int3的地址的。

现在再来了解下,用户态调试器是怎么处理异常的。
详细内容可以参考: Windows用户态调试器原理
调试器使用WaitForDebugEvent等待调试事件,
处理了异常或者事件之后,调用ContinueDebugEvent,恢复线程。
有一点要清楚,并不是所有的int3断点异常OD都会处理的。
OD也是可以配置的:
【矛与盾】调戏调试器:反断点技术
这里的忽略异常,是指的未知来源的异常,什么是未知来源异常呢?
就是指异常不是OD引发的。是OD自己干的"好事",它肯定要自己收拾。
如果OD所有的异常都处理的话,那检测调试器就很简单了:
设置好VEH、SEH或者UEF后,触发一个int3,如果没有跳转到异常处理器,就直接退出进程。

那么OD是怎么判断什么样的异常是它该处理的呢。
这个其实写过调试器的人都知道:
比如OD在0x00401000处下了一个断点。
OD会记录下0x00401000处的一个字节,再把0x00401000处写成0xCC(int3),并记录下0x00401000这个地址。
这一切在OD的反汇编界面是看不到的。
当触发断点异常后,OD会判断异常地址是否是自己设置的(0x00401000)。如果不是,那就放弃处理异常。
如果是的话,把记录下的那一字节再写回0x00401000处,当用户按F9运行时,od还会设置单步异常。
在下一条指令单步异常被触发时候,OD又把0x00401000处改写成0xCC。这样断点就不至于只用一次。
所以说,当检测到有字节被修改,并且是0xCC,那基本就是调试器下的断点无疑的。
这时候,配置异常记录结构,和线程上下文。
把异常类型设置成EXCEPTION_BREAKPOINT,异常地址和线程的eip配置成前面检测到被修改的地址。
然后抛出异常,这样OD就中计了,其实根本没有被触发int3异常。
但是为什么OD收到假异常,再执行进程就会崩溃,这个就不是很清楚了,反正我们的目的达到了。
只要你敢下int3,我就给你抛异常玩 【矛与盾】调戏调试器:反断点技术

但是这种方法也有弊端。就是.text太大的话,效率肯定不快。
其实还可以只对IAT入口第一字节执行断点检测就行。