前 言
绝大多数计算机程序是使用C或C++等高级语言编写的,该类编程语言无法直接运行。在使用它们之前,必须先将其编译为包含计算机可运行的机器语言的二进制可执行文件。但是,你如何知道编译后的程序与高级源代码是否具有相同的语义?令人不安的答案是你无法知道!
高级语言和二进制机器语言之间存在很大的语义鸿沟,因此很少有人知道它们如何进行联系。大多数程序员对其程序在底层运行的知识了解很有限,他们只是简单地相信编译后的程序会符合他们的意图。结果是,许多编译器错误、细微的实现错误、二进制级别的后门程序和恶意寄生虫可能会被忽略。
更糟糕的是,在工业的系统、银行的系统和嵌入式系统中,有无数的二进制程序和库,这些源程序可能长期丢失或是私有的,这意味着无法使用常规方法对这些程序和库进行修补或在源代码级别上评估其安全性。即使对于大型软件公司,这也是一个现实存在的问题,例如,微软最近发布了精心制作的二进制补丁程序,用于解决Microsoft Office公式编辑器程序中的缓冲区溢出。
在本书中,你将学习如何在二进制级别上分析和修改程序。无论你是安全研究人员、恶意软件分析师、程序员,还是仅仅对二进制分析感兴趣的人,这些技术都将让你能掌握并深入了解你每天创建和使用的二进制程序。
什么是二进制分析,为什么需要它
二进制分析是分析计算机二进制程序(称为二进制文件)及其包含的机器代码和数据属性的科学和艺术。简而言之,所有二进制分析的目标是找出(并可能修改)二进制程序的真正属性——换句话说,它们真正在做什么,而不是我们认为它们应该做什么。许多人将二进制分析与逆向工程和反汇编联系起来,这种说法至少部分是正确的。在许多形式的二进制分析中,反汇编是重要的第一步,而逆向工程是二进制分析的常见应用,并且通常是记录专有软件或恶意软件行为的唯一方法。但是,二进制分析的领域远不止于此。从广义上说,你可以将二进制分析技术分为两类,或是这两类的组合,具体如下。
静态分析。静态分析技术可在不运行二进制文件的情况下对二进制文件进行分析。这种方法有两个优点:可以一次性分析整个二进制文件,且不需要特定的CPU来运行二进制文件。例如,你可以在x86计算机上静态分析ARM二进制文件。而这种方法的缺点是静态分析并不了解二进制文件运行时的状态,这会使分析非常具有挑战性。
动态分析。与静态分析相反,动态分析会运行二进制文件并在执行时对其进行分析。这种方法通常比静态分析更简单,因为你完全了解整个运行时状态,包括变量的值和条件分支的结果。但是,你仅能看到执行的代码,因此这种方法可能会遗漏程序中一些有趣的部分。
静态分析和动态分析各有优缺点,你将在本书中学习这两种方式的技巧。除了被动的二进制分析,你还将学习二进制插桩技术,用以在不需要源代码的情况下修改二进制程序。二进制插桩依赖于像反汇编这样的分析技术,同时它可以用来辅助二进制分析。鉴于二进制分析和插桩技术之间的这种共生关系,本书涵盖了这两块内容。
前面已经提到过,你可以使用二进制分析来对无源代码程序进行记录和测试。但是,即使有可用的源代码,二进制分析对于发现细微的错误也特别有用,这些错误在二进制级别的体现要比源码级别的更清晰。许多二进制分析技术对于高级调试也很有用。本书介绍了可以在这些场景中使用的二进制分析技术。
二进制分析具有挑战性,且相比源代码级别上的等价分析要困难得多。事实上,许多二进制分析任务基本上是不确定的,这意味着为这些问题构建一个总是返回正确结果的分析引擎是不可能的!为了让你了解即将遇到的挑战,这里列出了二进制分析困难的原由。遗憾的是,这个列表远远不够详尽。
- 没有符号信息。当我们用像C或C++这样的高级语言编写源代码时,我们用有意义的名称给变量、函数和类这样的结构命名。我们称这些名称为符号信息,或简称为符号。良好的命名约定使源代码更容易理解,但是在二进制级别上它们并没有真正的相关性。因此,二进制文件通常去掉了符号,这使得理解代码更加困难。
- 没有类型信息。高级语言的另一个特性是它们定义明确的变量类型(如int、float或string)以及更复杂的数据结构(如struct类型)。相反,在二进制级别上,类型从来没有被明确地声明过,这使得数据的用途和结构难以推断。
- 没有高级抽象。现代程序被划分为类和函数,但是编译器扔掉了这些高级信息。这意味着二进制文件看起来是大量的代码和数据,而不是结构良好的程序,因而将其恢复为高级结构的过程非常复杂,且容易出错。
- 混合代码和数据。二进制文件可以(并且确实)包含与可执行代码混合在一起的数据片段。这使得将数据解释为代码很容易。反之亦然,但这会导致错误的结果。
- 位置相关的代码和数据。由于二进制文件的设计初衷不包括可修改,所以即使只添加一条机器指令也可能会产生其他代码移位的问题,从而使内存地址和代码中其他地方的引用无效。因此,任何类型的代码或数据修改都极具挑战性,并且容易破坏二进制文件。
由于以上这些挑战,所以我们在实践中不得不经常面对不精确的分析结果。二进制分析的一个重要部分是,尽管存在分析错误,但仍要以创造性的方式构建可用的工具!
谁需要阅读这本书
这本书的目标读者包括安全工程师、学术安全研究人员、逆向工程师、恶意软件分析师和对二进制分析感兴趣的计算机科学专业学生。不过实际上,我尝试让所有对二进制分析感兴趣的人都能读懂这本书。
另外,因为这本书涵盖了较高级的知识,所以一些编程和计算机系统的基础知识是必需的。为了充分利用这本书,你应该具备以下几点:
- 可以使用C和C++进行有效编程;
- 了解操作系统的内部(进程是什么,虚拟内存是什么等);
- 了解如何使用Linux shell(尤其是bash);
- 具有x86/x86-64汇编程序的工作知识。如果你还不了解任何程序集,请确保先阅读附录A。
如果你以前从未编写过程序,或者不喜欢钻研计算机系统的底层细节,那么这本书可能不适合你。
这本书里有什么
本书的主要目标是使你成为全面的二进制分析人员,并熟悉该领域的所有重要主题,包括基本主题和高级主题,如二进制插桩、污点分析和符号执行。这本书并不是一个全面的资源,因为二进制分析领域和工具变化如此之快,一本全面的书可能很快就过时了。相反,这本书的目的是让你了解所有重要的主题,这样你就可以更独立地学习。同样,这本书也没有深入讲解如何对x86和x86-64代码进行逆向工程(尽管附录A涵盖了基础知识)或分析这些平台上的恶意软件的所有复杂之处。已经有许多关于这些主题的专门书籍,在这里重复它们的内容是没有意义的。
这本书分为4个部分。
第一部分“二进制格式”介绍二进制格式,这对理解本书的其余部分至关重要。如果你已经熟悉ELF和PE二进制格式以及libbfd,你可以跳过本部分的一章或多章。
第1章“二进制简介”提供二进制程序剖析的一般介绍。
第2章“ELF格式”介绍Linux上使用的ELF二进制格式。
第3章“PE格式简介”包含对PE的简要介绍,以及在Windows上使用的二进制格式。
第4章“使用libbfd创建二进制加载器”展示如何使用libbfd解析二进制文件,并构建本书其余部分使用的二进制加载器。
第二部分“二进制分析基础”包括基本的二进制分析技术。
第5章“Linux二进制分析”介绍Linux的基本二进制分析工具。
第6章“反汇编与二进制分析基础”涵盖基本的反汇编技术和基本的分析模式。
第7章“简单的ELF代码注入技术”,在本章,你将第一次体验如何使用寄生代码注入和十六进制编辑等技术来修改ELF二进制文件。
第三部分“高级二进制分析”介绍高级二进制分析技术。
第8章“自定义反汇编”展示如何使用Capstone创建自定义的反汇编工具。
第9章“二进制插桩”介绍如何用Pin修改二进制文件,Pin是一个成熟的二进制工具平台。
第10章“动态污点分析的原理”介绍动态污点分析的原理,这是一种非常先进的二进制分析技术,允许你跟踪程序中的数据流。
第11章“基于libdft的动态污点分析”教你用libdft构建自己的动态污点分析工具。
第12章“符号执行原理”专门介绍符号执行,这是另一种高级技术,你可以用它自动推理复杂的程序属性。
第13章“使用Triton实现符号执行”演示如何使用Triton构建实用的符号执行工具。
第四部分“附录”包括对你有用的资源。
附录A“x86汇编快速入门”为不熟悉x86汇编语言的读者提供简要介绍。
附录B“使用libelf实现PT_NOTE覆盖”提供在第7章中使用的elfinject工具的实现细节,并介绍libelf。
附录C“二进制分析工具清单”包含你可以使用的二进制分析工具列表。
预备知识
为了帮助你更好地理解本书,现在来简要回顾一下关于代码示例、汇编语法和开发平台的约定。
指令集架构
尽管你可以将本书中的许多技术推广到其他体系结构,不过实际示例将集中在Intel x86指令集体系结构(ISA)及其64位版本x86-64(简称x64)上。我将x86和x64 ISA简称为“x86 ISA”。除非另有说明,它的示例通常为x64代码。
x86 ISA之所以有趣,是因为它在消费市场(尤其是台式计算机和便携式计算机)和二进制分析研究中非常普遍(部分是由于它在最终用户计算机中的流行)。因此,许多二进制分析框架都是针对x86的。
此外,x86 ISA的复杂性可以使你了解一些在简单体系结构中不会发生的二进制分析难题。x86架构具有向后兼容的悠久历史(可追溯到1978年),从某种意义上讲,它导致了非常密集的指令集,绝大多数可能的字节值代表有效的操作码。这加剧了代码与数据的混合问题,使反汇编程序不太能清楚地区分它们,从而会错误地将数据解释为代码。此外,指令集是可变长度的,并且允许对所有有效字长进行非对齐的存储器访问。因此,x86允许使用独特的复杂二进制结构,例如(部分)重叠和非对齐的指令。换句话说,一旦你学会了处理像x86这样复杂的指令集,那么处理其他指令集(例如ARM)就会非常容易上手!
汇编语法
正如附录A所阐述的,有两种常用的语法格式——Intel语法和AT&T语法用于表示x86机器指令。这里,我将使用Intel语法,因为它更加简洁。在Intel语法中,将常量移动到edi寄存器中是这样实现的:
mov edi, 0x6
注意,目标操作数(edi)在前面。如果你不确定AT&T和Intel语法之间的差异,请参阅附录A,以简要了解每种格式的主要特征。
二进制格式和开发平台
我已经在Ubuntu Linux上开发了本书附带的所有代码示例,除了少数用Python编写的示例外,其余都是用C/C++编写的。这是因为许多流行的二进制分析库主要是针对Linux的,并且具有方便的C/C++或Python API。不过,本书使用的所有技术以及大多数库和工具也适用于Windows,所以如果Windows是你选择的平台,那么将你所学到的知识转移到Windows上应该没有什么困难。在二进制格式方面,本书主要关注Linux平台上默认的ELF二进制文件,尽管许多工具也支持Windows PE二进制文件。
代码示例和虚拟机
本书的每一章都提供了几个代码示例,还有一个预先配置的虚拟机(VM),该虚拟机包含了所有的示例。VM运行最新的Linux发行版Ubuntu 16.04,并安装了本书讨论的所有开源二进制分析工具。你可以使用虚拟机来试验代码示例,并在每一章的最后练习如何解决。
在No Starch出版社官网的该书的网页上,你还可以找到一个包含示例和练习源代码的归档文件。如果不想下载整个VM,可以下载它,但请记住,一些必需的二进制分析框架需要复杂的设置,如果你选择不使用VM,那么你必须自己进行这些设置。
要使用VM,你需要虚拟化软件。VM将用于VirtualBox,你可以从其官方网站免费下载。VirtualBox适用于所有流行的操作系统,包括Windows、Linux和macOS。
安装完VirtualBox后,运行它,然后依次打开选项File→Import Appliance,选择已下载的虚拟机。添加虚拟机之后,单击主VirtualBox窗口中标记为start的绿色箭头来启动它。VM启动之后,可以使用“binary”作为用户名和密码登录。然后,使用键盘快捷键Ctrl-Alt-T打开一个终端,你就可以按照本书进行操作了。
在目录~/code中,你会发现每个章节都有一个子目录,其中包含了该章节的所有代码示例和其他相关文件。例如,你会在目录~/code/chapter1中找到第1章的所有代码。还有一个名为~/code/inc的目录,它包含了多个章节中程序使用的通用代码。我为C++源文件使用.cc扩展名,为普通C文件使用.c扩展名,为头文件使用.h扩展名,为Python脚本使用.py扩展名。
要构建给定章节的所有示例程序,那么只需打开一个终端,导航到该章节的目录,然后执行make命令来构建该目录中的所有内容。这在所有情况下都有效,除了那些我明确提到要构建示例的其他命令的情况。
大多数重要的代码示例将在相应的章节中详细讨论。如果书中讨论的清单作为VM上的源文件时可用,那么它的文件名将显示在清单之前,如下所示:
filename.c
int
main(int argc, char *argv[])
{
return 0;
}
这个清单标题表示你将在filename.c文件中找到清单中所示的代码。除非另有说明,否则你将在示例出现的章节目录中找到它所列出的文件名下的文件。你还会遇到标题不是文件名的清单,这意味着这些只是书中使用的示例,在 VM中没有相应的副本。在VM上没有副本的短清单可能没有标题,如前面所示的汇编语法示例。
显示shell命令及其输出的清单,使用$符号来表示命令提示符,并使用粗体字表示包含用户输入的行。这些行是你可以在虚拟机上尝试的命令,而后面没有以提示符作为前缀或没有以粗体显示的行表示命令输出。例如,以下是VM上~/code目录的概述:
$ cd ~/code && ls
chapter1 chapter2 chapter3 chapter4 chapter5 chapter6 chapter7
chapter8 chapter9 chapter10 chapter11 chapter12 chapter13 inc
请注意,我有时会编辑命令输出以提高可读性,因此你在VM上看到的输出可能略有不同。
练习
在每一章的末尾,有一些练习和挑战来巩固你在这一章学到的技能。有些练习用你在本章中学到的技巧相对容易解决,而有些练习可能需要更多的努力或尝试一些独立的研究。