Ghidra权威指南
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第2章 逆向与反汇编工具

我们已经掌握了一些反汇编的背景知识,在深入学习Ghidra之前,了解其他一些用于逆向工程的工具,会很有帮助。这些工具中有许多早于Ghidra发布,并且仍可用于快速分析二进制文件,以及审查Ghidra的分析结果。如我们所见,Ghidra将这些工具的许多功能整合到用户界面中,为逆向工程提供了一个集成环境。

2.1 分类工具

在拿到一个未知文件时,先问自己一些简单的问题,如“这是个什么文件”。回到该问题的首要也是最基本的原则是,绝不要根据文件的扩展名来确定文件的类型。在脑子里建立起“文件扩展名并无实际意义”的印象后,再尝试学习下面几个小工具。

2.1.1 file

file命令是一个标准工具,包含在大多数*nix风格的操作系统以及Linux下的Windows子系统(WSL)[1]中。Windows用户通过安装Cygwin或MinGW[2]也可以获得该命令。file命令试图通过检查文件中的某些特定字段来识别文件的类型。有时,file还可以识别常见的字符串,如#!/bin/sh(shell脚本)和<html>(HTML文档)。

识别那些非ASCII内容的文件则更具挑战性。在这种情况下,file会尝试判断该文件的结构是否符合某种已知的文件格式。在许多情况下,它会搜索特定文件类型所特有的标签值(通常称为幻数[3])。下面的十六进制清单列出了几个用于识别某些常见文件类型的幻数。

file命令能够识别大量的文件格式,包括数种ASCII文本文件、各种可执行文件和数据文件。file执行的幻数检查由幻数文件所包含的规则所控制。默认的幻数文件因操作系统而异,但常见的位置包括/usr/share/file/magic、/usr/share/misc/magic 和/etc/magic。更多关于幻数文件的资料可以查阅 file的文档。

有时,file还可以区分某一给定文件类型中的细微变化。如下清单展示了file不仅能够识别ELF二进制文件的多种变体,而且还能够识别与二进制文件如何链接(静态或动态)以及是否去除了符号等信息。

WSL环境

适用于Linux的Windows子系统(Windows Subsystem for Linux,WSL),直接在Windows内提供了一个 GNU/Linux 命令行环境,而无须创建虚拟机。在 WSL 安装时,用户可以选择 Linux发行版,然后在WSL上运行它。它提供了对常用命令行软件(grep、awk)、编译器(gcc、g++)、解释器(Perl、Python、Ruby)、网络工具(nc、ssh)等的访问权限。一旦安装了WSL,就可以在Windows系统上编译和执行许多为Linux编写的程序。

当然,file及类似的工具也会出错。如果一个文件碰巧包含了某种文件格式的标记,那么file很可能会将其误识别。你可以使用十六进制编辑器将任何文件的前4个字节修改为Java的幻数序列CA FE BA BE,然后验证一下,此时file会将这个新修改的文件错误地识别为Java.class文件。同样,一个仅包含两个字符MZ的文本文件会被误认为是MS-DOS可执行文件。在逆向过程中,切记永远不要完全相信任何工具提供的结果,除非该结果得到了多种工具和手工分析的确认。

剥离二进制可执行文件

剥离(stripping)是指从二进制文件中删除符号。这些符号是编译时留在二进制目标文件中的。在创建最终的可执行文件或库时,在链接过程中会使用其中一些符号来解析文件之间的引用关系。在其他情况下,符号还可提供一些信息给调试器使用。在链接完成后,这里的很多符号就不需要再使用了。在构建时,可以通过传递给链接器的选项删除将不必要的符号。此外,还可以使用名为strip的工具从现有的二进制文件中删除符号。虽然剥离后的二进制文件比未剥离时要小,但功能保持不变。

2.1.2 PE Tools

PE Tools[4]是一组用于分析Windows系统中正在运行的进程和可执行文件的工具。图2-1显示了PE Tools的主界面,其中列出了所有的活动进程,并可通过该界面访问PE Tools的所有工具。

