飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 2754|回复: 3

[转贴] 脱壳的艺术(二)

[复制链接]

该用户从未签到

发表于 2010-5-31 16:27:39 | 显示全部楼层 |阅读模式
对策

如果软件断点被发现了逆向分析人员可以使用硬件断点来代替。如果需要在API内部下断,但是壳又检测API内部的断点,逆向分析人员可以在最终被ANSI版API调用的UNICODE版的API下断(如:用LoadLibraryExW代替LoadLibraryA),或者用相应的native API来代替。

3.2 Hardware Breakpoint Detection

另一种断点称之为硬件断点,硬件断点是通过设置名为Dr0到Dr7的调试寄存器12来实现的。Dr0-Dr3包含至多4个断点的地址,Dr6是个标志,它指示哪个断点被触发了,Dr7包含了控制4个硬件断点诸如启用/禁用或者中断于读/写的标志。

由于调试寄存器无法在Ring3下访问,硬件断点的检测需要执行一小段代码。壳利用了含有调试寄存器值的CONTEXT结构,CONTEXT结构可以通过传递给异常处理例程的ContextRecord参数来访问。

示例

这是一段查询调试寄存器的示例代码:

; set up exception handler

push         .exception_handler

push         dword [fs:0]

mov         [fs:0],esp



;eax will be 0xFFFFFFFF if hardware breakpoints are identified

xor           eax,eax



;throw an exception

mov         dword [eax],0



;restore exception handler

pop          dword [fs:0]

add          esp,4



;test if EAX was updated (breakpoint identified)

test          eax,eax

jnz           .breakpoint_found

:::

.exception_handler

;EAX = CONTEXT record

mov         eax,[esp+0xc]



;check if Debug Registers Context.Dr0-Dr3 is not zero

cmp         dword [eax+0x04],0

jne           .hardware_bp_found

cmp         dword [eax+0x08],0

jne           .hardware_bp_found

cmp         dword [eax+0x0c],0

jne           .hardware_bp_found

cmp         dword [eax+0x10],0

jne           .hardware_bp_found

jmp          .exception_ret



.hardware_bp_found

;set Context.EAX to signal breakpoint found

mov         dword [eax+0xb0],0xFFFFFFFF



.exception_ret

;set Context.EIP upon return

add          dword [eax+0xb8],6

xor           eax,eax

retn

有些壳也利用调试寄存器的值作为解密密钥的一部分。这些调试寄存器要么初始化为一个特定值要么为0。因此,如果这些调试寄存器被修改,解密将会失败。当解密的代码是受保护的程序或者脱壳代码的一部分的时候,将导致无效指令并造成程序一些意想不到的终止。

对策

如果壳没检测软件断点,逆向分析人员可以尝试使用软件断点,同样OllyDbg的内存读/写断点也可以使用。当逆向分析人员需要设置API断点的时候在native或者是UNICODE版的API内部设软件断点也是可行的。

3.3 Patching Detection via Code Checksum Calculation

补丁检测技术能识别壳的代码是否被修改(代码被修改则意味着反调试例程已经被禁用了),其次也能识别是否设置了软件断点。补丁检测是通过代码校验来实现的,校验计算包括从简单到复杂的校验和/哈希算法。

示例

下面是一个比较简单的校验和计算的例子:

mov                esi,Protected_Code_Start

mov                ecx,Protected_Code_End - Protected_Code_Start

xor                  eax,eax

.checksum_loop

movzx             ebx,byte [esi]

add                 eax,ebx

rol                   eax,1

inc                  esi

loop                .checksum_loop



cmp               eax,dword [.dwCorrectChecksum]

jne                  .patch_found

对策

如果代码校验例程识别出了软件断点,可以用硬件断点来代替。如果校验例程识别出了代码补丁,逆向分析人员可以通过在补丁地址设置内存访问断点来定位校验例程所在,一旦发现了校验例程,可以修改校验和为预期的值或者在比较失败后修改适当的标志。

4反分析技术                                                                  

反分析技术的目标是减缓逆向分析人员对受保护代码和(或)加壳后的程序分析和理解的速度。我们将讨论诸如加密/压缩、垃圾代码、代码变形、反-反编译等技术,这些技术的目的是为了混淆代码、考验耐心、浪费逆向分析人员的时间,解决这些问题需要逆向分析人员拥有耐心、聪慧等品质。

