一篇文章带你了解反调试技术

1.反调试技术:

         恶意代码编写者利用反调试技术来判断恶意代码是否被调试,以此来阻止调试器分析,或者使调试器失效。注意,也可以使用反调试来保护我们的加密代码,不一定是干扰或者破坏调试器,
参考资料:
1.逆向工程核心原理
2.恶意代码分析实战

2.检测调试器

2-1:利用windows数据结构

         PEB结构包含关于进程的诸多信息,其中也包含调试信息。关于进程是否处于调试状态与以下几个成员有关。

1
2
3
4
+0x02 BeingDebugged ;Uchar
+0x0c Ldr ;PEB_LDR_DATA
+0x18 ProcessHeap ;Void
+0x68 NtGlobalFlag ;uint48

         如何过去PEB的结构呢?FS寄存器指向的是PEB结构,PEB.ProcessEvnivornmentBlock(+0x30)指向的是PEB的结构体。由此可以来访问PEB结构了。

1
2
3
4
mov eax,fs:[30h]
或者:
mov eax,fs:[18h] ;获取TEB的地址
mov eax,[eax+30h] ;获取PEB的地址

         关于BeingDebugged是利用这个成员是否为0来判断进程是否处于调试状态。Ldr成员指向的PEB_LDR_DATA数据区域,如果进程处于调试状态,这个区域填充着特殊字符(0xfeeeeeee)
         ProcessHeap[4]是一个数组,关于反调试的是Flag(0xC或者0x40)和ForseFlag(0x10或者0x44)这两个成员。
         调试中的进程和正常运行的进程时不同的,凭借NtGlobalFlag这个未公开的成员可以判断进程是否处于调试状态,如果该成员的值是0x70得话,那么这个进程处于调试状态。

2-2:windowsAPI

         一下这两个函数是检测IsDebugged这个成员的。

1
2
IsDebuggedPresent()
CheckRemoteDebuggedPresent()

         利用NtQueryInformationProcess()来进行反调试。

1
2
3
4
5
6
7
NTSTATUS WINAPI NtQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);

  • 如果第二个参数是ProcessDebugPort的话,通过检查该参数返回的值来判断进程是否被调试,如果返回0,说明没有被调试,否是说明进程正在被调试。

         OutPutDebugString()函数用于返回调试信息,如果进程没有被调试,如果调用此函数,该函数会返回失败。利用如下代码

1
2
3
4
5
6
SetLastError(1234);
OutPutDebugString("Test for Debug");//如果该函数调用成功,不返回错误代码,说明处于被调试
if(GetLastError()==1234) //错误代码没有被更改,说明函数OutPutDebugString调用正常,说明进程处于调试状态
ExitProcess();
else
RunProcess();

         利用NtQuerySystemInformation()来判断系统是否处于调试状态。

1
2
3
4
5
6
NTSTATUS WINAPI NtQuerySystemInformation(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_Inout_ PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);

  • 如果进程处于调试状态,在第一个参数传入SystemKernelDebuggerInformation(0x23)后,返回第二个参数指向的system_kernel_Debug_Information的结构中的Debugedable设置为1.

         利用NtQueryObject来判断,当某个调试器在调试进程的时候,系统会创建一个内核调试对象,通过检查内核信息链表来查找是否存在内核调试对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NTSTATUS NtQueryObject(
_In_opt_手柄手柄,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_Out_opt_ PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength,
_Out_opt_ PULONG ReturnLength
);
typedef enum _OBJECT_INFORMATION_CLASS //未公开
{
ObjectBasicInformation,
ObjectNameInformation,
ObjectTypeInformation,
ObjectTypesInformation,
ObjectHandleFlagInformation
} OBJECT_INFORMATION_CLASS;

  • 在第二个参数传递ObjectBasicInformation共枚举类型中的ObjectAllTypesInformation(3号),通过返回在第三个参数中的指针对应的ObjectInformation来查找是否存在调试内核对象。
  • 破解:修改传入参数为ObjectBasicInformtion(0号)来避免反调试
             利用ZwSetInformationThread()来隐藏被调试进程以达到反调试的效果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    NTSTATUS ZwSetInformationThread(
    _In_ HANDLE ThreadHandle,
    _In_ THREADINFOCLASS ThreadInformationClass,
    _In_ PVOID ThreadInformation,
    _In_ ULONG ThreadInformationLength
    );
    //ntpsapi.h
    typedef enum _THREADINFOCLASS {
    ThreadBasicInformation,
    ThreadTimes,
    ThreadPriority,
    ThreadBasePriority,
    ThreadAffinityMask,
    ThreadImpersonationToken,
    ThreadDescriptorTableEntry,
    ThreadEnableAlignmentFaultFixup,
    ThreadEventPair_Reusable,
    ThreadQuerySetWin32StartAddress,
    ThreadZeroTlsCell,
    ThreadPerformanceCount,
    ThreadAmILastThread,
    ThreadIdealProcessor,
    ThreadPriorityBoost,
    ThreadSetTlsArrayAddress,
    ThreadIsIoPending,
    ThreadHideFromDebugger,//这个就是用来将线程对调试器隐藏
    ThreadBreakOnTermination,
    ThreadSwitchLegacyState,
    ThreadIsTerminated,
    MaxThreadInfoClass
    } THREADINFOCLASS;
    // end_ntddk end_ntifs
  • 在第二个参数传入ThreadHideFromDebugger,如果进程处于调试状态,该API函数会使调试器和进程终止.

  • 应对方法:修改传入参数,为0即可。

