admin 发表于 2010-9-28 20:33:49

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

四、检测-断点(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:
    mov    dword ptr fs:,esp
    xor    eax,eax;reset EAX invoke int3
    int    1h
    pop    dword ptr fs:;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

    ;check if Debug Registers Context.Dr0-Dr3 is not zero
    cmp   dword ptr ,0
    jne   hardware_bp_found
    cmp   dword ptr ,0
    jne   hardware_bp_found
    cmp   dword ptr ,0
    jne   hardware_bp_found
    cmp   dword ptr ,0
    jne   hardware_bp_found
    jmp   exception_ret

hardware_bp_found:
    ;set Context.EAX to signal breakpoint found
    mov   dword ptr ,0xFFFFFFFF
exception_ret:
    ;set Context.EIP upon return
    inc       dword ptr ;set ContextRecord.EIP
    inc       dword ptr ;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;

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 ;   //// 申请内存,也可用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;
BYTE tmpB;
DWORD Value=0;
dwThrdParam=* ((DWORD *)lpParam);
   dwThrdParam=* ((DWORD *)lpParam+1);
      dwThrdParam=* ((DWORD *)lpParam+2);
BYTE *addr_begin=(BYTE *)dwThrdParam;
BYTE *addr_end=(BYTE *)dwThrdParam;
DWORD sumValue=dwThrdParam;
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;
dwThrdParam=(DWORD)addr_begin;
dwThrdParam=(DWORD)addr_end;
dwThrdParam=sumValue;
    HANDLE hThread;

    hThread = CreateThread(
      NULL,                        // default security attributes
      0,                           // use default stack size
      CheckSum_ThreadFunc,         // thread function
      &dwThrdParam,                // 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
    popss    //反跟踪指令序列
    pushf    //反跟踪指令序列
    ;junk
    pop eax    //反跟踪指令序列
}
Pushf将会被执行,同时调试器无法设置压进堆栈的陷阱标志,应用程序通过检测陷阱标志就可以判断处是否被跟踪调试。

__asm
{
    push ebp
    mov ebp,esp
    push ss    //反跟踪指令序列
    ;junk
    popss    //反跟踪指令序列
    pushf    //反跟踪指令序列
    ;junk
    pop eax    //反跟踪指令序列
    andeax,0x00000100
    jnzrt_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
    mul   dword ptr
    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
   pushdword ptr fs:
   mov    dword ptr fs:,esp
   xor   eax,eax;reset EAX invoke int3
   _emit 0xf1
   pop    dword ptr fs:;restore exception handler
   add    esp,4
testeax,eax
jz    rt_label_f1
jmp    rf_label_f1

eh_f1:
   mov eax,dword ptr
mov    dword ptr ,0x00000001;set flag (ContextRecord.EAX)
   inc dword ptr
   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
    push eax
    push 0x40
    push 0x10
    push offset label3;
    call dword ptr ;
    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,0x106a90b0
    mov dword ptr,0x205CBF59
    mov dword ptr,0xAAF30040
    mov dword ptr,0x90909090
    mov dword ptr,offset label3
    lea eax, dword ptr;
    ;restore protect
    push eax
    push oldProtect
    push 0x10
    push offset label3;
    call dword ptr ;

    xor eax,eax
    mov esp,ebp
    pop ebp
    retn
rt_label:
    ;write back
    mov dword ptr,0x106a90b0
    mov dword ptr,0x205CBF59
    mov dword ptr,0xAAF30040
    mov dword ptr,0x90909090
    mov dword ptr,offset label3
    lea eax, dword ptr;
    ;restore protect
    push eax
    push oldProtect
    push 0x10
    push offset label3;
    call dword ptr ;

    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
    push eax
    push 0x40
    push 0x10
    push offset label3;
    call dword ptr ;
    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,0x106a90b0
    mov dword ptr,0x201CBF59
    mov dword ptr,0xAAF30040
    mov dword ptr,0x90909090
    mov dword ptr,offset label3
    lea eax, dword ptr;
    ;restore protect
    push eax
    push oldProtect
    push 0x10
    push offset label3;
    call dword ptr ;
    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;
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;
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 ;
    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真实数值的获得和存放问题。
页: [1]
查看完整版本: 反调试技巧总结-原理和实现(四、五、六)