反调试跟踪的一点心得

来源:互联网 发布:手机淘宝怎么合并订单 编辑:程序博客网 时间:2024/05/29 17:45
此贴转载于看雪:http://bbs.pediy.com/showthread.php?t=79205

本文所提到的一些方法,对于熟练的逆向调试者来说不值一提。写出来的目的只是帮助初学者解决调试中可能遇到的一些问题。这些问题以前我遇到过,当时解决起来花了不少时间。
1、  INT 3断点:
检测关键API的入口的第1个字节是否为INT 3(0xCC),是则OVER。相关代码如下:
if (* (unsigned char*) fun == 0xcc) //一定要定义成unsigned 否则比较出错
{
    ExitProcess(1);
}
同样可以在自己的重要代码处从头到尾进行遍历是否有0XCC。
2、  API函数IsdebuggerPresent:
IsDebuggerPresent可以检验用户级别调试器,对内核级别的无效。如果返回为TURE的话则说明程序正在被调试。然而使用这个API函数有个致命的弱点,即:我们可以找到并且绕过它。解决方式如下:将这个函数的反汇编代码嵌入到我们的代码中。
mov eax,dword ptr fs:[18] //获取TEB,注意:18是十六进制
mov eax,dword ptr ds:[eax+30] //指向PEB
movzx eax,byte ptr ds:[eax+2] //获取是否被调试信息
这个数据结构WIN NT中是固定的。其他的就不清楚了(对此可以采用GetVersion来判断操作系统)。
3、  清除硬件断点/更改程序流程:
这里需要利用SHE来进行处理,首先触发一个异常,然后:
UINT SEH_Handler(EXCEPTION_RECORD *_lpExceptionRecord,
         DWORD _lpSEH,
         CONTEXT *_lpContext,
         DWORD _lpDisPatcherContext)
{
…… //你的代码
  _lpContext->Eip = (DWORD)SEH;  //更改程序流程
  _lpContext->Dr0 = NULL;      //清除硬件断点
  _lpContext->Dr1 = NULL;
  _lpContext->Dr2 = NULL;
  _lpContext->Dr3 = NULL;
  _lpContext->Dr6 = NULL;
  _lpContext->Dr7 = NULL;
…… //你的代码
}
4、  检测INT 1:
因为SoftICE将“INT 1”用于单步追踪程序,所以必定把IDT中的“INT 1”设置为自己的中断处理。所以可以用以下方式检测:
int TestException(LPEXCEPTION_POINTERS pExceptionInfo)
{
DWORD ExceptionCode = pExceptionInfo->ExceptionRecord->ExceptionCode;
    if (ExceptionCode != STATUS_ACCESS_VIOLATION)
    {
      printf("SoftICE is present!");
    }
    return EXCEPTION_EXECUTE_HANDLER;
}
__try
{
_asm int 1; //执行INT 1中断
}
    __except(TestException(GetExceptionInformation()))
{
}
我发现这个方式在OD中下内存断点的时候一样起作用,具体为什么还不知道。
5、  设置单步标志
(单步标志是IA-32 CPU中EFLAGS寄存器中的标志位(TF),设置单步标志为1的时候,CPU每执行一条指令之后都会产生一个中断。)设置单步标志如果没有抛出异常。则可以认为是调试器把异常处理掉了。无论是用户模式还是内核模式都是有效的。以下是实现代码:
BOOL bExceptionHit = FALSE;
  __try
  {
    _asm
    {
      pushfd
      or dword ptr [esp],0x100 //设置单步标志为1
      popfd
    }
  }
  __except(EXCEPTION_EXECUTE_HANDLER)
  {
    bExceptionHit = TRUE;
  }
  if (bExceptionHit == FALSE)
  {
    printf("A debgger is present!\n");
  }
这个方法在单步跟踪的时候会走不下去,解决方式:将T标志位置0,再更改后面的流程。
6、  代码自校验:
对整个程序或者代码片段做校验,以防止被修改,因为中断会更改代码(硬件中断除外),另外还可以有效的防止patching,目前主流的是CRC校验。程序启动的时候校验的值和文件的特定位置值(校验前要清除)做比较。其流程大体如下:
A.  GetModuleHandle获取自身句柄。
B.  GetModuleFileName获取自身文件名。
C.  CreateFile打开自己。
D.  ReadFile 读自己。
E.  定位和读校验值并做保存。
F.  清空校验值。
G.  CRC自我校验并且和刚才获取的值进行比较。
这种方法也可以对单独的一段代码进行处理。
7、  迷惑反汇编器:
反编译方式有两种1:线性扫描,2:递归遍历。Softice和WinDBG属于前者,OD和IDA属于后者。
对于线性扫描,我们在代码段加入数据定义来迷惑反编译器,比如在汇编代码中嵌入:
  _asm _emit 0x0f;//_EMIT伪指令相当于MASM中的DB,但一次只能定义一个字节。
这样会让部分反编译器错误的将其错误识别。
递归遍历比较高级,简单的插入定义起不到作用。可以将_asm _emit 0x0f放在永远不能达到的条件分支后,比如比较1和1不相等则转跳。这样可以迷惑IDA,对OD无效。可能它对不同的分支都做了分析。
解决方式是:在转跳的目的的起始位置定义。
8、  利用调试工具漏洞:
OD在处理浮点数:9.2233720368547758080e+18 的时候会崩溃。
const int bod[] = {0xFFFFFFFF,0xFFFFFFFF,0x0000403D};
  _asm fld tbyte ptr [bod] //OD浮点漏洞
只要将这2句嵌入到VC中即可,不过因为现在大家基本不用原版的OD了,这个方式也起不了什么作用。当初我遇到这个问题的时候是因为我用的OD是原版的。
9、  检测OD标识性窗口:
  HWND DWindow = GetDesktopWindow();
  //获得顶端的子窗口的句柄
  DWindow = GetWindow(DWindow,GW_CHILD);
  while (DWindow)
  {
    if (GetWindowText(DWindow,ODText,MAX_LOADSTRING))
    {
      …… //你的判断
    }
    DWindow = GetWindow(DWindow,GW_HWNDNEXT);
  }
10、  检测进程中的OD名:
具体实现请参照其他资料。
11、  多线程:
以上的各种方式采用多线程互相监视以防止被绕过而失效等。
12、  封装函数参数:
函数参数封装到结构体内部,结构体再通过指针来访问变量。关键函数的返回值不使用Return而是将返回值放到结构体内部的一个指针中。
比如:fun(a,b,c,d,e,f,g)改为fun(x),其中x是结构体。
这样的函数不容易一眼看出用途,毕竟参数不是通过简单的堆栈和寄存器传递。如果每个函数都这样处理,逆向调试者最终可能忍受不了而放弃。
13、  内存拷贝法:
以前调试程序的时候遇到过一种通过内存互相拷贝来达到保护作用的程序。
比如数据A指向的是密码,这个程序的采取方式是A->B,B->C,C->D……,其中指针和内存地址全部是动态生成的,这样拷贝上百次,最后进行加密操作。没有函数接口和转跳,但可能使用SEH来改变部分流程。具体不记得了,我觉得这种方式有一定的可取性,首先内存断点几乎作废。在你的代码中拷贝这么多次,用内存断点跟谁受得了(当然这样的代码你别放到一块,你东放一个西放一个,在你准备加密前保证处理了 N次就OK了,N不用多,30次以上就差不多了,没几个人能耐住性子跟到最后)。
14、  算法选择:
尽量不要选那些特征显著的算法。否则内存以搜索就出来了,之前的保护都白费了。
总的说来反调试的方法太多,还总有人想出新点子,这里只是写一些常见的。