脱壳的艺术
Mark Vincent Yason
概述:脱壳是门艺术——脱壳既是一种心理挑战,同时也是逆向领域最为激动人心的智力游戏之一。为了甄别或解决非常难的反逆向技巧,逆向分析人员有时不得不了解操作系统的一些底层知识,聪明和耐心也是成功脱壳的关键。这个挑战既牵涉到壳的创建者,也牵涉到那些决心躲过这些保护的脱壳者。
本文主要目的是介绍壳常用的反逆向技术,同时也探讨了可以用来躲过或禁用这些保护的技术及公开可用的工具。这些信息将使研究人员特别是恶意代码分析人员在分析加壳的恶意代码时能识别出这些技术,当这些反逆向技术阻碍其成功分析时能决定下一步的动作。第二个目的,这里介绍的信息也会被那些计划在软件中添加一些保护措施用来减缓逆向分析人员分析其受保护代码的速度的研究人员用到。当然没有什么能使一个熟练的、消息灵通的、坚定的逆向分析人员止步的。
4反分析技术
反分析技术的目标是减缓逆向分析人员对受保护代码和(或)加壳后的程序分析和理解的速度。我们将讨论诸如加密/压缩、垃圾代码、代码变形、反-反编译等技术,这些技术的目的是为了混淆代码、考验耐心、浪费逆向分析人员的时间,解决这些问题需要逆向分析人员拥有耐心、聪慧等品质。
4.1 Encryption and Compression
加密和压缩是最基本的反分析形式。它们初步设防,防止逆向分析人员直接在反编译器内加载受保护的程序然后没有任何困难地开始分析。
加密 壳通常都既加密本身代码也加密受保护的程序。不同的壳所采用的加密算法大不相同,有非常简单的XOR循环,也有执行数次运算的非常复杂的循环。对于某些多态变形壳,为了防止查壳工具正确地识别壳,每次加壳所采用的加密算法都不同,解密代码也通过变形显得很不一样。
解密例程作为一个取数、计算、存诸操作的循环很容易辨认。下面是一个对加密过的DWORD值执行数次XOR操作的简单的解密例程。
0040A 07C LODS DWORD PTR DS:[ESI]
0040A 07D XOR EAX,EBX
0040A 07F SUB EAX,12338CC3
0040A 084 ROL EAX,10
0040A 087 XOR EAX, 799F 82D0
0040A 08C STOS DWORD PTR ES:[EDI]
0040A 08D INC EBX
0040A 08E LOOPD SHORT 0040A 07C ;decryption loop
这里是另一个多态变形壳的解密例程:
00476056 MOV BH,BYTE PTR DS:[EAX]
00476058 INC ESI
00476059 ADD BH,0BD
0047605C XOR BH,CL
0047605E INC ESI
0047605F DEC EDX
00476060 MOV BYTE PTR DS:[EAX],BH
00476062 CLC
00476063 SHL EDI,CL
:::More garbage code
00476079 INC EDX
0047607A DEC EDX
0047607B DEC EAX
0047607C JMP SHORT 0047607E
0047607E DEC ECX
0047607F JNZ 00476056 ;decryption loop
下面是由同一个多态壳生成的另一段解密例程:
0040C 045 MOV CH,BYTE PTR DS:[EDI]
0040C 047 ADD EDX,EBX
0040C 049 XOR CH, AL
0040C 04B XOR CH,0D9
0040C 04E CLC
0040C 04F MOV BYTE PTR DS:[EDI],CH
0040C 051 XCHG AH,AH
0040C 053 BTR EDX,EDX
0040C 056 MOVSX EBX,CL
::: More garbage code
0040C 067 SAR EDX,CL
0040C 06C NOP
0040C 06D DEC EDI
0040C 06E DEC EAX
0040C 06F JMP SHORT 0040C 071
0040C 071 JNZ 0040C 045 ;decryption loop
上面两个示例中高亮的行是主要的解密指令,其余的指令都是用来迷惑逆向分析人员的垃圾代码。注意寄存器是如何交换的,还有两个示例之间解密方法是如何改变的。
Compression 压缩的主要目的是为了缩小可执行文件代码和数据的大小,但是由于原始的包含可读字符串的可执行文件变成了压缩数据,因此也有那么一些混淆的作用。看看几款壳所使用的压缩引擎:UPX使用NRV(Not Really Vanished)和LZMA(Lempel-Ziv-Markov chain-Algorithm),FSG使用aPLib,Upack使用LZMA,yoda加密壳使用LZO。这其中有些压缩引擎可以自由地使用于非商业应用,但是商业应用需要许可/注册。
对策
解密和解压缩循环很容易就能被躲过,逆向分析人员只需要知道解密和解压缩循环何时结束,然后在循环结束后面的指令上下断点。记住,有些壳会在解密循环中检测断点。
4.2 Garbage Code and Code Permutation
Garbage Code 在脱壳的例程中插入垃圾代码是另一种有效地迷惑逆向分析人员的方法。它的目的是在加密例程或者诸如调试器检测这样的反逆向例程中掩盖真正目的的代码。通过将本文描述过的调试器/断点/补丁检测技术隐藏在一大堆无关的、不起作用的、混乱的指令中,垃圾代码可以增加这些检测的效果。此外,有效的垃圾代码是那些看似合法/有用的代码。
示例
下面是一段在相关的指令中插入了垃圾代码的解密例程:
0044A 21A JMP SHORT sample .0044A 21F
0044A 21C XOR DWORD PTR SS:[EBP],6E4858D
0044A 223 INT 23
0044A 225 MOV ESI,DWORD PTR SS:[ESP]
0044A 228 MOV EBX, 2C 322FF0
0044A 22D LEA EAX,DWORD PTR SS:[EBP+6EE5B321]
0044A 233 LEA ECX DWORD PTR DS:[ESI+543D583E]
0044A 239 ADD EBP, 742C 0F 15
0044A 23F ADD DWORD PTR DS:[ESI],3CB3AA25
0044A 245 XOR EDI,7DAC77E3
0044A 24B CMP EAX,ECX
0044A 24D MOV EAX,5ACAC514
0044A 252 JMP SHORT sample .0044A 257
0044A 254 XOR DWORD PTR SS:[EBP],AAE47425
0044A 25B PUSH ES
0044A 25C ADD EBP,5BAC 5C 22
0044A 262 ADC ECX,3D 71198C
0044A 268 SUB ESI,-4
0044A 26B ADC ECX, 3795A 210
0044A 271 DEC EDI
0044A 272 MOV EAX, 2F 57113F
0044A 277 PUSH ECX
0044A 278 POP ECX
0044A 279 LEA EAX,DWORD PTR SS:[EBP+3402713D]
0044A 27F EDC EDI
0044A 280 XOR DWORD PTR DS:[ESI],33B568E3
0044A 286 LEA EBX,DWORD PTR DS:[EDI+57DEFEE2]
0044A 28C DEC EDI
0044A 28D SUB EBX,7ECDAE21
0044A 293 MOV EDI, 185C 5C 6C
0044A 298 MOV EAX,4713E635
0044A 29D MOV EAX,4
0044A 2A 2 ADD ESI,EAX
0044A 2A 4 MOV ECX, 1010272F
0044A 2A 9 MOV ECX, 7A 49B614
0044A 2AE CMP EAX,ECX
0044A 2B0 NOT DWORD PTR DS:[ESI]
示例中相关的解密指令是:
0044A 225 MOV ESI,DWORD PTR SS:[ESP]
0044A 23F ADD DWORD PTR DS:[ESI],3CB3AA25
0044A 268 SUB ESI,-4
0044A 280 XOR DWORD PTR DS:[ESI],33B568E3
0044A 29D MOV EAX,4
0044A 2A 2 ADD ESI,EAX
0044A 2B0 NOT DWORD PTR DS:[ESI]
Code Permutation 代码变形是更高级壳使用的另一种技术。通过代码变形,简单的指令变成了复杂的指令序列。这要求壳理解原有的指令并能生成新的执行相同操作的指令序列。
一个简单的指令置换示例:
mov eax,ebx
test eax,eax
转换成下列等价的指令:
push ebx
pop eax
or eax,eax
结合垃圾代码使用,代码变形是一种有效地减缓逆向分析人员理解受保护代码速度的技术。
示例
为了说明,下面是一个通过代码变形并在置换后的代码间插入了垃圾代码的调试器检测例程:
004018A 8 MOV ECX,A104B412
004018AD PUSH 004018C 1
004018B2 RETN
004018B3 SHR EDX,5
004018B6 ADD ESI,EDX
004018B8 JMP SHORT 004018BA
004018BA XOR EDX,EDX
004018BC MOV EAX,DWORD PTR DS:[ESI]
004018BE STC
004018BF JB SHORT 004018DE
004018C 1 SUB ECX,EBX
004018C 3 MOV EDX, 9A 01AB 1F
004018C 8 MOV ESI,DWORD PTR FS:[ECX]
004018CB LEA ECX DWORD PTR DS:[EDX+FFFF7FF7]
004018D1 MOV EDX,600
004018D6 TEST ECX,2B73
004018DC JMP SHORT 004018B3
004018DE MOV ESI,EAX
004018E0 MOV EAX,A35ABDE4
004018E5 MOV ECX,FAD 1203A
004018EA MOV EBX,51AD5EF2
004018EF DIV EBX
004018F 1 ADD BX, 44A 5
004018F 6 ADD ESI,EAX
004018F 8 MOVZX EDI,BYTE PTR DS:[ESI]
004018FB OR EDI,EDI
004018FD JNZ SHORT 00401906
其实这是一个很简单的调试器检测例程:
00401081 MOV EAX,DWORD PTR FS:[18]
00401087 MOV EAX,DWORD PTR DS:[EAX+30]
0040108A MOVZX EAX,BYTE PTR DS:[EAX+2]
0040108E TEST EAX,EAX
00401090 JNZ SHORT 00401099
对策
垃圾代码和代码变形是一种用来考验耐心和浪费逆向分析人员的时间的方式。因此,重要的是知道这些混淆技术背后隐藏的指令是否值得去理解(是不是仅仅执行解密、壳的初始化等动作)。
避免跟踪进入这些难懂的指令的方法之一是在壳最常用的API下断点(如:VirtualAlloc,VitualProtect,LoadLibrary,GetProcAddress等)并把这些API当作跟踪的标志。如果在这些跟踪标志之间出了错,这时候就对这一段代码进行详细的跟踪。另外,设置内存访问/写入断点也让逆向分析人员能有针对性地分析那些修改/访问受保护进程最有趣的部分的代码,而不是跟踪大量的代码最终却(很可能)发现是一个确定的例程。
最后,在VMWare中运行OllyDbg并不时地保存调试会话快照,这样一来逆向分析人员就可以回到某一个特定的跟踪状态。如果出了错,可以返回到某一特定的跟踪状态继续跟踪分析。
4.3 Anti-Disassembly
用来困惑逆向分析人员的另一种方法就是混乱反编译输出。反-反编译是使通过静态分析理解二进制代码的过程大大复杂化的有效方式。如果结合垃圾代码和代码变形一起使用将会更具效果。
反-反编译技术的一个具体的例子是插入一个垃圾字节然后增加一个条件分支使执行跳转到垃圾字节(译者注:即我们常说的花指令)。但是这个分支的条件永远为FALSE。这样垃圾代码将永远不会被执行,但是反编译引擎会开始反编译垃圾字节的地址,最终导致不正确的反编译输出。
示例
这是一个加了一些反-反编译代码的简单PEB.BeingDebugged标志检查例子。高亮的行是主要指令,其余的是反-反编译代码。它用到了垃圾字节0xff并增加了用来迷惑反编译引擎的跳到垃圾字节的假的条件跳转。
;Anti-disassembly sequence #1
push .jmp_real_01
stc
jnc .jmp_fake_01
retn
.jmp_fake_01:
db 0xff
.jmp_real_01:
;--------------------------------
mov eax,dword [fs:0x18]
;Anti-disassembly sequence #2
push .jmp_real_02
clc
jc .jmp_fake_02
retn
.jmp_fake_02:
db 0xff
.jmp_real_02:
;--------------------------------
mov eax,dword [eax+0x30]
movzx eax,byte [eax+0x02]
test eax,eax
jnz .debugger_found
下面是WinDbg中的反汇编输出:
0040194A 6854194000 PUSH 0X401954
0040194F F9 STC
00401950 7301 JNB image00400000+0x1953(00401953)
00401952 C 3 RET
00401953 FF 64A 118 JMP DWORD PTR [ECX+0X18]
00401957 0000 ADD [EAX],AL
00401959 006864 ADD [EAX+0X64],CH
0040195C 194000 SBB [EAX],EAX
0040195F F8 CLC
00401960 7201 JB image00400000+0x1963 (00401963)
00401962 C 3 RET
00401963 FF8B40300FB6 DEC DWORD PTR [EBX+0XB 60F 3040]
00401969 40 INC EAX
0040196A 0285C 0750731 ADD AL,[EBP+0X 310775C 0]
OllyDbg中的反汇编输出:
0040194A 6854194000 PUSH 00401954
0040194F F9 STC
00401950 7301 JNB SHORT 00401953
00401952 C 3 RETN
00401953 FF 64A 118 JMP DWORD PTR DS:[ECX+18]
00401957 0000 ADD BYTE PTR DS:[EAX], AL
00401959 006864 ADD BYTE PTR DS:[EAX+0X64],CH
0040195C 194000 SBB DWORD PTR DS:[EAX],EAX
0040195F F8 CLC
00401960 7201 JB SHORT 00401963
00401962 C 3 RETN
00401963 FF8B40300FB6 DEC DWORD PTR DS:[EBX+B 60F 3040]
00401969 40 INC EAX
0040196A 0285C 0750731 ADD AL,BYTE PTR SS:[EBP+ 310775C 0]
最后IDAPro中的反汇编输出:
0040194A push (offset loc_401953+1)
0040194F stc
00401950 jnb short loc_401953
00401952 retn
00401953 ;------------------------------------------------------------------
00401953
00401953 loc-401953: ;CODE XREF: sub_401946+A
00401953 ;DATA XREF: sub_401946+4
00401953 jmp dword ptr [ecx+18h]
00401953 sub_401946 endp
00401953
00401953 ;------------------------------------------------------------------
00401957 db 0
00401958 db 0
00401959 db 0
0040195A db 68h; h
0040195B dd offset unk_401964
0040195F db 0F 8h;
00401960 db 72h; r
00401961 db 1
00401962 db 0C 3h;+
00401963 db 0FFh
00401964 unk_401964 db 8Bh; i ;DATA XREF: text:0040195B
00401965 db 40h; @
00401966 db 30h; 0
00401967 db 0Fh
00401968 db 0B6h;|
00401969 db 40h; @
0040196A db 2
0040196B db 85h;
0040196C db 0C 0h;+
0040196D db 75h; u
注意所有这三个反编译引擎/调试器是如何落入反-反编译陷阱的,分析这样的反汇编代码对于逆向分析人员来说是很不容易的。还有其它的几种干扰反编译引擎的手段,这只是一个例子。另外这些反-反编译代码可以编码成一个宏,这样汇编源码就清晰多了。
建议读者参考Eldad Eliam13的一本精彩的逆向书籍,里面包含了反-反编译的详细信息和其它一些逆向话题。