4.1 Encryption and Compression

加密和压缩是最基本的反分析形式。它们初步设防,防止逆向分析人员直接在反编译器内加载受保护的程序然后没有任何困难地开始分析。

加密 壳通常都既加密本身代码也加密受保护的程序。不同的壳所采用的加密算法大不相同,有非常简单的XOR循环,也有执行数次运算的非常复杂的循环。对于某些多态变形壳,为了防止查壳工具正确地识别壳,每次加壳所采用的加密算法都不同,解密代码也通过变形显得很不一样。

解密例程作为一个取数、计算、存诸操作的循环很容易辨认。下面是一个对加密过的DWORD值执行数次XOR操作的简单的解密例程。

0040A07C       LODS DWORD PTR DS:[ESI]

0040A07D       XOR EAX,EBX

0040A07F        SUB EAX,12338CC3

0040A084        ROL EAX,10

0040A087        XOR EAX,799F82D0

0040A08C       STOS DWORD PTR ES:[EDI]

0040A08D       INC EBX

0040A08E        LOOPD SHORT 0040A07C ;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

下面是由同一个多态壳生成的另一段解密例程:

0040C045        MOV CH,BYTE PTR DS:[EDI]

0040C047        ADD EDX,EBX

0040C049        XOR CH,AL

0040C04B        XOR CH,0D9

0040C04E        CLC

0040C04F        MOV BYTE PTR DS:[EDI],CH

0040C051        XCHG AH,AH

0040C053        BTR EDX,EDX

0040C056        MOVSX EBX,CL

::: More garbage code

0040C067        SAR EDX,CL

0040C06C       NOP

0040C06D       DEC EDI

0040C06E        DEC EAX

0040C06F        JMP SHORT 0040C071

0040C071        JNZ 0040C045 ;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 在脱壳的例程中插入垃圾代码是另一种有效地迷惑逆向分析人员的方法。它的目的是在加密例程或者诸如调试器检测这样的反逆向例程中掩盖真正目的的代码。通过将本文描述过的调试器/断点/补丁检测技术隐藏在一大堆无关的、不起作用的、混乱的指令中,垃圾代码可以增加这些检测的效果。此外,有效的垃圾代码是那些看似合法/有用的代码。

示例

下面是一段在相关的指令中插入了垃圾代码的解密例程:

0044A21A        JMP SHORT sample.0044A21F

0044A21C       XOR DWORD PTR SS:[EBP],6E4858D

0044A223        INT 23

0044A225        MOV ESI,DWORD PTR SS:[ESP]

0044A228        MOV EBX,2C322FF0

0044A22D        LEA EAX,DWORD PTR SS:[EBP+6EE5B321]

0044A233        LEA ECX DWORD PTR DS:[ESI+543D583E]

0044A239        ADD EBP,742C0F15

0044A23F        ADD DWORD PTR DS:[ESI],3CB3AA25

0044A245        XOR EDI,7DAC77E3

0044A24B        CMP EAX,ECX

0044A24D       MOV EAX,5ACAC514

0044A252        JMP SHORT sample.0044A257

0044A254        XOR DWORD PTR SS:[EBP],AAE47425

0044A25B        PUSH ES

0044A25C       ADD EBP,5BAC5C22

0044A262         ADC ECX,3D71198C

0044A268        SUB ESI,-4

0044A26B        ADC ECX,3795A210

0044A271        DEC EDI

0044A272        MOV EAX,2F57113F

0044A277        PUSH ECX

0044A278        POP ECX

0044A279        LEA EAX,DWORD PTR SS:[EBP+3402713D]

0044A27F        EDC EDI

0044A280        XOR DWORD PTR DS:[ESI],33B568E3

0044A286         LEA EBX,DWORD PTR DS:[EDI+57DEFEE2]

0044A28C       DEC EDI

0044A28D       SUB EBX,7ECDAE21

0044A293        MOV EDI,185C5C6C

0044A298        MOV EAX,4713E635

0044A29D       MOV EAX,4

0044A2A2       ADD ESI,EAX

0044A2A4       MOV ECX,1010272F

0044A2A9       MOV ECX,7A49B614

0044A2AE       CMP EAX,ECX

0044A2B0        NOT DWORD PTR DS:[ESI]

示例中相关的解密指令是:

