- 献词
- 致谢
- 前言
- 第一部分 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 交叉引用
1.4 如何反汇编
现在,你已经知道了反汇编的目的,接下来介绍如何反汇编。以反汇编器所面临的一个艰巨任务为例: 对于一个 100 KB 的文件,请区分其中的代码与数据,并把代码转换成汇编语言显示给用户。在整个过程中,不要遗漏任何信息 。在这个任务中,我们还可以附加许多特殊要求,如要求反汇编器定位函数,识别跳转表并确定局部变量,这进一步增加了反汇编器工作的难度。
为了满足所有要求,反汇编器必须从大量算法中选择一些适当的算法,来处理我们提供的文件。反汇编器所使用算法的质量及其实施算法的效率,将直接影响所生成的反汇编代码的质量。在这一节中,我们将讨论当前对机器代码反汇编时所使用的两种基本算法。在介绍这些算法的同时,我们还将指出它们的缺陷,以便于你对反汇编器失效的情形有所防备。了解反汇编器的局限后,就可以通过手动干预来提高反汇编输出的整体质量了。
1.4.1 基本的反汇编算法
为方便初学者,首先开发一个以机器语言为输入、以汇编语言为输出的简单算法。这样做有助于我们了解自动反汇编过程中的挑战、假设和折中方案。
- 第一步 。确定进行反汇编的代码区域。这并不像看起来那么简单。通常,指令与数据混杂在一起,区分它们就显得非常重要。以最常见的情形——反汇编可执行文件——为例,该文件必须符合可执行文件的某种通用格式,如 Windows 所使用的 可移植可执行 (Portable Executable ,PE)格式或许多 Unix 系统常用的 可执行和链接格式 (Executable and linking format ,ELF )。这些格式通常含有一种机制,用来确定文件中包含代码和代码入口点1 的部分的位置(通常表现为层级文件头的形式)。
1. 代码入口点是一个指令地址,一旦程序加载到内存,操作系统就将控制权交给该指令。
第二步 。知道指令的起始地址后,下一步就是读取该地址(或文件偏移量)所包含的值,并执行一次表查找,将二进制操作码的值与它的汇编语言助记符对应起来。根据被反汇编的指令集的复杂程度,这个过程可能非常简单,也可能需要几个额外的操作,如查明任何可能修改指令行为的前缀以及确定指令所需的操作数。对于指令长度可变的指令集,如 Intel x86 ,要完全反汇编一条指令,可能需要检索额外的指令字节。
第三步 。获取指令并解码任何所需的操作数后,需要对它的汇编语言等价形式进行格式化,并将其在反汇编代码中输出。有多种汇编语言输出格式可供选择。例如,x86 汇编语言所使用的两种主要格式为 Intel 格式和 AT & T 格式。
第四步 。输出一条指令后,继续反汇编下一条指令,并重复上述过程,直到反汇编完文件中的所有指令。
x86 汇编语法:AT&T 和 Intel
汇编语言源代码主要采用两种语法:AT & T 语法和 Intel 语法。尽管它们都属于第二代语言,但这二者的语法在变量、常量、寄存器访问、段和指令大小重写、间接寻址和偏移量等方面都存在巨大的差异。AT & T 汇编语法以%作为所有寄存器名称的前缀,以$作为文字常量(也叫做立即操作数)的前缀。它这样对操作数排序:源操作数位于左边,目的操作数位于右边。使用 AT & T 语法,EAX 寄存器加 4 的指令为:
add
$0x4,%eax
。GNU 汇编器(Gas)和许多其他 GNU 工具(如 gcc 和 gdb)都使用 AT & T 语法。Intel 语法与 AT & T 语法不同,它不需要寄存器和文字前缀,它的操作数排序方式与 AT & T 语法操作数恰恰相反:源操作数位于右边,目的操作数位于左边。使用 Intel 语法,上述加法的指令为:
add
eax, 0x4
。使用 Intel 语法的汇编器包括微软汇编器(MASM)、Borland 的 Turbo 汇编器(TASM )和 Netwide 汇编器(NASM )。
有大量算法可用于确定从何处开始反汇编,如何选择下一条反汇编的指令,如何区分代码与数据,以及如何确定何时完成对最后一条指令的反汇编。 线性扫描 (linear sweep)和 递归下降 (recursive descent )是两种最主要的反汇编算法。
1.4.2 线性扫描反汇编
线性扫描反汇编算法采用一种非常简单的方法来确定需要反汇编的指令的位置:一条指令结束、另一条指令开始的地方。因此,确定起始位置最为困难。常用的解决办法是,假设程序中标注为代码(通常由程序文件的头部指定)的节所包含的全部是机器语言指令。反汇编从一个代码段的第一个字节开始,以线性模式扫描整个代码段,逐条反汇编每条指令,直到完成整个代码段。这种算法并不会通过识别分支等非线性指令来了解程序的控制流。
进行反汇编时,可以维护一个指针来标注当前正在反汇编的指令的起始位置。在反汇编过程中,每一条指令的长度都被计算出来,并用来确定下一条将要反汇编的指令的位置。为此,对由长度固定的指令构成的指令集(如 MIPS)进行反汇编有时会更加容易,因为这时可轻松定位随后的指令。
线性扫描算法的主要优点,在于它能够完全覆盖程序的所有代码段。线性扫描方法的一个主要缺点,是它没有考虑到代码中可能混有数据。代码清单 1-1 就说明了这个问题,它显示的是用线性扫描反汇编器反汇编一个函数所得到的输出结果。这个函数包含一个 switch
语句,这里使用的编译器选择使用跳转表来执行 switch
语句。而且,编译器选择在函数本身中嵌入一个跳转表。 401250
(➊)处的 jmp
语句引用了一个以 401257
(➋)为起始位置的地址表。但是,反汇编器把(➋)作为一条指令来处理,并错误地生成了其对应的汇编语言形式。
代码清单 1-1 线性扫描反汇编
40123f: 55 push ebp 401240: 8b ec mov ebp,esp 401242: 33 c0 xor eax,eax 401244: 8b 55 08 mov edx,DWORD PTR [ebp+8] 401247: 83 fa 0c cmp edx,0xc 40124a: 0f 87 90 00 00 00 ja 0x4012e0 ➊ 401250: ff 24 95 57 12 40 00 jmp DWORD PTR [edx*4+0x401257] ➋ 401257: e0 12 loopne 0x40126b 401259: 40 inc eax 40125a: 00 8b 12 40 00 90 add BYTE PTR [ebx-0x6fffbfee],cl 401260: 12 40 00 adc al,BYTE PTR [eax] 401263: 95 xchg ebp,eax 401264: 12 40 00 adc al,BYTE PTR [eax] 401267: 9a 12 40 00 a2 12 40 call 0x4012:0xa2004012 40126e: 00 aa 12 40 00 b2 add BYTE PTR [edx-0x4dffbfee],ch 401274: 12 40 00 adc al,BYTE PTR [eax] 401277: ba 12 40 00 c2 mov edx,0xc2004012 40127c: 12 40 00 adc al,BYTE PTR [eax] 40127f: ca 12 40 lret 0x4012 401282: 00 d2 add dl,dl 401284: 12 40 00 adc al,BYTE PTR [eax] 401287: da 12 ficom DWORD PTR [edx] 401289: 40 inc eax 40128a: 00 8b 45 0c eb 50 add BYTE PTR [ebx+0x50eb0c45],cl 401290: 8b 45 10 mov eax,DWORD PTR [ebp+16] 401293: eb 4b jmp 0x4012e0
如果将(➋)处开始的连续 4 字节组作为小端(little endian )2 值分析,我们发现,每个字节组都代表一个指向邻近地址的指针。实际上,这个地址是许多跳转的目的地址( 004012e0
、 0040128b
、 00401290
…)中的一个。因此,(➋)处的 loopne
指令并不是一条指令;相反,它说明线性扫描算法无法正确地将嵌入的数据与代码区分开来。
2. 如果 CPU 首先存储一个多字节值的最高有效字节,则称该 CPU 为大端(big-endian)CPU ;如果该 CPU 首先存储最低有效字节,则称为小端(little-endial )CPU 。
GNU 调试器(gdb)、微软公司的 WinDbg 调试器和 objdump
实用工具的反汇编引擎均采用线性扫描算法。
1.4.3 递归下降反汇编
递归下降采用另外一种不同的方法来定位指令。递归下降算法强调控制流的概念。控制流根据一条指令是否被另一条指令引用来决定是否对其进行反汇编。为便于理解递归下降,我们根据指令对 CPU 指令指针的影响对它们分类。
1. 顺序流指令
顺序流指令将执行权传递给紧随其后的下一条指令。顺序流指令的例子包括简单算术指令,如 add
;寄存器与内存之间的传输指令,如 mov
;栈操作指令,如 push
和 pop
。这些指令的反汇编过程以线性扫描方式进行。
2. 条件分支指令
条件分支指令(如 x86 jnz
)提供两条可能的执行路径。如果条件为真,则执行分支,并且必须修改指令指针,使其指向分支的目标。但是,如果条件为假,则继续以线性模式执行指令,并使用线性扫描方法反汇编下一条指令。因为不可能在静态环境中确定条件测试的结果,递归下降算法会反汇编上述两条路径。同时,它将分支目标指令的地址添加到稍后才进行反汇编的地址列表中,从而推迟分支目标指令的反汇编过程。
3. 无条件分支指令
无条件分支并不遵循线性流模式,因此,它由递归下降算法以不同的方式处理。与顺序流指令一样,执行权只能传递给一条指令,但那条指令不需要紧接在分支指令后面。事实上,如代码清单 1-1 所示,根本没有要求规定在无条件分支后必须紧跟一条指令。因此,也就没有理由反汇编紧跟在无条件分支后面的字节。
递归下降反汇编器将尝试确定无条件跳转的目标,并将目标地址添加到要反汇编的地址列表中。遗憾的是,某些无条件分支可能会给递归下降反汇编器造成麻烦。如果跳转指令的目标取决于一个运行时值,这时使用静态分析就无法确定跳转目标。x86 的 jmp eax
指令就证实了这个问题。只有程序确实正在运行时, eax
寄存器中才会包含一个值。由于寄存器在静态分析过程中不包含任何值,因此无法确定跳转指令的目标,也就无法确定该从什么地方继续反汇编过程。
4. 函数调用指令
函数调用指令的运行方式与无条件跳转指令非常相似(包括反汇编器无法确定 call eax
等指令的目标),唯一的不同在于,一旦函数完成,执行权将返回给紧跟在调用指令后面的指令。在这方面,它们与条件分支指令类似,因为它们都生成两条执行路径。调用指令的目标地址被添加到推迟进行反汇编的地址列表中,而紧跟在调用后面的指令则以类似于线性扫描的方式进行反汇编。
从被调用函数返回时,如果程序的运行出现异常,递归下降就有可能失败。例如,函数中的代码可能会有意窜改该函数的返回地址,这样,在函数完成时,控制权将返回到一个反汇编器无法预知的地址。下面的错误代码就是一个简单的例子。在这个例子中,函数 foo
在返回调用方之前,对返回地址加了 1。
foo proc near FF 04 24 inc dword ptr [esp] ; increments saved return addr C3 retn foo endp ; ------------------------------------- bar: E8 F7 FF FF FF call foo 05 89 45 F8 90 ➊add eax, 90F84589h
结果,在调用 foo 之后,控制权实际上并未返回给(➊)处的 add 指令。正确的反汇编过程如下所示。
foo proc near FF 04 24 inc dword ptr [esp] C3 retn foo endp ; ------------------------------------- bar: E8 F7 FF FF FF call foo 05 db 5 ;formerly the first byte of the add instruction 89 45 F8 Y mov [ebp-8], eax 90 nop
以上代码更清楚地展示了程序的实际流程。实际上,函数 foo
将控制权返回给了位于➋处的 mov
指令。值得注意的是,线性扫描反汇编器可能也同样无法正确对这段代码反汇编,只是原因稍有不同。
5. 返回指令
有时,递归下降算法访问了所有的路径。而且,函数返回指令(如 x86 ret
)没有提供接下来将要执行的指令的信息。这时,如果程序确实正在运行,则可以从运行时栈顶部获得一个地址,并从这个地址开始恢复执行指令。但是,反汇编器并不具备访问栈的能力,因此反汇编过程会突然终止。这时,递归下降反汇编器会转而处理前面搁置在一旁的延迟反汇编地址列表。反汇编器从这个列表中取出一个地址,并从这个地址开始继续反汇编过程。递归下降反汇编算法正是因此而得名。
递归下降算法的一个主要优点在于,它具有区分代码与数据的强大能力。作为一种基于控制流的算法,它很少会在反汇编过程中错误地将数据值作为代码处理。递归下降算法的主要缺点在于,它无法处理间接代码路径,如利用指针表来查找目标地址的跳转或调用。然而,通过采用一些用于识别指向代码的指针的 启发 (heuristics )式方法,递归下降反汇编器能够提供所有代码,并清楚地区分代码与数据。代码清单 1-2 是对前面代码清单 1-1 中的 switch
语句应用递归下降反汇编器所得到的结果。
代码清单 1-2 递归下降反汇编
0040123F push ebp 00401240 mov ebp, esp 00401242 xor eax, eax 00401244 mov edx, [ebp+arg_0] 00401247 cmp edx, 0Ch ; switch 13 cases 0040124A ja loc_4012E0 ; default 0040124A ; jumptable 00401250 case 0 00401250 jmp ds:off_401257[edx*4] ; switch jump 00401250 ; ---------------------------------------------------00401257 off_401257: 00401257 dd offset loc_4012E0 ; DATA XREF: sub_40123F+11r 00401257 dd offset loc_40128B ; jump table for switch statement 00401257 dd offset loc_401290 00401257 dd offset loc_401295 00401257 dd offset loc_40129A 00401257 dd offset loc_4012A2 00401257 dd offset loc_4012AA 00401257 dd offset loc_4012B2 00401257 dd offset loc_4012BA 00401257 dd offset loc_4012C2 00401257 dd offset loc_4012CA 00401257 dd offset loc_4012D2 00401257 dd offset loc_4012DA 0040128B ; ---------------------------------------------------0040128B 0040128B loc_40128B: ; CODE XREF: sub_40123F+11j 0040128B ; DATA XREF: sub_40123F:off_401257o 0040128B mov eax, [ebp+arg_4] ; jumptable 00401250 case 1 0040128E jmp short loc_4012E0 ; default 0040128E ; jumptable 00401250 case 0
注意,跳转目标表已被识别出来,并进行了相应的格式化。IDA Pro 是一种最为典型的递归下降反汇编器。了解递归下降过程有助于我们识别 IDA 无法进行最佳反汇编的情形,以及制定策略来改进 IDA 的输出结果。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论