反汇编基础

反汇编概念

在传统软件开发的过程中,程序员使用编译器、汇编器和链接器其中一个或几个来创建可执行程序。为了回溯编程编程过程,以汇编语言甚至是机器语言进行输入,其输出结果为高级语言的过程称为反汇编。反汇编通常需要使用反汇编工具,并且反汇编的过程是困难的,结果并不是百分百正确。原因如下
编译过程会造成损失。机器语言中没有高级语言所使用的变量或函数名,变量信息只有通过数据的用途(而不是显式的类型声明)来确定。比如,看到一个32位的数据被传送(如mov eax,[ebp+12]),需要进行一番分析才能确定这个32位数据表示的是一个整数、一个32位浮点值还是一个指针
编译属于多对多操作。源程序可以通过许多不同的方式转换成汇编语言,而机器语言也可以通过许多不同的方式转换成源程序。比如,源码为i+=2,反汇编后代码可能变成了i=i+1;i=i+1;,形式不同结果等价。再如,gcc在编译时不同的优化等级--debug/-o1/-o2生成的汇编代码通常不同,但执行程序时是等价的。因此,编译一个文件,并立即反编译,可能得到与输入时截然不同的源文件。
反编译器非常依赖于语言和库。用专门用来生成C代码的反编译器处理由Delphi编译器生成的二进制文件,可能会得到非常奇怪的结果。同样,用不支持Windwos API的反编译器来处理编译后的Windows二进制文件,也不会有所收获。
要想准确地反编译一个二进制文件,需要近乎完美的反汇编能力。几乎可以肯定,反汇编阶段的任何错误或遗漏都会影响反编译代码。

反汇编用途

通常,使用反汇编工具是为了在没有源代码的情况下了解程序。以下是反汇编常见使用场景

  • 分析恶意软件
  • 分析闭源软件的漏洞
  • 分析闭源软件的互操作性
  • 分析编译器生成的代码,以验证编译器的性能和准确性
  • 在调试时显示程序指令

分析恶意软件

除了基于恶意脚本的蠕虫,通常,我们无法获取到恶意软件的源代码。由于缺乏源代码,要准确地了解恶意软件的运行机制,需要用到动态分析和静态分析两种技术。动态分析是指在严格控制的换件(沙盒)中执行恶意软件,并使用系统检测实用工具来记录其所有行为。相反,静态分析则试图通过浏览程序代码来理解程序的行为。此时,要分析的是对恶意软件进行反汇编后得到的代码。

漏洞分析

安全审核的过程划分为3个步骤:发现漏洞、分析漏洞和开发破解程序。发现漏洞是发现程序中潜在的可供利用的条件。一般情况下,我们可以通过模糊测试等动态技术来达到这一目的,也可以通过静态分析实现(通常需要付出更大的努力)。一旦发现漏洞,需要确定该漏洞是否可被利用,可以在什么情况下利用。反汇编代码可以提供编译器分配程序变量的详细信息,例如,程序员声的一个70字节的字符数组,在由编译器分配时,会扩大到80字节,知道这点会很有用。另外,要了解编译器到底如何对全局声明或在函数中声明的所有变量进行排序,查看反汇编代码是唯一的办法。在开发破解程序时,了解变量之间的这些空间关系非常重要。最后,通过结合使用反汇编器和调试器,就可以开发出破解程序。

软件互操作性

如果仅以二进制形式发布软件,竞争对手要想创建可以和它互操作的软件,或者为该软件提供插件,将会非常困难,静态代码分析几乎是唯一的可行方式。通常,为了理解嵌入式固件,还需要分析软件驱动程序以外的代码。

编译器验证

由于编译器(或汇编器)的用途是生成机器语言,因此通常需要使用反汇编工具来验证编译器是否符合设计规范。分析人员还可以从中寻找优化编译器输出的机会;从安全角度看,还可查知编译器本身是否容易被攻破、以至于可以在生成的代码中插入后门,等等。

显示调试信息

在调试器生成反汇编代码可能是反汇编器最常见的一种用途。遗憾的是,调试器内嵌的反汇编器往往相当简单,它们通常不能批量反汇编,在无法确定函数边界时,它们可能会拒绝工作。因此,在调试过程中,为了解相信的环境和背景信息,最好结合使用调试器和优秀的反汇编器

如何反汇编

假设你面临这样一个任务:对一个100KB的文件,请区分其中的代码与数据,并将代码转换成汇编语言显示给用户。在整个过程中,不要遗漏任何信息。在这个任务中,还可以附加很多要求,如定位函数、识别跳转表并确定局部变量。这就是反汇编器面临的艰巨任务。为了满足所有要求,反汇编器必须从大量算法中选择一些适当的算法来处理二进制文件。反汇编器所使用算法的质量及其实施算法的效率,将直接影响所生成的反汇编代码的质量。

基本的反汇编算法