0044A225        MOV ESI,DWORD PTR SS:[ESP]

0044A23F        ADD DWORD PTR DS:[ESI],3CB3AA25

0044A268        SUB ESI,-4

0044A280        XOR DWORD PTR DS:[ESI],33B568E3

0044A29D       MOV EAX,4

0044A2A2       ADD ESI,EAX

0044A2B0        NOT DWORD PTR DS:[ESI]

Code Permutation 代码变形是更高级壳使用的另一种技术。通过代码变形,简单的指令变成了复杂的指令序列。这要求壳理解原有的指令并能生成新的执行相同操作的指令序列。

一个简单的指令置换示例:

mov        eax,ebx

test          eax,eax

转换成下列等价的指令:

push        ebx

pop          eax

or            eax,eax

结合垃圾代码使用,代码变形是一种有效地减缓逆向分析人员理解受保护代码速度的技术。

示例

为了说明,下面是一个通过代码变形并在置换后的代码间插入了垃圾代码的调试器检测例程:

004018A8        MOV ECX,A104B412

004018AD       PUSH 004018C1

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

004018C1        SUB ECX,EBX

004018C3        MOV EDX,9A01AB1F

004018C8        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,FAD1203A

004018EA        MOV EBX,51AD5EF2

004018EF        DIV EBX

004018F1               ADD BX,44A5

004018F6               ADD ESI,EAX

004018F8               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 C3                         RET

00401953 FF64A118                     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 C3                         RET

00401963 FF8B40300FB6       DEC DWORD PTR [EBX+0XB60F3040]

00401969 40                          INC EAX

0040196A 0285C0750731       ADD AL,[EBP+0X310775C0]

OllyDbg中的反汇编输出:

0040194A 6854194000           PUSH 00401954

0040194F F9                         STC

00401950 7301                      JNB SHORT 00401953

00401952 C3                         RETN

00401953 FF64A118                     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 C3                         RETN

00401963 FF8B40300FB6       DEC DWORD PTR DS:[EBX+B60F3040]

00401969 40                          INC EAX

0040196A 0285C0750731       ADD AL,BYTE PTR SS:[EBP+310775C0]

最后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 0F8h;

00401960                       db 72h; r

00401961                       db 1

00401962                       db 0C3h;+

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 0C0h;+

0040196D                      db 75h; u

注意所有这三个反编译引擎/调试器是如何落入反-反编译陷阱的,分析这样的反汇编代码对于逆向分析人员来说是很不容易的。还有其它的几种干扰反编译引擎的手段,这只是一个例子。另外这些反-反编译代码可以编码成一个宏,这样汇编源码就清晰多了。

建议读者参考Eldad Eliam13的一本精彩的逆向书籍,里面包含了反-反编译的详细信息和其它一些逆向话题。

5 调试器攻击技术                                                              

本节罗列了壳用来主动攻击调试器的技术,如果进程正在被调试那么执行会突然停止、断点将被禁用。和前面描述的技术类似,结合反-反编译技术隐藏起来使用效果会更佳。

5.1 Misdirection and Stopping Execution via Exceptions

线性地跟踪能够让逆向分析人员容易理解并掌握代码的真正目的。因此壳使用一些技术使得跟踪代码不再是线性的且更加费时。

一个普遍使用的技巧是在脱壳的过程中抛出一些异常,通过抛出一些可捕获的异常,逆向分析人员必需熟悉异常发生的时候EIP指向何处,当异常处理例程执行完之后EIP又指向何处。

另外异常是壳用来反复停止脱壳代码执行的手段之一,因为当进程被调试时抛出异常,调试器会暂停脱壳代码的执行。

壳通常使用结构化异常处理(SEH)14作为异常处理的机制,然而新壳也开始使用向量化异常15。

示例

下面示例代码抛出溢出异常(通过INTO)产生错误,通过数轮循环后由ROL指令来修改溢出标志。但是由于溢出异常是一个陷阱异常,EIP将指向JMP指令。如果逆向分析人员使用OllyDbg并且没有将异常传递给进程(通过Shift+F7/F8/F9)而是继续步进,进程将会进入一个死循环。

;set up exception handler

push         .exception_handler

push         dword [fs:0]

mov         [fs:0],esp



;throw an exception

mov         ecx,1

.loop:

rol            ecx,1

into