从进程列表中,用户可以将进程的内存映像转储到文件中,也可以利用PE Sniffer工具确定可执行文件是由何种编译器构建的,或者该文件是否经过某种已知混淆工具的处理。Tools菜单提供了分析磁盘文件的类似选项。使用内嵌的PE Editor工具,用户还可以查看文件的PE文件头,或者方便地修改任何文件头的值。通常,如果想要从一个被混淆文件中还原一个有效的 PE,就需要修改 PE文件头。

图2-1 PE Tools工具

二进制文件混淆

混淆是指任何试图掩盖事物真相的行为。应用于可执行文件时,混淆则是指任何试图掩盖程序真实行为的行为。程序员可能出于不同的原因而采用混淆技术,如保护专有算法和掩盖恶意行为。几乎所有的恶意软件都利用混淆来防止被分析。广泛可用的工具可帮助程序作者生成混淆程序。我们将在第21章详细讨论混淆工具及技术,以及它们对逆向工程的影响。

2.1.3 PEiD

PEiD[5]是另一款Windows工具,主要用于识别构建某一特定Windows PE二进制文件所使用的编译器,并识别任何用于混淆Windows PE二进制文件的工具。图2-2显示了如何使用PEiD来识别用于混淆某个Gaobot[6]蠕虫变体的工具(本例中为ASPack)。

图2-2 PEiD工具

PEiD的许多其他功能与PE Tools的功能相同,包括显示PE文件头、收集有关正在运行的进程的信息、执行基本的反汇编等。

2.2 摘要工具

由于我们的目标是对二进制程序文件进行逆向工程,因此,在对文件进行初步分类后,需要用更复杂的工具来提取详细信息。本节讨论的工具不仅能够识别它们所处理的文件的格式,在大多数情况下,它们还能理解特定的文件格式,并从输入文件中提取出特定的信息。

2.2.1 nm

将源文件编译成目标文件时,编译器必须嵌入一些有关全局(外部)符号的位置信息,以便链接程序在组合目标文件以创建可执行文件时,能够解析对这些符号的引用。除非被告知要去除最终可执行文件中的符号,否则链接程序通常会将目标文件中的符号带入最终的可执行文件。根据nm手册的描述,该工具的作用是“列出目标文件中的符号”。

使用 nm 检查中间目标文件(扩展名为.o 的文件,而不是可执行文件)时,将输出文件中声明的所有函数和全局变量的名称。nm工具的输出如下所示:

可以看到,nm列出了每个符号以及该符号的相关信息。开头的字母表示符号的类型,下面逐一解释前面出现的字母。

·U,未定义符号(通常是外部符号引用)。

·T,在文本部分定义的符号(通常是函数名称)。

·t,在文本部分定义的本地符号。在C程序中,通常等同于一个静态函数。

·D,已初始化的数据值。

·C,未初始化的数据值。

注意:大写字母表示全局符号,小写字母表示局部符号。更多信息,包括字母代码的详细解释,可以查阅nm手册。

使用 nm 检查可执行文件中的符号时,将会显示出更多信息。在链接过程中,符号将被解析为虚拟地址(如有可能),此时运行nm会得到更多的信息。下面是使用nm处理一个可执行文件所得到的部分输出。

在这个例子中,链接过程给一些符号(如main)分配了虚拟地址,引入了一些新的虚拟符号(如__libc_csu_init),以及修改了一些符号(如 my_unitialized_global)的类型。而其他符号由于继续引用外部符号,仍然是未定义符号。本例所使用的二进制文件是动态链接的,所以未定义符号是在 C语言共享库中定义的。

2.2.2 ldd

创建可执行文件后,其引用的任何库函数的地址必须能被解析。链接器有两种方法可以解析对库函数的调用:静态链接和动态链接。传递给链接器的命令行参数决定了具体使用哪一种方法。因此,一个可执行文件可以是静态链接、动态链接,或二者兼具[7]

