第52章 动态反调试技术

本文最后更新于:2022年5月27日 下午

第52章动态反调试技术

本章讲解动态组别中的反调试技术。运用动态反调试技术可以不断阻止对程序代码的跟踪调试。与静态反调试技术相比,动态反调试技术难度更高,破解难度也更大。

52.1动态反调试技术的目的

反调试技术的目的就是隐藏和保护程序代码与数据,使之无法进行逆向分析。PE保护器中一般会大量应用动态反调试技术,以保护源程序的核心算法。在调试器中调试运行(应用了动态反调试技术的).程序时,动态反调试技术就会干扰调试器,使之无法正常跟踪查找源程序的核心代码(OEP)。

一名优秀的代码逆向分析入员能够克服各种困难,顺利完成逆向分析任务。但是分析应用了动态反调试技术的程序时,仍然会比较费力,且分析时间也会大大增加

52.2异常

异常(Exception)常用于反调试技术。正常运行的进程发生异常时,在SEH机制的作用下, OS会接收异常,然后调用进程中注册的SEH处理。但是,若进程(被调试者)在调试运行中发生异常,调试器就会接收处理。利用该特征可判断进程是正常运行还是调试运行,然后根据不同结果执行不同操作,这就是反调试技术的原理。

关于利用SEH的反调试工作原理及破解之法请参考第48章。

52.2.1 SEH

代码52-1列出了Windows操作系统中的一些典型异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
STILL_ACTIVE						(0x00000103L)
EXCEPTION_ACCESS_VIOLATION (0xC0000005L)
EXCEPTION_DATATYPE_MISALIGNMENT (0x80000002L)
EXCEPTION_BREAKPOINT (0x80000003L)
EXCEPTION_SINGLE_STEP (0x80000004L)
EXCEPTION_ARRAY_BOUNDS_EXCEEDED (0xC000008CL)
EXCEPTION_FLT_DENORMAL_OPERAND (0xC000008DL)
EXCEPTION_FLT_DIVIDE_BY_ZERO (0xC000008EL)
EXCEPTION_FLT_INEXACT_RESULT (0xC000008FL)
EXCEPTION_FLT_INVALID_OPERATION (0xC0000090L)
EXCEPTION_FLT_OVERFLOW (0xC0000091L)
EXCEPTION_FLT_STACK_CHECK (0xC0000092L)
EXCEPTION_FLT_UNDERFLOW (0xC0000093L)
EXCEPTION_INT_DIVIDE_BY_ZERO (0xC0000094L)
EXCEPTION_INT_OVERFLOW (0xC0000095L)
EXCEPTION_PRIV_INSTRUCTION (0xC0000096L)
EXCEPTION_IN_PAGE_ERROR (0xC0000006L)
EXCEPTION_ILLEGAL_INSTRUCTION (0xC000001DL)
EXCEPTION_NONCONTINUABLE_EXCEPTION (0xC0000025L)
EXCEPTION_STACK_OVERFLOW (0xC00000FDL)
EXCEPTION_INVALID_DISPOSITION (0xC0000026L)
EXCEPTION_GUARD_PAGE (0x80000001L)
EXCEPTION_INVALID_HANDLE (0xC0000008L)
CONTROL_C_EXIT (0xC000013AL)

EXCEPTION_BREAKPOINT

Windows操作系统中最具代表性的异常是断点异常。Breakpoint指令触发异常时,若程序处于正常运行状态,则自动调用已经注册过的SEH;若程序处于调试运行状态,则系统会立刻停止运行程序,并将控制权转给调试器。一般而言,异常处理器中都含有修改EIP值的代码。修改调试器选项可以把处在调试中的进程产生的相关异常转给操作系统,自动调用SEH处理。但即便如此,在异常处理器中适当应用静态反调试技术,也能够轻松判断进程是否处于调试状态。此外,EIP值在异常处理器内部如何变化也不得而知,这意味着,必须跟踪进入异常处理器才能继续调试。

若程序中仅应用了几个基于SEH的反调试技法,则很容易破解。但若应用了数十乃至数百个这样的反调试技法,调试速度就会大大降低,失误的风险也会大增。

  • 练习

先在OllyDbg调试器中打开示例程序(DynAD_SEH.exe),然后在401000地址处设置好断点并运行,如图52-1所示。

图52-1中的代码是基于INT3异常的反调试代码,代码执行流如图52-2所示。

下面逐个分析代码执行流各阶段的代码(请各位边调试边跟着学习)。

#1.安装SEH

首先在以下代码中安装SEH (40102C)。

1
2
3
00401011	PUSH 40102C					; SEH
00401016 PUSH DWORD PTR FS:[0]
0040101D MOV DWORD PTR FS:[0],ESP