jmp          .loop



;restore exception handler

pop          dword [fs:0]

add          esp,4

:::

.exception_handler

;EAX = CONTEXT record

mov         eax,[esp+0xc]

;set   Context.EIP upon return

add          dword [eax+0xb8],2

xor           eax,eax

retn

壳通常会抛出违规访问(0xC0000005)、断点(0x80000003)和单步(0x80000004)异常。

对策

当壳使用可捕获的异常仅仅是为了执行不同的代码时,可以通过选项-> 调试选项 -> 异常选项卡配置OllyDbg使得异常处理例程自动被调用。下面是异常处理配置对话框的屏幕截图。逆向分析人员也可以添加那些不能通过复选框选择的自定义的异常。

当壳在异常处理例程内部执行重要操作时,逆向分析人员可以在异常处理例程中下断,其地址可以在OllyDbg中通过视图->SEH链看到。然后Shift+F7/F8/F9将控制移交给异常处理例程。

5.2 Blocking Input

为了防止逆向分析人员控制调试器,当脱壳主例程运行的时候,壳可以通过调用user32!BlockInput() API 来阻断键盘和鼠标的输入。通过垃圾代码和反-反编译技术进行隐藏使用这种方法,如果逆向分析人员没有识别出来的话是很有效的。一旦生效系统看上去没有反应,只剩下逆向分析人员在那里莫名其妙。

典型的场景可能是逆向分析人员在GetProcAddress()内下断,然后运行脱壳代码直到被断下。但是跳过一段垃圾代码之后壳调用BlockInput()。当GetProcAddress()断点断下来后,逆向分析人员会突然困惑地发现无法控制调试器了,不知究竟发生了什么。

示例

BlockInput()需要一个boolean型的参数fBlockIt。如果这个参数是true,键盘和鼠标事件被阻断;如果是false,键盘和鼠标事件被解除阻断:

; Block input

push                TRUE

call                  [BlockInput]



;...Unpacking code...



;Unblock input

push                FALSE

call                  [BlockInput]

对策

幸好最简单的方法就是补丁 BlockInput()使它直接返回。这是补丁user32!BlockInput()入口的ollyscript脚本:

gpa          "BlockInput","user32.dll"

mov        [$RESULT],#C20400#  //retn 4

Olly Advanced插件同样有补BlockInput()的选项。另外,可以同时按CTRL+ALT+DELETE键手工解除阻断。

5.3 ThreadHideFromDebugger

这项技术用到了常常被用来设置线程优先级的API ntdll!NtSetInformationThread(),不过这个API也能够用来防止调试事件被发往调试器。

NtSetInformationThread()的参数列表如下。要实现这一功能,ThreadHideFromDebugger(0x11)被当作ThreadInformationClass参数传递,ThreadHandle通常设为当前线程的句柄(0xFFFFFFFE):

NTSTATUS NTAPI NtSetInformationThread(

HANDLE                                     ThreadHandle,

THREAD_INFORMATION_CLASS       ThreadInformaitonClass,

PVOID                                               ThreadInformation,

ULONG                                              ThreadInformationLength

);

ThreadHideFromDebugger内部设置内核结构ETHREAD16的HideThreadFromDebugger成员。一旦这个成员设置以后,主要用来向调试器发送事件的内核函数_DbgkpSendApiMessage()将不再被调用。

示例

调用NtSetInformationThread()的一个典型示例:

push         0                                               ;InformationLength

push         NULL                                        ;ThreadInformation

push         ThreadHideFromDebugger           ;0x11

push         0xfffffffe                                          ;GetCurrentThread()

call           [NtSetInformationThread]

对策

可以在ntdll!NtSetInformationThread()里下断,断下来后,逆向分析人员可以操纵EIP防止API调用到达内核,这些都可以通过ollyscript来自动完成。另外,Olly Advanced插件也有补这个API的选项。补过之后一旦ThreadInformaitonClass参数为HideThreadFromDebugger,API将不再深入内核仅仅执行一个简单的返回。

5.4 Disabling Breakpoints

另外一种攻击调试器的方法就是禁用断点。壳通过CONTEXT结构修改调试寄存器来禁用硬件断点。

示例

在这个示例中,通过传入异常处理例程的CONTEXT记录,调试寄存器被清空了。

;set up exception handler

push         .exception_handler