2-3:检测系统痕迹

         通过扫描调试器在系统中残留的痕迹来判断进程是否处于调试状态。可以选择注册表HKLM\SOFTWARE\MICROSOTF\WINDOWS NT\CURRENTVERSION\AeDebug或者查看窗口是否存有调试器名称(FindWindow)或者使进程等方面来判断。

3.TIME CHECK(时钟检测)

         逐行跟踪代码比正常运行代码所花费的时间多得多。通过比较计算运行代码之间的时间差来判断进程是否处于调试状态。一般的,利用windows提供的时间和CPU的计数器(TSC)来测量时间差。TSC基于cpu内部的计数器,所以计算精度会更高。利用以下伪代码实现

1
2
3
4
5
6
7
Time1=GetTime();
RunCode();
Time2=GetTime();
if(Time2-Time2>0xfffffffff)
printf("Debugged");
else
printf("No Debugged");

3-1:rdtsc指令

         CPU中存在一个名为TSC(时间戳计数器)的64为寄存器。RDTSC是一个读取该寄存器的指令,得到的值高32位存储在EDX中,低32位保存在EAX中。

3-2:利用GetTickCount或者QueryPerformanceCounter来判断

         利用这两个函数方法和伪代码一样。if(Time2-Time2>0xfffffffff)语句中0xfffffffff是一个处于0xffff到0xffffffff中的一个任意值,因为单步一个指令所用的时间必定大于0xffffffff

4.干扰调试器

4-1:陷阱标志SEH反调试

         CPU第9个标志位TF为陷进标志位,当TF位1的时候,CPU进入单步模式,没执行一条指令,就会触发一个单步异常。如果进程处于调试状态,触发异常后,异常会交给调试器,此时不会执行SEH处理函数。如过程序处于非调试状态,则会触发SEH异常处理。如图,在401011处安装一个SEH处理函数,401024用于置陷进标志位,因为无法直接修改寄存器,先用栈保存寄存器数据,然后修改第8位比特数。利用nop触发单步异常,如果进程处于调试状态就会触发这个异常,进程转入40102F和401034,如果进程没有处于调试状态,就会转入异常处理函数,然后结束异常处理。【关于陷进标志,请看逆向工程核心原理第566页】

         解决方法:先忽略异常,在SEH处理函数和处理完毕后下断点,运行即可。

4-2:Int 3结合SEH反调试

         Int 3是软件中断,操作码是0xcc,一般反调试的手法都是结合SEH来进行的,如果进程处于调试状态,触发异常后,异常会交给调试器,此时不会执行SEH处理函数,一般这个地方都是设置一个趋于死亡的函数跳转如:mov eax,1;jmp eax。如果程序正常,则异常交给SEH处理函数,通过SEH处理函数,最后回归正常的代码。如图所示:

  • 在调试器选项中勾选忽略所有异常(这里是断点异常)
  • 在SEH处理函数处(40102A)和结束异常处理后(401044)设置断点。
  • 运行程序即可。(部分环境下使用单步会使得调试奔溃,建议使用F9运行)

    4-3:Int 2D

             Int 2D是内核模式用于触发断点的指令,在用户模式也是可以执行的,但是调试器不会触发此异常,只是忽略,如果遇到Int 2D指令,调试器无法执行单步指令,知道遇到断点才能中断。如图遇到int 2d断点异常。
  • 使用隐藏OD插件吧,我调了很久还是过不了.

    4-4:TLS反调试

             利用TLS(线程本地存储技术)可以让代码优先于程序制动的OEP入口点运行,根据恶意代码查杀实战的说法可以使得一些敏感代码处于TLS回调函数中优先执行,这样就不会被调试,我想采取更加积极主动的反调试措施,在TLS回调函数中直接插入反调试代码进行反调试(书上只是隐藏关键代码)。
             解决方法:利用TLS技术,程序会产生TLS节区,如果存在这个节区就要怀疑程序使用了反调试,我们应该设置调试器,中断在system Break-Poit让od在tls回调函数之前暂停。关于TLS反调试技术请见浅谈TLS反调试技术。

    5.调试器特征检测

    5-1:0xcc检测

             0xCC是软件断点的机器码,od在下断点的时候是否0xCC来代替原指令,但是又偷梁换柱的显示的是原指令,这样起到了中断的效果。但是,单纯的检测0xCC指令是不对的,因为很多其他指令也是使用0xCC机器码(移位,立即数)。

    5-2:API检测

             我们如果要调试一个程序的局部功能,最简单的方法是对API函数下个断点,然后执行到返回(或者根据堆栈查看返回地址)所以这种API下断也成为恶意代码编写者设置反调试的重点区域。在机器码层面上,调试器如果对某个API下断,其首个机器码变成0xcc(但是没显示出来)通过检测API的首个机器码来判断程序是否被调试。
             解决方法:不对API函数的第一条指令下断点,或者使用硬件断点(0xcc属于软件断点)

    5-3:求校验和

             微小的更改都可以使得代码缓冲区校验和发生改变,通过比较原始校验和和新校验和的值来判断是否进行调试。

    6.利用调试器的漏洞

             od1.0版本存在两个PE结构处理的漏洞,所以可以凭此来反调试,但是在2.0已经修复了该漏洞。