当使用静态链接时,链接器会将应用程序的目标文件与所需的库文件副本进行合并,生成一个可执行文件。在程序运行时,就不需要再定位库代码,因为它已经包含在可执行文件中了。静态链接的优点是:(1)函数调用更快;(2)发布二进制文件更容易,因为无须考虑用户系统中的库函数。静态链接的缺点是:(1)生成的可执行文件更大;(2)库组件发生改变时升级程序的难度更大,因为每次库发生变化,程序都必须重新链接。从逆向工程的角度来看,静态链接会使问题更加复杂。在分析静态链接的二进制文件时,要回答“该二进制文件链接了哪些库”和“这些函数中的哪个是库函数”可不那么容易。我们将在第13章讨论对静态链接代码进行逆向工程时遇到的挑战。

动态链接与静态链接的不同点在于,链接器无须复制它需要的任何库。相反,链接器只需在最终的可执行文件中插入对所需库(通常是.so 或.dll 文件)的引用,如此生成的可执行文件更小,升级库代码也容易得多。由于一个库仅需维护一个副本即可被多个二进制文件引用,所以,用新版本的库替换过时的库后,依赖该库的二进制文件所创建的任何新进程都将使用该库的新版本。使用动态链接的一个缺点是,它的加载过程更加复杂。所有必需的库都需要定位并加载到内存中,而不是仅加载一个包含全部库代码的静态链接文件。动态链接的另一个缺点是,供应商不仅需要发布他们自己的可执行文件,还需要发布该文件所依赖的所有库文件。如果程序在一个缺少其所需库文件的系统上运行,那么必然会出错。

下面展示了如何将程序分别编译成动态和静态链接版本、生成的二进制文件的大小,以及使用file工具识别的结果。

为了确保动态链接正常运行,动态链接的二进制文件必须指明它们所依赖的库,以及每个库所需的特定资源。因此,与静态链接的二进制文件不同,确定它们所依赖的库文件非常简单。ldd(listdynamic dependencies)工具可用于列出任何可执行文件所需的动态库。在下面这个例子中,我们使用ldd确定Apache Web服务器所依赖的库:

Linux和BSD系统都提供了ldd工具。在macOS系统上,使用otool工具并添加-L选项(otool-L filename)也可以实现类似的功能。在Windows系统上,可以使用Visual Studio工具套件中的dumpbin工具列出依赖库:dumpbin/dependents filename。

当心你的工具!

尽管ldd似乎是一个简单的工具,但ldd手册指出:“切勿对不受信任的可执行文件使用ldd,因为这可能会导致任意代码执行。”尽管在大多数情况下不太可能出现,但可以提醒我们,即使是简单的软件逆向工程(SRE)工具,在检查不受信任的文件时,也可能产生意外的后果。很明显,执行不受信任的二进制文件是不太安全的。更明智的做法是,即使是对不受信任的二进制文件做静态分析时,也要做好预防措施,并且假定执行SRE任务的计算机及其数据,或者与之相连的其他主机,都会受到该SRE活动的危害。

2.2.3 objdump

与专用的ldd不同,objdump[8]的功能更加丰富,其目标是显示来自目标文件的信息。这是一个相当宽泛的目标,objdump为此提供了超过30个命令行选项,以提取目标文件中的各种信息。objdump可用于显示与目标文件有关的以下(甚至更多)信息。

·节头部,程序文件中每个节的摘要信息。

·专用头部,程序内存分布信息和运行时加载器所需的其他信息,包括由 ldd 等工具生成的库列表。

·调试信息,程序文件中嵌入的所有调试信息。

·符号信息,以类似nm工具的方式转储符号表信息。

·反汇编代码清单,objdump工具对文件中标记为代码的部分执行线性扫描反汇编。反汇编x86代码时,objdump可以生成AT&T或Intel语法,并且可以将反汇编代码保存到文本文件中。这样的文本文件名为反汇编死代码清单(dead listing),它们当然可以用于逆向工程,但很难做到有效导航,也很难以一致且无错的方式进行修改。