push         dword [fs:0]

mov         [fs:0],esp



;throw an exception

xor           eax,eax

mov         dword [eax],0



;restore exception handler

pop          dword [fs:0]

add          esp,4

:::



.exception_handler

;EAX = CONTEXT record

mov         eax,[esp+0xc]



;Clear Debug Registers: Context.Dr0-Dr3,Dr6,Dr7

mov         dword [eax+0x04],0

mov         dword [eax+0x08],0

mov         dword [eax+0x0C],0

mov         dword [eax+0x10],0

mov         dword [eax+0x14],0

mov         dword [eax+0x18],0



;set Context.EIP upon return

add          dword [eax+0xb8],6

xor           eax,eax

retn

对于软件断点,壳可以直接搜索INT3(0xCC)并用任意/随机的操作码加以替换。这样做以后,软件断点失效并且原始的指令将会被破坏。

对策

显然当硬件断点被检测以后可以用软件断点来代替,反之亦然。如果两者都被检测,可以试试OllyDbg的内存访问/写入断点功能。

5.5 Unhandled Exception Filter

MSDN文档声明当一个异常到达Unhandled Exception Filter(kernel32!UnhandledExceptionFilter)并且程序没有被调试时,Unhandled Exception Filter将会调用在kernel32!SetUnhandledExceptionFilter()API作为参数指定的高层exception Filter。壳利用了这一点,通过设置exception Filter然后抛出异常,如果程序被调试那么这个异常将会被调试器接收,否则,控制被移交到exception Filter运行得以继续。

示例

下面的示例中通过SetUnhandledExceptionFilter()设置了一个高层的exception Filter,然后抛出一个违规访问异常。如果进程被调试,调试器将收到两次异常通知,否则exception Filter将修改CONTEXT.EIP并继续执行。

;set the exception filter

push                .exception_filter

call                  [SetUnhandledExceptionFilter]

mov                 [.original_filter],eax



;throw an exception

xor                  eax,eax

mov                 dword [eax],0



;restore exception filter

push                dword [.original_filter]

call                  [SetUnhandledExceptionFilter]



:::



.exception_filter:

;EAX = ExceptionInfo.ContextRecord

mov               eax,[esp+4]

mov               eax,[eax+4]



;set return EIP upon return

add                 dword [eax+0xb8],6



;return EXCEPTION_CONTINUE_EXECUTION

mov               eax,0xffffffff

retn

有些壳并不调用SetUnhandledExceptionFilter()而是直接通过kernel32!_BasepCurrentTopLevelFilter手工设置exception Filter,以防逆向分析人员在那个API上下断。

对策

有意思的是kernel32!UnhandledExceptionFilter()内部实现代码是使用ntdll!NtQueryInformationProcess(ProcessDebugPort)来确定进程是否被调试,从而决定是否调用已注册的exception Filter。因此,处理方法和DebugPort调试器检测技术相同。

5.6 OllyDbg:OutputDebugString() Format String Bug

这个调试器攻击手段只对OllyDbg有效。已知OllyDbg面对能导致崩溃或执行任意代码的格式化字符串漏洞是脆弱的,这个漏洞是由于向kernel32!OutputDebugString()传递了不当的字符串参数引起的。这个漏洞在当前OllyDbg(1.10)依然存在并且仍然没有打补丁。

示例

下面这个简单的示例将导致OllyDbg抛出违规访问异常或不可预期的终止。

push         .szFormatString

call                  [OutputDebugStringA]

:::

.szFormatString db "%s%s",0

对策

可以通过补丁 kernel32!OutputDebugStringA()入口使之直接返回来加以解决。

6. 高级及其它技术                                                        

本节罗列了不属于前面任一分类的一些高级和其它的反逆向技术。

6.1 Process Injection

进程注入已经成为某些壳的一个特点。脱壳代码打开一个选定的宿主进程(自身、explorer.exe、iexplorer.exe等)然后将脱壳后的程序注入到这个宿主进程。

下面是一个支持进程注入的壳的屏幕截图。

恶意代码利用壳的这个特点使它们能躲过一些防火墙,这些防火墙通过检查进程是否在获准进行外部网络连接的应用程序列表中而决定是否放行。

壳所采用的执行进程注入的一种方法如下:

