第53章 高级反调试技术
本文最后更新于:2022年5月27日 下午
本章将与各位一起学习高级反调试技术(Advanced Anti-Debugging ),这些技术大量应用于各种著名的程序保护器。下面调试示例程序来了解各种高级反调试技术,相信各位的水平会得到很大提高。
53.1 高级反调试技术
PE保护器中使用的高级反调试技术有一些共同特征,如技术难度较高等,令代码逆向分析人员身心俱疲。
应用了这些高级反调试技术的程序包含大量垃圾代码、条件分支语句、循环语句、加密/解密代码以及“深不见底”的调用树(Call-Tree),代码逆向分析人员一旦陷人其中便会迷失方向,根本无法访问到实际要分析的代码,只是在无关紧要的地方徘徊。这些混乱加上代码中动态反调试技术的干扰,使代码逆向分析人员处于束手无策的尴尬境地。
当然,这并不是说调试全无可能,只是说调试的难度大大增加了。对于一名经验丰富的代码逆向分析人员而言,分析PE保护器也是一个非常棘手的问题,需要花费大量的时间与精力。而且, “完美分析”本身就是极其艰巨的任务。
本章将向各位介绍几种典型的高级反调试技术,激起大家的学习兴趣,从而帮助各位进一步提高自身的调试水平。
53.2 垃圾代码
向程序添加大量无意义的代码来增加代码调试的难度,这就是“垃圾代码”反调试技术。尤其是,这些垃圾代码中还含有真正有用的代码或者应用其他反调试技术时,调试程序会变得更加困难。
图53-1显示的是垃圾代码(Garbage code )示例之一。 图53-1所示的代码中,一些指令(PUSH/POP、XCHG、MOV)拥有相同的操作数,最终执行的是一些毫无意义的运算(命令执行后没有什么变化)。 图53-2的示例二的垃圾代码利用SUB与ADD指令为EBX设置值,最后执行4041A0地址处的JMP EBX
指令。除此之外,其余指令全部都是垃圾代码,原本用1条JMP XXXXXXXX指令即可实现操作,结果却用了很长、很复杂的代码来实现。
以上示例代码非常简单,调试过程中很容易跳过它们。但是实际的垃圾代码往往具有精巧又复杂的形态,含有大量条件分支语句和无尽的函数调用,想要跳过它们并非易事。
53.3扰乱代码对齐
熟悉了IA-32指令后,巧妙编写汇编代码即可干扰调试器的反汇编结果,反汇编代码看上去会乱作一团,如图53-3所示。
从图53-3的代码可以看岀,41510F地址处的JMP指令用来跳转到415117地址处,但是415117地址处的反汇编代码却未能正常显示。这是由于扰乱代码对齐(Breaking Code Alignment)使OllyDbg调试器生成了错误的反汇编代码。
415115地址处的指令中,操作码为“A3”,对应于MOV指令,用来处理4个字节大小的立即数值。所以该地址处的指令长度最终被解析为5个字节,这正是扰乱代码对齐的花招。415115地址处的“A368” 指令是故意添加的代码,用来扰乱反汇编代码,程序中未使用它,实际的代码仅为415117地址处的 “7201”。
关于IA-32指令解析的内容请参考第49章。
借助StepInto命令进入415117地址,显示正常代码,如图53-4所示。
415117地址处的JB指令也应用了相同技法,打乱了代码对齐。这种“向代码插入(经过精巧设计的)不必要的代码来降低反汇编代码可读性”的技术称为扰乱代码对齐。
尚未完全掌握代码就贸然使用StepOver(F8)等命令追踪调试,很有可能遭遇其他反调试技术拦截。总之,扰乱代码对齐技术是最令代码逆向分析人员苦恼的技术之一。
大部分调试器都拥有IA-32指令智能解析功能,用来生成反汇编代码。OllyDbg调试器也有类似的分析(Analysis)功能,用来提高反汇编代码的可读性。在图53-3中 按Ctrl+A快捷键,弹出图53-5所示的警告窗口(初次调试程序时,若有异常,也会弹出该警告窗口)。
如图53-6所示,OllyDbg调试器无法正常解析指令,反汇编代码解析(Analysis ) 从415115地址开始就失败了。这是因为解析结果中415115~415116地址间的指令(“A368”)被认为语义不正确。
单击“是(Y)“,显示图53-6所示的代码。
从代码流看,程序执行要跳转到415117地址处,但是415112地址与415117地址间的A368指令未能解析为正常的IA-32指令(2字节大小)(A3是总长度为5字节的指令,但根据前后代码看,它仅有2个字节)。此时关闭OllyDbg调试器的解析功能反而会更好。单击鼠标右键,在弹出菜单中依次选择Analysis-Remove analysis frommodule,如图53-7所示。
这样,OllyDbg调试器就不会对代码进行智能解析,而是直接显示原先的反汇编代码,如图53-3所示。
接下来,使用带有强大反汇编功能的IDAPro来分析相同代码,如图53-8所示。
可以看到出现了相同的现象。去往实际地址前,程序代码在OllyDbg与IDA Pro 中都处在代码非对齐状态。
我们通常把纠缠混合在一起的代码称为“混乱代码”(Obfuscated Code ),它们会增加阅读分析代码的难度。灵活运用垃圾代码与扰乱代码对齐技术能够产生非常棒的“混乱代码”。
53.4加密/解密
加密/解密(Encryption/Decryption )是压缩器与保护器中经常使用的技术,用来隐藏程序代码与数据,从而有效防止调试分析程序。
计算机领域中将“为正常代码加密”的行为称为“编码”(Encoding),而把“解 密代码”的行为称为“解码”(Decoding)。
53.4.1简单的解码示例
下面看个简单的解码示例
1 |
|
40B00040B00E地址间的代码是解码循环,用来对40B01040B110地址区域进行解码(XOR 7F )。40B010地址以后的代码只有经过解码才能正常显示。
反转储技术中,加密代码被解码为正常代码后,有时会被再次加密。转储运行中的进程内存代码时,得到的代码仍然处于加密状态。
53.4.2 复杂的解码示例
下面看个更复杂的解码循环,其内部包含大量垃圾代码,如代码53-2所示。
**005910D1 CALL 005910E2 ;()
005910D6 HLT
005910D7 SBB EAX,19606392
005910DC FIDIVR WORD PTR DS:[EDI+DBEAD58C]
005910E2 JNB 005910ED
005910E8 ADC DX,0A953
005910ED POP EBX ; EBX = 5910D6
005910F3 JNB 0059110B
005910F9 JMP 0059110B
0059110B POP ESI
0059110C ADD EBX,0A42 ; EBX = 591B18
00591112 PUSH 7FFFAA31
00591117 MOV EDI,7944ECA2
0059111C POP ESI
0059111D MOV EAX,252 ; EAX = 252 (loop count)
00591122 JMP 00591138
…
00591138 MOV ECX,DWORD PTR DS:[EBX]
0059113A MOV EDI,22AC5676
0059113F SUB ECX,425C7573
00591145 MOV ESI,EDI
00591147 ADD ECX,77193C30
0059114D PUSH EDI
0059114E CALL 00591164
…
00591164 MOV ESI,4B1DFA57
00591169 POP EDI
0059116A POP EDI
0059116B SUB ECX,233570A9
00591171 PUSH ESI
00591172 JG 0059117D
00591178 MOV EDI,0A7E8B74
0059117D POP EDI
0059117E MOV DWORD PTR DS:[EBX],ECX
00591180 PUSH EBX
00591181 MOV DX,CX
00591184 POP EDX
00591185 SUB EBX,4E777037
0059118B JMP 00591197
00591190 RCL DWORD PTR DS:[EAX],CL
00591192 OR DWORD PTR DS:[ESI],ECX
00591194 DAS
00591195 CMP AL,0C5
00591197 ADD EBX,4E777033
0059119D MOV DH,8B
0059119F SUB EAX,1
005911A2 JNZ 005911C3
005911A8 SUB DX,421F
005911AD JMP 005911D4 ; 解码结束后跳到解密后的代码
005911B2 XOR EAX,B1583BCA
005911B7 XCHG EAX,ESI
005911B8 POP SS
005911B9 ADD AL,0ED
005911BB AND DH,BYTE PTR DS:[EBX+F6EE970]
005911C1 PUSHFD
005911C2 MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESIJ
005911C3 JMP 00591138 ; 继续解码
以上代码中,有效指令与垃圾指令混在一起,看上去比较复杂。但跟踪调试可以发现,上述代码就是解码循环,并且我们能够从中找出有效指令(以上代码用黑粗体表示有效指令)。其中最核心的指令如下所示:
1 |
|
上述代码中,先从EBX寄存器所指地址中读取DWORD(4个字节)值,然后与0x11875614相加,再写入原地址。也就是说,在原值基础上加了0x11875614。
EAX寄存器用来为解码循环计数,它先被59111D地址处的MOV指令初始化为0x252,然后被59119F地址处的SUB指令减去1。要解密区域的地址存储在EBX寄存器中,在005910D1、005910ED、0059110C地址处指令的作用下,EBX寄存器的初始值被设置为591B18,然后在以下2条指令的作用下减去4 (EBX的范围为5911D4~591B18)。
1 |
|
最后,所有解码完成后,执行5911AD地址处的JMP指令,跳转到5911D4地址处(解密代码的起始位置)。
1 |
|
从代码53-2中删除垃圾指令后,对代码简单整理如下:
005910D1 MOV EAX,252
005910D6 MOV EBX,00591B18
005910DB MOV ECX,DWORD PTR DS:[EBX]
005910DD ADD ECX,11875614
005910E3 MOV DWORD PTR DS:[EBX],ECX
005910E5 SUB EBX,4
005910E8 DEC EAX
005910E9 JNZ SHORT 005910DB
005910EB JMP 005911D4
53.4.3 特殊情况:代码重组
有些程序保护器为了降低代码可读性,增加代码跟踪难度,采用了实时组合执行代码的技术手法。
图53-9中,4150E3地址处的SUB指令与4150E9地址处的DEC指令用来修改其下的代码(分别为4150EF、4150EC)。执行这2条指令后,其下的代码变形如图53-10所示。
可以看到,重新生成了4150EC地址处的指令,CPU会执行新的指令代码。
该技术的另一个优点是,用户在解码的代码处设置软件断点(0xCC)后,程序运行就会引发运行时错误。这是因为,设有断点的区域被0xCC取代,从而出现完全不同的计算结果(OllyDbg 调试器中,为了保护设有软件断点的地址中的数据,干脆禁止写入新值)。
以上代码为PESpin保护器(PESpin(1.32).exe)的EP代码
53.5 Stolen Bytes (Remove OEP)
Stolen Bytes(或者Remove OEP)技术将部分源代码(主要是OEP代码)转移到压缩器/保护器创建的内存区域运行。
该技术的优点是,转储进程内存时,一部分OEP代码会被删除,转储的文件无法正常运行反转储技术。另一优点是,应用Stolen Bytes技术的文件再次经过压缩器/保护器压缩后,会给逆向分析人员造成很大混乱。文件脱壳后,得到的不是熟悉的OEP代码,而是其他形态的代码,这很难判断是脱壳成功还是需要继续操作,容易引起代码逆向分析人员的混乱(我们常用“迷路、徘徊”等词汇描述这种状态)。为帮助各位理解该技术,下面分析练习示例(stolen_bytes.exe)。首先在OllyDbg调试器中打开示例程序,进入程序的EP代码,如图53-11所示。
示例程序(stolen_bytes.exe)是用Microsoft Visual C++ 6.0工具编译的可执行文件,图53-11为其EP代码(使用Visual C++ 2008/2010编译的可执行文件的EP代码形态与此不同)。在PESpin保护器中开启“RemoveOEP”选项,打开示例程序文件,执行“Protect”(保护文
件)操作(stolen_bytes_pespin.exe)。在OllyDbg调试器中打开stolen_bytes_pespin.exe程序文件,转到OEP附近的地址处(参考图53-12)。
使用作者提供的二进制文件,发现是一些垃圾代码,并没有被替换成NULL。
从图53-12可以看到,401088地址之前的代码都被替换为NULL值(请与前图中的EP代码比较)。虽然图53-12并未显示全部代码,但可以看到OEP(401041)~401087区域中的代码已被删除。删除的代码被保存到PESpin添加的节区,脱壳后调用执行。以下代码就是保存在PESpin节区中的“消失的OEP代码”。
从以上代码可以看岀,OEP代码采用了扰乱代码对齐技术拆分保存。最终执行40CD76地址处的JMP指令,将程序执行跳转到源代码节区(401088)。
不同类型保护器的处理方式不同,有些保护器会先保存Stolen Bytes再运行,而有些保护器运行完Stolen Bytes后会将它们直接从内存中删除(分配内存-解密Stolen Bytes代码-运行-释放内存)。
53.6 API 重定向
在主要的Win32 API(文件、注册表、进程、网络等)处设置断点,就能在调试程序时快速掌握代码流。
图53-13是OllyDbg调试器的断点窗口,列岀了主要API起始代码中设置的断点。在该状态下运行(RUN(F9))被调试进程,每当调用以上断点列表中的API时,程序暂停执行,返回地址被存储到栈(参考图53-14)。接下来,只要从返回地址继续调试就可以了。要调试的代码非常多时,采用该方法非常高效,且能轻松进入核心代码调试。
API重定向就是破解上面这种调试手法的技术。程序保护器通常会先将全部(或部分)主要的API代码复制到其他内存区域,然后分析要保护的目标进程代码,修改调用API的代码,从而使自身复制的API代码得以执行。这样,即使在原API地址处设置断点也没用(此外,该技术还支持反转储功能)。
下面分析一段应用API重定向技术的代码,帮助各位加深理解。
53.6.1 原代码
首先在OllyDbg调试器中打开示例程序(api_redirection_org.exe),原代码如图53-15所示。
4010B5地址处的CALL DWORD PTR DS:[406000]
指令中,地址406000即为IAT区域,其中含有kernel32!GetCommandLineA()API地址(7C812FAD)。kernel32!GetCommandLineA()API的实际代码如图53-16所示。
可以看到其代码非常简单,仅返回7C8855F4地址(kernel32.dll的.data区域)中存储的值。严格地说,DWORD PTR DS:[7C8855F4]为kernel32.dll的全局变量。
53.6.2 API重定向示例#1
下面分析api_redirection1.exe示例程序,它在上面原代码的基础上应用了API重定向技术。
与原代码(图53-15湘比,IAT地址由原来的406000变为40FE1F,且40FE1F地址的值为3F0000(原代码中该地址为kernel32.dll内API的地址)。
api_redirection1.exe文件是使用PESpin保护器的API重定向选项制作的。使用调试器进行运行时解压缩后,运行到OEP处就会出现上图中的(变形后的)原代码。解压缩后也应用API重定向技术。
地址3F0000是保护器分配的内存区域的起始地址,保护器将主要的API代码复制到该地址区域。在图53-17中跟踪(StepInto(F7))位于4010B5地址处的CALL指令,查看3F0000地址处的代码。
从图53-18可以看到,代码中应用了扰乱代码对齐技术,跟踪JMP指令可以看到实际的API代码。
图53-19中的代码与实际的kernel32!GetCommandLineA()API代码(参考图53-16)完全相同。保护器将原API代码复制到该处。
保护器会像这样重新组织原程序的IAT,并全部修改调用相关API的代码。最终调用的不是Kernel32模块中的原API,而是3F0000地址区域中的API。
53.6.3 API重定向示例#2
下面看个更复杂的API重定向示例,该示例程序(api_redirection2.exe)是使用ASProtect的
Advanced Import Protection与Emulate standard system function功能制作的,如图53-20所示。
在原程序与本示例中比较4010B5地址处的CALL指令(参考图53-15)。
虽然2条CALL指令相同,但操作码却不同。操作码“FF15” 表示间接调用7C812FAD地址(该
地址存储在406000地址中)处的函数。操作码“E8” 表示直接调用(指定地址值(76EF46)加 上Next EIP(401 OBA)得到的)新地址(76EF46+4010BA=B70000)中的代码。
原来的6字节CALL指令(FF15 00604000)被改为5字节CALL指令(E8
46EF7600), 4010BA地址处仅剩下B8,这意味着代码中会出现代码对齐混乱效果(运 行B70000函数代码,返回地址被修改为与原代码相同的地址4010BB)。
B70000地址区域是ASProtect在脱壳过程中分配的众多内存区域之一(参考图53-21)。
跟踪进入(StepInto(F7))B70000函数,代码如图53-22所示。
从图中可以看到,代码中含有垃圾代码,也应用了扰乱代码对齐技术。实际的
GetCommandLineA()API代码要一段时间后才显示。
每次使用调试器转到B70000地址时,代码形态均不同。因为ASProtect的混淆代
码生成器每次都会生成新的垃圾代码。我们把这种能产生相同结果而又具有不同形态
的代码称为多态代码(Polymorphic Code)。前面还介绍过一种“混乱代码”(Obfuscated
Code),今后会经常用到它们,希望各位借此机会记住。图53-22中的代码同时具有“多
态代码”和“混乱代码”的特征(代码形态随时变化,我们很难把握)。
B70000地址处的代码是ASProtect添加的垃圾代码,用来设置调试障碍,增加调试难度。在
OllyDbg调试器中跟踪调试时,要运行约3万条指令(包含循环中反复调用的指令),然后返回原
代码中的4010BB地址处(参考图53-23)。
也就是说,原代码中的1条CALL DWORD PTR DS:[406000]指令被换成了3万条指令(除
Kernel32!GetCommandLineA()API夕卜,其他很多API也采用这种调用方式)。该方式执行效率非常
低,但是对保护代码与增加代码逆向分析难度来说,效果非常棒。
垃圾代码中包含实际调用API的代码,如图53-24所示。
与前面介绍的PESpin示例程序(api_redirectionl.exe)不同,该示例程序会直接调用头际
的kernel32!GetCommandLineA()API,并修改返回地址(4010BA—4010BB),代码如图53-25
所示。
通过跟踪图53-20中4010B5地址处的CALL B70000指令调试到此,图53-25中[EAX]=
[12FF3C]=4010BA即是调用B70000函数后的返回地址。若直接返回4010BA地址就会引发错误,
所以,借助455E9D地址处的MOV指令将返回地址修改为4010BB,如图53-26所示。
图53-25的地址区域(455E9D)是ASProtect的代码节区区域(参考图53-21)。与 B70000区域一样,它不是为了生成垃圾代码而分配的内存区域,而是实实在在的
ASProtect的代码节区区域。
最后返回原代码的4010BB地址处,代码如图53-26所示
图53-26中的代码(B800B3)也是ASProtect创建的多态&混乱代码,每次调试都会变化。
我们至此学习了具有复杂形态的API重定向示例,API重定向这种方式牺牲了代码的运行速
度,却大大提高了代码的复杂性,从而获得了很好的反调试效果。
若代码逆向分析人员事先并不知道程序中调用了哪些API(或者要花很长时间才能查明),就
会使代码逆向分析工作变得十分困难。因此,API重定向是一种相当有效的反调试技术,许多程
序保护器都支持它。
参考上面学过的内容,请各位亲自跟踪调试B70000处的函数。我借助OllyDbg的 “硬件断点”功能,获取了图53-25、图53-26中出现的455E9D与B800B3地址。进 入B70000函数后,返回地址存储在栈中,在ESP值(我在12FF3C地址处设置硬件断
点后获取了 455E9D地址)与4010BB地址处设置断点跟踪,就会见到B800B3地址处
的JMP指令。
API重定向技术在结构上与API钩取技术有很多类似的地方:它们都不直接调用原API,而是
添加自身代码并执行后再调用。二者最大的不同在于,它们的目的是不一样的:APJ重定向用来
增加代码调试的难度,而API钩取则用来在API调用前/后添加另外的功能。
53.7 Debug Blocker(Self Debugging)
Debug Blocker(Self Debugging)也是一种高级反调试技术,顾名思义,它在调试模式下运行自身进程。
图53-27中的PESpin(1.32).exe进程为PESpin保护器。可以看到,同一进程以父进程(PID: 184) / 子进程(PID: 1424)形式运行。其实,它们是调试器(PID: 184)与被调试者(PID: 1424)的关系。PESpin运行后会查找自身的可执行文件,然后以调试模式执行。
Debug Blocker是自我创建技术(以子进程形式运行自身进程)的演进形式。自我创建技术中,子进程负责执行实际原代码,父进程负责创建子进程、修改内存(代码/数据)、更改EP地址等。所以仅调试父进程将无法转到OEP代码处,这样能起到很好的反调试效果。但调试时若用附加命令将子进程附加到调试器,这种反调试手法就会失去作用。Debug Blocker技术的出现正是为了弥补这一不足。
Debug Blocker技术有如下优点。
第一,防止代码调试。因子进程运行实际的原代码且已处于调试之中,原则上就无法再使用其他调试器进行附加操作了(第57章中将介绍一种方法,它可以解决该问题并顺利实现调试)。
第二,能够控制子进程(Debuggee,被调试者)。调试器-被调试者关系中,调试器具有很大权限,可以处理被调试进程的异常、控制代码执行流程等。
Debug Blocker技术的第二个优点使代码调试变得非常困难。下面比较常规反调试技术与Debug Blocker技术。
图53-28左侧为应用常规SEH技术的反调试示例,右侧为应用Debug Blocker技术的反调试示例。常规SEH技术中,异常处理器代码位于相同的进程内存空间;但Debug Blocker技术中,(处理被调试进程所发异常的)异常处理器代码位于调试进程(请注意,对于被调试进程所发的异常,调试器拥有优先处理权)。
所以,为了调试子进程,必须先断开与已有调试器的连接,但这样子进程又无法正常运行。这正是逆向分析Debug Blocker最难的部分。
第57章中将调试应用了Debug Blocker的示例程序,帮助各位进一步了解
Nanomite技术由Debug Blocker技术发展而来,该技术会查找被调试进程内部的代码,将所有条件跳转指令(Jcc指令)修改为INT3(0xCC)指令(软件断点),或其他触发异常的代码。并且,调试器内部有表格,含有被修改的Jcc指令的实际地址位置以及要跳转的地址。执行被调试者内部修改后的指令就会触发异常,控制权即被转交给调试器。调试器通过发生异常的地址从(自身持有的)表格中获取要跳转的地址,然后通知被调试者。图53-29是含有条件跳转指令的原代码。
使用PESpin保护器向前面的原代码应用Nanomite技术,出现如图53-30所示的代码。
认真比较可以发现,2个字节的Jcc/MOV指令全部被修改为LEA EAX,EAX这类怪异的指令。执行这种代码时就会触发EXCEPTION_ILLEGAL_INSTURCTION异常,系统会将控制权转移给调试器。
若想正常调试这种应用了Nanomite技术的代码,需要把修改后的代码恢复为原代码。逐一手动修改会非常费力,大量恢复工作要实现自动化处理,这需要我们具备一定的编程思维与能力。所以,调试这种应用了Nanomite技术的代码是很有难度的。
53.8 小结
本章讲解了PE保护器中常用的高级反调试技术的相关内容(随着反调试技术的不断发展,各种技术会应运而生)。
我刚开始学习代码逆向分析技术时,曾经尝试调试ASProtector。连续分析了好几天,但是连OEP代码的边都没摸到,总是在奇怪的地方徘徊。这是因为受到了反调试代码的阻碍,这些代码常被称为“死亡代码”。若想顺利通过数量庞大的代码,必须坚信自己最后一定能够找到OEP代码。其实,陷入“死亡代码”的沼泽是绝对不可能找到OEP代码的。调试时会不断出现混乱代码,让人疲惫不堪。我当时跟踪调试这些代码时也深受其苦,最后竟然不知不觉间打起了瞌睡,那时才真正体会到反调试技术的可怕。这些反调试技术从精神与肉体上折磨逆向分析人员,打消他们调试代码的念头。两年后,我再次挑战ASProtector,学习从网络上获取的各种反调试相关资料,最终顺利达到OEP处,当时真是高兴坏了。
但那也只是回避了反调试技术而到达OEP处而已,其实仍未能完全掌握ASProtector的工作原理(内部算法)。我当时再次感受到自身的不足,觉得要学的东西还很多,一定要虚心学习。另一方面也认为编写该PE保护器的人实在太有水平了。各位的逆向分析技术达到一定水平后,我建议大家调试一下PE保护器。查找、学习相关资料,在调试过程中认真分析,相信各位会学到大量知识并积累丰富经验,进一步提高代码调试水平。