objdump工具是GNU binutils[9]工具套件的一部分,可在Linux、FreeBSD和Windows(通过WSL或Cygwin)系统上使用。需要注意的是,objdump依赖于binutils中的二进制文件描述符库(libbfd)来访问目标文件,因此能够解析libbfd所支持的文件格式(ELF、PE等)。对于ELF文件的解析,还有一个名为readelf的工具可用,该工具提供的大多数功能与objdump相同,主要区别在于readelf并不依赖libbfd。

2.2.4 otool

otool工具可以简单理解为macOS版的objdump,用于解析macOS上Mach-O二进制文件的信息。下面展示了如何使用otool显示Mach-O二进制文件的动态库依赖关系,类似于ldd的功能。

otool工具可用于显示与文件头和符号表相关的信息,并对文件的代码部分进行反汇编。更多关于otool功能的信息,可以参阅相关手册。

2.2.5 dumpbin

dumpbin命令行工具包含在微软Visual Studio工具套件中。与otool和objdump一样,dumpbin能够显示与Windows PE文件相关的信息。下面的例子展示了如何使用dumpbin以类似ldd的方式显示Windows记事本程序的动态依赖关系。

dumpbin的其他选项可以从PE二进制文件的各个部分提取信息,包括符号、导入函数名、导出函数名和反汇编代码等。更多关于dumpbin使用的信息可以从微软网站[10]获得。

2.2.6 c++filt

由于每一个重载的函数都使用与原函数相同的名称,因此,支持函数重载的语言必须拥有一种机制,以区分同一个函数的多个重载版本。下面的C++示例展示了名为demo的函数的多个重载版本的原型:

通常,在目标文件中不可能有两个相同名称的函数。为支持重载,编译器通过合并描述函数参数类型序列的信息,为重载函数生成唯一的名称。为具有相同函数名的函数生成唯一名称的过程称为名称改编(name mangling)[11]。如果使用nm来转储前面C++代码编译后的文件中的符号,将得到类似下面的输出(经过滤以高亮显示demo的重载版本):

C++标准中并未定义名称改编方案的标准,所以编译器设计人员可以自行定义。要想破译demo函数的重载版本,我们需要一个能够理解编译器(这里是 g++)名称改编方案的工具。c++filt 正是这样一个工具,它将每个输入都视为改编后的名称,并尝试确定用于生成该名称的编译器。如果该名称是一个合法的改编名称,则输出改编前的原始名称;如果该名称无法被c++filt识别,那么就不做修改按原样输出。

将上面nm的输出结果传递给c++filt,就可以恢复出这些函数的原始名称,如下所示:

需要注意的是,改编名称还包含其他与函数相关的信息。这些信息虽然无法使用 nm 查看,但对逆向工程可能会很有帮助,在更复杂的情况下,这些额外信息可能包含有关类名或函数调用约定的数据。

2.3 深度检测工具

到目前为止,我们已经讨论了一些有用的工具,利用它们可以在对文件内部结构知之甚少的情况下进行粗略的分析,也可以在深入了解文件结构之后,从文件中提取特定的信息。在本节中,我们将介绍一些不用考虑文件类型即可从中提取特定信息的工具。

2.3.1 strings

有时候,提出一些与文件内容相关的通用性问题,即那些不需要了解文件结构即可回答的问题,会有一定的帮助。例如:“这个文件中是否包含字符串?”当然,在此之前我们必须先回答另一个问题:“到底什么是字符串?”我们可以将字符串简单定义为连续的可打印字符序列。该定义通常还需要指定一个最小长度和一个特定的字符集。因此,我们可以指定搜索至少包含4个连续的ASCII可打印字符的字符串,并将结果打印到控制台。对此类字符串的搜索通常不会受到文件结构的限制。在ELF二进制文件中搜索字符串就像在微软Word文档中搜索字符串一样简单。