#2.发生INT3异常

以下代码用来触发INT3异常。

1
00401024 INT3

#3-1.调试运行——终止进程

若进程处于调试运行状态,则需要由调试器(此处为OllyDbg)处理异常。INT3指令是CPU中断(Interrupt)命令,在用户模式的调试器中什么也不做,继续执行其下的命令。

1
2
00401025	MOV EAX,-1		; -1 (0xFFFFFFFF)
0040102A JMP EAX

因进程处于调试中,故跳转到非法地址处(FFFFFFFF ),无法继续调试。

以上练习示例中,进程处于调试状态时会直接终止运行,逆向分析入员能够借此轻松把握在什么地方遭受了反调试。但是有些保护器会将代码执行跳转到垃圾代码。逆向分析入员跟踪这些冗长的垃圾代码时会精疲力尽,而且进程终止时,也很难把握在哪里遭受了反调试技术的“狙击”。这是一种非常狡猾的伎俩,很容易让入陷入迷途。

#3-2.正常运行(非调试运行)——运行SEH

若进程为非调试运行,那么执行到INT3指令时就会调用执行前面已经注册过的SEH。

1
2
3
4
5
0040102C	MOV EAX,DWORD PTR SS:[ESP+C]
00401031 MOV EBX,401040
00401036 MOV DWORD PTR DS:[EAX+B8],EBX
0040103D XOR EAX,EAX
0040103F RETN

SS:[ESP+C]CONTEXT *pContext结构体的指针,而CONTEXT *pContext结构体正是SEH的第三个参数,它是一个发生异常的线程CONTEXT结构体。DS:[EAX+B8]指向pContext→Eip成员,所以401036地址处的MOV指令用来将该结构体的EIP值修改为401040。然后,异常处理器返回0 (ExceptionContinueExecution)。接下来,发生异常的线程再次从修改后的EIP地址处(401040)开始运行。SEH函数定义如下,请参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
EXCEPTION_DISPOSITION ExceptHandler
(
EXCEPTION_RECORD *pRecord,
EXCEPTION_REGISTRATION_RECORD *pFrame,
CONTEXT *pContext,
PVOID pValue
);


typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution = 0, // 继续执行代码
ExceptionContinueSearch = 1, // 运行下一个异常处理器
ExceptionNestedException = 2, // 在OS内部使用
ExceptionCollidedUnwind = 3 // 在OS内部使用
} EXCEPTION_DISPOSITION;

