宝峰科技

 找回密码
 注册

QQ登录

只需一步,快速开始

智能终端设备维修查询系统注册会员邮箱认证须知!
查看: 2364|回复: 0

[推荐] 反调试技巧总结-原理和实现(四、五、六)

[复制链接]
  • TA的每日心情
    开心
    2024-12-9 18:45
  • 签到天数: 124 天

    [LV.7]常住居民III

    admin 发表于 2010-9-28 20:33:49 | 显示全部楼层 |阅读模式

    欢迎您注册加入!这里有您将更精采!

    您需要 登录 才可以下载或查看,没有账号?注册

    x
    四、  检测-断点(FB_)
    这一部分内容较少,但实际上可用的方法也比较多,我没有深入研究,不敢乱写,照抄了几个常用的方法:
    //find breakpoint
    bool FB_HWBP_Exception();
    DWORD FB_SWBP_Memory_CRC();
    bool FB_SWBP_ScanCC(BYTE * addr,int len);
    bool FB_SWBP_CheckSum_Thread(BYTE *addr_begin,BYTE *addr_end,DWORD sumValue);

    4.1 FB_HWBP_Exception
      在异常处理程序中检测硬件断点,是比较常用的硬件断点检测方法。在很多地方都有提到。
      __asm
      {
        push   offset exeception_handler; set exception handler
        push   dword ptr fs:[0h]
        mov    dword ptr fs:[0h],esp  
        xor    eax,eax;reset EAX invoke int3
        int    1h
        pop    dword ptr fs:[0h];restore exception handler
        add    esp,4
        ;test if EAX was updated (breakpoint identified)
        test   eax,eax
        jnz     rt_label
        jmp    rf_label

    exeception_handler:
        ;EAX = CONTEXT record
        mov     eax,dword ptr [esp+0xc]

        ;check if Debug Registers Context.Dr0-Dr3 is not zero
        cmp     dword ptr [eax+0x04],0
        jne     hardware_bp_found
        cmp     dword ptr [eax+0x08],0
        jne     hardware_bp_found
        cmp     dword ptr [eax+0x0c],0
        jne     hardware_bp_found
        cmp     dword ptr [eax+0x10],0
        jne     hardware_bp_found
        jmp     exception_ret

    hardware_bp_found:
        ;set Context.EAX to signal breakpoint found
        mov     dword ptr [eax+0xb0],0xFFFFFFFF
    exception_ret:
        ;set Context.EIP upon return
        inc       dword ptr [eax+0xb8];set ContextRecord.EIP
        inc       dword ptr [eax+0xb8];set ContextRecord.EIP
        xor     eax,eax
        retn
      }
    4.2 FB_SWBP_Memory_CRC()
      由于在一些常用调试器中,比如OD,其是将代码设置为0xcc来实现普通断点,因此当一段代码被设置了普通断点,则其中必定有代码的修改。因此对关键代码进行CRC校验则可以实现侦测普通断点。但麻烦的是每次代码修改,或更换编译环境,都要重新设置CRC校验值。
      下面的代码拷贝自《软件加解密技术》,里面完成的是对整个代码段的CRC校验,CRC校验值保存在数据段。CRC32算法实现代码网上有很多,就不列出来了。
    DWORD FB_SWBP_Memory_CRC()
    {
      //打开文件以获得文件的大小
      DWORD fileSize,NumberOfBytesRW;
      DWORD CodeSectionRVA,CodeSectionSize,NumberOfRvaAndSizes,DataDirectorySize,ImageBase;
      BYTE* pMZheader;
      DWORD pPEheaderRVA;
      TCHAR  *pBuffer ;
      TCHAR szFileName[MAX_PATH];

      GetModuleFileName(NULL,szFileName,MAX_PATH);
      //打开文件
      HANDLE hFile = CreateFile(
        szFileName,
        GENERIC_READ,
        FILE_SHARE_READ,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
       if ( hFile != INVALID_HANDLE_VALUE )
       {
        //获得文件长度 :
        fileSize = GetFileSize(hFile,NULL);
        if (fileSize == 0xFFFFFFFF) return 0;
        pBuffer = new TCHAR [fileSize];     //// 申请内存,也可用VirtualAlloc等函数申请内存
        ReadFile(hFile,pBuffer, fileSize, &NumberOfBytesRW, NULL);//读取文件内容
        CloseHandle(hFile);  //关闭文件
       }
       else
         return 0;
      pMZheader=(BYTE*)pBuffer; //此时pMZheader指向文件头
      pPEheaderRVA = *(DWORD *)(pMZheader+0x3c);//读3ch处的PE文件头指针
      ///定位到PE文件头(即字串“PE\0\0”处)前4个字节处,并读出储存在这里的CRC-32值:

      NumberOfRvaAndSizes=*((DWORD *)(pMZheader+pPEheaderRVA+0x74));//得到数据目录结构数量
      DataDirectorySize=NumberOfRvaAndSizes*0x8;//得到数据目录结构大小
      ImageBase=*((DWORD *)(pMZheader+pPEheaderRVA+0x34));//得到基地址
      //假设第一个区块就是代码区块
      CodeSectionRVA=*((DWORD *)(pMZheader+pPEheaderRVA+0x78+DataDirectorySize+0xc));//得到代码块的RVA值
      CodeSectionSize=*((DWORD *)(pMZheader+pPEheaderRVA+0x78+DataDirectorySize+0x8));///得到代码块的内存大小
      delete pBuffer;  // 释放内存
      return CRC32((BYTE*)(CodeSectionRVA+ImageBase),CodeSectionSize);
    }

    4.3 FB_SWBP_ScanCC
    扫描CC的方法,比照前面校验代码CRC数值的方法更直接一些,它直接在所要检测的代码区域内检测是否有代码被更改为0xCC,0xcc对应汇编指令为int3 ,对一些常用的调试器(如OD)其普通断点就是通过修改代码为int3来实现的。但使用时要注意是否正常代码中就包含CC。通常这个方法用于扫描API函数的前几个字节,比如检测常用的MessageBoxA、GetDlgItemTextA等。

    bool FB_SWBP_ScanCC(BYTE * addr,int len)
    {
      FARPROC Func_addr ;
      HMODULE hModule = GetModuleHandle("USER32.dll");
      (FARPROC&) Func_addr =GetProcAddress ( hModule, "MessageBoxA");
      if (addr==NULL)
        addr=(BYTE *)Func_addr;//for test
      BYTE tmpB;
      int i;
      __try
      {
        for(i=0;i<len;i++,addr++)
        {
          tmpB=*addr;
          tmpB=tmpB^0x55;
          if(tmpB==0x99)// cmp 0xcc
            return true;
        }
      }
      __except(1)
        return false;
      return false;
    }
      
    4.4 FB_SWBP_CheckSum_Thread(BYTE *addr_begin,BYTE *addr_end,DWORD sumValue);
    此方法类似CRC的方法,只是这里是检测累加和。它与CRC的方法有同样的问题,就是要在编译后,计算累加和的数值,再将该值保存到数据区,重新编译。在这里创建了一个单独的线程用来监视代码段。
    DWORD WINAPI CheckSum_ThreadFunc( LPVOID lpParam )
    {
      DWORD dwThrdParam[3];
      BYTE tmpB;
      DWORD Value=0;
      dwThrdParam[0]=* ((DWORD *)lpParam);
         dwThrdParam[1]=* ((DWORD *)lpParam+1);
          dwThrdParam[2]=* ((DWORD *)lpParam+2);
      BYTE *addr_begin=(BYTE *)dwThrdParam[0];
      BYTE *addr_end=(BYTE *)dwThrdParam[1];
      DWORD sumValue=dwThrdParam[2];
      for(int i=0;i<(addr_end-addr_begin);i++)
        Value=Value+*(addr_begin+i);
      /* //if sumvalue is const,it should be substract.
      DWORD tmpValue;
      Value=Value-(sumValue&0x000000FF);
      tmpValue=(sumValue&0x0000FF00)>>8;
      Value=Value-tmpValue;
      tmpValue=(sumValue&0x0000FF00)>>16;
      Value=Value-tmpValue;
      tmpValue=(sumValue&0x0000FF00)>>24;
      Value=Value-tmpValue;*/
      if (Value!=sumValue)
        MessageBox(NULL,"SWBP is found by CheckSum_ThreadFunc","CheckSum_ThreadFunc",MB_OK|MB_ICONSTOP);
        return 1;
    }

    bool FB_SWBP_CheckSum_Thread(BYTE *addr_begin,BYTE *addr_end,DWORD sumValue)
    {
        DWORD dwThreadId;
      DWORD dwThrdParam[3];
      dwThrdParam[0]=(DWORD)addr_begin;
      dwThrdParam[1]=(DWORD)addr_end;
      dwThrdParam[2]=sumValue;
        HANDLE hThread;

        hThread = CreateThread(
            NULL,                        // default security attributes
            0,                           // use default stack size  
            CheckSum_ThreadFunc,         // thread function
            &dwThrdParam[0],                // argument to thread function
            0,                           // use default creation flags
            &dwThreadId);                // returns the thread identifier
        // Check the return value for success.

       if (hThread == NULL)
          return false;
       else
       {
          Sleep(1000);
          CloseHandle( hThread );
        return true;
       }
    }
    五、  检测-跟踪(FT_)
    个人认为,反跟踪的一些技巧,多数不会非常有效,因为在调试时,多数不会被跟踪经过,除非用高超的技巧将关键代码和垃圾代码及这些反跟踪技巧融合在一起,否则很容易被发现或被无意中跳过。
    函数列表如下:
    //Find Single-Step or Trace
    bool FT_PushSS_PopSS();
    void FT_RDTSC(unsigned int * time);
    DWORD FT_GetTickCount();
    DWORD FT_SharedUserData_TickCount();
    DWORD FT_timeGetTime();
    LONGLONG FT_QueryPerformanceCounter(LARGE_INTEGER *lpPerformanceCount);
    bool FT_F1_IceBreakpoint();
    bool FT_Prefetch_queue_nop1();
    bool FT_Prefetch_queue_nop2();

    5.1 FT_PushSS_PopSS
    这个反调试在<<windows anti-debug reference>>里有描述,如果调试器跟踪经过下面的指令序列:
      __asm
      {
        push ss    //反跟踪指令序列
        ;junk
        pop  ss    //反跟踪指令序列
        pushf    //反跟踪指令序列
        ;junk
        pop eax    //反跟踪指令序列
    }
    Pushf将会被执行,同时调试器无法设置压进堆栈的陷阱标志,应用程序通过检测陷阱标志就可以判断处是否被跟踪调试。

      __asm
      {
        push ebp
        mov ebp,esp
        push ss    //反跟踪指令序列
        ;junk
        pop  ss    //反跟踪指令序列
        pushf    //反跟踪指令序列
        ;junk
        pop eax    //反跟踪指令序列
        and  eax,0x00000100
        jnz  rt_label

        xor eax,eax
        mov esp,ebp
        pop ebp
        retn
    rt_label:
        xor eax,eax
        inc eax
        mov esp,ebp
        pop ebp
        retn
      }

    5.2 FT_RDTSC
    通过检测某段程序执行的时间间隔,可以判断出程序是否被跟踪调试,被跟踪调试的代码通常都有较大的时间延迟,检测时间间隔的方法有很多种。比如RDTSC指令,kernel32_GetTickCount函数,winmm_ timeGetTime 函数等等。
    下面为RDTSC的实现代码。

      int time_low,time_high;
      __asm
      {
        rdtsc
        mov    time_low,eax
        mov    time_high,edx
      }

    5.3 FT_GetTickCount
      GetTickCount函数检测时间间隔简单且常用。直接调用即可。具体可查MSDN。

    5.4 FT_SharedUserData_TickCount
      直接调用GetTickCount函数来检测时间间隔的方法,虽然简单却容易被发现。而使用GetTickCount的内部实现代码,直接读取SharedUserData数据结构里的数据的方法,更隐蔽些。下面的代码是直接从GetTickCount里扣出来的,其应该是在位于0x7FFE0000地址处的SharedUserData数据接口里面直接取数据,不过这个代码应该有跨平台的问题,我这里没有处理。大家可以完善下。
      DWORD tmpD;
      __asm
      {
        mov     edx, 0x7FFE0000
        mov     eax, dword ptr [edx]
        mul     dword ptr [edx+4]
        shrd    eax, edx, 0x18
        mov    tmpD,eax
      }
      return tmpD;

    5.5 FT_timeGetTime
      使用winmm里的timeGetTime的方法也可以用来检测时间间隔。直接调用这个函数即可。具体可查MSDN。

    5.6 FT_QueryPerformanceCounter
      这是一种高精度时间计数器的方法,它的检测刻度最小,更精确。
      if(QueryPerformanceCounter(lpPerformanceCount))
            return lpPerformanceCount->QuadPart;
      else
         return 0;

    5.7 FT_F1_IceBreakpoint
      在<<Windows anti-debug reference>>中有讲述这个反跟踪技巧。这个所谓的"Ice breakpoint" 是Intel 未公开的指令之一, 机器码为0xF1.执行这个指令将产生单步异常.,如果程序已经被跟踪, 调试器将会以为它是通过设置标志寄存器中的单步标志位生成的正常异常. 相关的异常处理器将不会被执行到.下面是我的实现代码:

    __asm
      {
      push   offset eh_f1; set exception handler
         push  dword ptr fs:[0h]
         mov    dword ptr fs:[0h],esp  
         xor   eax,eax;reset EAX invoke int3
         _emit 0xf1
         pop    dword ptr fs:[0h];restore exception handler
         add    esp,4
      test  eax,eax
      jz    rt_label_f1
      jmp    rf_label_f1

    eh_f1:
         mov eax,dword ptr[esp+0xc]
      mov    dword ptr [eax+0xb0],0x00000001;set flag (ContextRecord.EAX)
         inc dword ptr [eax+0xb8]
         xor eax,eax
         retn
    rt_label_f1:
      inc    eax
      mov    esp,ebp
         pop    ebp
         retn
    rf_label_f1:
      xor    eax,eax
      mov    esp,ebp
         pop    ebp
         retn
      }


    5.8 FT_Prefetch_queue_nop1
    这个反调试是在<<ANTI-UNPACKER TRICKS>>中给出的,它主要是基于REP指令,通过REP指令来修改自身代码,在非调试态下,计算机会将该指令完整取过来,因此可以正确的执行REP这个指令,将自身代码完整修改,但在调试态下,则在修改自身的时候立即跳出。
    这个反跟踪技巧个人觉得用处不大,因为只有在REP指令上使用F7单步时,才会触发这个反跟踪,而我个人在碰到REP时,通常都是F8步过。下面是利用这个CPU预取指令的特性的实现反跟踪的一种方法,正常情况下,REP指令会修改其后的跳转指令,进入正常的程序流程,但在调试态下,其无法完成对其后代码的修改,从而实现反调试。

       DWORD oldProtect;
       DWORD tmpProtect;
       __asm
       {
        lea eax,dword ptr[oldProtect]
        push eax
        push 0x40
        push 0x10
        push offset label3;
        call dword ptr [VirtualProtect];
        nop
    label3:
        mov al,0x90
        push 0x10
        pop ecx
        mov edi,offset label3
        rep stosb
        jmp rt_label
        nop
        nop
        nop
        nop
        nop
    rf_label:
        ;write back
        mov dword ptr[label3],0x106a90b0
        mov dword ptr[label3+0x4],0x205CBF59
        mov dword ptr[label3+0x8],0xAAF30040
        mov dword ptr[label3+0xc],0x90909090
        mov dword ptr[label3+0x6],offset label3
        lea eax, dword ptr[tmpProtect];
        ;restore protect
        push eax
        push oldProtect
        push 0x10
        push offset label3;
        call dword ptr [VirtualProtect];

        xor eax,eax
        mov esp,ebp
        pop ebp
        retn
    rt_label:
        ;write back
        mov dword ptr[label3],0x106a90b0
        mov dword ptr[label3+0x4],0x205CBF59
        mov dword ptr[label3+0x8],0xAAF30040
        mov dword ptr[label3+0xc],0x90909090
        mov dword ptr[label3+0x6],offset label3
        lea eax, dword ptr[tmpProtect];
        ;restore protect
        push eax
        push oldProtect
        push 0x10
        push offset label3;
        call dword ptr [VirtualProtect];

        xor eax,eax
        inc eax
        mov esp,ebp
        pop ebp
        retn
      }


    5.9 FT_Prefetch_queue_nop2
      与5.8节类似,这是根据CPU预取指令的这个特性实现的另一种反跟踪技巧。原理是通过检测REP指令后的ECX值,来判断REP指令是否被完整执行。在正常情况下,REP指令完整执行后,ECX值应为0;但在调试态下,由于REP指令没有完整执行,ECX值为非0值。通过检测ECX值,实现反跟踪。
      DWORD oldProtect;
      DWORD tmpProtect;
      __asm
      {
        lea eax,dword ptr[oldProtect]
        push eax
        push 0x40
        push 0x10
        push offset label3;
        call dword ptr [VirtualProtect];
        mov ecx,0
    label3:
        mov al,0x90
        push 0x10
        pop ecx
        mov edi,offset label3
        rep stosb
        nop
        nop
        nop
        nop
        nop
        nop
        push ecx
        ;write back
        mov dword ptr[label3],0x106a90b0
        mov dword ptr[label3+0x4],0x201CBF59
        mov dword ptr[label3+0x8],0xAAF30040
        mov dword ptr[label3+0xc],0x90909090
        mov dword ptr[label3+0x6],offset label3
        lea eax, dword ptr[tmpProtect];
        ;restore protect
        push eax
        push oldProtect
        push 0x10
        push offset label3;
        call dword ptr [VirtualProtect];
        pop ecx

        test ecx,ecx
        jne rt_label
      }
    rf_label:
      return false;
    rt_label:
      return true;

    六、  检测-补丁(FP_)
    这部分内容也较少,方法当然也有很多种,原理都差不多,我只选了下面三种。这几种方法通常在一些壳中较常用,用于检验文件是否被脱壳或被恶意修改。
    函数列表如下:
    //find Patch
    bool FP_Check_FileSize(DWORD Size);
    bool FP_Check_FileHashValue_CRC(DWORD CRCVALUE_origin);
    bool FP_Check_FileHashValue_MD5(DWORD MD5VALUE_origin);

    6.1 FP_Check_FileSize(DWORD Size)
      通过检验文件自身的大小的方法,是一种比较简单的文件校验方法,通常如果被脱壳,或被恶意修改,就可能影响到文件的大小。我用下面的代码实现。需注意的是,文件的大小要先编译一次,将首次编译得到的数值写入代码,再重新编译完成。
      DWORD Current_Size;
      TCHAR szPath[MAX_PATH];
      HANDLE hFile;

      if( !GetModuleFileName( NULL,szPath, MAX_PATH ) )
            return FALSE;

      hFile = CreateFile(szPath,
        GENERIC_READ ,
        FILE_SHARE_READ,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
      if (hFile == INVALID_HANDLE_VALUE)
        return false;
      Current_Size=GetFileSize(hFile,NULL);
      CloseHandle(hFile);
      if(Current_Size!=Size)
        return true;
      return false;

    6.2 FP_Check_FileHashValue_CRC
      检验文件的CRC数值,是比较常用的文件校验方法,相信很多人都碰到过了,我是在《软件加解密技术》中了解到的。需注意的是文件原始CRC值的获得,及其放置位置,代码编写完成后,通常先运行一遍程序,使用调试工具获得计算得到的数值,在将这个数值写入文件中,通常这个数值不参加校验,可以放置在文件的尾部作为附加数据,也可以放在PE头中不用的域中。
      下面的代码只是个演示,没有保存CRC的真实数值,也没有单独存放。

      DWORD fileSize,NumberOfBytesRW;
      DWORD CRCVALUE_current;
      TCHAR szFileName[MAX_PATH];
      TCHAR  *pBuffer ;
      GetModuleFileName(NULL,szFileName,MAX_PATH);
      HANDLE hFile = CreateFile(
        szFileName,
        GENERIC_READ,
        FILE_SHARE_READ,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
      if (hFile != INVALID_HANDLE_VALUE )
      {
        fileSize = GetFileSize(hFile,NULL);
        if (fileSize == 0xFFFFFFFF) return false;
        pBuffer = new TCHAR [fileSize];  
        ReadFile(hFile,pBuffer, fileSize, &NumberOfBytesRW, NULL);
        CloseHandle(hFile);
      }
      CRCVALUE_current=CRC32((BYTE *)pBuffer,fileSize);
      if(CRCVALUE_origin!=CRCVALUE_current)
        return true;
      return false;

    6.3 FP_Check_FileHashValue_MD5
    与6.2节的原理相同,只是计算的是文件的MD5数值。仍要注意6.2节中同样的MD5真实数值的获得和存放问题。
    您需要登录后才可以回帖 登录 | 注册

    本版积分规则

    免责声明

    本站中所有被研究的素材与信息全部来源于互联网,版权争议与本站无关。本站所发布的任何软件编程开发或软件的逆向分析文章、逆向分析视频、补丁、注册机和注册信息,仅限用于学习和研究软件安全的目的。全体用户必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。学习编程开发技术或逆向分析技术是为了更好的完善软件可能存在的不安全因素,提升软件安全意识。所以您如果喜欢某程序,请购买注册正版软件,获得正版优质服务!不得将上述内容私自传播、销售或者用于商业用途!否则,一切后果请用户自负!

    QQ|Archiver|手机版|小黑屋|联系我们|宝峰科技 ( 滇公网安备 53050202000040号 | 滇ICP备09007156号-2 )

    Copyright © 2001-2023 Discuz! Team. GMT+8, 2025-5-7 23:40 , File On Powered by Discuz! X3.49

    快速回复 返回顶部 返回列表