1. 向kernel32!CreateProcess()传递CREATE_SUSPENDED进程创建标志,将宿主进程作为一个挂起的子进程打开。这时一个初始化了的线程被创建并挂起,由于loader例程(ntdll!LrdInitializeThunk)还没有被调用,DLL还没有被载入。这个线程的上下文中包含PEB地址、宿主进程入口点信息的寄存器值被设置。

2. 使用kernel32!GetThreadContext()获取子进程初始化线程的上下文。

3. 通过CONTEXT.EBX获取子进程的PEB地址。

4. 读PEB.ImageBase(PEB+0x8)获取子进程的映像基址。

5. 将BaseAddress参数指向检索到的映像基址,调用ntdll!NtUnmapViewOfSection()来unmap子进程中的原始宿主映像。

6. 脱壳代码使用kernel32!VirtualAllocEx()在子进程中分配一段内存,dwSize参数等于脱壳后程序的映像大小。

7. 使用kernel32!WriteProcessMemory()将脱壳后的程序的PE头和每个节写入子进程。

8. 将子进程的PEB.ImageBase更新以匹配脱壳后的程序映像基址。

9. 通过kernel32!SetThreadContext()更新子进程初始化线程的上下文,将其中的CONTEXT.EAX设置为脱壳后程序的入口点。

10. 通过kernel32!ResumeThread()恢复子进程的执行。

为了从入口点开始调试打开的子进程,逆向分析人员可以在WriteProcessMemory()中设置断点,当包含入口点的节被写入子进程的时候,将入口点代码补丁为”跳往自身”指令(0xEB0xFE)。当子进程的主线程被恢复,子进程将在入口点进入一个死循环。这时逆向分析人员就可以附加一个调试器到子进程,恢复被修改的指令,继续正常的调试。

6.2 Debugger Blocker

Armadillo壳引入了称之为Debugger Blocker的功能,它可以阻止逆向分析人员将调试器附加到一个受保护的进程。这个保护是通过调用Windows提供的调试函数来实现的。

具体来说就是脱壳代码扮演一个调试器的角色(父进程),通过它打开、调试/控制包含脱壳后程序的子进程。

由于受保护的进程已经被调试,通过kernel32!DebugActiveProcess()来附加调试器将会失败,原因是相应的native API ntdll!NtDebugActiveProcess()将返回STATUS_PORT_ALREADY_SET。 NtDebugActiveProcess()的失败的根本原因在于内核结构EPROCESS的DebugPort成员已经被设置过了。

为了附加调试器到受保护的进程,好几个逆向论坛发布的解决方法是在父进程的上下文里调用dernel32!DebugActiveProcessStop()。可以通过附加调试器到父进程,在kernel32!WaitForDebugEvent()内部下断,断下来后,注入一段调用DebugActiveProcessStop(childProcessID)的代码并执行,一旦调用成功,这时就可以附加调试器到受保护的进程了。

6.3 TLS Callbacks

另一个被壳使用的技术就是在实际的入口点代码执行之前执行代码,这是通过使用Thread Local Storage (TLS)回调函数来实现的。壳通过这些回调函数执行调试器检测及解密例程,这样逆向分析人员将无法跟踪这些例程。

TLS回调可以使用诸如pedump之类的PE文件分析工具来识别。如果可执行文件中存在TLS条目,数据条目将会显示出来。

Data directory

EXPORT                        rva:00000000  size:00000000

IMPORT                        rva:00061000  size:000000E0

:::

TLS                               rva:000610E0 size:00000018

:::

IAT                                rva:00000000  size:00000000

DELAY_IMPORT            rva:00000000  size:00000000

COM_DESCRPTR          rva:00000000  size:00000000

unused                           rva:00000000  size:00000000

接着显示TLS条目的实际内容。AddressOfCallBacks成员指向一个以null结尾的回调函数数组。

TLS directory:

StartAddressOfRawData:               00000000

EndAddressOfRawData:                 00000000

AddressOfIndex:                           004610F8

AddressOfCallBacks:                  004610FC

SizeOfZeroFill:                              00000000

Characteristics:                             00000000

在这个例子中,RVA 0x4610fc指向回调函数指针(0x490f43和0x44654e):

默认情况下OllyDbg载入这个例子将会暂停在入口点。由于TLS回调函数是在实际的入口点执行之前被调用的,OllyDbg应该配置一下使其在TLS回调被调用之前中断在实际的loader。

