- 献词
- 致谢
- 前言
- 第一部分 IDA 简介
- 第 1 章 反汇编简介
- 第 2 章 逆向与反汇编工具
- 第 3 章 IDA Pro 背景知识
- 第二部分 IDA 基本用法
- 第 4 章 IDA 入门
- 第 5 章 IDA 数据显示窗口
- 第 6 章 反汇编导航
- 第 7 章 反汇编操作
- 第 8 章 数据类型与数据结构
- 第 9 章 交叉引用与绘图功能
- 第 10 章 IDA 的多种面孔
- 第三部分 IDA 高级应用
- 第 11 章 定制 IDA
- 第 12 章 使用 FLIRT 签名来识别库
- 第 13 章 扩展 IDA 的知识
- 第 14 章 修补二进制文件及其他 IDA 限制
- 第四部分 扩展 IDA 的功能
- 第 15 章 编写 IDA 脚本
- 第 16 章 IDA 软件开发工具包
- 第 17 章 IDA 插件体系结构
- 第 18 章 二进制文件与 IDA 加载器模块
- 第 19 章 IDA 处理器模块
- 第五部分 实际应用
- 第 20 章 编译器变体
- 第 21 章 模糊代码分析
- 第 22 章 漏洞分析
- 第 23 章 实用 IDA 插件
- 第六部分 IDA 调试器
- 第 24 章 IDA 调试器
- 第 25 章 反汇编器/ 调试器集成
- 第 26 章 其他调试功能
- 附录 A 使用 IDA 免费版本 5.0
- 附录 B IDC/SDK 交叉引用
21.1 反静态分析技巧
反静态分析技巧的主要目的是防止分析人员不必运行程序就知道该程序的用途。这些技巧所针对的目标恰恰是 IDA 之类的反汇编器。因此,如果你选择使用 IDA 逆向工程二进制文件,它们应引起你的极大关注。下面介绍几种反静态分析技巧。
21.1.1 反汇编去同步
这是一种较为古老的技巧,它专门破坏反汇编过程,即创造性地使用指令和数据,以阻止反汇编器找到一条或多条指令的起始地址。通常,这种方法将令反汇编器“迷失自己”,无法生成反汇编代码清单,或者至少生成错误的反汇编代码清单。
在下面的代码中,IDA 正努力反汇编 Shiva1 反逆向工程工具:
1. 2003 年,Shaun Clowes 和 Neel Mehta 首次在 CanSecWest 上推出 Shiva。参见: http://www.cansecwest.com/core03/shiva.ppt 。
LOAD:0A04B0D1 call ➊ near ptr loc_A04B0D6+1 LOAD:0A04B0D6 LOAD:0A04B0D6 loc_A04B0D6: ; CODE XREF: start+11 ↓ p ➋ LOAD:0A04B0D6 mov dword ptr [eax-73h], 0FFEB0A40h LOAD:0A04B0D6 start endp LOAD:0A04B0D6 LOAD:0A04B0DD LOAD:0A04B0DD loc_A04B0DD: ; CODE XREF: LOAD:0A04B14C↓ j LOAD:0A04B0DD loopne loc_A04B06F LOAD:0A04B0DF mov dword ptr [eax+56h], 5CDAB950h ➌ LOAD:0A04B0E6 iret LOAD:0A04B0E6 ;--------------------------------------------------------------- ➍ LOAD:0A04B0E7 db 47h LOAD:0A04B0E8 db 31h, 0FFh, 66h LOAD:0A04B0EB ;--------------------------------------------------------------- LOAD:0A04B0EB LOAD:0A04B0EB loc_A04B0EB: ; CODE XREF: LOAD:0A04B098↑ j LOAD:0A04B0EB mov edi, 0C7810D98h
这个例子执行了一次调用(➊,使用跳转会更加方便),调用对象在现有指令的中间(➋)。由于 IDA 认为这个函数调用将会返回,它继续反汇编地址 0A04B0D6
(➋)处的指令(并不正确)。调用指令的真正目标—— loc_A04B0D6+1(0A04B0D7) ——
将不会被反汇编,因为相关字节已经作为 0A04B0D6
处的 5 字节指令的一部分分配。如果注意到这种情况,剩下的反汇编代码清单应引起我们的怀疑。其他证据包括出人意料的用户空间指令(➌,这里为 iret
2 )及杂项数据类型(➍)。
2. x86 iret
指令用于从一个中断处理例程返回。中断处理例程最常见于内核空间。
注意,这种行为并不仅限于 IDA 。无论它们使用的是递归下降算法还是线性扫描算法,几乎所有反汇编器都会成为这种技巧的受害者。
处理这种情况的正确方法是对包含调用目标字节的指令取消定义,然后在调用目标地址处定义一条指令,以重新同步反汇编代码清单。当然,使用交互式反汇编器可大大简化这个过程。使用 IDA ,将光标放在处,应用 Edit ▶Undefine(热键 U),然后将光标放在 0A04B0D7
地址处,应用 Edit ▶Code(热键 C),可以得到下面的代码:
LOAD:0A04B0D1 call loc_A04B0D7 LOAD:0A04B0D1 ;------------------------------------------------------------ ➊ LOAD:0A04B0D6 db 0C7h ; ¦ LOAD:0A04B0D7 ;------------------------------------------------------------ LOAD:0A04B0D7 LOAD:0A04B0D7 loc_A04B0D7: ; CODE XREF: start+11↑ p ➋ LOAD:0A04B0D7 pop eax LOAD:0A04B0D8 lea eax, [eax+0Ah] LOAD:0A04B0DB LOAD:0A04B0DB loc_A04B0DB: ; CODE XREF: start:loc_A04B0DB ↑ j ➌ LOAD:0A04B0DB jmp short near ptr loc_A04B0DB+1 LOAD:0A04B0DB start endp LOAD:0A04B0DB LOAD:0A04B0DB ;------------------------------------------------------------ LOAD:0A04B0DD db 0E0h ; a
从这个代码段中,我们发现,很明显,地址 0A04B0D6
(➊)处的字节从未执行。地址 0A04B0D7
(➋)(调用目标)的指令用于从栈上删除返回地址(来自虚假调用),然后执行继续。值得注意的是,不久之后,我们这里讨论的反逆向工程技巧又被重新利用,这次它使用的是地址 0A04B0DB
(➌)处的一个 2 字节跳转指令,它实际上跳转到自身之中。这时,我们同样必须取消一条指令的定义,以到达下一条指令的开始位置。再一次应用取消定义(地址 0A04B0DB
处)和重新定义(地址 0A04B0DC
处)过程,得到下面的反汇编代码清单:
➋ LOAD:0A04B0D7 pop eax ➌ LOAD:0A04B0D8 lea eax, [eax+0Ah] LOAD:0A04B0D8 ; -------------------------------------------------------------- LOAD:0A04B0DB db 0EBh ; d LOAD:0A04B0DC ; -------------------------------------------------------------- ➊ LOAD:0A04B0DC jmp eax LOAD:0A04B0DC start endp
结果,跳转指令的目标是另一条跳转指令(➊)。但是,反汇编器不可能跟踪这里的跳转(分析人员也会感到困惑),因为跳转的目标包含在寄存器(EAX)中,并在运行时计算。这是另一种类型的反静态分析技巧,将在 21.1.2 节中讨论。在这个例子中,鉴于在跳转之前的指令序列相对简单,确定 EAX 寄存器包含的值并不是非常困难。➋处的 pop
指令将前一个例子中调用指令( 0A04B0D6
)的返回地址加载到 EAX 寄存器中,随后的指令(➌)再给 EAX 加上 10。因此,跳转指令的目标为 0A04B0E0
,我们必须从这个地址恢复反汇编过程。
最后一个去同步的例子摘自一个不同的二进制文件,它说明如何使用处理器标志将条件跳转转换成绝对跳转。下面的反汇编代码清单说明如何使用 x86 Z 标志实现这个目的:
➊ .text:00401000 xor eax, eax ➋ .text:00401002 jz short near ptr loc_401009+1 ➌ .text:00401004 mov ebx, [eax] ➍ .text:00401006 mov [ecx-4], ebx .text:00401009 .text:00401009 loc_401009: ; CODE XREF: .text:00401002↑ j ➎ .text:00401009 call near ptr 0ADFEFFC6h .text:0040100E ficom word ptr [eax+59h]
这里的 xor 指令(➊)用于清零 EAX 寄存器和设置 x86 Z 标志。知道设置 Z 标志后,程序员利用一个始终被接受的“遇零跳转”(jz)指令(➋)实现无条件跳转。因此,跳转与跳转目标之间的指令➌和➍从未执行,仅起到迷惑分析人员的作用。值得注意的是,这个例子同样通过跳转到一条指令中间(➎),隐藏了真实的跳转目标。正确反汇编后的代码如下所示:
.text:00401000 xor eax, eax .text:00401002 jz short loc_40100A .text:00401004 mov ebx, [eax] .text:00401006 mov [ecx-4], ebx .text:00401006 ; ------------------------------------------------------------- ➋ .text:00401009 db 0E8h ; F .text:0040100A ; ------------------------------------------------------------- .text:0040100A .text:0040100A loc_40100A: ; CODE XREF: .text:00401002↑ j ➊ .text:0040100A mov eax, 0DEADBEEFh .text:0040100F push eax .text:00401010 pop ecx
由于额外的字节(➋)首先导致了去同步,跳转的真实目标(➊)已经显露出来。当然,在执行条件跳转之前,你可以使用更加迂回的方式设置和检查标志。在检查 CPU 标志位的值之前,影响标志位的操作数量越多,分析这类代码的困难程度就越大。
21.1.2 动态计算目标地址
请不要将本节的标题与反动态分析技巧混淆。这里的“动态计算”指接下来的执行地址在运行时计算得出。在本节中,我们讨论几种获取这种地址的方法。这类技巧的目的是隐藏(模糊)一个二进制文件的真实控制流路径,以阻止他人进行静态分析。
上一节中有一个应用这种技巧的例子。这个例子使用一个 call
语句将一个返回地址压入栈中。然后,这个返回地址直接由栈进入寄存器,再给寄存器加上一个常量值,得到最后的目标地址。最终,通过执行一个跳转指令,跳转到寄存器内容指定的位置,再到达目标地址。
我们可以开发无数类似的代码序列,获得一个目标地址,并将控制权转交给这个地址。下面的代码将最初的启动顺序包装在 Shiva 中,提供了另一种动态计算目标地址的方法:
LOAD:0A04B3BE mov ecx, 7F131760h ; ecx = 7F131760 LOAD:0A04B3C3 xor edi, edi ; edi = 00000000 LOAD:0A04B3C5 mov di, 1156h ; edi = 00001156 LOAD:0A04B3C9 add edi, 133AC000h ; edi = 133AD156 LOAD:0A04B3CF xor ecx, edi ; ecx = 6C29C636 LOAD:0A04B3D1 sub ecx, 622545CEh ; ecx = 0A048068 LOAD:0A04B3D7 mov edi, ecx ; edi = 0A048068 LOAD:0A04B3D9 pop eax LOAD:0A04B3DA pop esi LOAD:0A04B3DB pop ebx LOAD:0A04B3DC pop edx LOAD:0A04B3DD pop ecx ➊ LOAD:0A04B3DE xchg edi, [esp] ; TOS = 0A048068 LOAD:0A04B3E1 retn ; return to 0A048068
右边的注释记录了每一条指令对各种 CPU 寄存器所做的更改。这个过程以一个获取到的值被移入栈顶部( TOS
)而告终(➊ ),从而使返回指令将控制权转交给计算得出的位置(这里为 0A04B068
)。这样的代码序列能够显著增加静态分析的工作量,因为分析人员必须动手运行代码,才能确定程序的具体控制流路径。
近些年来,我们已经开发并利用更加复杂的控制流隐藏技巧。在最复杂的情形中,一个程序将使用多个线程或子进程计算控制流信息,并通过某种形式的进程间通信(对于子进程)或线程同步(对于多线程)接收这些信息。在这类情况下,进行静态分析将会非常困难,因为你不仅需要理解多个可执行实体的行为,而且需要了解这些实体交换信息的方式。例如,一个线程在一个共享的 semaphore3 对象上等待,而第二个线程则计算值或修改代码,一旦第二个线程通过 semaphore 发出操作完成的信号,第一个线程将利用第二个线程的操作结果。
3. 可以把一个 semaphore 看成是一个令牌,在进入一个房间执行某种操作之前,你必须拥有这个令牌。如果你拥有这个令牌,其他人将不能进入房间。当你在房间中完成任务后,你可以离开,并将令牌交给其他人,然后,这个人将进入房间,并利用你刚刚完成的工作(你并不知道这一点,因为你这时并不在房间内)。semaphore 常用于对程序的代码或数据实施互斥锁。
另一种技巧常用在面向 Windows 的恶意软件中,它配置一个异常处理程序4 ,并有意触发一个异常,然后在处理异常时操纵进程的寄存器的状态。下面的例子被 tElock 反逆向工程工具用于隐藏程序的真实控制流:
4. 有关 Windows 结构化异常处理(SEH )的更多信息,参见 http://www.microsoft.com/msj/0197/exception/exception.aspx 。
➊ .shrink:0041D07A call $+5 ➋ .shrink:0041D07F pop ebp ➌ .shrink:0041D080 lea eax, [ebp+46h] ; eax holds 0041D07F + 46h .shrink:0041D081 inc ebp ➍ .shrink:0041D083 push eax .shrink:0041D084 xor eax, eax ➎ .shrink:0041D086 push dword ptr fs:[eax] ➏ .shrink:0041D089 mov fs:[eax], esp ➐ .shrink:0041D08C int 3 ; Trap to Debugger .shrink:0041D08D nop .shrink:0041D08E mov eax, eax .shrink:0041D090 stc .shrink:0041D091 nop .shrink:0041D092 lea eax, ds:1234h[ebx*2] .shrink:0041D099 clc .shrink:0041D09A nop .shrink:0041D09B shr ebx, 5 .shrink:0041D09E cld .shrink:0041D09F nop .shrink:0041D0A0 rol eax, 7 .shrink:0041D0A3 nop .shrink:0041D0A4 nop ➑ .shrink:0041D0A5 xor ebx, ebx ➒ .shrink:0041D0A7 div ebx ; Divide by zero .shrink:0041D0A9 pop dword ptr fs:0
首先,这段代码使用一个调用指令(➊)调用下一条指令(➋),这个调用指令将 0041D07F
作为返回地址压入栈中,随后这个返回地址立即由栈进入 EBP 寄存器(➋)。接下来(➌),EAX 寄存器被设置为 EBP 和 46h
的和,即 0041D0C5
,并将这个地址作为一个异常处理函数的地址压入栈中(➍ )。剩下的异常处理程序设置在➎ 和➏ 处发生,它们将新的异常处理程序链接到由 fs:[0]
5 引用的现有异常处理程序链中。下一步是有意生成一个异常(➐),这里为 int 3
,它是调试器使用的一个软件陷阱(中断)。在 x86 程序中, int 3
指令被调试器用于实现一个软件断点。正常情况下,这时一个依附于进程的调试器将获得控制权。实际上,如果一个调试器已经依附于进程,它将有机会第一个处理异常(把它看成是一个断点)。在这个例子中,程序已做好处理异常的准备,因此,任何依附的调试器应将异常递交给程序处理。无法使程序处理异常可能会导致错误操作,甚至会使程序崩溃。如果不了解如何处理 int 3
异常,你将无法知道这个程序下一步将如何执行。如果我们假定程序会在 int 3
后继续执行,那么最终指令➑和➒将触发一个“除以零”异常。
5. Windows 配置 FS 寄存器指向当前线程环境块(TEB )的基址。TEB 中的第一项(偏移量为 0)是一个指向异常处理函数的指针链接表中的第一个指针,如果程序中出现异常,即调用上面的异常处理函数。
与前面的代码有关的异常处理程序从地址 0041D0C5
开始。这个函数的第一部分如下所示:
.shrink:0041D0C5 sub_41D0C5 proc near ; DATA XREF: .stack:0012FF9C ↑o .shrink:0041D0C5 .shrink:0041D0C5 pEXCEPTION_RECORD = dword ptr 4 .shrink:0041D0C5 arg_4 = dword ptr 8 ➊ .shrink:0041D0C5 pCONTEXT = dword ptr 0Ch .shrink:0041D0C5 ➍ .shrink:0041D0C5 mov eax, [esp+pEXCEPTION_RECORD] ➋ .shrink:0041D0C9 mov ecx, [esp+pCONTEXT] ; Address of SEH CONTEXT ➌ .shrink:0041D0CD inc [ecx+CONTEXT._Eip] ; Modify saved eip ➎ .shrink:0041D0D3 mov eax, [eax] ; Obtain exception type ➏ .shrink:0041D0D5 cmp eax, EXCEPTION_INT_DIVIDE_BY_ZERO .shrink:0041D0DA jnz short loc_41D100 .shrink:0041D0DC inc [ecx+CONTEXT._Eip] ; Modify eip again ➐ .shrink:0041D0E2 xor eax, eax ; Zero x86 debug registers .shrink:0041D0E4 and [ecx+CONTEXT.Dr0], eax .shrink:0041D0E7 and [ecx+CONTEXT.Dr1], eax .shrink:0041D0EA and [ecx+CONTEXT.Dr2], eax .shrink:0041D0ED and [ecx+CONTEXT.Dr3], eax .shrink:0041D0F0 and [ecx+CONTEXT.Dr6], 0FFFF0FF0h .shrink:0041D0F7 and [ecx+CONTEXT.Dr7], 0DC00h .shrink:0041D0FE jmp short locret_41D160
传递给异常处理函数的第三个参数(➊ )是一个指向一个 Windows CONTEXT
结构体(在 Windows API 头文件 winnt.h 中定义)的指针。 CONTEXT
结构体使用异常发生时所有 CPU 寄存器的内容进行初始化。一个异常处理程序有机会检查和修改(如有必要) CONTEXT
结构体的内容。如果异常处理程序认为它已经更正了导致异常的问题,它可以通知操作系统,允许导致异常的线程继续执行。这时,操作系统会从提供给异常处理程序的 CONTEXT
结构体中,为这个线程重新加载 CPU 寄存器,线程将恢复执行,就好像什么也没有发生一样。
在上面的例子中,异常处理程序首先访问线程的 CONTEXT
结构体(➋),以递增指令指针(➌),从而移动到生成异常的指令之外。接下来,异常的类型代码 [EXCEPTION_RECORD
➍ 中的一个字段]被检索(➎),以确定异常的性质。这部分的异常处理程序通过将所有 x86 硬件调试寄存器6 清零(➐),处理前一个例子中生成的“除以零”错误(➏)。如果不分析剩余的 tElock 代码,你不能立即了解清零调试寄存器的原因。在这个例子中,tElock 正清除前一个操作的值,在前一个操作中,它使用调试寄存器设置了 4 个断点以及我们前面看到的 int 3
。除了模糊程序的真正控制流外,清除或修改 x86 调试寄存器可能会对应用软件调试器(如 OllyDbg)或 IDA 的内部调试器造成重大影响。这类反调试技巧将在 21.2 节中讨论。
6. 在 x86 中,调试寄存器 0~7(Dr0~Dr7)用于控制硬件辅助断点的使用。Dr0~Dr3 用于指定断点地址,而 Dr6 和 Dr7 则用于启用和禁用特定的硬件断点。
操作码模糊
到目前为止,我们讨论的技巧可以形成(实际上,旨在形成)一种障碍,以防止他人了解程序的控制流。但是,还没有一种技巧能够阻止你查看你所分析的程序的正确反汇编代码清单。去同步会在很大程度上影响反汇编代码清单,但是,通过重新格式化反汇编代码清单,使其反映正确的指令流,你就可以轻易破坏这种技巧。
阻止正确反汇编的一种更加有效的方法是在创建可执行文件时编码或加密具体的指令。模糊指令对 CPU 没有用处,在被 CPU 提取并执行之前,它们必须经过去模糊处理,以恢复到原始状态。因此,程序必须至少有一个部分没有被加密,以充当启动例程。在模糊程序中,启动例程通常负责对一些或所有的剩余程序进行去模糊处理。模糊过程的一般概况如图 21-1 所示。
图 21-1 常规模糊过程
如图 21-1 所示,模糊过程的输入是用户出于某种原因希望进行模糊的程序。许多时候,输入的程序使用标准编程语言和构建工具(编辑器、编译器等)编写,并且很少考虑到将要进行模糊处理。生成的可执行文件被输入到一个模糊实用工具中,后者将原始程序转换成一个功能相同但经过模糊的二进制文件。模糊实用工具负责模糊原始程序的代码和数据节,并增加另一段代码(一个去模糊存根),在运行时访问原始功能之前,这个存根负责对代码和数据进行去模糊处理。模糊实用工具还修改程序的头部,将程序的入口点重定向到去模糊存根,确保从去模糊过程开始执行。在去模糊后,执行通常会进入原始程序的入口点,这时,程序将开始执行,就好像它根本没有被模糊处理一样。
用于创建模糊二进制文件的模糊实用工具不同,这个过于简化的模糊过程也明显不同。可用于处理模糊过程的实用工具日益增多。这类实用工具提供的功能包括:压缩、反—反汇编和反调试技巧。相关程序包括:UPX7 (压缩器,也用于 ELF )、ASPack8 (压缩器)、ASProtect(ASPack 的制造者开发的反逆向工程工具)、用于 Windows PE 文件的 tElock9 (压缩和反逆向工程工具)、用于 Linux ELF 二进制文件的 Burneye10 (加密)和 Shiva11 (加密和反调试)。模糊实用工具的功能已经取得很大的进步,一些反逆向工程工具,如 WinLicense12 ,能够为整个构建过程提供更紧密的集成,允许程序员在构建过程的每一个步骤(从源代码到已编译二进制文件的后处理阶段)集成反逆向工程功能。
7. 参见 http://upx.sourceforge.net/ 。
8. 参见 http://www.aspack.com/ 。
9. 参见 http://www.softpedia.com/get/Programming/Packers-Crypters-Protectors/Telock.shtml 。
10. 参见 http://packetstormsecurity.org/groups/teso/indexdate.html 。
11. 参见 http://cansecwest.com/core03/shiva.ppt (工具: http://www.securiteam.com/tools/5XP041FA0U.html )。
12. 参见 http://www.oreans.com/winlicense.php 。
模糊程序领域的最近发展涉及使用虚拟机执行引擎来包装原始可执行文件。根据虚拟化模糊器的复杂程度,原始的机器代码可能永远不会直接执行,而是由面向字节码的虚拟机来解释。非常复杂的虚拟化模糊器能够在每次运行时生成唯一的虚拟机实例,因而很难创建多功能的去模糊算法来破解它们。VMProtect13 就是一个虚拟化模糊器。VMProtect 用于模糊处理 Clampi14 木马。
13. 参见 http://www.vmpsoft.com/ 。
14. 参见 http://www.symantec.com/connect/blogs/inside-jaws-trojanclampi 。
和任何侵犯性技术一样,人们也已经开发出一些防范措施来对抗许多反逆向工程工具。多数情况下,这类工具的目的是找到原始的、不受保护的可执行文件(或者一个合适的摹本),然后使用反汇编器和调试器等更加传统的工具对它进行分析。有一个专用于对 Windows 可执行文件进行去模糊处理的工具,叫做 QuickUnpack15 。和其他许多自动化的解压程序一样,QuickUnpack 以调试器的方式运行,并允许一个模糊的二进制文件执行,直到它的去模糊阶段,然后从内存捕获进程映像。需要小心的是,这类工具可用于运行潜在的恶意程序,希望在这些程序解压或去模糊后,但又在它们有机会执行任何恶意行为之前,阻止这些程序执行。因此,你应该始终在类似沙盒的环境中执行这样的程序。
15. 参见 http://qunpack.ahteam.org/wp2/ (俄罗斯)或 http://www.woodmann.com/collaborative/tools/index.php/Quick_Unpack 。
使用一个纯粹的静态分析环境分析模糊代码是一个充满挑战的任务。由于不能执行去模糊存根,在开始反汇编模糊代码之前,必须采取某种方法解压或解密二进制文件被模糊处理的部分。一个已经使用 UPX 压缩程序打包的可执行文件的布局如图 21-2 所示。在这个文件的地址空间中,IDA 唯一能够识别的部分是处的窄条,它恰巧是 UPX 解压缩存根。
图 21-2 一个使用 UPX 打包的二进制文件的 IDA 导航带
分析地址空间的内容,可以发现➊左边的空白空间,以及➊和➋之间区域内明显的随机数据。这些随机数据是 UPX 压缩过程的结果。解压缩存根的作用是在最后将控制权转交给解压代码之前,将随机数据解压到导航带左边的空白区域。注意,导航栏的这种少见的外观是一种潜在的暗示,说明这个二进制文件已经以某种形式被模糊处理。实际上,使用 IDA 查看已被模糊处理的二进制文件时,通常你会得到许多暗示。说明二进制文件被模糊处理的一些可能的暗示如下所示。
很少有代码在导航带中突出显示。
“Functions ”(函数)窗口中列出的函数非常少,通常仅显示
start
函数。“Imports”(导入)窗口中列出的导入函数非常少。
“Strings ”(字符串)窗口(默认情况下不会打开该窗口)显示的可辨别字符串非常少。通常仅显示少数导入库和函数的名称。
一个或多个程序节既可写,又可执行。
使用
UPX0
或.shrink
等非标准的节名称。
沙盒环境
在逆向工程过程中,使用沙盒环境可以在执行程序时观察程序的行为,而该行为不会损害逆向工程平台的关键组件。沙盒环境通常使用 VMware16 等平台虚拟化软件构建,但它们也可以在一些专用的系统上构建。在执行任何恶意软件之后,这类专用系统可以恢复到一个已知良好的状态。
16. 参见 http://www.vmware.com/ 。
沙盒系统的一个共同特点是它们通常都受到严密检测,以观察和收集与沙盒中的程序行为有关的信息。收集到的数据包括与程序的文件系统活动有关的信息、(Windows)程序的注册表活动、与程序生成的任何网络活动有关的信息。
导航栏中显示的信息可以与二进制文件中的每个段的属性关联起来,以确定每个窗口中显示的信息是否一致。这个二进制文件的段代码清单如下所示:
Name Start End R W X D L Align Base Type Class ➊ UPX0 00401000 00407000 R W X . L para 0001 public CODE ➋ UPX1 00407000 00409000 R W X . L para 0002 public CODE UPX2 00409000 0040908C R W . . L para 0003 public DATA .idata 0040908C 004090C0 R W . . L para 0003 public XTRN UPX2 004090C0 0040A000 R W . . L para 0003 public DATA
在这个例子中,由段 UPX0
(➊)和段 UPX1
(➋)组成的整个地址范围( 00401000~00409000
)被标记为可执行文件(已设置 X
标志)。基于这一事实,我们应该会看到整个导航带以彩色显示,表示它们是代码。但情况并非如此,而且观察发现, UPX0
的整个范围全为空,未被占用,这些都应引起我们的高度怀疑。在 IDA 中, UPX0
的节头部包含以下行:
UPX0:00401000 ; Section 1. (virtual address 00001000) UPX0:00401000 ; Virtual size : 00006000 ( 24576.) UPX0:00401000 ; ➊ Section size in file : 00000000 ( 0.) UPX0:00401000 ; Offset to raw data for section: 00000200 UPX0:00401000 ; ➋ Flags E0000080: Bss Executable Readable Writable
使用 IDA 在静态上下文中(根本不执行二进制文件)执行解压操作的技巧将在 21.3 节讨论。
21.1.3 导入的函数模糊
为了避免泄漏与二进制文件可能执行的与可能操作有关的信息,另一种反静态分析技巧专用于难以确定模糊二进制文件所使用的共享库和库函数。多数情况下,这种技巧可以令 dumpbin
、 ldd
和 objdump
等工具失效,无法列出库依赖关系。
这类模糊对 IDA 的影响在“导出”窗口中表现得尤为明显。前面 tElock 示例的“导出”窗口的整个内容如下所示:
Address Ordinal Name Library 0041EC2E GetModuleHandleA kernel32 0041EC36 MessageBoxA user32
只有两个外部函数被引用: GetModulehandleA
(来自 kernel32.dll )和 MessageBoxA
(来自 user32.dll )。从这个简短的代码段,几乎不可能推断出程序的任何行为。那么,这样一个程序如何完成有用的任务呢?同样,在这方面,程序采用的技巧多种多样,但是基本上归结于一个事实,即程序本身必须加载它依赖的任何其他库,一旦库被加载,程序必须在这些库中定位所需的任何函数。多数情况下,这些任务由去模糊存根完成,然后再将控制权转交给去模糊后的程序。这个过程的最终目的是正确初始化程序的导入表,就好像整个过程是由操作系统自己的加载器执行的一样。
对 Windows 二进制文件而言,一种简单的方法是使用 LoadLibrary
函数按名称加载所需的库,然后在每个库中使用 GetProcAddress
函数执行函数地址查询。为了使用这些函数,程序要么显式链接它们,要么采取其他方法查询它们。tElock 示例的“名称”列表中并未包含任何一个这样的函数,而下面 UPX 示例的“名称”(Name)列表则包含了这两个函数。
Address Ordinal Name Library 0040908C LoadLibraryA KERNEL32 00409090 GetProcAddress KERNEL32 00409094 ExitProcess KERNEL32 0040909C RegCloseKey ADVAPI32 004090A4 atoi CRTDLL 004090AC ExitWindowsEx USER32 004090B4 InternetOpenA WININET 004090BC recv wsock32
负责重建导入表的 UPX 代码如代码清单 21-1 所示。
代码清单 21-1 UPX 中的导入表重建
UPX1:0040886C loc_40886C: ; CODE XREF: start+12E↓ j UPX1:0040886C mov eax, [edi] UPX1:0040886E or eax, eax UPX1:00408870 jz short loc_4088AE UPX1:00408872 mov ebx, [edi+4] UPX1:00408875 lea eax, [eax+esi+8000h] UPX1:0040887C add ebx, esi UPX1:0040887E push eax UPX1:0040887F add edi, 8 ➊ UPX1:00408882 call dword ptr [esi+808Ch] ; LoadLibraryA UPX1:00408888 xchg eax, ebp UPX1:00408889 UPX1:00408889 loc_408889: ; CODE XREF: start+146↓ j UPX1:00408889 mov al, [edi] UPX1:0040888B inc edi UPX1:0040888C or al, al UPX1:0040888E jz short loc_40886C UPX1:00408890 mov ecx, edi UPX1:00408892 push edi UPX1:00408893 dec eax UPX1:00408894 repne scasb UPX1:00408896 push ebp ➋ UPX1:00408897 call dword ptr [esi+8090h] ; GetProcAddress UPX1:0040889D or eax, eax UPX1:0040889F jz short loc_4088A8 ➌ UPX1:004088A1 mov [ebx], eax ; Save to import table UPX1:004088A3 add ebx, 4 UPX1:004088A6 jmp short loc_408889
这个例子包含一个负责调用 LoadLibraryA
17 (➊)的外层循环和一个负责调用 GetProcAddress
(➋)的内层循环。每次成功调用 GetProcAddress
后,新获取的函数地址存储在重建后的导入表中(➌)。
17. 许多接受字符串参战的 Windows 函数分为两个版本:一种版本接受 ASCII 字符串,一种版本接受 Unicode 字符串。这些函数的 ASCII 版本带 A 后缀,而 Unicode 版本则带 W 后缀。
这些循环作为 UPX 去模糊存根的最后一部分执行,因为每个函数包含指向一个库名称或函数名称的字符串指针参数,且相关的字符串保存在压缩数据区域,以避免被 strings
实用工具检测。因此,在 UPX 中,只有所需的字符串被解压后,相关库才能被加载。
回到 tElock 示例,它遇到的问题有所不同。由于只有两个导入函数,既不是 LoadLibraryA
也不是 GetProcAddress
,那么,tElock 实用工具如何像 UPX 一样执行函数解析任务呢?所有 Windows 进程根据 kernel32.dll 运行,这表示 kernel32.dll 为所有进程保存在内存中。如果一个程序能够定位 kernel32.dll ,那么要定位这个 DLL 中的任何函数,包括 LoadLibraryA
和 GetProc- Address
,就相对容易一些。如前所述,使用这两个函数,你可以加载进程所需的任何其他库,并定位这些库中的所有必需函数。在论文“Understanding Windows shellcode ”18 中,Skape 讨论了一些技巧,说明如何完成这个任务。虽然 tElock 并没有使用 Skape 详细介绍的技巧,但是它们之间有许多相似之处,其最终目的是模糊加载和链接过程。如果不仔细跟踪程序的指令,你很容易忽略程序加载了一个库或查询了一个函数地址。下面的代码片段说明了 tElock 如何定位 LoadLibraryA
的地址:
18. 参见 http://www.hick.org/code/skape/papers/win32-shellcode.pdf ,尤其是第 3 章的 3.3 节。
.shrink:0041D1E4 cmp dword ptr [eax], 64616F4Ch .shrink:0041D1EA jnz short loc_41D226 .shrink:0041D1EC cmp dword ptr [eax+4], 7262694Ch .shrink:0041D1F3 jnz short loc_41D226 .shrink:0041D1F5 cmp dword ptr [eax+8], 41797261h .shrink:0041D1FC jnz short loc_41D226
很明显,这段代码快速连续进行了几次比较。但我们并不十分清楚这些比较的作用。重新格式化每次比较所使用的操作数,我们获得一些启示。格式化后的代码如下所示:
.shrink:0041D1E4 cmp dword ptr [eax], 'daoL' .shrink:0041D1EA jnz short loc_41D226 .shrink:0041D1EC cmp dword ptr [eax+4], 'rbiL' .shrink:0041D1F3 jnz short loc_41D226 .shrink:0041D1F5 cmp dword ptr [eax+8], 'Ayra' .shrink:0041D1FC jnz short loc_41D226
每个十六进制常量实际上是一个由 4 个 ASCII 字符组成的序列,它们按顺序(前面讲过,x86 是一种小端处理器,因此我们需要颠倒顺序读取这些字符)拼写成 LoadLibraryA
。如果这 3 个比较成功,则说明 tElock 已经定位了 LoadLibraryA
的导出表条目,再经过几个简单的操作,将可以获得这个函数的地址,并用它加载其他库。使用 tElock 进行函数查询有一个有趣的特点,即它似乎有些“抗拒”字符串分析,因为直接嵌入到程序指令中的 4 字节常量看起来并不像更加标准的、以零为终止符的字符串,因而并未包含在 IDA 生成的字符串列表中。
使用 UPX 和 tElock 时,通过仔细分析程序代码手动重建一个程序的导入表要更加容易一些,因为最终它们都将包含一些 ASCII 字符数据,我们可以利用这些数据确定程序到底引用了哪些库和函数。Skape 的论文详细介绍了一个函数解析过程,在这个过程中,代码中根本没有出现任何字符串。论文讨论的基本概念是为你需要解析的每个函数的名称预先计算一个唯一的散列19 值。要解析每一个函数,首先搜索一个库的导出名称表,然后对表中的每个名称进行散列处理,再将得到的散列值与为相关函数预先计算的散列值进行比较,如果这两个散列值相互匹配,则说明你已经找到这个函数的位置,并在相关库的导出地址表中轻易找到它的地址。为了静态分析以这种方式模糊处理的二进制文件,你需要了解对每个函数名称使用的散列算法,并将该算法应用于程序搜索的库导出的所有名称。拥有完整的散列表后,你就可以直接查询你在程序中遇到的每一个散列值,并确定该散列值对应哪一个函数。20 如下所示是一个由 kernel32.dll 生成的散列表的一部分:
19. 散列函数是一个算术过程,它由一个任意大小的输入(如一个字符串)获得一个固定大小(如 4 字节)的结果。
20. Hex-Rays 在 http://www.hexblog.com/?p=93 中介绍了 IDA 的调试功能以计算这些散列值。
➊ GetProcAddress : 8A0FB5E2 GetProcessAffinityMask : B9756EFE GetProcessHandleCount : B50EB87C GetProcessHeap : C246DA44 GetProcessHeaps : A18AAB23 GetProcessId : BE05ED07
需要注意的是,散列值特定于某个特殊的库所使用的散列函数,并且可能因库而异。使用这个特殊的表,如果在程序中遇到散列值 8A0FB5E2
(➊ ),我们可以迅速确定程序正尝试查询 GetProcAddress
函数的地址。
Skape 用于解析函数名称的散列值,最初是为了供利用 Windows 漏洞的破解程序使用而开发和记录的,但是它们已经被用在模糊程序中。例如,WinLincense 模糊实用工具就利用这类散列技巧来隐藏它的行为。
关于导入表的最后一点提示是:IDA 有时会为你提供线索,指出一个程序的导入表存在问题。模糊 Windows 二进制文件常常会使用经过大量修改的导入表,这时 IDA 会通知你,这样的二进制文件似乎有些不正常。在这类情况下,IDA 显示的警告对话框如图 21-3 所示。
图 21-3 导入段被改编的警告对话框
这个对话框提供了一条最早的提示,指出一个二进制文件可能经过某种形式的模糊处理。该对话框可以作为一个警告,说明该二进制文件可能难以分析。因此,在分析该文件时,你应当小心行事。
21.1.4 有针对性地攻击分析工具
提到这类反逆向工程工具,是因为它具有阻止逆向工程的潜力。许多逆向工程工具都可以看成是高度专一化的解析器,它们处理输入数据,提供某种摘要信息或显示相关细节。作为软件,它们也和所有其他软件一样,存在各种类型的漏洞。具体来说,错误地处理用户提供的数据,有时可能会导致可被他人利用的条件。
除了我们已经讨论的技巧外,希望防止软件被分析的程序员可能会采取更加主动的方式阻止反逆向工程。利用精心构造的输入文件,可以创建一个特殊的程序,这个程序既能够正常运行,又存在很大的缺陷,足以利用逆向工程工具中存在的漏洞。这类漏洞并不常见,但已经被人们记录下来,包括 IDA 中的漏洞21 。攻击者的目的是在某个时候将恶意软件加载到 IDA 中。至少,攻击者可以拒绝服务,这样,在能够创建数据库之前,IDA 就会崩溃。另外,攻击者可以访问分析人员的计算机和相关网络。关注这类攻击的用户应考虑在沙盒环境中执行所有初步分析任务。例如,你可以在一个沙盒中运行 IDA ,为所有二进制文件创建初始数据库。然后,再将初始数据库(理论上它们没有任何恶意功能)分发给其他分析人员,这样这些分析人员根本不需要接触原始的二进制文件。
21. 参见 http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2005-0115 。更多详情参见 http://labs.idefense.com/intelligence/vulnerabilities/display.php?id=189 。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论