以下是CONTEXT结构体的定义(已标出各成员的偏移量)。

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
33
34
35
typedef struct _CONTEXT {

DWORD ContextFlags;

DWORD Dr0; // 04h
DWORD Dr1; // 08h
DWORD Dr2; // 0Ch
DWORD Dr3; // 10h
DWORD Dr6; // 14h
DWORD Dr7; // 18h

FLOATING_SAVE_AREA FloatSave;

DWORD SegGs; // 88h
DWORD SegFs; // 90h
DWORD SegEs; // 94h
DWORD SegDs; // 98h

DWORD Edi; // 9Ch
DWORD Esi; // A0h
DWORD Ebx; // A4h
DWORD Edx; // A8h
DWORD Ecx; // ACh
DWORD Eax; // B0h

DWORD Ebp; // B4h
DWORD Eip; // B8h
DWORD SegCs; // BCh // MUST BE SANITIZED
DWORD EFlags; // C0h // MUST BE SANITIZED
DWORD Esp; // C4h
DWORD SegSs; // C8h

BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

若不处理异常且EIP值保持不变,返回ExceptionContinueExecution时会再次执行401024地址处的INT3指令,同时再次调用执行40102C地址处的SEH,最终陷入无限循环,造成栈溢出,导致进程终止。

#4.删除SEH

1
2
00401040	POP DWORD PTR FS:[0]
00401047 ADD ESP,4

进程正常运行时,#1中注册的SEH(40102C)就会被删除(若进程处于调试运行,#3-1中的代码就会造成进程非正常终止)。

  • 破解之法

如图52-3所示,在Debugging options对话框的Exceptions选项卡中,复选“INT3 breaks”后,调试器就会忽略被调试进程中发生的INT3异常,而由自身的SEH处理。

如图52-3设置好调试选项,进程调试过程中遇到INT3指令时,调试器不会停下来,而会自动调用执行被调试进程的SEH (与正常运行一样)。请各位自行测试,先分别在SEH(40102C)与代码正常运行处(401040)设置断点,然后按F9键运行程序。

有时,在某些环境(OS、调试器插件Bug等)中使用StepInto(F7)或StepOver(F8)命令跟踪INT3指令会导致调试器非正常终止。遇到这种情况时,请按照以上说明设置好断点后再按F9运行程序。

52.2.2 SetUnhandledExceptionFilter()

进程中发生异常时,若SEH未处理或注册的SEH根本不存在,会发生什么呢?此时会调用执行系统的kernel32!UnhandledExceptionFilter() API。该函数内部会运行系统的最后一个异常处理器(名为Top Level Exception Filter或Last Exception Filter)。系统最后的异常处理器通常会弹出错误消息框,然后终止进程运行,如图52-4所示。

值得注意的是,kernel32!UnhandledExceptionFilter()内部调用了ntdll!NtQuerylnformationProcess(ProcessDebugPort)API (静态反调试技术),以判断是否正在调试进程。若进程正常运行(非调试运行),则运行系统最后的异常处理器;若进程处于调试中,则将异常派送给凋试器。通过kernel32!SetUnhandledExceptionFilter()API可以修改系统最后的异常处理器(Top Level Exception Filter),函数原型如下:

1
2
3
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
[in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

调用该函数修改系统最后异常处理器时,只要将新的Top Level Exception Filter函数地址传递给函数的IpTopLevelExceptionFilter参数即可(返回值为上一个Last Exception Filter函数地址)。TopLevel Exception Filter函数定义如下:

1
2
3
4
5
6
7
8
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

LONG TopLevelExceptionFilter(
PEXCEPTION_POINTERS pExcept
);

基于异常的反调试技术中,通常先特意触发异常,然后在新注册的Last Exception Filter内部判断进程正常运行还是调试运行,并根据判断结果修改EIP值。系统在此过程中自行判断调试与否。这种反调试技术融合了静态与动态方法,下面通过练习示例进一步学习。

  • 练习

首先在OllyDbg调试器中打开示例程序(DynAD_SUEF.exe),在401030地址处设置断点后运行(参考图52-5)。

下面边调试代码边了解程序执行流及反调试工作原理。首先调用printf()函数输出字符串,代码如下所示:

1
2
3
4
5
00401030     55             PUSH EBP
00401031 8BEC MOV EBP,ESP
00401033 68 A0994000 PUSH DynAD_SU.004099A0 ; ASCII "SEH : SetUnhandledExceptionFilter()
00401038 E8 4A000000 CALL DynAD_SU.00401087
0040103D 83C4 04 ADD ESP,4

然后,调用SetUnhandledExceptionFilter()来注册新的Top Level Exception Filter(新的Exception Filter函数中包含异常处理代码)。

1
2
3
00401040     68 00104000    PUSH DynAD_SU.00401000			; pTopLevelFilter = DynAD_SU.00401000
00401045 FF15 00804000 CALL DWORD PTR DS:[<&KERNEL32.SetUnhandledExceptionFilter>
0040104B A3 3CCB4000 MOV DWORD PTR DS:[40CB3C],EAX ; 保存Old Filter的地址

在Top Level Exception Filter(401000)与Kernel32!UnhandledExceptionFilter() API设置好断点。位于401045地址处的CALL指令用来将401000地址处的函数注册为Top Level Exception Filter。‘然后强制触发异常,代码如下所示:

1
2
00401050     33C0           XOR EAX,EAX
00401052 8900 MOV DWORD PTR DS:[EAX],EAX

若执行401052地址处的指令,程序将尝试向尚未定义的进程虚拟内存地址(0)写入值,这会引发无效的内存非法访问异常。接着,程序会在设有断点的Kernel32!UnhandledExceptionFilter()API内部自动暂停(因异常未处理,故系统要运行它),如图52-6所示。

跟着书本调试到这里的时候发现图52-6和Window 7 展示出来的有很大不同,和XP的一致。看起来微软对这个API进行了一些修改。

请注意7C863EF1地址处的CALL ntdll!NtQueryInformationProcess()API指令,其第二个参数传入的值为ProcessDebugPort (7)。前一章中讲过,它是一种静态反调试技术,用来探测调试器。

也就是Windows官方也使用了一定的反调试手段。

为了继续调试,调用该函数后需要将第三个参数[EBP-124]的值(原值为FFFFFFFF)修改为0 (参考图52-7)。

继续跟踪调试,出现图52-8所示的代码。

7C86402F地址处的CALL EBX指令用来调用前面注册的New Top Level Exception Filter函数(401000)。接下来继续跟踪Exception Filter函数,如图52-9所示。

地址401003代码中的[40CB3C]是Old Top Level Exception Handler地址(在图52-5的代码中备份过)。在401009地址处再次调用SetUnhandledExceptionFilter(),恢复Exception Filter。401015地址处的ADD指令将EIP的值增加了4。由图52-5可知,发生异常的代码地址为401052,将该值加4变为401056。也就是说,返回Exception Filter后,继续从401056地址处执行代码(在401056地址处设置好断点后即可继续调试)。以上示例代码并不复杂,多调试几次就能充分理解其原理。

  • 破解之法

利用SetUnhandledExceptionFilter()API反调试的技术综合运用了静态&动态技术。因此,破解时要先使Kernel32!UnhandledExceptionFilter()(静态技术)内部调用的ntdll!NtQueryInformationProcess()API失效(使用API钩取等技术)。然后调用SetUnhandledExceptionFilter() API跟踪注册的Exception Filter,在正常运行时确定要跳到哪个地址即可。

除上述内容外,还有许多基于异常触发的反调试技术。特别是基于SEH的反调试技术在实际中使用得非常多,希望各位通过大量练习来掌握。

52.3 Timing Check

在调试器中逐行跟踪程序代码比程序正常运行(非调试运行)耗费的时间要多岀很多。Timing Check技术通过计算运行时间的差异来判断进程是否处于被调试状态(参考图52-10)。

基于Timing Check的反调试原理相当简单,破解之法也不难,只要直接操作获取的时间信息或比较时间的语句即可。但实际操作中,该反调试技术通常与其他反调试技术并用,导致反调试的破解过程变得异常困难(特别是这些代码不明显时,破解的难度会更大)。

Timing Check技术也常常用作反模拟技术(Anti-Emulating)。程序在模拟器中运行时,运行速度要比程序正常运行(非模拟器中运行)慢很多,所以Timing Check技术也能用来探测程序是否在模拟器中运行。

52.3.1 时间间隔测量法

测量时间间隔的方法有很多种,常用方法如下所示:

  1. Counter based method

RDTSC

kernel32!QueryPerformanceCounter() / ntdll!NtQueryPerformanceCounter()

kernel32!GetTickCount()

2.
Time based method

timeGetTime()

_ftime()

如代码52-4所示,测量时间间隔的方法大致分为两大类,一类是利用CPU的计数器(Counter),另一类是利用系统的实际时间(Time)。接下来学习基于RDTSC (Read Time Stamp Counter,读取时间戳计数器)的反调试技术。

计数器的准确程度由高到低排列如下:
RDTSC>NtQueryPerformanceCounter()>GetTickCount()

NtQueryPerformanceCounter()与 GetTickCount()使用相同硬件(Performance Counter),但二者准确程度不同(NtQueryPerformanceCounter()准确度更高)。而 RDTSC是CPU内部的计数器,其准确程度最高。基于时间的方法与基于计数器的方法在实现过程上比较类似,原理也差不多。

52.3.2 RDTSC

x86 CPU中存在一个名为TSC (Time Stamp Counter,时间戳计数器)的64位寄存器。CPU对

每个Clock Cycle (时钟周期)计数,然后保存到TSC。RDTSC是一条汇编指令,用来将TSC值读

入EDX:EAX寄存器(TSC大小为64位,其高32位被保存至EDX寄存器,低32位被保存至EAX寄

存器)。

  • 练习:DynAD_RDTSC.exe

为了加深各位对Timing Check反调试技术的认识,下面调试示例程序(DynAD_RDTSC.exe)。 在OllyDbg调试器中打开示例程序,转到401000地址处,如图52-11所示。

下面简单介绍图52-11中的代码流(请各位亲自调试)

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
33
34
35
;第一次执行RDTSC指令-将TSC保存到EDX:EAX (64位)
0040101C RDTSC

;将结果值放入栈
0040101E PUSH EDX
0040101F PUSH EAX

;用于消耗时间的揭环(实际代码相当复杂)
00401020 XOR EAX,EAX
00401022 MOV ECX,3E8
00401027 INC EAX
00401028 LOOPD SHORT 00401027

;第二次执行RDTSC指令
0040102A RDTSC

;第一次求得的TSC从栈中输入EDI:ESI
0040102C POP ESI
0040102D POP EDI

;比较1 - Count的high order bits
0040102E CMP EDX,EDI
00401030 JA SHORT 0040103E

;比较2 - Count的low order bits
;若比特定值(0xFFFFFF)大,则断定处于调试状态
00401032 SUB EAX,ESI
00401034 MOV DWORD PTR SS:[EBP-4],EAX
00401037 CMP EAX,0FFFFFF
0040103C JB SHORT 00401042

;在比较语句作用下进入异常触发代码,进程非正常终止
0040103E XOR EAX,EAX
00401040 MOV DWORD PTR DS: [EAX],EAX ;异常! !! ;忽略比较语句,继续运行
00401042 POPAD

从上述代码可以看出,2次RDTSC指令调用之间存在一定的时间间隔,通过计算时间差值(Delta)来判断进程是否处于调试状态。Delta值不固定,一般在0xFFFF0xFFFFFFFF之间取值。40101C40102A地址间的代码区域中,只要执行1次StepInto(F7)或StepOver(F8)命令,Count的间隔就会大于0xFFFFFFFF。

  • 破解之法

有几种方法可以破解以上反调试技术。

(1)不使用跟踪命令,直接使用RUN命令越过相关代码。 在40102C地址处设置断点后运行。虽然运行速度略慢于正常运行速度,但与代码跟踪相比要快很多。

(2)操作第二个RDTSC的结果值(EDX:EAX)0

操作第二个RDTSC的结果值,使之与第一个结果值相同,从而顺利通过CMP语句。

(3)操纵条件分支指令(CMP/Jcc)。

在调试器中强制修改Flags的值,阻止执行跳转至40103E地址处。大部分Jcc指令会受CF或ZF的影响,只要修改这些标志即可控制Jcc指令。

CF与ZF全为0时,JA指令执行跳转动作。只要将CF与ZF之一的值修改为1,JA指令即失效。继续调试,40103C地址处JB指令会直接跳过异常触发代码(401040)(参考图52-12、图52-13)。

若想学习更多有关Jcc指令分支条件的知识,请前往Intel网站参考用户手册(Intel 64 and IA-32 Architectures Software Developer’s Manual)。

(4)利用内核模式驱动程序使RDTSC指令失效。

利用内核模式驱动程序可以从根本上使基于RDTSC的动态反调试技术失效(其实,Olly Advanced Plugin就釆用了该方法)。

以上练习示例仅用于向各位说明相应的工作原理,所以代码都非常简单。但实际的反调试代码中,RDTSC指令与CMP/Jcc条件分支指令并不醒目,而是巧妙地设置到代码各处,再加上与其他反调试技术(SEH、动态方法)并用,效果非常强大,破解起来也比较困难。

52.4 陷阱标志

陷阱标志指EFLAGS寄存器的第九个(Index 8)比特位,如图52-14所示。

52.4.1 单步执行

TF值设置为1时,CPU将进入单步执行(Single Step)模式。单步执行模式中,CPU执行1条指令后即触发1个EXCEPTION_SINGLE_STEP异常,然后陷阱标志会自动清零(0)。该EXCEPTION_SINGLE_STEP异常可以与SEH技法结合,在反调试技术中用于探测调试器。

  • 练习

下面用个简单的练习来了解“修改陷阱标志进行反调试”的工作原理。首先在OllyDbg调试器中打开示例程序(DynAD_SingleStep.exe),转到401000地址处,如图52-15所示。

下面对重要的程序代码进行说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;注册SEH
00401011 68 36104000 PUSH 00401036
00401016 64:FF35 00000 PUSH DWORD PTR FS:[0]
0040101D 64:8925 00000 MOV DWORD PTR FS:[0],ESP ; Installs SE handler 401036

;通过栈修改EFLAGS寄存器(无法直接修改)
00401024 9C PUSHFD ;将EFLAGS寄存器的值压入栈
00401025 36:810C24 000 OR DWORD PTR SS:[ESP],00000100 ;将TF位设置为1
0040102D 9D POPFD ;将修改后的TF值存入EFLAGS

;执行下列指令后触发EXCEPTION_SINGLE_STEP异常
;1)若为正常运行,则运行前面注册过的SEH (401036)
;2)若为调试运行,则继续执行以下指令
0040102E 90 NOP

;调试运行时继续运行以下代码
0040102F B8 FFFFFFFF MOV EAX,-1
00401034 FFE0 JMP EAX

从上述代码可以看岀,因无法直接修改EFLAGS寄存器的值,故使用PUSHFD/POPFD指令与OR运算指令修改陷阱标志的值。 在OllyDbg调试器中继续运行程序代码到40102E地址处,如图52-16所示。

从寄存器窗口可以看到,EFLAGS寄存器(EFL)的值已经被修改为312,陷阱标志已成功设置为1 (我的系统环境下,EFLAGS的初始值为212)。从现在开始,CPU进入单步执行模式。下面执行40102E地址处的NOP指令(使用StepInto(F7)、StepOver(F8)、Run(F9)中的任意一个)。

如预想的一样,发生了EXCEPTION_SINGLE_STEP异常,如图52-17所示。

这里不知道为什么用Ollydbg2.0的时候POPFD没能更改EFLAGS,使用Ollydbg1.0才成功

观察图52-17中的寄存器窗口可以发现,EFLAGS寄存器(EFL)的值又变为了202。也就是说,单步执行模式下,CPU执行完1条指令后,陷阱标志即被自动清零(0)。这也意味着CPU在发生EXCEPTION_SINGLE_STEP异常后又切换至正常运行模式。如图所示,发生异常时,若程序进程非调试运行,则运行SEH执行正常代码;若程序进程处于调试中,则无法转到SEH,继续执行40102F地址处的指令。在40102F地址处执行StepInto(F7)命令,调试继续进行(请注意陷阱标志已经清零了)。然后执行401034地址处的JMP EAX (0xFFFFFFF)指令,进程非正常终止。程序的运行就像这样被分为正常运行与调试运行。

以上示例代码耍了个“陷阱标志”花招,使进程终止执行。有时,程序中可能会包含大量类似的伪代码来迷惑代码逆向分析入员。他们调试程序时甚至都不会发现自己已经遭受反调试技术的误导,陷入伪代码调试的迷雾。经过相当一段时间的调试后才猛然发现有些不对劲,再回过头去寻找迷途之处可就不容易了。有些程序中存在着很多类似“花招”,从精神和肉体上折磨着代码逆向分析入员,防止他们调试程序。

  • 破解之法

首先,修改OllyDbg调试器选项(忽略EXCEPTION_SINGLE_STEP异常),让被调试者直接处理EXCEPTION_SINGLE_STEP异常,如图52-18所示。

然后,在注册SEH的地址处(401036)设置断点。执行40102E地址处的指令后,调试器就会停在SEH的断点处。在新的EIP地址处再次设置断点,接着运行即可跟踪正常代码(参考图52-19)。

为什么Ollydbg不能直接跟踪新的EIP呢?

52.4.2 INT 2D

INT 2D原为内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。但程序调试运行不会触发异常,只是忽略。这种在正常运行与调试运行中表现出的不同可以很好地应用于反调试技术。下面调试INT 2D指令,了解其几个有趣的特征。

  1. 忽略下条指令的第一个字节

调试模式中执行完INT 2D指令后(StepInto/StepOver),下条指令的第一个字节将被忽略,后一个字节会被识别为新的指令继续执行,如图52-20所示。

图52-20中,40101E地址处的INT 2D指令(CD 2D执行完后,401020地址处的MOV EAX,5DEB指令(B8 EB5D0000)中,第一个字节B8将被忽略(参考图52-21)。

最终,401021地址处的指令被重新解析为2条指令:JMP 401080 (EB5D)、 ADD BYTE PTRDS:[EAX],AL (0000),它们完全不同于原指令MOV EAX,5DEB(B8 EB5D0000)。像这样,基于INT 2D的反调试技术能够形成较强的代码混淆(Obfuscated Code)效果,从而在一定程度上防止代码逆向分析入员调试程序。

改变代码字节顺序(Code Byte Ordering)扰乱程序代码的方法称为代码混淆技术,该技术常用于动态反调试技术。

  1. —直运行到断点处

INT 2D指令的另一特征是,使用StepInto(F7)或StepOver(F8)命令跟踪INT 2D指令时,程序不会停在其下条指令开始的地方,而是一直运行,直到遇到断点,就像使用RUN(F9)命令运行程序一样。

以上只是INT 2D指令在OllyDbg调试中表现出的特征,它在其他调试器中的行为略有不同。在OllyDbg调试中执行INT 2D指令后,程序不会单步暂停,而是一直运行。原因在于,执行完INT 2D指令后,原有的代码字节顺序被打乱了。也就是说,若指令在程序执行过程中改变,则程序不能单步暂停,而是一直执行,可以将其视为一种Bug。所以执行完INT 2D指令后,要想停止跟踪代码,需要事先在相应地址处设置断点。

练习:DynAD_INT 2D.exe

为了帮助各位进一步了解基于INT 2D的反调试技术工作原理,下面做个简单的调试练习(DynAD_INT 2D.exe)。首先在调试器中打开示例程序,转到401000地址处,如图52-22所示。

程序正常运行(非调试运行)时,执行完40101E地址处的INT 2D指令后,发生异常,转去运行SEH (40102A)。在异常处理器中先把EIP值修改为401044,然后将[EBP-4]变量(局部变量[JEBP-4]是BOOL类型变量,用来检测是否存在调试器)的值设置为0 (FALSE)。然后转到401044地址处继续执行,最后执行40105B地址处的CMP/JE条件分支指令,向控制台输岀字符串(“Not Debugging”)。

程序调试运行时,执行INT 2D指令后不会运行SEH(就像前面所说的,忽略。),而是跳过1个字节(90),继续执行401021地址处的MOV指令,将[EBP-4]变量设置为1 (TRUE),然后跳转到401044地址处继续执行,向控制台输出“Debugging”字符串。可用图52-23简单表示上面2个执行过程。

  • 破解之法

我们可以从以上练习示例中看到,401044地址到40105B地址(条件分支指令(CMP/JE))间的代码都会被执行,所以只要简单修改代码即可破解这种反调试技术。但实际的程序调试过程中,有时必须跟踪SEH逐行调试代码,此时就需要一种方法使程序执行到SEH。利用陷阱标志能够使程序轻松进入SEH执行。

首先,设置OllyDbg调试器的选项,使之忽视EXCEPTION_SINGLE_STEP异常,如图52-18所示。然后运行程序至40101E地址处,如图52-24所示。

如图52-24所示,调试器在40101E地址的INT 2D指令处暂停,然后在要前往的(已注册过的)SEH处(40102A)设置断点。

如图56-25所示 ,双击TF或修改EFL值(+0x100),将TF设置为1。从现在开始,CPU进入单步执行模式。单步执行模式下,CPU执行1条指令即触发异常,然后进入SEH处理(请参考前面介绍过的单步执行示例)。接下来,按F7键(Steplnto)或F8键(StepOver)执行40101E地址处的INT 2D指令。

虽然执行了40101E地址处的INT 2D指令,但并未发生异常,TF的值也未变为0 (在前面的DynAD_SingleStep.exe示例中我们知道,CPU在单步模式下执行1条指令即触发异常,且TF值会清零),如图52-26所示。原因在于,INT 2D指令原为内核指令,在用户模式的调试器中不会被识别为正常指令。因此调试器在401020地址处的NOP指令处暂停。TF=0时,跟踪INT 2D指令后,其下条指令的第一个字节会被忽略,程序继续执行;但TF=1时,其后面的1个字节不会被忽略,代码仍被正常识别。接下来,按F7键(Steplnto)或F8键(StepOver)执行NOP指令。

在单步执行模式下执行正常指令NOP后,就会触发异常,调试暂停在设有断点的SEH处,同 时TF值清零(参考图52-27)。这样我们就能进入指定SEH继续调试了。

52.5 0xCC 探测

程序调试过程中,我们一般会设置许多软件断点。断点对应的x86指令为 “0xCC”。若能检测到该指令,即可判断程序是否处于调试状态。基于这一想法的反调试技术称为 “0xCC探测”技术。

我们需要认真思考在代码中查找断点的方法。因为0xCC既可以用作操作码,也可以用作移位值、立即数、数据、地址等。所以,“在进程内存的代码区域中只扫描0xCC”的做法并不可靠。

图52-28中,010073AC地址处设置了断点。虽然调试器会将8B视作操作码,但被调试进程的实际内存中,8B已被修改成CC。而指令的移位值中也存在CC。因此,单纯扫描CC很难准确判断断点。

52.5.1 API 断点

若只调试程序中的某个局部功能,一个比较快的方法是先在程序要调用的API处设置好断点,再运行程序。运行暂停在相应断点处后,再查看存储在栈中的返回地址。“跟踪返回地址调试相应部分”的方式能够大幅缩小代码调试范围。反调试技术中,探测这些设置在API上的断点就能准确判断当前进程是否处于调试状态。一般而言,断点都设置在API代码的开始部分,所以,只要检测API代码的第一个字节是否为CC即可判断岀当前进程是否处于调试之中。

逆向分析人员常用的API列表

[内存] [文件] [寄存器] [网络] [其他]
ReadProcessMemory CreateFile RegCreateKeyEx WSAStartup OpenProcessToken
WriteProcessMemory ReadFile RegDeleteKey socket OpenSCManager
VirtualAlloc WriteFile RegDeleteValue inetaddr ControlService
VirtualAllocEx CopyFile RegEnumKeyEx closesocket SetServiceStatus
VirtualProtect CreateDirectory RegQueryValueEx getservbyname OpenMutex
VirtualProtectEx DeleteFile RegSetValue gethostbybname GetProcAddress
VirtualQuery MoveFile RegSetValueEx htons OutputDebugString
VirtualQueryEx MoveFileEx connect LookupPrivilegeValue
FindFirstFile inethtoa CreateService
FindNextFile recv DeleteService
GetFileSize send QueryServiceStatusEx
GetWindowsDirectory HttpOpenRequest FindWindow
GetSystemDirectory HttpSendRequest GetModuleFileNameA
GetFileAttributes HttpQuerylnfo AdjustTokenPrivileges
SetFileAttributes InternetCloseHandle OpenService
SetFilePointer InternetConnect RegisterServiceCtrlHandler
CreateFileMapping InternetGetConnectedState CreateMutex
MapViewOfFile InternetOpen LoadLibrary
MapViewOfFileEx InternetOpenUrl GetCommandLine
UnmapViewOfFile InternetReadFile
_open URLDownloadToFile
_write
_read
_lseek
_tell
  • 练习

下面以保存文件时使用的kernel32!CreateFileW()API为例,向各位介绍基于API断点检测的反调试方法。首先,在OllyDbg调试器中单击鼠标右键,依次选择Search for - Name in all modules菜单,如图52-29所示

选择Name in all modules菜单后,在All name对话框中双击CreateFileW()API,然后按F2键在API代码的开始处设置好断点,如图52-30所示。

这样就在API代码开始的第一个字节设置好了断点(虽然在调试器中未看到代码发生改变,但其实API代码开始的第一个字节已被改为CC)。然后获取kernel32!CreateFileW() API的起始地址检测代码的第一个字节,即可判断进程是否处于调试之中。

  • 破解之法

针对上面这种反调试技术的行之有效的方法是,向系统API设置断点时尽量避开第一个字节,将之设置在代码的中间部分(图52-30中将断点设置在7C8107F2之下)。此外,设置硬件断点也能避开上面这种反调试技术。

52.5.2 比较校验和

检测代码中设置的软件断点的另一个方法是,比较特定代码区域的校验和(Checksum)值。比如,假定程序中401000~401070地址区域的校验和值为0x12345678,在该代码区域中调试时,必然会设置一些断点(0xCC),如此一来,新的校验和值就与原值不一样了。像这样,比较校验和值即可判断进程是否处于调试状态(参考图52-31)。

练习:DynAD_Checksum.exe

计算校验和来检测软件断点是常用的反调试技术之一,下面做个简单的调试练习,帮助各位理解该反调试技术的工作原理。在OllyDbg调试器中打开示例程序(DynAD_Checksum.exe)并转到401000地址处,如图52-32所示。

如图52-32所示,程序的核心代码是求校验和值的循环(位于40102A地址处),及其下方的CMP/JE条件分支指令(位于401035地址处)。先分析一下求校验和值的循环。

1
2
3
4
5
0040102A     3E:0FB61E       MOVZX EBX,BYTE PTR DS:[ESI]
0040102E 03C3 ADD EAX,EBX ; EAX: new checksum value
00401030 D1C0 ROL EAX,1
00401032 46 INC ESI
00401033 E2 F5 LOOPD SHORT DynAD_Ch.0040102A

40102A地址处ESI的初始值为401000 (参考40101F地址处的MOV指令),而ECX被用作LoopCount (循环计数),其值为70 (参考401024地址处的SUB指令)。代码中的循环用来计算401000-40106F区域的校验和值,先读取1个字节值,再执行ADD与ROL指令计算,然后将值保存到EAX寄存器(循环次数就是循环计数)。

求代码缓冲区之校验和的方法多种多样,也有众多可应用的算法,只要验证相关内存区域当前的校验和值与原值是否一样即可。实际运用中通常会使用CRC32(Cyclic Redundancy Check,循环冗余校验)算法,它检错能力强,运算速度快。

像这样求得校验和值后,接下来要将其与原值比较,并执行条件分支语句。

1
2
00401035     3B05 C0BD4000  CMP EAX,DWORD PTR DS:[40BDC0]           ;  Original checksum value
0040103B 74 07 JE SHORT DynAD_Ch.00401044

在上面循环中求得的当前校验和值被保存到EAX寄存器,程序开发时计算的校验和值保存在[40BDC0]中。比较它们,若不同,则表明40100~40106F代码区域中设有断点,或者代码已被修改。

  • 破解之法

从理论上讲,只要不在计算CRC的代码区域中设置断点或修改其中代码,基于校验和的反调试技术就会失效。但这本身也可能成为反调试技术觊觎的地方(因为调试变得更加困难)。因此,最好的破解方法是修改CRC比较语句。比如在前面的示例中,只要将40103B地址处的指令修改为JMP 40105D即可。当然也可以在调试器中强制修改要跳转(JMP)的地址。与其他反调试技术类似,基于校验和比较的反调试代码会巧妙隐藏于程序各处,可能存在数十个乃至数百个比较校验和的代码,这大大增加了破解难度,调试自然也变得困难多了。


第52章 动态反调试技术
https://m0ck1ng-b1rd.github.io/1999/05/02/逆向工程核心原理/第52章 动态反调试技术/
作者
何语灵
发布于
1999年5月2日
许可协议