该算法以机器语言为输入,输出汇编语言

  • 第一步:确定进行反汇编代码的区域。通常,指令与数据混杂在一起,以最常见的情形——反汇编可执行文件为例,该文件必须符合可执行文件的某种通用格式(PE或ELF)。这些格式通常含有一种确定文件中代码和代码入口点位置的机制(通常表现为层级头文件的形式)。
  • 第二步:知道指令的起始地址后,下一步是读取该地址(文件偏移量)所包含的值,并执行一次表查找,将二进制操作码的值与它所代表的汇编语言助记符相关联。
  • 第三步:获取指令并解码任何所需的操作数后,需要对它的汇编语言等价形式进行格式化,并将其在反汇编代码中输出。
  • 第四步:输出一条指令后,继续反汇编下一条指令,并重复上述过程,直到反汇编完文件中所有指令。

有大量的算法可用于确定从何处开始反汇编,如何选择下一条反汇编指令,如何区分代码与数据,以及如何确定何时完成对最后一条指令的反汇编。线性扫描与递归下降是两种最主要的反汇编算法。

线性扫描反汇编

原理

  • 线性扫描反汇编算法采用一种非常简单的方法来确定需要反汇编的指令和位置:一条指令结束的地方便是另一条指令开始的地方。因此,确定起始位置最为困难。常用的解决办法是,假设程序中标注为代码(通常由程序文件的头部决定)的节所包含的全部是机器语言指令。反汇编从一个代码段的第一个字节开始,以线性模式扫描整个代码段,逐条反汇编每条指令,直到扫描完整个代码段。
  • 这种算法并不会通过识别分支等非线性指令来了解程序控制流。
  • 进行反汇编时,可以维护一个指针来标注当前正在反汇编指令的起始位置。在反汇编过程中,每一条指令的长度都被计算出来,并以此确定下一条将要反汇编指令的位置。因此固定长度指令集使用此方法效率更高。

优势与不足

  • 线性扫描算法的主要优点在于它能够完全覆盖程序的所有代码段,执行效率高。
  • 线性扫描算法的主要缺点,是它没有考虑到代码中可能混有数据,无法正确地将嵌入的数据与代码区分开。

GNU调试器、WinDbg调试器和objdump的反汇编引擎均采用线性扫描算法。

递归下降反汇编

原理

  • 递归下降算法强调控制流的概念。控制流根据一条指令是否被另一条指令引用来决定是否对其进行反汇编。根据指令对CPU指针的影响分为三种类型

顺序流指令

  • 顺序流指令将执行权传递给紧随其后的下一条指令,这类指令的反汇编过程以线性扫描的方式进行
  • 例子:简单的算术指令(add)、寄存器与内存之间的传输指令(mov)、栈操作指令(push、pop)

条件分支指令

  • 条件分支指令(如jnz)提供两条可能的执行路径。如果条件为真,则执行分支,并且必须修改指令指针,使其指向分支的目标。如果条件为假,则继续以线性模式执行指令。
  • 由于不可能在静态环境中确定条件测试的结果,递归下降算法会反汇编上述两条路径。同时,它将分支目标指令的地址添加到稍后才进行反汇编的地址列表中,从而推迟分支目标指令的反汇编过程

无条件分支指令

  • 无条件分支指令将执行权传递给下一条指令,与顺序流指令不同,传递的指令不紧跟在分支指令后面。反汇编器将确定无条件跳转的目标,并将目标地址添加到要反汇编的地址列表中
  • 如果跳转指令的目标取决于一个运行时值,静态分析无法确定跳转目标。

函数调用指令

  • 函数调用指令运行方式与无条件跳转指令非常类似(包括反汇编器无法确定的call eax等指令的目标)不同之处在于一旦函数完成,执行权将返回给紧跟在调用指令后面的指令。调用指令的目标地址添加到推迟进行反汇编的地址列表中,而紧跟在调用后面的指令以类似于线性扫描的方式进行反汇编。
  • 从被调用函数返回时,如果程序的运行出现异常,递归下降可能失败。(如函数篡改了函数的返回地址)

返回指令

  • 当递归下降算法访问了当前所有路径,且函数的返回指令(如ret)没有提供接下来将要执行的指令的信息时,递归下降反汇编器从延迟反汇编地址列表取出一个地址,并从这个地址开始继续反汇编过程。

优势与不足

  • 优势:具有区分代码与数据的能力,作为一种基于控制流的算法,很少会将数据值作为代码处理
  • 不足:无法处理间接代码路径,如利用指针表查找目标地址的跳转或调用(如switch())。(然而,通过采用一些用于识别指向代码指针的启发式方法,递归下降反汇编器能够提供所有代码,并清楚区分代码与数据)

IDA Pro是一种最为典型的递归下降反汇编器,了解递归下降的过程有助于识别IDA无法进行最佳反汇编的清醒,以及指定策略来改进IDA的输出结果。

小结

在使用反汇编器时,要深入了解反汇编算法,同时要选择一个得心应手的反汇编工具。一切都是为了准确的反汇编代码。