可以通过选择选项->调试选项->事件->第一次中断于->系统断点来设置中断于ntdll.dll内的实际loader代码。

这样设置以后,OllyDbg将会中断在位于执行TLS回调的ntdll!LdrpRunInitializeRoutines()之前的ntdll!_LdrpInitializeProcess(),这时就可以在回调例程中下断并跟踪了。

关于PE文件格式的更多信息及包括pedump的二进制/源码可以在如下的链接获得:

An In-Depth Look into the Win32 Portable Executable File Format by Matt Pietrek

http://msdn.microsoft.com/msdnmag/issues/02/02/PE/default.aspx

An In-Depth Look into the Win32 Portable Executable File Format,Part 2 by Matt Pietrek

http://msdn.microsoft.com/msdnmag/issues/02/03/PE2/

最新版本的微软PE文件格式可以通过如下链接获得:

Microsoft Portable Executable and Common Object File Format Specification

http://www.microsoft.com/whdc/sy ... irmware/PECOFF.mspx

6.4 Stolen Bytes

代码抽取基本上就是壳移走受保护程序的一部分(通常是入口点的少量指令),这部分指令被复制并在分配的内存中执行。这在某种程度上保护了程序,因为如果从内存中dump受保护进程,被抽取的指令将不会被恢复。

这是一个可执行文件的原始入口点代码:

004011CB         MOV EAX,DWORD PTR FS:[0]

004011D1         PUSH EBP

004011D2         MOV EBP,ESP

004011D4         PUSH -1

004011D6         PUSH 0047401C

004011DB        PUSH 0040109A

004011E0         PUSH EAX

004011E1         MOV DWORD PTR FS:[0],ESP

004011E8         SUB ESP,10

004011EB         PUSH EBX

004011EC         PUSH ESI

004011ED        PUSH EDI

下面是被Enigma加密壳偷取了前两个指令的同一段代码:

004011CB        POP EBX

004011CC        CMP EBX,EBX

004011CE        DEC ESP

004011CF        POP ES

004011D0         JECXZ SHORT 00401169

004011D2         MOV EBP,ESP

004011D4         PUSH -1

004011D6         PUSH 0047401C

004011DB        PUSH 0040109A

004011E0         PUSH EAX

004011E1         MOV DWORD PTR FS:[0],ESP

004011E8         SUB ESP,10

004011EB         PUSH EBX

004011EC         PUSH ESI

004011ED        PUSH EDI

这是被ASProtect壳偷取了数条指令的相同例子。它增加了一条jump指令,指向内存中一段执行被偷代码的过程,被偷的指令和垃圾代码搀杂在一起,想要恢复被偷的代码困难重重。

004011CB        JMP 00B70361

004011D0         JNO SHORT 00401198

004011D3         INC EBX

004011D4         ADC AL,0B3

004011D6         JL SHORT 00401196

004011D8         INT1

004011D9         LAHF

004011DA        PUSHFD

004011DB        MOV EBX,1D0F0294

004011E0         PUSH ES

004011E1         MOV EBX,A732F973

004011E6         ADC BYTE PTR DS:[EDX-E],CH

004011E9         MOV ECX,EBP

004011EB         DAS

004011EC         DAA

004011ED        AND DWORD PTR DS:[EBX+58BA76D7],ECX

6.5 API Redirection

API重定向是用来防止逆向分析人员轻易重建受保护程序输入表的一种方法。原始的输入表被销毁,对API的调用被重定向到位于内存中的例程,然后由这些例程负责调用实际的API。

在这个例子中代码调用了kernel32!CopyFileA() API:

00404F05         LEA EDI,DWORD PTR SS:[EBP-20C]

00404FOB        PUSH EDI

00404FOC        PUSH DWORD PTR SS:[EBP-210]

00404F12         CALL <JMP.&KERNEL32.CopyFileA>

被调用的代码是一个JMP指令,跳转到输入表中的函数地址。

004056B8         JMP DWORD PTR DS:[<&KERNEL32.CopyFileA>]

然而当ASProtect壳重定向KERNEL32!CopyFileA() API时,这段代码被修改为一个call指令,调用壳自己分配的内存中的过程。

004056B8         CALL 00D90000