strings工具专门用于提取文件中的字符串内容,通常不必考虑文件的格式。在strings的默认设置下(包含至少4个字符的7位ASCII序列)可得到以下内容。

strings为何改变策略?

曾经在默认情况下,使用strings检查可执行文件时,它只会在二进制文件中可加载的已初始化数据节区搜索字符串。这要求strings使用libbfd之类的库解析二进制文件以找到那些节区。但当它被用于解析不受信任的二进制文件时,这些库中的漏洞可能会导致任意代码执行[12]。正因如此,strings改变了其默认行为,去检查整个二进制文件而不再解析那些节区(同-a选项)。如果想要调用原来的默认行为,则可以使用-d选项。

尽管我们看到一些似乎是程序输出的字符串,但其他字符串则像是函数名和库名。因此,绝不能仅仅根据这些字符串来判定程序的功能。分析人员往往会掉入试图根据strings的输出来推断程序行为的陷阱。需要记住的是,二进制文件中包含某个字符串,并不代表该文件曾以某种方式使用它。

下面是使用strings时的一些注意事项。

·默认情况下,strings不会指出字符串在文件中的位置。使用命令行参数-t可以让strings打印出每个字符串在文件中的偏移信息。

·许多文件使用了其他字符集。使用命令行参数-e 可以让 strings 搜索宽字符,例如 16 位的Unicode字符。

2.3.2 反汇编器

如前所述,有很多工具可以生成二进制目标文件的死代码清单形式的反汇编代码。PE、ELF和Mach-O二进制文件可分别使用dumpbin、objdump和otool进行反汇编。但这些工具都不能处理任意的二进制数据块。有时你会遇到一些不符合常用文件格式的二进制文件,并且需要从某个用户指定的偏移量开始反汇编过程,那么就需要使用一些其他工具。

ndisasm和diStorm[13]是其中两个用于x86指令集的流式反汇编器(stream disassembler)。ndisasm工具包含在 NASM[14]中。下面的例子展示了如何使用 ndisasm 反汇编一段由 Metasploit[15]框架生成的shellcode。

灵活的流式反汇编在很多场景下非常有用。例如,在分析网络数据包中可能包含shellcode的计算机网络攻击时,流式反汇编器就可用于反汇编数据包中包含shellcode的部分,从而分析恶意载荷的行为。另一种情况是分析那些找不到布局参考的ROM映像。ROM中有些部分是数据,另一些部分则是代码,此时可以使用流式反汇编器来反汇编映像中被认为是代码的部分。

2.4 小结

本章所讨论的工具不一定是同类中最好的,但它们确实是二进制文件逆向分析人员最常用的。更重要的是,这些工具大大促进了Ghidra的开发。在以后的章节中,我们还会重点讨论这些独立工具,它们的功能与集成到Ghidra中的功能类似。对这些工具的了解将极大地增进你对Ghidra用户界面以及Ghidra显示的许多信息的理解。


[1] 参见链接2-1。

[2] Cygwin参见链接2-2。MinGW参见链接2-3。

[3] 幻数(magic number)是某些文件格式规范要求的特殊标签值,其存在表明文件符合这种规范。有时,人们在选择幻数时加入了幽默的因素。例如,MS-DOS的可执行文件头中的MZ标签是MS-DOS原架构师Mark Zbikowski姓名的首字母缩写。而选择十六进制数0xcafebabe作为Java.class文件的幻数,仅仅是因为它是一个容易记住的十六进制字符串。

[4] 参见链接2-4。

[5] 参见链接2-5。

[6] 参见链接2-6。

[7] 有关链接的更多内容,请参阅John R.Levine的著作Linkers and Loaders (Morgan Kaufmann,1999)。

[8] 参见链接2-7。

[9] 参见链接2-8。

[10] 参见链接2-9。

[11] 有关名称改编的概述,请参阅链接2-10。

[12] 参见CVE-2014-8485和链接2-11。

[13] 参见链接2-12。

[14] 参见链接2-13。

[15] 参见链接2-14。