下图说明了被偷的指令是如何被安置的。前7条KERNEL32!CopyFileA()代码中的指令被复制过来,另外0x7C83005E Call指令指向的代码也被复制过来。通过一个RETN指令,将控制移交回kernel32.dll领空KERNEL32!CopyFileA()中间的0x7C830063地址处:

有些壳则更进一步将整个DLL映像载入到一段分配的内存中,然后重定向API调用到这些DLL映像的拷贝。 这个技术使得在实际的API中下断点变难了。

6.6 Multi-Threaded Packers

对于多线程壳,另一个线程常常用于执行一些诸如解密受保护程序这样必需的操作。多线程壳复杂度增加了,由于跟踪代码变得复杂,理解代码的难度也大大增加了。

PECrypt是一款多线程壳壳,它用第2个线程来解密数据,然后这些数据被主线程使用,这些线程之间通过事件对象进行同步。

PECrypt壳操作并同步线程:

6.7 Virtual Machines

使用虚拟机的概念很简单:逆向分析人员最终会想出如何躲过/解决反调试和反逆向技术,当受保护的程序最终需要在内存中解密并执行时,面对静态分析就显得脆弱不堪了。

随着虚拟机的出现,受保护部分的代码被转换成了p-code,p-code在执行时可以转换成机器码。原始的机器指令被替换,理解代码所作所为的复杂度成指数上升。

下面是这个概念的简单图示:

像Oreans technologies的CodeVirtualizer和StraForce这些最新的壳都应用了虚拟机的概念来保护程序。

对付虚拟机需要分析p-code是如果组织并被虚拟机转换的,尽管这一切并不简单。获得足够的信息之后,就可以开发一款反编译引擎来分析P-code并将它们转换成机器码或者是可理解的指令。

一个开发p-code反编译引擎的例子和虚拟机实现的详细信息可以通过如下链接获得:

Defeating HyperUnpackMe2 With an IDA Processor Module, Rolf Rolles III

http://www.openrce.org/articles/full_view/28

7. 工具                                                                        

本节列举了逆向分析人员和恶意代码分析人员可以用来分析、脱壳的公开可用的工具。

免责声明:这些都是第三方工具,笔者对这些工具可能导致的系统不稳定和可能影响系统的其他问题不负任何责任。建议总是在测试或恶意代码分析环境中运行这些工具。

7.1 OllyDbg

http://www.ollydbg.de/

逆向分析人员和恶意代码分析人员使用的一款强大Ring3调试器。它的插件功能允许其他的逆向分析人员创建更多的插件,使得逆向和脱壳变得越来越简单。

7.2 Ollyscript

http://www.openrce.org/downloads/details/106/OllyScript

一个OllyDbg的插件,允许通过使用类似于汇编语言的脚本实现自动设置/处理断点、补丁代码/数据等。在执行重复性的工作或者是自动脱壳是尤其有用。

7.3 Olly Advanced

http://www.openrce.org/downloads/details/241/Olly_Advanced

针对逆向分析人员如果说壳有盔甲的话,那么这个OllyDbg的插件就是逆向分析人员调试器的盔甲。它有很多选项用来躲过反调试技术,隐藏OllyDbg使其不被壳检测到。

7.4 OllyDump

http://www.openrce.org/downloads/details/108/OllyDump

成功脱壳后,这个OllyDbg插件可以用来dump进程并且重建输入表。

7.5 ImpRec

http://www.woodmann.com/crackz/Unpackers/Imprec16.zip

最后,这是另一款dump进程和重建输入表的工具。它是一款独立的工具,它提供了最出色的输入表重建能力。
PYG19周年生日快乐!
  • TA的每日心情
    开心
    2024-1-22 10:56
  • 签到天数: 167 天

    [LV.7]常住居民III

    发表于 2015-10-29 11:15:12 | 显示全部楼层
    学习一下,谢谢分享
    PYG19周年生日快乐!
  • TA的每日心情
    开心
    2016-4-1 13:26
  • 签到天数: 4 天

    [LV.2]偶尔看看I

    发表于 2016-3-31 14:30:23 | 显示全部楼层
    进来学习学习
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2017-9-29 13:03
  • 签到天数: 271 天

    [LV.8]以坛为家I

    发表于 2016-6-8 18:24:38 | 显示全部楼层
    谢谢分享,支持楼猪
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 加入我们

    本版积分规则

    快速回复 返回顶部 返回列表