精通逆向工程(一)
原文:
annas-archive.org/md5/4edb9a969a22ca6c6dd931a00e8c6024译者:飞龙
序言
逆向工程是一种用于分析软件、发现其弱点并加强防御的工具。黑客使用逆向工程作为工具来暴露安全漏洞和可疑的隐私实践。本书将帮助您掌握使用逆向工程的技巧。
本书适合的人群
如果您是安全工程师、分析师或系统程序员,并且想使用逆向工程来改善软件和硬件,那么本书非常适合您。如果您是想探索和学习逆向工程的开发者,本书对您也非常有用。
为了最大化地利用本书
-
具备一些编程/脚本编写知识会是一个额外的加分项。
-
了解信息安全和 x86 汇编语言将是一个优势。
-
使用的操作系统:Windows 和 Linux(版本将取决于 VirtualBox 的要求)
-
至少四核处理器,4 GB 内存,和 250 GB 硬盘空间。
-
您可能需要提前从微软下载虚拟机,因为这些文件可能需要一些时间才能下载。请参阅开发者页面:
developer.microsoft.com/en-us/microsoft-edge/tools/vms/。
下载示例代码文件
您可以从您的帐户下载本书的示例代码文件,网址是www.packt.com。如果您在其他地方购买了本书,可以访问www.packt.com/support,并注册以直接将文件发送到您的电子邮箱。
您可以按照以下步骤下载代码文件:
-
登录或注册到www.packt.com。
-
选择“SUPPORT”选项卡。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
下载文件后,请确保使用以下最新版本解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,地址为:github.com/PacktPublishing/Mastering-Reverse-Engineering。如果代码有更新,它将在现有的 GitHub 库中更新。
我们还提供了来自丰富书籍和视频目录的其他代码包,您可以在**github.com/PacktPublishing/**上找到它们。快来看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书使用的截图/图示的彩色图片。您可以在此下载:www.packtpub.com/sites/default/files/downloads/9781788838849_ColorImages.pdf
使用的约定
本书中使用了多种文本约定。
CodeInText:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 句柄。以下是一个示例:“hkResult用于由RegEnumValueA开始枚举注册表键下的每个注册表值。”
代码块设置如下:
while (true) {
for (char i = 1; i <= 255; i++) {
if (GetAsyncKeyState(i) & 1) {
sprintf_s(lpBuffer, "\\x%02x", i);
LogFile(lpBuffer, (char*)"log.txt");
}
}
当我们希望引起您对代码块特定部分的注意时,相关行或项目将用粗体标记:
87 to base-2
87 divided by 2 is 43 remainder 1.
43 divided by 2 is 21 remainder 1.
21 divided by 2 is 10 remainder 1.
10 divided by 2 is 5 remainder 0.
5 divided by 2 is 2 remainder 1.
粗体:表示新术语、重要词汇或屏幕上可见的单词。例如,菜单或对话框中的单词在文本中显示如此。以下是一个示例:“在 VirtualBox 中,单击 File|Import Appliance。”
警告或重要注释如下。
提示和技巧出现如下。
联系我们
我们始终欢迎读者的反馈。
总体反馈:如果您对本书的任何方面有疑问,请在消息主题中提及书名,并通过电子邮件联系我们,邮箱为customercare@packtpub.com。
勘误:尽管我们已经尽最大努力确保内容准确性,错误确实偶尔会发生。如果您在本书中发现错误,我们将不胜感激您向我们报告。请访问www.packt.com/submit-erra…,选择您的书籍,点击错误提交表格链接并填写详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法副本,请提供给我们位置地址或网站名称将不胜感激。请通过链接联系我们,链接为copyright@packt.com。
如果您有兴趣成为作者:如果您精通某个主题,并且有意撰写或为书籍做出贡献,请访问authors.packtpub.com。
评论
请留下您的评价。一旦您阅读并使用了本书,为什么不在购买的网站上留下您的评论呢?潜在的读者可以通过您公正的意见来做出购买决定,我们在 Packt 可以了解到您对我们产品的看法,我们的作者可以看到您对他们书籍的反馈。谢谢!
有关 Packt 的更多信息,请访问packt.com。
第一章:准备进行逆向分析
在本章中,我们将介绍逆向工程并解释它的用途。我们将从讨论已经在各个领域应用的一些见解开始,帮助读者理解逆向工程是什么。在本章中,我们将简要介绍软件逆向工程的过程和使用的工具类型。这里还提供了有关正确处理恶意软件的提示。本章的最后一部分展示了如何使用可以轻松下载的工具设置我们的初始分析环境。以下主题将会覆盖:
-
逆向工程的用途
-
应用逆向工程
-
逆向工程中使用的工具类型
-
恶意软件处理指南
-
设置逆向工程环境
逆向工程
将某物拆解并重新组装是一个帮助人们理解事物如何制造的过程。一个人可以通过首先展开折纸来重新制作和复制一件折纸作品。了解汽车的工作原理需要理解每个主要和次要的机械部件及其用途。人体解剖的复杂性要求人们了解身体的每一个部位。如何做到?通过解剖。逆向工程是一种帮助我们理解事物如何设计、为何如此存在、何时触发、如何工作以及它的目的是什么的方法。实际上,这些信息被用来重新设计和改进,以提升性能和降低成本。它甚至可以帮助修复缺陷。
然而,逆向工程涉及伦理问题,并且仍然是一个持续的争论。类似于弗兰肯斯坦的情况,存在一些违反自然法则的问题,这些问题在人类看来是不可接受的。如今,简单的重新设计如果没有经过深思熟虑,可能会引发版权侵权。一些国家和地区有法律规定禁止逆向工程。然而,在软件安全行业,逆向工程是必不可少的,也是一个常见的使用案例。
想象一下,如果特洛伊木马在允许进入城市的大门之前被彻底检查并拆解。这可能会导致一些士兵在城门外为保护城市而牺牲。下次城里再收到一只特洛伊木马时,弓箭手们就知道该把箭指向哪里。这次就没有死士兵了。恶意软件分析也是如此——通过逆向工程了解某种恶意软件的行为,分析师可以为网络推荐各种保护措施。可以把特洛伊木马看作恶意软件,把分析师看作最初检查木马的士兵,而城市则是计算机网络。
任何想要成为逆向工程师或分析师的人都应具备足够的资源整合能力。搜索互联网是逆向工程的一部分。分析师不会仅仅依赖我们在本书中提供的工具和信息。有时,分析可能甚至需要逆向工程师自己开发工具。
软件审计可能需要进行逆向工程。除了高级别的代码审查过程外,一些软件质量验证还涉及实施逆向工程。这些测试活动的目的是确保漏洞被发现并修复。在软件的设计和开发过程中,有许多因素未被考虑。大多数这些因素是随机输入和外部因素,可能会导致泄露,从而产生漏洞。这些漏洞可能会被用于恶意目的,不仅干扰软件的正常运行,还可能造成损害,甚至危及其安装的系统环境。系统监控和模糊测试工具通常在软件测试时使用。今天的操作系统具有更好的保护机制来防止崩溃。操作系统通常会报告发现的任何异常,如内存或文件损坏。同时也提供额外的信息,如崩溃转储。通过这些信息,逆向工程师可以准确定位软件中需要检查的部分。
在软件安全行业中,逆向工程是必备的核心技能之一。每一次攻击,通常以恶意软件的形式出现,都需要进行逆向分析。通常,第一步是清理网络和系统,防止系统进一步被破坏。分析师会确定恶意软件如何安装并保持持久性。然后,他们会制定卸载恶意软件的步骤。在反恶意软件阶段,这些步骤将被用来制定清理程序,一旦反恶意软件产品能够检测到系统已经受到侵害。
该分析提供了恶意软件如何能够侵入系统的信息。通过这些信息,网络管理员能够采取措施来缓解攻击。如果恶意软件是因为用户打开了一个包含 JavaScript 代码的电子邮件附件而进入系统的,网络管理员就会实施阻止包含 JavaScript 附件的电子邮件。
一些管理员甚至被建议重新构建他们的网络基础设施。一旦系统被攻破,攻击者可能已经获取了关于网络的所有信息,并能轻松发起同样的攻击。进行重大改变将大大有助于防止同样的攻击再次发生。
重构基础设施的一部分是教育。防止系统被攻破的最佳方法是通过教育用户如何保护信息,包括他们的隐私。了解社会工程学并拥有之前攻击的经验使用户意识到安全问题。了解攻击者如何能够破坏一个机构,以及他们能够造成的损害是非常重要的。因此,安全政策被实施,备份被设置,持续学习被执行。
更进一步,目标公司可以将攻击报告给相关当局。即使是一小段信息也能为当局提供线索,帮助他们追踪嫌疑人并关闭恶意软件通信服务器。
系统可能通过利用软件漏洞被攻破。在攻击者了解目标后,攻击者可以编写利用已知软件漏洞的代码。除了对基础设施进行更改外,任何使用的软件也应保持最新,具备安全功能和补丁。逆向工程也需要找到脆弱的代码。这有助于通过回溯源代码来定位脆弱的代码。
所有这些活动都是基于逆向工程的输出进行的。从逆向工程中收集的信息会影响基础设施需要如何重构。
技术要求
我们将在一个将使用虚拟化软件的环境中工作。建议我们拥有一台启用了虚拟化功能的物理机器,处理器至少有四个核心,4 GB 的内存和 250 GB 的磁盘空间。请预先安装 Windows 或 Linux 操作系统在这台物理机器上。
我们将在我们的设置中使用 VirtualBox。主机操作系统的版本(Windows 或 Linux)将取决于 VirtualBox 的要求。请查看 VirtualBox 的最新版本:www.virtualbox.org/ 并查看推荐的要求。
你可能需要提前从 Microsoft 下载虚拟机,因为这些下载可能需要一些时间。请参考开发者页面:developer.microsoft.com/en-us/microsoft-edge/tools/vms/。Windows 10 可以通过以下链接下载:www.microsoft.com/en-us/software-download/windows10
逆向工程作为一个过程
像任何其他活动一样,逆向工程也是一个过程。我们可以遵循一个指南,帮助我们生成对分析师和利益相关者都有帮助的信息。
寻求批准
道德要求任何进行软件逆向工程的人必须获得软件所有者的批准。然而,许多软件在操作系统报告时,前台就显示了其漏洞。一些公司对于未经批准的逆向工程更为宽容,但如今的惯例是,任何发现的漏洞应该直接报告给软件所有者,而不是公开。这由所有者决定何时将漏洞报告给社区,以防止攻击者在软件补丁发布之前利用漏洞。
当涉及到恶意软件或黑客攻击时,情况则不同。当然,逆向分析恶意软件不需要得到恶意软件作者的批准。实际上,恶意软件分析的目标之一就是抓住作者。如果不确定,始终咨询律师或公司法务部门。
静态分析
在没有执行的情况下,通过查看文件的二进制并解析每个字节,可以提供继续进一步分析所需的大部分信息。仅仅知道文件的类型,就可以帮助分析员准备特定的工具集和参考资料。搜索文本字符串也可以提供关于程序作者的信息、程序的来源以及程序的功能。
动态分析
这种分析类型是在被分析的对象被执行时进行的。它需要一个封闭的环境,以防止可能会影响生产系统的行为发生。封闭环境的设置通常通过虚拟机来完成,因为它们可以更容易地进行控制。在动态分析过程中,会实现监控和记录常见环境行为的工具。
低级分析
在静态分析和动态分析过程中,有些信息可能会被忽视。程序的流程遵循一条依赖特定条件的路径。例如,只有在特定进程运行时,程序才会创建一个文件。或者,程序只有在 64 位 Windows 操作系统中运行时,才会在Wow6432Node注册表项下创建一个条目。调试工具通常用于在低级分析中分析程序。
报告
在进行分析时,每一条信息都应该被收集并记录。记录逆向工程对象是常见的做法,这有助于未来的分析。分析作为一个知识库,供开发者用来保护他们未来的程序免受缺陷的影响。例如,通过逆向工程的程序提示可能的缓冲区溢出,现在可以通过设置边界验证来保护一个简单的输入。
一份好的报告回答以下问题:
-
逆向工程对象的工作原理
-
何时触发特定行为
-
为什么在程序中使用了特定的代码
-
它原本打算在什么地方工作
-
整个程序的作用
工具
进行逆向工程的第一步是理解每个比特和字节的含义。仅仅查看包含的字节需要开发工具来帮助读取文件和对象。解析并为每个字节添加意义则需要另一个工具。逆向工程随着新软件技术的出现不断演化,工具也在不断更新。在这里,我们将这些工具分为二进制分析工具、反汇编器、反编译器、调试器和监控工具。
二进制分析工具
二进制分析工具用于解析二进制文件并提取文件信息。分析员能够识别哪些应用程序能够读取或执行该二进制文件。文件类型通常通过其魔术头字节来识别。这些魔术头字节通常位于文件的开头。例如,一个 Microsoft 可执行文件(EXE 文件)以 MZ 头(MZ 被认为是微软 DOS 时期开发者 Mark Zbikowski 的首字母)开头。另一方面,Microsoft Office Word 文档的魔术头字节为以下四个字节:
前面截图中的十六进制字节显示为DOCFILE。其他信息,例如文本字符串,也提供了线索。以下截图显示的信息表明该程序很可能是使用 Windows Forms 构建的:
反汇编器
反汇编器用于查看程序的低级代码。阅读低级代码需要了解汇编语言。使用反汇编器进行的分析提供了有关程序在执行时将执行的操作条件和系统交互的信息。然而,阅读低级代码时的亮点是程序使用应用程序接口(API)函数时。以下截图显示了一个使用GetJob() API 的程序模块代码片段。此 API 用于获取打印作业的信息,如下所示:
调试器
反汇编器可以显示代码树,但分析员可以通过使用调试器验证代码流向的分支。调试器按行执行代码。分析员可以跟踪代码,例如循环、条件语句和 API 执行。由于调试器属于动态分析范畴,并执行逐步代码执行,因此调试是在封闭环境中进行的。不同的文件类型有不同的反汇编器。在 .NET 编译的可执行文件中,最好是反汇编 p-code 并推敲每个操作符的含义。
监控工具
监控工具用于监控系统在文件、注册表、内存和网络方面的行为。这些工具通常会通过 API 或系统调用进行拦截或挂钩,然后记录如新创建的进程、更新的文件、新的注册表项以及由报告工具生成的传入 SMB 数据包等信息。
反编译器
反编译器类似于反汇编器。它们是尝试恢复程序的高级源代码的工具,而反汇编器则尝试恢复程序的低级(汇编语言)源代码。
这些工具相互配合工作。从监控工具生成的日志可以用来追踪反汇编程序中的实际代码。在调试时也是如此,分析师可以查看反汇编的低级代码概览,同时根据监控工具的日志预测在哪里设置断点。
恶意软件处理
本书的读者在处理恶意软件文件时需要采取预防措施。以下是一些初步的建议,可以帮助我们防止主机被攻破:
-
在封闭的环境中进行分析,例如单独的计算机或虚拟机中。
-
如果不需要网络访问,切断网络连接。
-
如果不需要互联网访问,切断网络连接。
-
在手动复制文件时,将文件重命名为不会执行的文件名。例如,将
myfile.exe重命名为myfile.foranalysis。
基本分析实验室设置
一个典型的设置要求系统能够运行恶意软件,而不被外部影响。然而,也有一些情况可能需要从互联网获取外部信息。首先,我们将模拟一个家庭用户的环境。我们的设置将尽可能使用免费的开源工具。以下图示显示了理想的分析环境设置:
这里的沙盒环境是我们进行文件分析的地方。图示右侧提到的 MITM,指的是中间人环境,它用于监控进出网络的活动。沙盒应该恢复到其原始状态。这意味着每次使用后,我们都应该能够恢复或还原其未被修改的状态。最简单的设置方法是使用虚拟化技术,因为这样就可以轻松地恢复到克隆镜像。有许多虚拟化程序可供选择,包括 VMware、VirtualBox、Virtual PC 和 Bochs。
还应该注意,有些软件能够检测到自己正在被运行,并且不喜欢在虚拟化环境中运行。在这种情况下,可能需要物理机器来设置。可以存储镜像或重新映像磁盘的磁盘管理软件是我们在这里的最佳解决方案。这些程序包括 Fog、Clonezilla、DeepFreeze 和 HDClone。
我们的设置
在我们的设置中,我们将使用 VirtualBox,它可以从www.virtualbox.org/下载。我们将使用的 Windows 操作系统是 Windows 7 32 位,可以从developer.microsoft.com/en-us/microsoft-edge/tools/vms/下载。在以下图示中,系统安装了两个虚拟机:一个来宾沙箱和一个来宾 MITM,且系统已连接互联网:
- 下载并安装 VirtualBox 并运行它。VirtualBox 有适用于 Windows 和 Linux 的安装程序。下载 Windows 7 32 位镜像,如下所示:
-
从微软网站下载的镜像是压缩的,应先解压。在 VirtualBox 中,点击文件|导入虚拟机。您将看到一个对话框,允许我们导入 Windows 7 32 位镜像。
-
只需浏览并选择从 ZIP 文件解压出来的 OVA 文件,然后点击“下一步”,如下所示:
- 在继续之前,可以更改设置。默认 RAM 设置为 4096 MB。分配更多的 RAM 并设置更多的 CPU 核心时,运行或调试时的性能会有所提升。然而,增加的 RAM 会使得创建镜像快照时消耗相同量的磁盘空间。这意味着,如果我们分配了 1GB 的 RAM,创建一个快照也会消耗至少 1GB 的磁盘空间。我们将 RAM 设置为 2048 MB,这对于我们来说是一个合理的工作量:
- 点击“导入”,它应该开始生成虚拟磁盘镜像。一旦完成,我们需要创建第一个快照。建议在关闭状态下创建快照,因为此时消耗的磁盘空间最小。找到“快照”标签,然后点击“创建”。填写快照名称和快照描述字段,然后点击“确定”按钮。这将快速创建您的第一个快照。
在开机状态下,虚拟机中的 RAM 和修改后的磁盘空间总和等于快照所消耗的总磁盘空间。
- 点击“启动”以开始运行 Windows 7 镜像。您应该会看到如下窗口。如果系统要求输入密码,默认密码是
Passw0rd!:
此时,网络设置为 NAT。这意味着虚拟机所需的任何网络资源将使用主机计算机的 IP 地址。虚拟机的 IP 地址来自 VirtualBox 的虚拟 DHCP 服务。记住,虚拟机中的所有网络通信都将使用主机计算机的 IP 地址。
由于我们无法防止某些恶意软件将信息发送到网络,以便将信息返回到我们的虚拟机,因此需要注意的是,一些 ISP 可能会监控常见的恶意软件行为。最好查看与他们的合同,并在必要时做出决定。
我们的大部分逆向工程都涉及恶意软件,截至目前,攻击者通常以 Windows 系统为目标。我们的设置使用的是 Microsoft Windows 7 32 位。你可以自由选择使用其他版本。我们建议安装 32 位版本的 Microsoft Windows,因为它在低级调试时更容易追踪虚拟地址和物理地址。
样本
我们将构建自己的程序来验证和理解低级代码的工作原理。
它的行为和外观。以下是我们将用来构建程序的软件列表:
-
Dev C++ (
www.bloodshed.net/devcpp.htm) -
Visual Studio C++ (
www.visualstudio.com/downloads/) -
MASM32 (
www.masm32.com/)
如果你对恶意软件感兴趣,可以从以下网站获取样本:
总结
逆向工程已经存在多年,并且一直是一项有用的技术,用于了解事物的运作方式。在软件行业中,逆向工程有助于验证和修复代码流和结构。通过这些任务获得的信息可以提高软件、网络基础设施以及人类意识的各个方面的安全性。作为反恶意软件行业的核心技能要求,逆向工程有助于创建检测和修复信息;这些信息也被用来构建保护措施,保障机构的服务器安全。它还被当局和法医专家用来追踪犯罪集团。
有一些基本步骤可以帮助建立逆向工程的信息。一旦分析师获得原作者的批准进行逆向工程,他们可以从静态分析、动态分析开始,然后进行低级分析。接下来是报告软件的概述和细节。
在进行分析时,会使用各种工具,包括静态分析工具、反汇编器、反编译器、调试器和系统监控工具。在对恶意软件进行逆向工程时,最好在一个没有或有限访问网络的环境中使用这些工具,以避免个人用途或工作中的网络被入侵。这可以防止你的基础设施被破坏。恶意软件应该得到妥善处理,我们列出了几种防止意外双击的方式。
然而,恶意软件分析仍然需要互联网,以便进一步了解恶意软件的工作原理及其行为。可能会有一些法律问题,需要你查阅所在国家的法律以及当地互联网服务提供商(ISP)的政策,以确保你不会违反其中的任何规定。
分析实验室设置的核心要求是目标操作系统能够恢复到未修改的状态。
恶意软件样本可以从以下链接获取:github.com/PacktPublishing/Mastering-Reverse-Engineering/tree/master/tools。这些样本将在本书中贯穿使用。
现在我们已经完成了基本设置,让我们开始逆向工程的旅程吧。
第二章:隐藏组件的识别与提取
目前,逆向工程最常见的应用是针对恶意软件。像其他任何软件一样,恶意软件也有其安装过程。不同之处在于它不会请求用户的安装许可。恶意软件甚至不会安装到Program files文件夹中,那里是其他合法应用程序的安装位置。相反,它倾向于将恶意软件文件安装到用户不常访问的文件夹中,从而避免被注意到。然而,有些恶意软件会被注意到,并在几乎所有显眼的文件夹中生成其副本,比如桌面。它的目的是让用户通过意外的双击或好奇心来执行其副本。这就是我们通常所说的恶意软件持久性。
持久性是指恶意软件持续在后台运行。在本章中,我们将指出恶意软件通常用来实现持久性的技术方法。我们还将解释恶意软件文件存储的常见位置。还将展示恶意软件的主要行为及一些能够识别恶意软件如何在系统中安装的工具。理解恶意软件的传播方式将帮助逆向工程师解释攻击者是如何成功入侵系统的。
本章我们将学习以下内容:
-
操作系统环境基础
-
典型的恶意软件行为:
-
恶意软件传递
-
恶意软件持久性
-
恶意软件有效载荷
-
-
用于识别隐藏组件的工具
技术要求
讨论将使用 Windows 环境。我们将使用在前一章节中创建的虚拟机设置。此外,您还需要下载并安装以下软件:SysInternals 套件(docs.microsoft.com/en-us/sysinternals/downloads/sysinternals-suite)。
操作系统环境
进行逆向工程要求分析人员了解正在逆向的软件运行的环境。软件在操作系统中运行所需的主要部分是内存和文件系统。在 Windows 操作系统中,除了内存和文件系统,微软还引入了注册表系统,实际上这些数据存储在被保护的文件中,称为注册表蜂巢。
文件系统
文件系统是直接将数据存储到物理硬盘驱动器的地方。这些文件系统管理文件和目录在磁盘上的存储方式。各种磁盘文件系统有自己高效读写数据的变体。
有不同的磁盘文件系统,如FAT、NTFS、ex2、ex3、XFS 和 APFS。Windows 常用的文件系统是 FAT32 和 NTFS。文件系统中存储了关于目录路径和文件的信息,包括文件名、文件大小、日期戳和权限。
以下截图显示了存储在文件系统中的 bfsvc.exe 信息:
在以前的 MacOS X 版本中,文件信息和数据存储在资源分支中。资源分支实际上已被弃用,但在最近版本的 MacOS 中仍然存在向后兼容性。一个文件在文件系统中有两个分支,数据分支和资源分支。数据分支包含非结构化数据,而资源分支包含结构化数据。资源分支包含诸如可执行机器代码、图标、警告框的形状、文件中使用的字符串等信息。例如,如果你想通过简单地将一个 Mac 应用程序移动到 Windows 硬盘上再移动回来来备份它,那么该 Mac 应用程序将无法再打开。在转移过程中,只有文件被转移,而资源分支会被剥离。简单的复制工具不会尊重分支。相反,Mac 开发者开发了同步文件到外部磁盘的工具。
内存
当 Windows 可执行文件执行时,系统会分配一个内存空间,将可执行文件从磁盘中读取,写入到分配的内存的预定位置,然后允许代码从该位置执行。这个内存块被称为进程块,并且与其他进程块相连。基本上,每个执行的程序都会消耗一个内存空间,作为一个进程。
以下截图显示了 Windows 任务管理器中进程列表的视图:
注册表系统
在 Windows 中,注册表是一个包含系统范围配置和应用程序设置的公共数据库。以下是存储在注册表中的一些信息示例:
-
执行特定文件的关联程序:
-
DOCX 文件与 Microsoft Word 关联
-
PDF 文件与 Adobe Reader 关联
-
-
与特定文件和文件夹关联的图标
-
软件设置:
-
卸载配置
-
更新站点
-
使用的端口
-
产品 ID
-
-
用户和组配置文件
-
打印机设置:
-
默认打印机
-
驱动程序名称
-
-
指定服务的驱动程序
注册表存储在蜂窝文件中。蜂窝文件的列表也可以在注册表中找到,如以下截图所示:
从注册表写入和读取信息需要使用 Windows 注册表 API。可以使用注册表编辑器以可视化方式查看注册表。注册表编辑器右侧窗格中的条目是注册表键。在左侧窗格中,注册表值位于名称列下,如以下截图所示:
常见恶意软件行为
恶意软件简单定义为恶意软件。一旦恶意软件进入系统,你会预期系统环境会发生不好的事情。一旦典型的恶意软件进入系统,它会做两件基本的事情:安装自身并进行其恶行。为了强制自己安装在系统中,恶意软件不需要通知用户。相反,它直接对系统进行更改。
持久化
恶意软件在系统中进行的一个变化是使其成为常驻程序。恶意软件的持久性意味着恶意软件将在后台持续运行,并尽可能长时间运行。例如,恶意软件会在每次系统启动后执行,或者恶意软件会在某个特定时间执行。恶意软件实现持久性最常见的方式是将其副本放入系统的某个文件夹中,并在注册表中创建一个条目。
以下注册表编辑器视图显示了GlobeImposter勒索病毒的注册表条目:
在注册表项HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run下的任何条目都预计在每次 Windows 启动时运行。在这种情况下,存储在C:\Users\JuanIsip\AppData\Roaming\huVyja.exe中的GlobeImposter勒索病毒的可执行文件变得持久化。BrowserUpdateCheck是注册表值,而路径是注册表数据。在这个注册表项下,重要的是路径,不论注册表值名称是什么。
在注册表中有几个区域可以触发恶意软件可执行文件的执行。
启动项
在这些注册表键下的注册表数据中输入文件路径将在 Windows 启动时触发执行,正如以下 Windows 64 位版本的注册表路径所示。
-
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run -
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce -
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnceEx -
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServices -
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\N\RunServicesOnce -
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run -
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Windows\CurrentVersion\Run
在这些注册表键下列出的程序将在当前用户登录时触发执行,正如以下注册表路径所示:
-
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run -
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce -
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnceEx -
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServices -
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServicesOnce -
HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Windows\Run
含有 Once 的键名将列出仅运行一次的程序。如果恶意软件继续将其文件路径放置在 RunOnce、RunOnceEx 或 RunServicesOnce 键下,它可能仍会持续存在。
加载和运行值
以下注册表值,在其相应的注册表项下,将在任何用户登录时触发执行:
-
HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Windows-
Load = <文件路径> -
Run = <文件路径>
-
BootExecute 值
-
HKEY_LOCAL_MACHINE\SYSTEM\ControlSetXXX\Control\Session Manager-
XXX在ControlSetXXX中是一个三位数,通常是ControlSet001、ControlSet002或ControlSet003。 -
BootExecute = <文件路径>BootExecute的默认值是autocheck autochk *
-
Winlogon 键
-
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon-
该注册表项下的活动在 Windows 登录时执行
-
UserInit = <文件路径>Userinit的默认值是C:\Windows\system32\userinit.exe
-
Notify = <dll 文件路径>- 默认情况下未设置
Notify。它应该是一个动态链接库文件
- 默认情况下未设置
-
Shell = <exe 文件路径>Shell的默认值是explorer.exe
-
-
HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon-
Shell = <exe 文件路径>Shell的默认值是`explorer.exe`
-
策略脚本键
-
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Shutdown\0\N-
其中
N是从0开始的数字。在关机过程中,可以运行多个脚本或可执行文件 -
Script = [可执行文件或脚本的文件路径]
-
-
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0\N-
这里的
N是从0开始的数字。在启动过程中,可以运行多个脚本或可执行文件。 -
Script = [可执行文件或脚本的文件路径]
-
-
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Logon\0\N-
这里的
N是从0开始的数字。在用户注销时,可以运行多个脚本或可执行文件。 -
Script = [可执行文件或脚本的文件路径]
-
-
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Logoff\0\N-
其中 N 是从 0 开始的数字。在用户注销时,可以运行多个脚本或可执行文件
-
Script = [可执行文件或脚本的文件路径]
-
AppInit_DLLs 值
-
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows-
AppInit_DLLs = [DLL 列表]- DLL 列表由逗号或空格分隔
-
LoadAppInit_DLLs = [1 或 0]- 这里,
1表示启用,0表示禁用
- 这里,
-
服务键
-
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\[服务名称]-
这里的
ServiceName是服务的名称 -
ImagePath = [sys/dll 文件路径] -
加载系统文件(
.sys)或库文件(.dll),这是驱动程序可执行文件 -
服务触发取决于启动值:
-
0(SERVICE_BOOT_START在操作系统加载时触发) -
1(SERVICE_SYSTEM_START在操作系统初始化时触发) -
2(SERVICE_AUTO_START在服务管理器启动时触发) -
3(SERVICE_DEMAND_START在手动启动时触发) -
4(SERVICE_DISABLED。服务被禁用,无法触发)
-
-
文件关联
-
HKEY_CLASSES_ROOT或在HKEY_LOCAL_MACHINE\SOFTWARE\Classes\[文件类型或扩展名]\shell\open\command中-
(
Default)注册表值中的条目执行由[文件类型或扩展名]描述的文件 -
以下代码显示了与可执行文件或
.EXE文件相关的条目:-
`<显示 HKEY_LOCAL_MACHINE\SOFTWARE\Classes\exefile\shell\open\command 中的 exefile 条目的图像>` -
(
Default)值包含"%1" %*。%1指的是正在运行的可执行文件,而%*指的是命令行参数。恶意软件通过追加自身的可执行文件实现持久性。例如,(Default)值被设置为malware.exe "%1" %*。因此,malware.exe会运行,并使用%1(正在运行的可执行文件)和%*作为其参数。接着,malware.exe负责使用其%*运行%1。
-
-
启动值
启动注册表值包含一个文件夹路径,文件夹内的文件在用户登录后执行。默认文件夹位置是%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup。
-
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell FoldersStartup = [启动文件夹路径]
-
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell FoldersStartup = [启动文件夹路径]
-
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell FoldersCommon Startup = [启动文件夹路径]
-
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders`Common Startup = [启动文件夹路径]`
Image File Execution Options 键
设置在Image File Execution Options键中的文件路径会在调试进程或通过CreateProcess API 运行时执行:
-
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\[进程名称]-
Debugger = [可执行文件] -
[进程名称]指的是正在运行的可执行文件的文件名 -
这个持久性仅在需要
[进程名称]调用调试器时触发
-
浏览器助手对象键
-
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\[CLSID]-
拥有
CLSID作为子键意味着它作为 Internet Explorer BHO 已安装并启用 -
CLSID在HKEY_CLASSES_ROOT\CLSID\[CLSID]\InprocServer32键下注册(Default)值指向与 BHO 关联的 DLL 文件
-
每次打开 Internet Explorer 时,都会加载 DLL 文件
-
除了注册表条目,执行文件还可以通过任务调度程序或cron作业按计划触发。执行文件或脚本甚至可以在特定条件下被触发。例如,以下是一个 Windows 任务调度程序的截图:
恶意软件持久化的方法有很多种,远不止之前列举的那些。这些是逆向工程师在遇到新技术时学习到的挑战。
恶意软件传播
在软件安全行业中,攻击者传播并妥协系统的活动被称为恶意软件攻击活动。恶意软件进入系统的方式有很多种。这些恶意软件可执行文件最常见的传播方式是通过电子邮件附件发送给目标用户。随着通信技术的变化,这些攻击活动的物流也会随之调整,以适应现有技术。这包括寻找目标系统中的漏洞并利用漏洞渗透系统。
电子邮件
作为电子邮件传播的恶意软件要求收件人打开附件。邮件的构造方式使收件人对打开附件产生好奇心。这些未经请求的电子邮件被传播到多个地址,这些邮件被称为电子邮件垃圾邮件。它们通常包含一个主题和消息正文,通过社会工程学吸引收件人的注意,最终让收件人执行恶意软件。以下截图展示了一个例子:
欺骗个人或一群人进行某种活动的行为被称为社会工程学。由于安全意识差,用户可能会陷入这个著名的俗语陷阱:好奇害死猫。
即时消息
除了电子邮件,还有我们所称的 SPIM(即时消息垃圾邮件)。这是一种发送到即时消息应用程序的垃圾邮件,如 Facebook、Skype 和 Yahoo Messenger。这还包括通过 Twitter、Facebook 和其他社交网络服务发送的公共或私人消息垃圾邮件。这些消息通常包含指向已被入侵并包含恶意软件或间谍软件的站点的链接。一些支持文件传输的服务被恶意软件垃圾邮件滥用。如今,这些社交网络服务已经实施了后端安全措施以减轻 SPIM 的影响。然而,在撰写本文时,仍然有少数恶意软件通过即时消息传播的事件。下面的截图可以看到一个例子:
图片来自CSPCert.ph的 John Patrick Lita
上面的截图是来自 Facebook 即时消息的私人消息,包含一个 ZIP 文件,实际上其中包含了一个恶意软件文件。
计算机网络
如今,计算机必须连接到网络,以便用户能够互相访问资源。无论是局域网(LAN)还是广域网(WAN),每台计算机都与其他计算机相连,文件共享协议也为攻击者滥用提供了机会。恶意软件可以尝试将自己的副本拷贝到文件共享中。然而,恶意软件依赖于远程端的用户运行共享中的恶意软件文件。这类恶意软件被称为网络蠕虫。
要列出 Windows 中的共享文件夹,可以使用net share命令,如下图所示:
作为分析员,我们可以就如何处理这些共享文件夹提出建议。我们可以建议,如果这些共享文件夹未被使用,则将其删除。我们还可以对这些文件夹的权限进行审核,查看谁可以访问以及哪些用户可以拥有何种权限(如读写权限)。这样,我们可以帮助保护网络不受网络蠕虫的侵害。
媒体存储
网络管理员在使用闪存驱动器时非常严格。主要原因是外部存储设备,如 USB 闪存驱动器、光盘、DVD、外部硬盘,甚至智能手机,都是恶意软件可以存储自己的介质。一旦存储设备被挂载到计算机上,它就像一个普通的驱动器一样工作。恶意软件可以直接将自己的副本拷贝到这些存储驱动器中。与网络蠕虫类似,这些蠕虫依赖用户来运行恶意软件。但是,如果启用了 Windows 的自动运行功能,一旦驱动器被挂载,恶意软件可能会自动执行,如下图所示:
上一张图片是插入包含设置软件的光盘时遇到的默认对话框。
驱动器根目录中的autorun.inf文件包含有关自动执行文件的信息。此文件由存储在光盘中的软件安装程序使用,这样当磁盘插入时,它会自动运行安装程序。这一功能被恶意软件滥用,步骤如下:
-
将恶意软件文件的副本投放到可移动驱动器中
-
随着恶意软件副本的投放,它生成一个
autorun.inf文件,指向投放的可执行文件,如下例所示:
autorun.inf文件用于之前显示的 VirtualBox 设置自动播放对话框,其中包含之前截图中的文本。open属性包含需要执行的可执行文件。
漏洞利用和被攻陷的网站
漏洞利用也属于恶意软件的一种。漏洞利用是为攻击特定软件或网络服务的漏洞而制作的。这些通常以二进制数据的形式存在。漏洞利用利用了漏洞,从而导致目标软件或服务的行为按照攻击者的意图进行。通常,攻击者的目的是获得对目标系统的控制,或者仅仅是让它瘫痪。
一旦攻击者识别出目标的漏洞,就会制作一个包含恶意代码的漏洞利用程序,该恶意代码可以下载恶意软件,进一步扩大攻击者的访问权限。这个概念被用来开发漏洞利用工具包。漏洞利用工具包是一组已知的漏洞扫描器和已知的漏洞,打包成一个工具包。
以下图示给出了一个例子:
在恶意软件攻击中,社会工程学被用来诱使用户访问实际已被攻陷的链接。通常,这些受害网站是手动黑客攻击的,并且已被注入隐藏的脚本,重定向到另一个网站。恶意链接通过电子邮件、即时消息和社交网络站点进行传播。访问被恶意广告感染的合法网站也可以作为诱饵。这些网站包括软件或媒体盗版站点、暗网,甚至是色情网站。一旦用户点击链接,通常情况下,网站会重定向到另一个被攻陷的网站,接着再重定向到另一个,直到到达漏洞利用工具包的登陆页面。从用户的互联网浏览器中,漏洞利用工具包页面会收集关于机器的信息,例如软件版本,然后确定该软件是否已知存在漏洞。然后,它会将所有适用于该漏洞软件的漏洞程序交付。漏洞程序通常包含下载并执行恶意软件的代码。结果,毫无察觉的用户会感染到受损的系统。
软件盗版
黑客工具、盗版软件、序列号生成工具和盗版媒体文件只是一些可能包含恶意软件或广告软件的分发软件。例如,盗版软件的安装程序的安装文件可能会在后台下载恶意软件并在未征得用户同意的情况下进行安装。
恶意软件文件属性
常见恶意软件的初始行为是生成一个副本,嵌入恶意软件组件,或者下载其恶意软件组件。它会创建被投放的文件,通常这些文件可以在以下文件夹中找到:
-
Windows 系统文件夹:
C:\Windows\System32 -
Windows 文件夹:
C:\Windows -
用户配置文件文件夹:
C:\Users\[username] -
Appdata 文件夹:
C:\Users\[username]\AppData\Roaming -
回收站文件夹:
C:\$Recycle.Bin -
桌面文件夹:
C:\Users\[username]\Desktop -
临时文件夹:
C:\Users\[username]\AppData\Local\Temp
作为社会工程学的一部分,另一个廉价的技术是将恶意软件文件的图标更改为吸引用户打开它的样式,例如,文件夹图标、Microsoft Office 图标或 Adobe PDF 图标。它还使用具有欺骗性的文件名,如INVOICE、New Folder、Scandal、Expose、Pamela、Confidential等。以下截图展示了实际恶意软件模仿已知文档的例子:
注意,突出显示的伪造 PDF 文件实际上是一个应用程序。
有效载荷——其中的恶意内容
攻击者开发恶意软件是有目的的,通常是为了对目标造成伤害,可能是出于仇恨、娱乐、金钱或,可能是政治原因。以下是一些在实际情况中见过的典型恶意软件有效载荷:
-
为赎金加密文件
-
删除所有文件
-
格式化驱动器
-
完全访问系统和网络
-
偷取账户和密码
-
偷取文档、图片和视频
-
更改特定配置和设置
-
将计算机转变为代理服务器
-
安装
cryptocoin矿工 -
持续打开网站——广告或色情网站
-
安装更多恶意软件
-
安装广告软件
逆向工程师在报告中列出的一个结论是有效载荷。这决定了恶意软件在安装后对计算机的实际影响。
工具
识别与恶意软件相关的注册表项、已丢弃的文件和正在运行的进程需要工具。我们可以使用现有的工具来提取这些对象。我们应该考虑两种分析事件:恶意软件执行后的分析和恶意软件执行前的分析。由于本章的目标是提取组件,我们将讨论能够帮助我们找到可疑文件的工具。关于我们提取可疑恶意软件后的分析工具将在后续章节中讨论。
当系统已经被攻破时,分析员需要使用可以识别可疑文件的工具。每个可疑文件将进一步分析。首先,我们可以基于持久性来识别它。
-
列出所有进程及其相关的文件信息
-
从已知的注册表持久性路径列表中,查找包含文件路径的条目
-
提取可疑文件
上述步骤可能需要使用微软 Windows 的预安装工具,例如:
-
注册表编辑器(
regedit/regedt32)用于搜索注册表 -
你还可以使用命令行访问注册表
reg.exe,如下面的截图所示: -
任务管理器(
taskmgr)列出进程 -
使用 Windows 资源管理器(
explorer)或命令提示符(cmd)遍历目录并获取文件。
然而,我们也可以使用一些第三方工具来帮助列出可疑文件。以下是我们将简要讨论的几种工具:
-
启动项
-
进程资源管理器
Autoruns
我们在本章前面看到的启动列表,涵盖了注册表条目、计划任务和文件位置。总体来说,该工具涵盖了所有这些内容,包括我们尚未讨论的其他领域,例如 Microsoft Office 插件、编解码器和打印机监视器,如下屏幕截图所示:
有 32 位和 64 位版本的 autoruns 工具。上述屏幕截图显示了基于 SysInternals 作者 Mark Russinovich 和 Bryce Cogswell 的研究的可执行文件的所有可能触发器。该屏幕截图还对每个 autorun 条目进行了分类,并显示了每个条目的描述,并指示了与条目相关的文件路径。
至于逆向工程师,通过了解在系统受到威胁之前启动的文件,可以确定可疑文件的身份。持续的实践和经验将使逆向工程师能够轻松地识别哪些是好的或可疑的可执行文件。
进程资源管理器
本质上,进程资源管理器工具类似于任务管理器,如下屏幕截图所示:
该工具的优势在于它可以显示有关进程本身的更多信息,例如如何运行,包括使用的参数,甚至其自启动位置,如下面的示例所示:
此外,进程资源管理器具有工具可以将其发送到 VirusTotal 进行识别,显示从其图像识别的字符串列表以及与其关联的线程。从逆向工程师的角度来看,此处高度使用的信息是命令行用法和自启动位置。VirusTotal 是一个在线服务,可以使用多个安全软件扫描提交的文件或 URL,如下屏幕截图所示:
结果并不是最终结论,但它给提交者一个关于文件是否为合法软件或恶意软件的想法。
概要
在第一章中,我们学习了反向工程及其在分析恶意软件时的重要性。要开始我们的反向工程探险,我们必须了解我们正在分析的系统。我们讨论了 Windows 操作系统环境中的三个主要区域:内存、磁盘和注册表。在本章中,我们旨在通过提取可疑文件来从受 Compromise 的 Windows 系统中查找恶意软件。为此,我们列出了系统中可以搜索的常见启动区域。这些区域包括注册表、任务计划和启动文件夹。
我们了解到,典型的恶意软件通过安装自身并运行危害系统的代码来表现。恶意软件基本上是为了持久性而安装自身,这导致恶意软件文件大多数时间触发系统在线状态。然后我们列出了一些行为,解释为何将恶意软件称为恶意。这些恶意代码包括任何涉及经济或政治利益犯罪的行为,例如勒索和后门访问。
我们通过列出可以用来轻松识别可疑文件的工具来结束这一章节。我们首先介绍了预先存在的 Windows 工具,如注册表编辑器、任务管理器和任务计划程序。然后,我们介绍了来自 SysInternals 的另外两个工具:autoruns 和 Process explorer。有了这些工具在手,我们应该能够列出我们怀疑的文件。然而,就像其他任何任务一样,通过实践和经验,我们将能够更快地掌握识别技能。
进一步阅读
-
msdn.microsoft.com/en-us/library/windows/desktop/ms724871(v=vs.85).aspx -
medium.com/@johnpaticklita/cryptomalware-spreads-on-facebook-79a299590116
第三章:低级语言
任何反向工程师在开始之前需要掌握的主要知识是汇编语言。理解汇编语言就像学习反向工程的 ABC。它可能一开始看起来很难,但最终会变得像肌肉记忆一样。汇编语言是与计算机交流的语言。程序的源代码是人类可以理解的,但机器无法理解。源代码必须被编译成汇编语言代码形式,才能被计算机理解。
但是,作为人类,如果源代码不可用呢?我们唯一能够理解程序如何运行的方式就是读取它的汇编代码。从某种程度上来说,我们在这里构建的是一种将汇编语言代码还原为源代码的方法。这就是为什么它被称为反向工程。
我们将简要介绍汇编语言,重点讲解 x86 英特尔架构。那么,为什么选择 x86 呢?市面上有很多架构,如 8080、ARM、MIPS、PowerPC 和 SPARC,但我们专注于英特尔 x86,因为它是当今最流行和广泛使用的架构。
在本章中,我们将学习汇编语言的基础知识。我们将从回顾二进制数开始,然后使用汇编语言指令实现二进制算术,接着学习如何编译自己的低级程序,最后学习如何调试程序。
本章已被划分为多个部分。我们将学习以下内容:
-
二进制数、进制和 ASCII 表
-
x86 架构
-
汇编语言指令
-
用于编辑和编译汇编语言源代码的工具
-
调试工具
-
异常和错误处理
-
Windows API
-
高级语言结构
我们将提供设置和开发汇编语言代码的说明。这里还包含一些练习,可能会激发你使用汇编语言开发程序的灵感。
技术要求
最好但不是必需的,读者具备一定的编程语言背景。拥有编程背景将帮助读者更快地理解汇编语言。本章末尾提供了一些参考资料,读者可以利用它们进行进一步的编程开发和研究,这些内容在本书中没有提供。
我们将在此使用的一些工具包括:
-
二进制编辑器,如 HxD 编辑器或 HIEW(黑客视图)
-
文本编辑器,如 Notepad++
二进制数
计算机是设计用来通过信号进行电子数据处理和存储的。信号就像一个开关,其中“开”和“关”位置分别用数字“1”和“0”表示。这两个数字就是我们所说的二进制数。下一部分将讨论二进制数如何使用,以及它与其他进制的关系。
进制
数字中每个位置的值决定了该位置的值。在标准的十进制数字中,一个位置的值是它右边位置值的十倍。十进制数字系统也叫做基数为 10 的系统,它由数字 0 到 9 组成。
假设位置 1 在整个数字的最右边,如下所示:
2018
Place value at position 1 is 1 multiplied by 8 represents 8.
Place value at position 2 is 10 multiplied by 1 represents 10.
Place value at position 3 is 100 multiplied by 0 represents 0.
Place value at position 4 is 1000 multiplied by 2 represents 2000.
所有表示的数字之和就是实际值。遵循这个概念将帮助我们读取或转换成其他数字基数。
在二进制数中,一个位置的值是它右边位置值的 2 倍。二进制只使用 2 个数字,分别是 0 和 1。在本书中,我们会在数字后面加上小写字母b,表示该数字是二进制格式。二进制数字也被称为二进制数。二进制字符串中的每一位被称为比特。以下是一个例子:
11010b
Place value at position 1 is 1 multiplied by 0 represents 0.
Place value at position 2 is 2 multiplied by 1 represents 2.
Place value at position 3 is 4 multiplied by 0 represents 0.
Place value at position 4 is 8 multiplied by 1 represents 8.
Place value at position 5 is 16 multiplied by 1 represents 16.
The equivalent decimal value of 11010b is 26.
在十六进制数中,一个位置的值是它右边位置值的 16 倍。十六进制由数字 0 到 9 和字母 A 到 F 组成,其中 A 等于 10,B 等于 11,C 等于 12,D 等于 13,E 等于 14,F 等于 15。在本书中,我们将用字母h表示十六进制数字。十六进制数字如果位数为奇数,将前面加上0(零)。十六进制数字也可以用"0x"(零和小写字母 x)作为前缀。0x是多种编程语言中表示该数字为十六进制格式的标准:
BEEFh
Place value at position 1 is 1 multiplied by 0Fh (15) represents 15.are
Place value at position 2 is 16 multiplied by 0Eh (14) represents 224.
Place value at position 3 is 256 multiplied by 0Eh (14) represents 3584.
Place value at position 4 is 4096 multiplied by 0Bh (11) represents 45056.
The equivalent decimal value of BEEFh is 48879.
基数转换
我们已经将十六进制和二进制数转换成了十进制数,或者说是基数为 10 的数。将基数为 10 的数字转换成其他基数,只需要对要转换的基数进行除法运算,同时记录余数。
以下是一个二进制的例子
87 to base-2
87 divided by 2 is 43 remainder 1.
43 divided by 2 is 21 remainder 1.
21 divided by 2 is 10 remainder 1.
10 divided by 2 is 5 remainder 0.
5 divided by 2 is 2 remainder 1.
2 divided by 2 is 1 remainder 0.
1 divided by 2 is 0 remainder 1.
and nothing more to divide since we're down to 0.
base-2 has digits 0 and 1.
Writing the remainders backward results to 1010111b.
以下是一个十六进制的例子:
34512 to base-16
34512 divided by 16 is 2157 remainder 0.
2157 divided by 16 is 134 remainder 13 (0Dh)
134 divided by 16 is 8 remainder 6.
6 divided by 16 is 0 remainder 6.
base-16 has digits from 0 to 9 and A to F.
Writing the remainders backward results to 66D0h.
将十六进制转换为二进制只需要知道十六进制数字中有多少个二进制位。十六进制数中最高的数字是0Fh(15),它等于1111b。请注意,每个十六进制数字等于 4 个二进制位。这里展示了一个转换的例子:
ABCDh
0Ah = 1010b
0Bh = 1011b
0Ch = 1100b
0Dh = 1101b
Just combine the equivalent binary number.
ABCDh = 1010101111001101b
在从二进制转换为十六进制时,将二进制数分为每四位一组,如下所示:
1010010111010111b
1010b = 10 (0Ah)
0101b = 5
1101b = 13 (0Dh)
0111b = 7
1010010111010111b = A5D7h
那么,为什么计算机使用二进制和十六进制,而不是我们日常使用的十进制呢?对于二进制来说,有两种状态:开和关信号。状态可以很容易地被电子方式读取和传输。十六进制压缩了十进制数的二进制等效表示。例如,数字 10:这个数字表示为1010b,并占用 4 个位。为了最大化 4 个位可以存储的信息,我们可以表示 0 到 15 之间的数字。
4 位值也叫做 nibble,是字节的一半。字节可以表示字母、数字和字符。这些字符的表示在 ASCII 表中进行了映射。ASCII 表有三部分:控制字符、可打印字符和扩展字符。ASCII 表有 255 个字符(FFh)。可打印字符的列表以及一些扩展字符与键盘格式,可以在 github.com/PacktPublishing/Mastering-Reverse-Engineering/tree/master/ch3 找到。
尽管从英文键盘上无法直接看到符号,但可以通过使用字符的等效代码来显示。
二进制算术
由于字节是计算机中常用的单位,我们来玩玩它。我们可以从基本的算术运算开始:加法、减法、乘法和除法。传统的纸笔方法仍然是进行二进制运算的一种强大方法。二进制算术与十进制数的算术非常相似,唯一的区别是只有 1 和 0 两个数字。
加法按以下方式进行:
1b 10101b
+ 1b + 1111b
10b 100100b
一个减法示例如下:
10b 1101b
- 1b - 111b
1b 110b
乘法操作如下进行:
101b 1b x 1b = 1b
x 10b 1b x 0b = 0b
000
101
1010b
二进制除法按以下方式进行:
1010b 1000b
10b | 10100b 11b | 11010b
-10 -11
010 0010
-10 -000
00 10b (remainder)
-0
0
有符号数
二进制数可以按有符号或无符号进行构造。对于有符号数或整数,最高有效位决定了数值的符号。这要求二进制数有明确的大小,比如 BYTE、WORD、DWORD 和 QWORD。BYTE 大小为 8 位,WORD 大小为 16 位,DWORD(双 WORD)为 32 位,QWORD(四倍 WORD)为 64 位。基本上,随着位数的增加,大小会加倍。
在我们的例子中,假设使用 BYTE。识别一个正数的二进制表示很简单。在正数中,最高有效位或字节中的第 8 位为 0,剩余的从第 0 位到第 7 位的值即为实际数值。对于负数的二进制表示,最高有效位设置为 1,然而从第 0 位到第 7 位的值则需要计算为 2 的补码值:
01011011b = +91
11011011b = -37
10100101b = -91
00100101b = +37
一个值的“2 的补码”通过两步计算:
-
反转 1 和 0,使得 1 变为 0,0 变为 1,例如,
1010b变为0101b。这一步叫做 1 的补码。 -
将前一步的结果加 1,例如,
0101b + 1b = 0110b。
要写出 -63 的二进制等价表示,假设它是一个 BYTE,我们只取第 0 位到第 7 位:
- 使用前述方法转换为二进制:
63 = 0111111b
- 做“1 的补码”如下:
0111111b -> 1000000b
- 在前面的结果上加 1,得到“2 的补码”结果:
1000000b + 1 = 1000001b
- 由于这是一个负数,所以将最高有效位设置为 1:
11000001b = -63
下面是如何写下负二进制数的十进制表示:
- 请注意,符号位是 1,因此为负号:
10111011b
- 先做“1 的补码”,然后加 1:
01000100b
+ 1b
01000101b
- 将结果转换为十进制,并将负号放在最前面,因为这是一个负数:
- 01000101b = -69
x86
和其他编程语言一样,汇编语言有其自身的变量、语法、操作和函数。每一行代码处理的是一小部分数据。换句话说,每一行代码读取或写入一个字节的数据。
寄存器
在编程中,处理数据需要使用变量。你可以简单地将寄存器视为汇编语言中的变量。然而,并非所有寄存器都被当作普通变量使用,而是每个寄存器都有其特定的用途。寄存器可以被分类为以下几种类型:
-
通用寄存器
-
段寄存器
-
标志寄存器
-
指令指针
在 x86 架构中,每个通用寄存器都有其指定的用途,并且按WORD大小存储,即 16 位,具体如下:
-
累加器(AX)
-
计数器(CX)
-
数据寄存器(DX)
-
基址寄存器(BX)
-
栈指针(SP)
-
基址指针(BP)
-
源索引(SI)
-
目的索引(DI)
对于寄存器 AX、BX、CX 和 DX,可以通过较小的寄存器访问最低和最高有效字节。对于 AX,低 8 位可以使用 AL 寄存器读取,而高 8 位可以使用 AH 寄存器读取,如下所示:
执行代码时,系统需要识别代码所在的位置。指令指针(IP)寄存器包含下一个要执行的汇编指令所在的内存地址。
执行代码的系统状态和逻辑结果存储在FLAGS 寄存器中。FLAGS 寄存器的每一位都有其特定的用途,以下表格列出了其中的一些定义:
| 偏移量 | 缩写 | 描述 |
|---|---|---|
| 0 | CF | 进位标志。当加法操作需要进位时,设置此标志。当减法操作需要借位时,也会设置此标志。 |
| 1 | 保留 | |
| 2 | PF | 奇偶标志。此标志指示上一条指令操作中设置的位数是奇数还是偶数。 |
| 3 | 保留 | |
| 4 | AF | 调整标志。用于二进制编码十进制(BCD)。当低位到高位的进位或高位到低位的借位发生时,设置此标志。 |
| 6 | ZF | 零标志。当上一条指令操作的结果为零时,设置此标志。 |
| 7 | SF | 符号标志。当上一条指令操作的结果为负数时,设置此标志。 |
| 8 | TF | 陷阱标志。用于调试时。当遇到断点时,设置此标志。设置陷阱标志会导致每条指令都触发异常,从而启用调试工具进行逐步调试。 |
| 9 | IF | 中断标志。如果设置了此标志,处理器会响应中断。中断是指由硬件或软件触发的错误、外部事件或异常。 |
| 10 | DF | 方向标志。当设置时,数据从内存中倒序读取。 |
| 11 | OF | 溢出标志。如果算术操作的结果超过寄存器能容纳的值,则会设置此标志。 |
| 12 到 13 | IOPL | 输入/输出特权级。IOPL 显示程序访问 IO 端口的能力。 |
| 14 | NT | 嵌套任务标志。它控制中断任务或进程的链式执行。如果设置,则与链表链接。 |
| 15 | 保留 | |
| 16 | RF | 恢复标志。它暂时禁用调试异常,以便下一条正在调试的指令可以在不产生调试异常的情况下被中断。 |
| 17 | VM | 虚拟模式。设置程序与 8086 处理器兼容运行。 |
| 18 | AC | 对齐检查。该标志在内存引用(如栈)上的数据是非字(4 字节边界)或非双字(8 字节边界)时设置。然而,在 486 架构之前,这个标志更为有用。 |
| 19 | VIF | 虚拟中断标志。类似于中断标志,但在虚拟模式下工作。 |
| 20 | VIP | 虚拟中断挂起标志。表示已触发的中断正在等待处理。仅在虚拟模式下工作。 |
| 21 | ID | 标识符标志。指示是否可以使用 CPUID 指令。CPUID 可以确定处理器类型及其他处理器信息。 |
| 22 | 保留 | |
| 23 到 31 | 保留 | |
| 32 到 63 | 保留 |
所有这些标志都有其目的,但最常被监控和使用的标志是进位标志、符号标志、零标志、溢出标志和奇偶标志。
所有这些寄存器都有一个“扩展”模式用于 32 位。它可以通过前缀“E”访问(EAX、EBX、ECX、EDX、ESP、EIP和EFLAGS)。64 位模式也是如此,可以通过前缀“R”访问(RAX、RBX、RCX、RDX、RSP和RIP)。
内存被划分为不同的段,例如代码段、栈段、数据段和其他段。段寄存器用于标识这些段的起始位置,如下所示:
-
栈段(SS)
-
代码段(CS)
-
数据段(DS)
-
扩展段(ES)
-
F 段(FS)
-
G 段(GS)
当程序加载时,操作系统会将可执行文件映射到内存。可执行文件包含有关数据如何映射到相应段的信息。代码段包含可执行代码。数据段包含数据字节,如常量、字符串和全局变量。栈段被分配用于存储运行时函数变量和其他处理过的数据。扩展段与数据段类似,但此空间通常用于在变量之间移动数据。一些 16 位操作系统,如 DOS,由于每个段只分配 64KB 的内存,因此使用 SS、CS、DS 和 ES。然而,在现代操作系统(32 位及更高系统)中,这四个段被设置在同一内存空间中,而 FS 和 GS 分别指向进程和线程信息。
内存寻址
一段数据的起始位置,即存储在内存中的一系列字节,可以通过其内存地址来定位。存储在内存中的每个字节都有一个内存地址,用来标识它的位置。当用户执行程序时,系统会读取可执行文件,然后将其映射到一个分配的内存地址。可执行文件包含了如何进行映射的信息,确保所有的可执行代码都在代码段内,所有初始化的数据都在数据段内,未初始化的数据则在 BSS 段内。代码段中的代码指令可以通过内存地址访问数据段中的数据,这些地址可以是硬编码的。数据也可以是一个地址列表,指向另一组数据。
字节序
在读取或写入数据到内存时,我们使用寄存器或内存以BYTE、WORD、DWORD甚至QWORD的形式来处理它们。根据平台或程序,数据可能以小端或大端格式进行读取。
在小端格式中,当数据块被读取为DWORD时,它会被反转。我们以以下数据为例:
AA BB CC DD
当文件或内存中的数据以小端格式呈现时,它将在DWORD值中读取为DDCCBBAAh。这种字节序在 Windows 应用程序中很常见。
在大端系统中,相同的数据块将被读取为AABBCCDDh。使用大端格式的优势在于读取流式数据时,例如文件、串行数据和网络流。
使用小端格式读取的优势在于,读取的地址保持固定,无论它是作为BYTE、WORD还是DWORD来读取。例如,考虑以下情况:
Address Byte
0x00000000 AA
0x00000001 00
0x00000002 00
0x00000003 00
在前面的例子中,我们尝试从0x00000000地址读取数据。当以BYTE读取时,它将是AAh。当以WORD读取时,它将是AAh。当以DWORD读取时,它将是AAh。
但在大端格式中,当以BYTE读取时,它将是AAh。当以WORD读取时,它将是AA00h。当以DWORD读取时,它将是AA000000h。
其实,相较于其他方式,这里有很多优势。这些方式中的任何一种都可以根据应用的目的使用。在x86汇编中,小端格式是标准。
基本指令
汇编语言由一行行的代码组成,遵循这种语法:
标签用于定义指令行的位置。它通常在没有事先知道代码将放置在哪个内存地址的情况下,在开发汇编代码时使用。一些调试器能够支持用户为地址添加可读的名称。助记符是人类可读的指令,例如 MOV、ADD 和 SUB。每个助记符由一个或多个字节表示,称为操作码。操作数是指令的参数,通常按 目标,源 的顺序读取。在上面显示的指令中,eax 寄存器是目标,而存储在地址 0x0AD4194 处的双字数据是源。最后,我们可以在程序的每条指令行中添加注释。
在汇编语言中,代码注释用分号(;)表示。
操作码字节
每条指令都有一个等效的操作码(操作代码)字节:
Address Opcode Instructions
00A92D7C B8 00000080 MOV EAX,80000000h
00A92D81 B9 02000000 MOV ECX,2
00A92D86 F7E1 MUL ECX
在上述代码中,MOV 指令等价于 B8 操作码字节。位于 00A92D81 地址的 MOV 指令等价于 B9。这两条 MOV 指令的区别在于它们将 DWORD 值移入的寄存器不同。MOV EAX, 80000000h 总共消耗了 5 个字节,其中包括操作码字节 B8 和操作数值 80000000h。MOV ECX, 2 也使用了相同数量的字节,而 MUL ECX 使用了 2 个字节。
MOV EAX, 80000000h 位于 00A92D7ch。加上 5 个字节(变为 00A92D81),我们就能找到下一个指令的地址。在内存中查看代码会像这样:
Address Bytes
00A92D7C B8 00 00 00 80 B9 02 00 00 00 F7 E1
内存转储通常以段落或每行 16 字节的方式显示在内存转储工具中,地址对齐为 10h。
汇编语言指令可以按以下类别划分:
-
数据复制和访问指令(例如,MOV、LEA 和 MOVB)
-
算术指令(例如,ADD、SUB、MUL 和 DIV)
-
二进制逻辑指令(例如,XOR、NOT、SHR 和 ROL)
-
流程控制(例如,JMP、CALL、CMP 和 INT)
数据复制
MOV 指令用于移动数据。通过它,数据可以在寄存器和内存地址之间进行移动。
mov eax, 0xaabbccdd 将 0xaabbccdd 的值放入 eax 寄存器。
mov eax, edx 将 edx 寄存器中的数据值放入 eax 寄存器。
让我们以以下内存条目为例:
Address Bytes
00000060: 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F
00000070: 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F
00000080: 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F
00000090: 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F
读取数据可能需要使用指令来帮助汇编器。我们使用 byte ptr、word ptr 或 dword ptr:
; the following lines reads from memory
mov al, byte ptr [00000071] ; al = 71h
mov cx, word ptr [00000071] ; cx = 7271h
mov edx, dword ptr [00000071] ; edx = 74737271h
; the following lines writes to memory
mov eax, 011223344h
mov byte ptr [00000080], al ; writes the value in al to address 00000080
mov word ptr [00000081], ax ; writes the value in ax to address 00000081
mov dword ptr [00000083], eax ; writes the value in eax to address 00000083
内存随后将如下所示:
00000060: 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F
00000070: 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F
00000080: 44 44 33 44 33 22 11 87 88 89 8A 8B 8C 8D 8E 8F
00000090: 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F
MOV 和 LEA
MOV 用于读取给定地址处的值,而 LEA(加载有效地址)则用于获取地址:
mov eax, dword ptr [00000060] ; stores 63626160h to eax
mov eax, dword ptr [00000060] ; stores 00000060h to eax
那么,如果你自己可以计算地址,LEA 指令又有什么帮助呢?让我们以以下 C 代码为例:
struct Test {
int x;
int y;
} test[10];
int value;
int *p;
// some code here that fills up the test[] array
for (int i=0; i<10, i++) {
value = test[i].y;
p = &test[i].y;
}
C 代码从定义 test[10] 开始,这是一个包含两个整数 x 和 y 的 struct Test 数组。for 循环语句取 y 的值和 y 在 struct test 元素中的指针地址。
假设测试数组的基址在 EBX 中,for-loop计数器i在ECX中,整数为DWORD类型,因此struct Test将包含两个DWORD值。知道DWORD有 4 个字节,那么value = test[i].y;在汇编语言中的等价代码将是mov edx, [ebx+ecx*8+4]。接着,p = &test[i].y;在汇编语言中的等价代码将是lea esi, [ebx+ecx*8+4]。实际上,即使不使用 LEA,地址仍然可以通过算术指令进行计算。然而,使用 LEA 可以更加轻松地计算地址:
; using MUL and ADD
mov ecx, 1111h
mov ebx, 2222h
mov eax, 2 ; eax = 2
mul ecx ; eax = 2222h
add eax, ebx ; eax = 4444h
add eax, 1 ; eax = 4445h
; using LEA
mov ecx, 1111h
mov ebx, 2222h
lea eax, [ecx*2+ebx+1] ; eax = 4445h
上面的代码显示,六行代码可以通过使用 LEA 指令优化为三行。
算术运算
x86 指令基于 CISC 架构,其中像 ADD、SUB、MUL 和 DIV 这样的算术指令背后有一组更底层的操作。算术指令依赖一组标志来指示在操作过程中需要满足的特定条件。
加法与减法
在加法(ADD)和减法(SUB)中,OF、SF和CF标志会受到影响。我们来看一些指令的使用示例。
add eax, ecx将ecx寄存器中的值加到eax中的值,eax中存储的是eax和ecx相加的结果。
让我们通过以下示例来看它是如何设置OF、SF和CF标志的:
mov ecx, 0x0fffffff
mov ebx, 0x0fffffff
add ecx, ebx
寄存器是 DWORD 类型。ecx和ebx寄存器被设置为0x0fffffff(268,435,455),将这些结果加上0x1ffffffe(536,870,910)。由于结果没有触及最高有效位(MSB),SF标志没有被设置。由于结果仍然在DWORD的范围内,CF标志没有被设置。假设这两个数是有符号数,结果仍然在有符号DWORD的范围内:
mov ecx, 0x7fffffff
mov ebx, 0x7fffffff
add ecx, ebx
ecx中的结果变为0xfffffffe(-2)。CF = 0;SF = 1;OF = 1。假设ecx和ebx都是无符号数,CF标志不会被设置。假设ecx和ebx都是有符号数且都是正数,则OF标志会被设置。由于最高有效位变为1,SF标志也被设置。
那么,两个负数相加会怎么样呢?我们来考虑以下示例:
mov ecx, 0x80000000
mov ebx, 0x80000000
add ecx, ebx
基本上,我们正在将ecx和ebx相加,这两个寄存器的值为0x80000000(-2,147,483,648),结果变为零(0)。CF = 1;SF = 0;OF = 1。由于结果的最高有效位(MSB)是 0,因此SF标志没有被设置。将ecx和ebx的最高有效位相加肯定会超出DWORD值的容量。从有符号数的角度来看,由于将两个负值相加超出了有符号DWORD的容量,OF标志也被设置。
让我们在下一个示例中尝试借位概念:
mov ecx, 0x7fffffff
mov edx, 0x80000000
sub ecx, edx
这里发生的情况是,我们正在从 0x7fffffff (2,147,483,647) 中减去 0x80000000 (-2,147,483,648)。事实上,我们期望的是 2,147,483,648 和 2,147,483,647 的和。ecx 中的结果变为 0xffffffff (-1)。CF = 1;SF = 1;OF = 1。记住,我们正在执行一个减法操作,因此由于借位,CF 会被置为 1。OF 标志也是如此。
增加和减少指令
INC 指令简单地加 1,而 DEC 减去 1。以下代码将导致 eax 变为零 (0):
mov eax, 0xffffffff
inc eax
以下代码将导致 eax 变为 0xffffffff:
mov eax, 0
dec eax
乘法和除法指令
MUL 用于乘法,DIV 用于除法。在乘法中,我们期望乘积的值超出寄存器的容量。因此,乘积将存储在 AX,DX:AX 或 EDX:EAX(长整型或 QWORD)中:
mov eax, 0x80000000
mov ecx, 2
mul ecx
存储在 eax 中的乘积为零 (0),而 edx 现在包含 0x00000001。SF =0;CF = 1;OF = 1。
对于除法,被除数放入 AX,DX:AX 或 EDX:EAX 中,除法操作后,商放入 AL,AX 或 EAX 中。余数存储在 AH,DX 或 EDX 中。
其他有符号操作
NEG
此操作执行二进制补码操作。
以以下示例为例:NEG EAX 或 NEG dword ptr [00403000]。
如果 EAX 为 01h,它将变为 FFFFFFFFh (-1)。
MOVSX
此指令将 BYTE 移动到 WORD 或将 WORD 移动到 DWORD,并包括符号。它比 CBW、CWDE、CWD 更灵活,因为它能容纳更多操作数。
以以下示例为例:MOVSX EAX, BX。
如果 BX 为 FFFFh (-1) 且符号标志被设置,则 EAX 将为 FFFFFFFFh (-1)。
CBW
类似于 MOVSX,它将 BYTE 转换为 WORD,包括符号。受影响的寄存器是 AL 和 AX。这是一条不带操作数的指令,类似于 MOVSX。其效果是将字节 AL 扩展为其对应的字(AX)。这种转换用“->”符号表示。例如,AL -> AX 表示我们将 8 位数字扩展为 16 位而不改变存储的值。
如果 AL 为 FFh (-1),则 AX 将变为 FFFFh (-1)。
CWDE
这与 CBW 类似,但将 WORD 转换为 DWORD。它影响 AX->EAX。
CWD
这与 CBW 类似,但将 WORD 转换为 DWORD。它影响 AX-> DX:AX。
IMUL/IDIV
这执行 MUL 和 DIV,但接受来自其他寄存器或内存的操作数。
位运算代数
布尔代数或位运算在底层编程中是必要的,因为它可以通过改变数字的位来执行简单的计算。它通常用于加密中的混淆和解码。
NOT
此操作会反转位。
以以下示例为例:NOT AL
如果 AL 等于 1010101b (55h),它将变为 10101010b (AAh)。
AND
此操作如果两个值都是 1,则将 bit 设置为 1,否则将 bit 设置为 0。
以以下示例为例:AND AL, AH
如果 AL 等于 10111010b(BAh)且 AH 等于 11101101b(EDh),则 AL 变为 10101000b(A8h)。
OR
该操作如果两个比特都是 0,则将比特设置为 0,否则将比特设置为 1。
以以下为例:OR AL, AH
如果 AL 等于 10111010b(BAh)且 AH 等于 11101100b(ECh),则 AL 变为 11111110b(FEh)。
XOR
该操作如果两个比特相等,则将比特设置为 0,否则设置为 1。
以以下为例:XOR EAX, EAX
对相同的值进行 XOR 操作将结果变为 0。因此,EAX 变为 0:
XOR AH, AL
如果 AH 为 100010b(22h)且 AL 为 1101011b(6Bh),则 AH 变为 1001001b(49h)。
SHL/SAL
该操作将位向左移动。
以以下为例:SHL AL, 3
如果 AL 为 11011101b(DDh),将其向左移动 3 位后,AL 等于 11101000b(E8h)。
SHR/SAR
该操作将位向右移动。
以以下为例:SHR AL, 3
如果 AL 为 11011101b(DDh),将其向右移动 3 位后,AL 等于 011011b(1Bh)。
ROL
该操作将位向左旋转。
以以下为例:ROL AL, 3
如果 AL 为 11011101b(DDh),将其向左旋转 3 位后,AL 等于 11101110b(EEh)。
ROR
该操作将位向右旋转。
以以下为例:ROR AL, 3
如果 AL 为 11011101b(DDh),将其向右旋转 3 位后,AL 等于 10111011b(BBh)。
控制流
程序的美妙之处在于,我们可以根据条件和状态执行多种不同的行为。例如,我们可以让某个任务重复执行,直到计数器达到定义的最大值。在 C 语言编程中,程序的流控制由诸如 if-then-else 和 for-loop 语句来实现。以下是汇编语言中常用的控制流指令,结合程序控制流使用。受影响的寄存器是索引指针 IP/EIP,它保存当前地址,指向下一条待执行的指令。
JMP
跳转的简写,表示操作数是它将跳转到的地址。它将 EIP 设置为下一条指令行。地址有两种主要的变体:直接和间接。
使用直接地址的 JMP 将字面意义地跳转到给定的地址。例如,考虑 JMP 00401000。这将把 EIP 设置为 00401000h。
使用间接地址的 JMP 指令将跳转到一个只能在跳转执行时才能知道的地址。该地址必须在 JMP 指令之前通过某种方式获取或计算。以下是一些示例:
jmp eax
jmp dword ptr [00403000]
jmp dword ptr [eax+edx]
jmp dowrd ptr [eax]
jmp dword ptr [ebx*4+eax]
CALL 和 RET
类似于 JMP,这将跳转到操作数中声明的地址,但在 CALL 指令执行后,将下一条指令的地址存储到堆栈中。该地址存储在堆栈中,稍后由 RET 指令使用,以将 EIP 指向该地址。例如,考虑以下情况:
Address Instruction
00401000 CALL 00401100
00401005 MOV ECX, EAX
00401007
...
00401100 MOV EAX, F00BF00B
00401105 RET
当调用发生在地址00401000时,堆栈顶部将包含值00401005h,这是返回地址。代码将它传递给地址00401100处的指令,在那里EAX被设置为F00bF00Bh。然后,RET指令从堆栈顶部获取返回地址并设置 EIP。子程序或过程是指从调用到返回地址的指令序列。
RET指令可以选择带有一个操作数。操作数是它在获取返回地址之前将释放的堆栈DWORD的数量。当堆栈在子程序中被使用时,这非常有用,因为它作为清理已用堆栈的操作。
条件跳转
这些是依赖于标志位和计数器寄存器的跳转指令:
| 指令 | 标志位 | 描述 |
|---|---|---|
JZ/JE | ZF = 1 | 如果为零/如果相等,则跳转 |
JNZ/JNE | ZF = 0 | 如果不为零/如果不相等,则跳转 |
JS | SF = 1 | 如果符号位为 1,则跳转 |
JNS | SF = 0 | 如果没有符号位,则跳转 |
JC/JB/JNAE | CF = 1 | 如果有进位/如果小于/如果不大于或等于,则跳转 |
JNC/JNB/JAE | CF = 0 | 如果没有进位/如果不小于/如果大于或等于,则跳转 |
JO | OF = 1 | 如果溢出,则跳转 |
JNO | OF = 0 | 如果没有溢出,则跳转 |
JA/JNBE | CF = 0 且 ZF = 0 | 如果大于/如果不小于或等于,则跳转 |
JNA/JBE | CF = 1 或 ZF = 1 | 如果不大于/如果小于或等于,则跳转 |
JG/JNLE | ZF = 0 且 SF = OF | 如果大于/如果不小于或等于,则跳转 |
JNG/JLE | ZF = 1 或 SF != OF | 如果不大于/如果小于或等于,则跳转 |
JL/JNGE | SF != OF | 如果小于/如果不大于或等于,则跳转 |
JNL/JGE | SF = OF | 如果不小于/如果大于或等于,则跳转 |
JP/JPE | PF = 1 | 如果有奇偶校验/如果偶校验为真,则跳转 |
JNP/JPO | PF = 0 | 如果没有奇偶校验/如果奇偶校验为假,则跳转 |
JCXZ | CX = 0 | 如果 CX 为零,则跳转。 |
JECXZ | ECX = 0 | 如果 ECX 为零,则跳转。 |
LOOP | ECX > 0 | 如果 ECX 不为零,则跳转。减少 ECX。 |
LOOPE | ECX > 0 且 ZF = 1 | 如果 ECX 不为零且零标志设置,则跳转。减少 ECX。 |
LOOPNE | ECX > 0 且 ZF = 0 | 如果 ECX 不为零且零标志未设置,则跳转。减少 ECX。 |
标志设置指令
除了算术、位操作、外部中断和函数返回值之外,这些指令还可以设置标志位。
CMP执行一个 SUB 指令,在第一个和第二个操作数上,但不修改寄存器或立即数。它只会影响标志位。
TEST对第一个和第二个操作数执行 AND 指令,但不修改寄存器或立即数。它只会影响标志位。
堆栈操作
栈是一个临时存储数据的内存空间。栈中数据的添加和移除遵循先进后出的原则。由 C 程序编译的子程序最初会在栈中分配空间,称为栈帧,用于其未初始化的变量。栈顶的地址存储在 ESP 寄存器中:
栈由两个常见指令控制:PUSH和POP。
PUSH 将栈顶地址减小DWORD大小,在 32 位地址空间中,然后存储其操作数的值。
作为示例,请考虑以下内容:PUSH 1
如果栈顶地址(存储在 ESP 中)为地址002FFFFCh,则 ESP 变为002FFFF8h,并将1存储到新的 ESP 地址。
POP 从栈顶(ESP)检索值,然后将其存储到操作数指示的寄存器或内存空间中。随后,ESP 增加DWORD大小。
作为示例,请考虑以下内容:POP EAX
如果栈顶地址(存储在 ESP 中)为地址002FFFF8h,并且栈顶存储的DWORD值为0xDEADBEEF,那么0xDEADBEEF将被存储到EAX寄存器中,而 ESP 变为002FFFFCh。
PUSHA/PUSHAD 都会将所有通用寄存器按此顺序压入栈中(适用于 32 位构建):EAX、ECX、EDX、EBX、EBP、ESP、EBP、ESI 和 EDI。PUSHA适用于 16 位操作数,而PUSHAD适用于 32 位操作数。不过,二者可能是同义的,取决于当前的操作数大小。
POPA/POPAD 都将所有通用寄存器从栈中弹出,并按与PUSHA/PUSHAD存储顺序的相反顺序恢复。
PUSHF 将EFLAGS压入栈中。
POPF 将EFLAGS从栈中弹出。
ENTER 通常用于子程序的开始。它用于为子程序创建栈帧。从内部来看,ENTER 8,0可能大致等同于以下操作:
push ebp ; save the current value of ebp
mov ebp, esp ; stores current stack to ebp
add esp, 8 ; create a stack frame with a size of 8 bytes
LEAVE用于撤销ENTER指令的操作,最终销毁创建的栈帧。
工具 – 构建器和调试器
在继续更多指令之前,最好尝试一下实际使用汇编语言编程。我们需要的工具是文本编辑器、汇编代码构建器和调试器。
流行的汇编器
所有编程语言都需要被构建成可执行文件,以便在程序构建所针对的系统平台上运行。除非你想手动输入每个操作码字节到二进制文件中,开发者们已经制作了工具,将源代码转换为机器可以理解的可执行文件。让我们来看一下目前最流行的汇编语言构建器。
MASM
也叫做微软宏汇编器(Microsoft Macro Assembler),MASM 已经存在超过 30 年。它由微软维护,是 Visual Studio 产品的一部分。MASM 用于将 x86 源代码编译成可执行代码。
编译分为两个步骤:将源代码编译成目标文件,然后将目标文件所需的所有模块链接成一个单独的可执行文件。
MASM 套件自带一个文本编辑器,菜单中包含编译器和链接器,用于将源代码构建为可执行文件。这非常方便,因为不需要通过命令行运行编译器和链接器来构建可执行文件。只需在以下源代码上执行 "Console Build All" 命令,即可生成一个可以在命令终端中运行的可执行文件:
MASM 可以从 www.masm32.com/ 下载。
NASM
NASM 是 Netwide Assembler 的缩写。NASM 与 MASM 非常相似,主要在语法、指令和变量声明上有些许不同。NASM 的一个优点是代码和数据的分段非常容易识别:
MASM 和 NASM 也都需要编译和链接来构建可执行文件:
然而,与 MASM 不同,NASM 的安装包没有自带编辑器。NASM 在 Linux 社区非常受欢迎,因为它作为开源软件进行开发。该包仅包含用于目标文件的编译器;你需要下载 GCC 编译器来生成可执行文件。
下载 NASM 的官方网站是 www.nasm.us/。对于 Windows,可以使用 MinGW (www.mingw.org/) 来生成可执行文件。
FASM
FASM 或 Flat Assembler 类似于 MASM 和 NASM。像 MASM 一样,它有自己的源代码编辑器;像 NASM 一样,代码段易于识别和配置,并且该软件有适用于 Windows 和 Linux 的版本:
FASM 可以从 flatassembler.net/ 下载。
在我们的汇编语言编程中,我们将使用 FASM,因为我们可以在 Windows 和 Linux 上使用它的编辑器。
x86 调试器
调试器是程序开发者用于跟踪代码的工具。这些工具用于验证程序是否按照预期的行为执行。通过调试器,我们可以逐行跟踪代码,看到每条指令的执行情况,以及它如何更改寄存器和存储在内存中的数据。在逆向工程中,调试器用于分析程序的低级细节。通过我们对汇编语言、目标编译程序和调试器的了解,我们能够进行逆向工程。
除了本书介绍的工具外,互联网上还有许多工具,它们可能有更多或更少的功能。关键是逆向工程依赖于工具,我们需要保持自己对最新工具的了解。随意下载其他你想探索的工具,看看哪个工具让你的逆向过程更舒适。
WinDbg
WinDbg是由微软开发的用于在 Microsoft Windows 上进行调试的强大工具,支持在用户模式和内核模式下调试。它可以加载内存转储和由于 Windows 自身错误标记的崩溃转储。在内核模式下,它可以远程调试设备驱动程序或 Windows 操作系统。它可以加载与程序关联的符号文件,帮助开发人员或分析师识别正确的库函数格式及其他信息。
WinDbg有一个图形用户界面,默认情况下显示一个命令框,你可以在其中输入并执行命令。你可以添加一组信息窗口并将其停靠。它可以显示反汇编、寄存器和标志、堆栈(使用内存转储窗口),以及输入的任何地址的内存转储:
Windbg可以从docs.microsoft.com/en-us/windows-hardware/drivers/debugger/.
Ollydebug
这是在 x86 32 位 Windows 平台上最流行的调试器,因为它的包文件非常轻量。其默认界面显示了逆向工程师需要的重要信息:一个反汇编视图,用于跟踪;寄存器和标志窗格;以及堆栈和内存视图。
OllyDebug 可以从www.ollydbg.de/下载。
x64dbg
这个调试器最为推荐,因为开发者保持它的更新,并与社区合作。它也支持 64 位和 32 位 Windows 平台,并提供许多有用的插件。它的界面与 Ollydebug 类似。
x64dbg可以从x64dbg.com/下载。
Hello World
我们将使用FASM来构建我们的第一个汇编语言程序。然后我们将使用x64dbg调试该可执行文件。
安装 FASM
使用我们的 Windows 设置,从flatassembler.net/下载 FASM,然后将 FASM 解压到你选择的文件夹中:
运行FASMW.EXE来启动FASM图形界面。
它可以工作!
在文本编辑器中写下以下代码,或者你可以直接从github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch3/fasmhello.asm做 Git 克隆。
format PE CONSOLE
entry start
include '%include%\win32a.inc'
section '.data' data readable writeable
message db 'Hello World!',0
msgformat db '%s',0
section '.code' code readable executable
start:
push message
push msgformat
call [printf]
push 0
call [ExitProcess]
section '.idata' import data readable writeable
library kernel32, 'kernel32.dll', \
msvcrt, 'msvcrt.dll'
import kernel32, ExitProcess, 'ExitProcess'
import msvcrt, printf, 'printf'
点击“文件”->“另存为...”,然后点击“运行”->“编译”来保存:
可执行文件将位于源文件保存的位置:
如果没有显示 "Hello World!",需要注意的是,这是一个控制台程序。你必须打开一个命令终端并从那里运行可执行文件:
处理构建过程中常见的错误
写入失败错误 — 这意味着构建器或编译器无法写入输出文件。可能是它要构建的可执行文件仍在运行。尝试查找之前运行的程序并终止它。你也可以从进程列表或任务管理器中终止它。
意外字符 — 检查指定行的语法。有时候,包含的文件也需要因为构建器的最新版本而更新语法。
无效的参数 — 检查指定行的语法。可能是定义或声明缺少参数。
非法指令 — 检查指定行的语法。如果你确信指令是有效的,可能是构建器版本与该指令有效的版本不匹配。在更新构建器到最新版本的同时,也要更新源文件以符合最新版本的要求。
解剖程序
现在我们已经构建了程序并使其正常工作,让我们讨论一下程序包含了什么以及它的用途。
程序主要由代码部分和数据部分构成。代码部分,顾名思义,就是放置程序代码的地方。而数据部分是程序代码使用的数据,如文本字符串所在的位置。程序在编译之前有一些要求。这些要求定义了程序将如何构建。例如,我们可以告诉编译器将这个程序构建为 Windows 可执行文件,而不是 Linux 可执行文件。我们还可以告诉编译器程序应该从代码的哪一行开始运行。下面给出了一个程序结构的示例:
我们还可以定义程序将使用的外部库函数。这个列表在一个单独的部分中描述,称为导入部分。编译器可以支持各种不同的部分。这些扩展部分的一个例子是资源部分,其中包含图标和图片等数据。
有了程序结构的基本概念后,让我们看看我们的程序是如何编写的。第一行,format PE CONSOLE,表示程序将被编译为一个 Windows PE 可执行文件,并构建为在控制台上运行,Windows 中更常见的称呼是命令提示符。
下一行,entry start,表示程序将开始执行位于start标签的代码。标签的名称可以由程序员根据需要进行更改。下一行,include '%include%\win32a.inc',将添加来自 FASM 库文件win32a.inc的声明。预期声明的函数用于调用printf和ExitProcess API 函数,这些函数将在idata部分讨论。
该程序中有三个内建部分:data、code和idata部分。这里的部分名称被标记为.data、.code和.idata。每个部分的权限也被指示为可读、可写和可执行。data部分是放置整数和文本字符串的地方,并使用定义字节(db)指令列出。code部分是执行指令代码的地方。idata部分是导入的 API 函数声明的地方。
下一行,我们看到数据部分被定义为可写部分:
section '.data' data readable writeable
程序的.data部分包含两个常量变量,message和msgformat。这两个文本字符串是ASCIIZ(ASCII-Zero)字符串,这意味着它们以零(0)字节结尾。这些变量是通过db指令定义的:
message db 'Hello World!',0
msgformat db '%s',0
下一行定义了代码部分。它被定义为具有读取和执行权限:
section '.code' code readable executable
.code部分是start:标签所在的位置,也是我们代码的位置。标签名称前缀是冒号字符。
在 C 编程中,printf是一个常用于打印消息到控制台的函数,使用的 C 语法如下:
int printf ( const char * format, ... );
第一个参数是包含格式说明符的消息。第二个参数包含填充格式说明符的实际数据。从汇编语言的角度来看,printf函数是一个在msvcrt库中的 API 函数。通过将参数放入内存堆栈空间中来设置 API 函数,然后调用该函数。如果你的程序是用 C 语言编写的,需要 3 个参数的函数(例如,myfunction(arg1, arg2, arg3))在汇编语言中的等效代码如下:
push <arg3>
push <arg2>
push <arg1>
call myfunction
对于 32 位地址空间,使用push指令将一个DWORD(32 位)数据写入堆栈的顶部。堆栈顶部的地址存储在 ESP 寄存器中。当执行push指令时,ESP 值减少 4。如果参数是文本字符串或数据缓冲区,则将地址推送到堆栈。如果参数是数值,则将值直接推送到堆栈。
按照相同的 API 调用结构,带有两个参数,我们的程序以这种方式调用了printf:
push message
push msgformat
call [printf]
在数据部分,地址message和msgformat作为调用printf函数前的设置被推入栈中。地址通常放在方括号[]中。如前所述,使用的是地址中的值。printf实际上是一个标签,它是程序中在.idata部分声明的本地地址。[printf]表示我们正在使用msvcrt库中printf API 函数的地址。因此,call [printf]将执行来自msvcrt库的printf函数。
对于ExitProcess也是一样的。ExitProcess是一个kernel32函数,用于终止正在运行的进程。它需要一个参数,即退出码。退出码为 0 表示程序将无错误地终止:
push 0
call [ExitProcess]
在 C 语法中,这段代码等同于ExitProcess(0),它终止程序并返回一个由零定义的成功结果。
程序的.idata部分包含外部函数,并设置为可读写权限:
section '.idata' import data readable writeable
在以下代码片段中,有两个部分。第一部分指示函数所在的库文件。library命令用于设置所需的库,并使用语法library <库名>, <库文件>。反斜杠\表示下一行是当前行的延续:
library kernel32, 'kernel32.dll', \
msvcrt, 'msvcrt.dll'
一旦库被声明,使用import命令来指定特定的 API 函数。语法为import <库名>, <函数名>, <库文件中的函数名>。这里导入了两个外部 API 函数,kernel32的ExitProcess和msvcrt的printf:
import kernel32, ExitProcess, 'ExitProcess'
import msvcrt, printf, 'printf'
程序的注释版本可以在github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch3/FASM%20commented.txt找到
API 函数库可以在 MSDN 库中找到(msdn.microsoft.com/en-us/library),该库也有一个离线版本,包含在 Visual Studio 安装程序中。它提供了有关 API 函数的用途以及如何使用它的详细信息。在线版本如下所示:
在 Hello 之后
我们遇到了对 printf 和 ExitProcess API 函数的外部调用。这些特定的函数是为 Windows 开发的,作为用户模式和内核模式之间的通信手段。通常,对于大多数操作系统来说,内核负责实际在显示器上显示输出、将文件写入磁盘、读取键盘输入、向 USB 端口传输数据、发送数据到打印机、通过网络传输数据等等。从本质上讲,所有与硬件相关的操作都必须通过内核。然而,我们的程序处于用户模式中,我们使用 API 来告诉内核为我们执行操作。
调用 API
在我们的程序中调用 API 只需要定义包含 API 函数的库文件和 API 函数本身的名称。正如我们在 Hello World 程序中所做的,我们通过在导入部分设置它来导入 API 函数:
section '.idata' import data readable writeable ; import section has read and write permissions
library kernel32, 'kernel32.dll', \ ; functions came from kernel32 and msvcrt dlls
msvcrt, 'msvcrt.dll'
import kernel32, ExitProcess, 'ExitProcess' ; program will use ExitProcess and printf functions
import msvcrt, printf, 'printf'
然后,我们通过 CALL 指令调用 API,如下所示:
call [printf]
call [ExitProcess]
常见的 Windows API 库
KERNEL32 包含 Windows 的基础函数,负责文件 I/O 操作和内存管理,包括进程和线程管理。有些函数是用于调用 NTDLL 库中更原生 API 的辅助函数。
USER32 包含处理显示和图形界面的函数,例如程序窗口、菜单和图标。它还包含控制窗口消息的函数。
ADVAPI32 包含与 Windows 注册表相关的函数。
MSVCRT 包含来自 Microsoft Visual C++ 运行时的标准 C 库函数,例如 printf、scanf、malloc、strlen、fopen 和 getch。
WS2_32、WININET、URLMON 和 NETAPI32 是包含与网络和互联网通信相关的函数的库。
常见 API 函数简短列表
API 函数可以根据其用途进行分类。完整列表可以在 MSDN 库中找到,但这里列出了最常见的函数:
| 用途 | API 函数 |
|---|---|
| 控制台输出 | KERNEL32!GetStdHandle, MSVCRT!printf |
| 文件处理 | KERNEL32!ReadFile, KERNEL32!WriteFile, KERNEL32!CreateFile |
| 内存管理 | KERNEL32!VirtualAlloc, KERNEL32!VirtualProtect, MSVCRT!malloc |
| 进程和线程 | KERNEL32!ExitProcess, KERNEL32!CreateProcess, KERNEL32!CreateThread, SHELL32!ShellExecute |
| 窗口管理 | USER32!MessageBoxA, USER32!CreateWindowExA, USER32!RegisterWindowMessageW |
| 字符串处理 | MSVCRT!strlen, MSVCRT!printf |
| 网络通信 | WININET!InternetAttemptConnect, WS2_32!socket, WS2_32!connect, URLMON!URLDownloadToFile |
| 加密 | CryptDecrypt, CryptEncrypt |
| 注册表 | RegDeleteKey, RegCreateKey, RegQueryValueExW, RegSetValueExW |
调试
在某些时候,我们的程序可能会产生不可预测的错误或无效输出。在这种情况下,我们需要通过逐行调试代码来追踪出错的原因。但在此之前,有一些常用的调试命令我们需要了解。
单步调试意味着逐行调试程序代码。单步调试有两种模式:step into 和 step over。在调试过程中,当被调试的行是一个 CALL 指令时,使用 step into 模式时,单步调试会进入子程序继续调试。而 step over 模式则不会进入子程序,而是让子程序继续执行,单步调试会在 CALL 指令后的下一行继续。请看以下对比:
| Step into | Step over |
|---|
|
CALL 00401000 ; <-- STEP INTO SUBROUTINE
MOV EBX, EAX
...
00401000:
MOV EAX, 37173 ; <- DEBUG POINTER GOES HERE
RET
|
CALL 00401000 ; <-- STEP OVER SUBROUTINE
MOV EBX, EAX ; <- DEBUG POINTER GOES HERE
...
00401000:
MOV EAX, 37173
RET
|
run 或 continue 使调试器连续执行指令,直到程序终止、遇到错误,或遇到手动设置的断点。
设置 断点 是让调试器中断已设置为自由运行的代码的一种方法。例如,如果我在以下代码的地址 0040200A 处设置了一个断点,并让调试器从 00402000 开始自动运行每条指令,调试器会在地址 0040200A 处停止,并允许用户继续进行单步调试或继续运行:
00402000 push 0040100D
00402005 push 0040100D
0040200A call dword ptr [printf] ; <-- breakpoint set here
00402010 push 0
00402012 call dword ptr [ExitProcess]
让我们调试我们的 Hello World 程序。
从x64dbg.com/下载 x64dbg。
这是一个 ZIP 压缩包,你需要解压它。解压后,打开 release 文件夹中的 x96dbg.exe。这将显示启动对话框,你可以选择 x32dbg(用于 32 位调试)和 x64dbg(用于 64 位调试)作为调试器:
我们开发的 Hello World 程序是一个 32 位程序,因此请选择 x32dbg。然后点击 File->Open,浏览并打开 helloworld.exe 程序。打开后,你会在反汇编窗口中看到 EIP 的位置,如下所示:
在窗口的底部,它显示:“系统断点已触发!”EIP 位于高内存区域地址,窗口标题也显示“模块:ntdll.dll - 线程:主线程”。所有这些都表明我们还没有进入 helloworld 程序,而是仍然在加载 helloworld 程序到内存、初始化并开始运行的 ntdll.dll 代码中。如果你进入 Options->Preferences,在设置窗口的 Events 表格中,默认情况下,系统断点 是勾选的。这会导致调试器在我们进入 helloworld 代码之前就停在 ntdll.dll 中。取消勾选系统断点,点击保存,然后退出调试器,如下所示:
现在我们已经移除了系统断点,请重新加载 helloworld 程序。此时,EIP 应该已经位于 helloworld 代码中:
点击调试菜单。你应该会看到有键盘快捷键分配给“单步进入”、“单步跳过”、“运行”以及更多调试选项:
堆栈帧窗口位于右下方。注意那里的信息,然后按 *F7* 或 F8 执行单步操作。PUSH helloworld.401000 指令刚刚将 "Hello World" 文本字符串的地址压入堆栈顶部。在右上方的寄存器和标志窗口中,所有变化的文本都会显示为红色。随着堆栈地址的变化,ESP 也会发生变化。由于我们现在执行的是下一条指令代码,EIP 也应有所改变。
再执行一步,推动 "%s" 的地址压入堆栈。此时,你应该已经在地址 0040200A。此时,执行单步跳过会执行 printf 函数,并到达地址 00402010。出于好奇,我们不妨选择单步进入。这会带我们进入 msvcrt 库,printf 函数就在其中:
要返回到我们的 helloworld 程序,可以执行 "Run to user code"(映射快捷键为 Alt + F9)或 "Execute till return"(Ctrl + F9)。用户代码指的是我们的 Hello World 程序。执行 "Run to user code" 会将我们带到地址 00402010,即 printf 调用之后的指令。执行 "Execute till return" 会将我们带到 RET 指令所在的地址。我们不妨选择执行 "Execute till return":
现在查看堆栈。正如之前讨论的 CALL-RET 指令,CALL 会将下一条指令的地址存储在堆栈顶部。此时,存储在堆栈顶部的地址是 00402010。进行单步操作后,我们应该回到我们的 hello world 程序。
继续执行单步跳过。最后两条指令应该会终止程序,调试会停止。
总结
汇编语言是一种低级语言,通过指令与计算机系统直接通信。计算机中使用的逻辑基于开关概念,从中衍生出了二进制 1 和 0。我们已经学会了如何从不同的数字进制中读写二进制,以及如何进行算术和位运算。
我们介绍了可以用来构建和验证我们程序的流行汇编器和调试器。接着,我们使用 FASM 编写并构建了我们的 Win32 低级 Hello World 程序,该程序使用 API 与内核进行通信。我们通过使用 x64dbg 调试器验证了我们构建的可执行程序。调试我们的 Hello World 程序是我们进入逆向工程世界的一个良好开端。
熟能生巧。我们列出了一些可以使用汇编语言开发的推荐程序。
了解代码的最低层次是我们逆向工程之旅的良好起点。当你完成本书的学习时,汇编语言会感觉像是在公园里散步一样轻松。
进一步阅读
英特尔的文档包含了完整的 x86 指令列表,并描述了每个指令在汇编语言中的语法和使用方法。你可以从www.intel.com/products/processor/manuals/获取这些文档。
第四章:静态与动态逆向分析
就像医院里的病人一样,文件需要经过一些初步评估,以确定资源的正确分配。文件评估的结果将告诉我们需要使用哪些工具,哪些逆向步骤需要执行,以及将使用哪些资源。进行逆向分析的步骤分为静态分析和动态分析。
在本章中,我们将介绍评估文件时使用的方法和工具。我们将以 32 位 Windows 操作系统为示例,接着检查我们可以用于静态和动态分析的工具。本章将帮助你生成一个检查清单,为你提供一个在最短时间内获取文件所有信息的指南。
在本章中,你将做以下内容:
-
了解目标评估
-
执行静态分析
-
执行动态分析
评估与静态分析
文件需要经过初步评估,以便我们确定所需的工具和分析方法。这个过程还帮助我们为分析文件制定策略。进行这样的评估需要进行轻量级的静态分析。以下是一些可能作为我们指南的评估思路:
-
它的来源:
- 逆向工程的一个目的就是帮助网络管理员防止类似的恶意软件渗透到网络中。了解文件的来源有助于确保用于传输文件的渠道。例如,如果分析的文件被确定为电子邮件附件,那么网络管理员应当加强电子邮件服务器的安全。
-
现有信息:
- 在互联网上搜索已有的信息可以非常有帮助。可能已经对该文件进行了现有的分析。我们可以确定预计的行为,这将有助于加快分析过程。
-
查看文件并提取其文本字符串:
- 使用工具查看文件帮助我们确定文件类型。从文件中提取可读文本也能为我们提供提示,告诉我们文件在打开或执行时将使用哪些消息、函数和模块。
-
文件信息:
-
文件类型是什么?
-
头部与类型分析
-
静态分析
静态分析将帮助我们记录在动态分析过程中需要做的事情。掌握x86汇编语言后,我们应该能够理解反汇编的Win32 PE文件及其分支。通过这样做,我们将能够根据文件类型准备适当的工具来读取、打开和调试文件,并根据文件格式理解文件的结构。
我们通过确定文件类型开始静态分析,然后继续了解文件格式。我们可以提取文本字符串,这些字符串可能帮助我们立即识别有用信息,例如使用的 API 函数、将使用的库模块、文件从哪种高级语言编译而来、它将尝试访问的注册表项,以及它可能尝试连接的网页或 IP 地址。
文件类型和头部分析
文件类型是触发整个分析过程的最重要信息。如果文件类型是 Windows 可执行文件,则会准备一组预设的PE工具。如果文件类型是 Word 文档,那么我们将使用的沙箱环境必须安装 Microsoft Office 以及可以读取OLE文件格式的分析工具。如果给定的分析目标是一个网站,我们可能需要准备能够读取 HTML 并调试 JavaScript 或 Visual Basic 脚本的浏览器工具。
从文件中提取有用信息
使用文件查看工具(如 HxD(mh-nexus.de/en/hxd/))手动解析文件的每一部分信息是非常有趣的。但由于查找文件文档需要一些时间,已有为逆向工程师开发的工具。这些工具在互联网上随时可用,可以轻松提取和显示文件信息,并具有识别文件类型的功能。这些提取的信息帮助我们确定我们正在处理的文件类型。
PEid 和 TrID
PEid 和 TrID 是能够检测文件类型、使用的编译器、加密工具以及使用的打包器和保护器的工具。压缩的可执行文件更常被称为打包器。这些打包器的一些例子包括 UPX、PECompact 和 Aspack。另一方面,保护器与打包器有些类似,但更先进,因为原始编译的代码会被保护,防止轻易被逆向。保护器的例子包括 Themida、AsProtect 和 Enigma Protector。
保护软件通常是商业软件。虽然这两个工具都没有再更新,但它们仍然运行得很好。下面是 PEiD 主界面的截图:
下面是如何在 Linux 终端中使用TrID的截图:
在写这篇文章时,这些工具可以通过以下链接下载:
PEid 可以从www.softpedia.com/get/Programming/Packers-Crypters-Protectors/PEiD-updated.shtml下载。 TriD 可以从mark0.net/soft-trid-e.html下载。
python-magic
这是一个能够检测文件类型的 Python 模块。然而,与 PEiD 和 TrID 不同,它还可以检测编译器和加壳工具:
它可以从 pypi.org/project/python-magic/ 下载。
文件
Linux 有一个内置的命令,称为 file。file 基于 libmagic 库,能够识别各种文件格式的文件类型:
MASTIFF
MASTIFF 是一个静态分析框架。它可以在 Linux 和 Mac 上运行。作为一个框架,静态分析基于 MASTIFF 作者和社区提供的插件。
这些插件包括以下内容:
trid:这是一个用于识别文件类型的工具。
ssdeep:ssdeep 是一个模糊哈希计算器。模糊哈希,或称为上下文触发的分段哈希(CTPH),可用于识别几乎相同的文件。这对于识别恶意软件家族的变种非常有用。
pdftools:这是 Didier Stevens 提供的插件,用于提取 PDF 文件的信息。
exiftool:显示图像文件的信息。
pefile:显示 PE 文件的信息。
disitool:这是 Didier Stevens 的另一个 Python 脚本,用于从签名的可执行文件中提取数字签名。
pyOLEscanner:这是一个用于从 OLE 文件类型(如 Word 文档和 Excel 表格)中提取信息的工具。
可以通过以下屏幕截图查看 MASTIFF 工作的示例:
MASTIFF 可以从 github.com/KoreLogicSecurity/mastiff 下载。
其他信息
作为静态信息收集的一部分,文件会被分配一个唯一的哈希值。这些哈希值用于从文件信息数据库中识别文件。哈希信息通常有助于分析人员共享有关文件的信息,而无需传输文件本身。
下面是 MASTIFF 在测试文件上的 file_info 结果示例:
PE 可执行文件
PE 可执行文件是适用于 Windows 的程序。可执行文件的扩展名为 .exe。动态链接库使用相同的 PE 文件格式,并使用 .dll 扩展名。Windows 设备驱动程序程序也采用 PE 文件格式,扩展名为 .sys。还有其他使用 PE 文件格式的扩展名,例如屏幕保护程序(.scr)。
PE 文件格式包含一个头部,分为 MZ 头部、DOS 存根和 PE 头部,随后是数据目录和节表,如下所示:
文件格式遵循原始的 MSDOS EXE 格式,但通过 PE 头扩展为 Windows 格式。如果在 MSDOS 环境下运行 Windows 程序,会显示以下消息:This program cannot be run in DOS mode.。
显示此消息的代码是 DOS 存根的一部分。
PE 头的段表包含了关于代码和数据在文件中位置的所有信息,以及它在作为进程加载到内存时如何映射。PE 头包含程序开始执行代码的地址——一个称为入口点的位置——并且会被设置在 EIP 寄存器中。
数据目录包含指向表的地址,这些表进一步包含诸如导入表之类的信息。导入表包含程序将使用的库和 API。该表遵循一个结构,指向一组地址,这些地址依次指向库的名称及其各自的导出函数:
MASTIFF中使用的peinfo模块能够显示导入的库和函数,如下所示:
HxD和HIEW是本章中使用的流行二进制编辑器;HxD更为流行,是免费的,可以轻松地用于对文件进行二进制编辑。更多信息和下载链接可以在mh-nexus.de/en/hxd/找到。如果你尝试使用HxD,你会看到类似于此屏幕截图的内容:
另一个有用的十六进制编辑工具是HIEW(黑客视图)。演示版和免费版能够解析PE头。该工具还可以显示导出和导入的 API 函数:
静态导入的模块、库和函数是我们可以预期程序访问的线索。例如,考虑到如果PE文件导入了KERNEL32.DLL库,那么我们应该预期文件包含核心 API,这些 API 可能会访问文件、进程和线程,或者动态加载其他库并导入函数。以下是我们应当注意的一些常见库:
-
ADVAPI32.DLL:此库包含将访问注册表的函数。 -
MSVCRXX.DLL(其中 XX 是版本号。示例包括MSVCRT.DLL和MSVCR80.DLL)——此文件包含 Microsoft Visual C 运行时函数。这直接告诉我们该程序是使用 Visual C 编译的。 -
WININET.DLL:此库包含访问互联网的函数。 -
USER32.DLL:此库包含与显示在显示器上的任何内容相关的窗口控制函数,如对话框、显示消息框和定位窗口框等。 -
NTDLL.DLL:此库包含直接与内核系统交互的原生函数。KERNEL32.DLL和像USER32.DLL、WININET.DLL、ADVAPI32.DLL这样的库具有将信息转发到原生函数以执行实际系统级操作的函数。
死列出
Deadlisting 是一种分析方法,我们可以分析文件的反汇编或反编译代码,并绘制执行时将发生的事件流。结果呈现的流程图将作为动态分析的指南。
IDA(交互式反汇编器)
我们之前介绍了 IDA 工具来显示给定文件的反汇编。它具有图形视图功能,显示代码块的概述和条件流的分支。在 Deadlisting 中,我们试图描述每个代码块及其可能产生的结果。这使我们了解程序的功能。
反编译器
一些高级程序使用 p-code 编译,例如 C#和 Visual Basic(p-code 版本)。相反,反编译器试图根据 p-code 重新创建高级源代码。高级语法通常有一个等效的 p-code 代码块,可以被反编译器识别。
使用 C 语言编译的程序以纯汇编语言的形式保存在文件中。但由于它仍然是一种高级语言,一些代码块可以被识别并还原为它们的 C 语法。IDA Pro 的付费版本有一个昂贵但非常有用的插件,称为 Hex-Rays,可以识别这些代码块并重新创建 C 源代码。
ILSpy – C#反编译器
用于反编译 C#程序的流行工具是 ILSpy。一些反编译器只会留下源代码供静态分析。但是,在 ILSpy 中,可以将反编译的源代码保存为 Visual Studio 项目。这使分析人员可以编译和调试以进行动态分析。
动态分析
动态分析是一种需要代码实时执行的分析类型。在静态分析中,我们最远可以到达的是 Deadlisting。例如,如果我们遇到一个解密或解压缩大量数据的代码,并且想要查看解码数据的内容,那么最快的选择就是进行动态分析。我们可以运行调试会话,让该代码区域为我们运行。静态分析和动态分析相辅相成。静态分析帮助我们识别代码中需要更深入理解和与系统进行实际交互的点。通过静态分析后进行动态分析,我们还可以看到实际数据,如文件句柄、随机生成的数字、网络套接字和数据包数据以及 API 函数结果。
存在一些可以进行自动化分析的工具,这些工具在沙盒环境中运行程序。这些工具要么记录运行时的更改,要么在快照之间记录:
-
Cuckoo(开源)– 这个工具在本地部署。它需要一个主机和沙盒客户端。主机充当 Web 控制台,文件被提交进行分析。文件在沙盒中执行,所有活动都被记录,然后发送回主机服务器。报告可以从 Web 控制台查看。
-
RegShot(免费) - 这个工具用于在运行程序之前和之后拍摄注册表和文件系统的快照。快照之间的差异使分析人员能够确定发生了哪些变化。这些变化可能包括操作系统所做的更改,分析人员需要识别哪些变化是由程序引起的。
-
Sandboxie(免费增值) - 这个工具用于程序运行的环境中。它声称内部使用了隔离技术。本质上,隔离技术分配磁盘空间,磁盘写入只会在程序通过 Sandboxie 执行时发生。这使得 Sandboxie 只通过查看隔离空间来确定变化。关于 Sandboxie 的下载链接和更多信息可以在
www.sandboxie.com/HowItWorks找到。 -
Malwr(免费) - 这是一个免费在线服务,使用 Cuckoo。文件可以提交到
malwr.com/。 -
ThreatAnalyzer(付费) - 最初称为 CWSandbox,这是安全行业中最流行的沙箱技术,用于自动提取运行中恶意软件的信息。该技术得到了很大的改进,特别是在报告方面。此外,它报告了发现的描述性行为,包括关于提交文件的云查询。它可以支持定制规则和灵活的 Python 插件,展示分析人员看到的行为。
-
Payload Security 的 Hybrid Analysis(免费) - 这是最受欢迎的免费在线服务之一,类似于 Malwr,报告内容与 ThreatAnalyzer 相似。
提交文件到在线服务减少了设置主机沙箱环境的需求。然而,某些人仍然倾向于自己搭建环境,以避免文件被分享给社区或在线服务。
对于恶意软件分析,建议在收到文件时进行自动化分析和网络信息收集。如果当局足够迅速地关闭这些网站,恶意软件获取更多数据的站点可能无法访问。
内存区域和进程的映射
在动态分析中,了解程序加载并执行时内存的状态非常重要。
由于 Windows 和 Linux 都支持多任务处理,每个进程都有自己的虚拟地址空间(VAS)。对于 32 位操作系统,VAS 的大小为 4 GB。每个 VAS 都通过其相应的页表映射到物理内存,并由操作系统的内核进行管理。那么,多个 VAS 如何适应物理内存呢?操作系统通过分页管理这一过程。分页有一个使用和未使用的内存列表,包括特权标志。如果物理内存不足,分页可以使用磁盘空间作为扩展物理内存的形式。一个进程及其模块依赖项并不会占用整个 4 GB 的空间,只有这些虚拟分配的内存段在页表中标记为已使用,并映射到物理内存中。
VAS 被分为两个区域:用户空间和内核空间,其中内核空间位于较高的地址区域。虚拟空间的划分在 Windows 和 Linux 之间有所不同:
每个 VAS 都有一个内核空间,在页表中列为具有独占权限的空间。通常,这些权限被称为内核模式和用户模式。它们特定地被标识为保护环。内核具有环 0 的特权,而我们使用的应用程序则在环 3 特权上运行。设备驱动程序位于环 1 或环 2 层,也被认为具有内核模式权限。如果用户模式程序尝试直接访问内核模式的内核空间,则会触发页故障。
一旦 VAS 被启用,用户空间最初会为栈、堆、程序和动态库分配空间。进一步的分配会在程序运行时通过调用内存请求 API(如 malloc 和 VirtualAlloc)进行:
上面的截图是 jbtest.exe 刚刚在 32 位 Windows 中加载时的映射视图。这里是一个更具描述性的标准布局,展示了程序在 Windows 中虚拟分配空间中的结构:
进程和线程监控
监控进程和线程,尤其是那些由我们分析的文件创建的线程,告诉我们比表面上看起来的更多行为。一个进程可以创建多个线程,这意味着它可能在同时执行多个行为。一个创建的进程意味着一个新程序刚刚被执行。
在 Windows 中,进程的终止、创建和打开可以通过第三方工具(如 Process Monitor)进行监控。尽管有内置的工具,如任务管理器,能够显示进程信息,但一些第三方工具可以提供更详细的关于进程及其线程的信息。
网络流量
服务器和客户端计算机之间传输的数据只有在动态分析过程中才能看到。在传输过程中捕获的数据包将帮助分析员了解程序向服务器发送了什么数据,以及服务器如何响应接收到的任何数据。
流行的工具,如 Wireshark 和 Fiddler,用于捕获数据包并将其存储为 pcap 文件。在 Linux 中,tcpdump 工具通常用于执行相同的操作。
监控系统变化
对于 Windows,我们需要监控三个方面:内存、磁盘和注册表。文件监控工具会监视创建、修改或删除的文件和目录。另一方面,注册表监控工具会监视创建、更新或删除的注册表键、值和数据。我们可以使用诸如 FileMon 和 RegMon 的工具来完成这项工作。
执行后的差异
比较在执行文件之前和之后拍摄的快照之间的差异,能够显示所有系统变化。这种分析方法无法识别发生在两者之间的任何事件。它对于找出软件安装程序如何安装程序非常有用。因此,差异结果在手动卸载软件时尤其有用。这里使用的工具是 RegShot。
调试
死列表提供了我们需要的大部分信息,包括程序的分支流程。现在,我们有机会验证程序在调试时将遵循的路径。我们可以看到暂时存储在寄存器和内存中的数据。而且,不必手动尝试理解解密代码,调试它会直接显示解密后的数据。
用于 Windows 调试的工具包括以下几种:
-
OllyDebug -
x86dbg -
IDA Pro
用于调试 Linux 的工具包括以下几种:
-
gdb -
radare2
亲自试试看
为了尝试我们学到的工具,让我们对 ch4_2.exe 进行一些静态分析。为了帮助,以下是我们需要找到的内容:
-
文件信息:
-
文件类型
-
导入的 DLL 和 API
-
文本字符串
-
文件哈希
-
-
文件的作用
直接获取文件信息,我们将使用 TrID(mark0.net/soft-trid-e.html)来识别文件类型。执行以下命令:
trid cha4_2.exe
TrID 结果告诉我们,我们这里有一个 Windows 32 位可执行文件,且经过 UPX 压缩:
知道这是一个 UPX 压缩文件后,我们可以尝试使用 UPX (upx.github.io/)工具的解压功能来帮助我们将文件恢复到压缩前的原始状态。压缩文件是一个在运行时会先解压再执行程序的可执行文件。压缩文件的主要目的是在保持程序原有行为的同时,减小可执行文件的大小。我们将在本书的第十章,压缩与加密,中详细讨论更多关于打包工具的内容。现在,我们只需使用 UPX 工具并加上-d参数来解压这个文件:
upx -d cha4_2.exe
这将导致文件被恢复到其原始形态:
如果这次使用TrID,我们应该得到不同的结果:
它仍然是一个 Windows 可执行文件,因此我们可以使用 CFF Explorer 来查看更多信息:
在左侧面板中,如果我们选择导入目录,我们应该看到它将使用的导入库文件和 API 函数列表,如下所示:
点击USER32.dll,我们看到程序将使用MessageBoxA API。
使用 bintext (b2b-download.mcafee.com/products/tools/foundstone/bintext303.zip)工具,我们可以看到文件中发现的文本字符串列表:
这些似乎是显著的文本字符串,暗示程序会检查时间并显示各种问候语。它可能会从互联网下载一个文件。它可能对File.txt文件执行某些操作。但所有这些都只是有根据的猜测,这对逆向工程来说是一个很好的练习,因为它帮助我们构建分析中各个方面之间关系的概览:
000000001134 000000402134 0 The system time is: %02d:%02d
000000001158 000000402158 0 Nice Night!
000000001164 000000402164 0 Good Morning
000000001174 000000402174 0 Good Afternoon
000000001184 000000402184 0 Good Evening
000000001198 000000402198 0 https://raw.githubusercontent.com/PacktPublishing/Mastering-Reverse-Engineering/master/ch4/encmsg.bin
000000001200 000000402200 0 File.txt
00000000122C 00000040222C 0 Reversing
文件的哈希值(MD5、SHA1、SHA256)将作为我们分析每个文件的参考。互联网上有很多生成文件哈希的工具。为了生成这个文件的哈希值,我们选择了一个名为 HashMyFiles 的工具。这是一个为 Windows 操作系统编译的工具,并且可以添加到 Windows 资源管理器的右键菜单中:
它可以显示文件的CRC、MD5、SHA1、SHA-256、SHA-512和SHA-384,如下所示:
MD5: 38b55d2148f2b782163a3a92095435af
SHA1: d3bdb435d37f843bf68560025aa77239df7ebb36
CRC: 0bfe57ff
SHA256: 810c0ac30aa69248a41c175813ede941c79f27ddce68a91054a741460246e0ae
SHA512: a870b7b9d6cc4d86799d6db56bc6f8ad811fb6298737e26a52a706b33be6fe7a8993f9acdbe7fe1308f9dbf61aa1dd7a95015bab72b5c6af7b7359850036890e
SHA384: b0425bb66c1d327d7819f13647dc50cf2214bf00e5fb89de63bcb442535860e13516de870cbf07237cf04d739ba6ae72
通常,我们只会使用MD5、SHA1或SHA256。
我们不应忘记通过简单的文件属性检查查看文件的大小和创建时间:
修改日期在文件实际编译时更为相关。创建日期是文件写入或复制到现在目录时的日期。这意味着当文件首次创建时,创建日期和修改日期是相同的。
为了静态分析文件的行为,我们将使用一个叫做 IDA Pro 的反汇编工具。IDA Pro 的免费版本可以在www.hex-rays.com/products/ida/support/download_freeware.shtml找到。但是,如果你能够负担它的付费版本(我们强烈推荐),请务必购买。我们发现付费版的功能和支持的架构要好得多。但对于本书,我们将使用所有不需要购买的工具。
目前已知有两个免费的 IDA Pro 版本。我们已将该工具的备份上传至github.com/PacktPublishing/Mastering-Reverse-Engineering/tree/master/tools/Disassembler%20Tools。由于我们处理的是一个 32 位的 Windows 可执行文件,请选择 32 位版本。
安装完 IDA Pro 后,打开其中的 cha4_2.exe。等待自动分析完成,它将把反汇编重定向到 WinMain 函数:
向下滚动将显示我们在第三章《低级语言》中学到的更多反汇编代码。对于死链行为,我们通常寻找调用 API 的指令。我们遇到的第一个 API 调用是 GetSystemTime:
按照代码的顺序,我们依次遇到了以下 API 函数:
-
vsprintf_s -
MessageBoxA -
InternetOpenA -
InternetConnectW -
InternetOpenUrlA -
memset -
InternetReadFile -
InternetCloseHandle -
strcpy_s -
CreateFileA -
WriteFile -
CloseHandle -
RegCreateKeyExW -
RegSetValueExA
利用我们在第三章《低级语言》中学到的知识,试着跟踪代码并推测文件在不执行的情况下会做什么。为了帮助你,这里是程序的预期行为:
-
根据当前系统时间显示不同的消息。消息可能为以下之一:
-
Good Morning -
Good Afternoon -
Good Evening -
`Nice Night`
-
-
从互联网读取文件内容,解密内容,并将其保存到名为
File.txt的文件中。 -
创建一个注册表键
HKEY_CURRENT_USER\Software\Packt,并将相同的解密数据存储在Reversing注册表值中。
对于初学者来说,这可能需要较长时间,但通过持续的练习,分析速度会逐渐加快。
摘要
静态分析和动态分析这两种方法都有各自提取信息的手段,并且在正确分析文件时都是必要的。在进行动态分析之前,建议先从静态分析开始。我们坚持从我们获得的信息中生成分析报告的目标。分析师不仅仅局限于使用这里列出的工具和资源来进行分析——互联网中的任何信息都是有用的,但通过自己的分析来验证这些信息将作为证据。提取文件中的所有项目,如显著的文本字符串、导入的 API 函数、系统变化、代码流程以及可能的行为块都很重要,因为这些在构建文件概述时可能会有帮助。
静态分析的结果总结了动态分析所需的准备工作和资源。例如,如果静态分析将文件识别为 Win32 PE 可执行文件,那么就需要准备分析 PE 文件的工具。
作为动态分析的一部分,我们讨论了**虚拟分配空间(VAS)**以及一个程序如何在内存中映射及其库依赖关系。当尝试进一步反向工程时,这些信息非常有用。
我们还介绍了几种可以用于静态和动态分析的方法,并以对一个 32 位 Windows PE 可执行文件的简短练习结束了本章。在下一章中,我们将展示如何在反向工程文件时更多地使用这些工具。
参考资料
本章使用的文件可以从github.com/PacktPublishing/Mastering-Reverse-Engineering下载。
第五章:工具介绍
在之前的章节中,我们使用了一些简单的反向工程工具,例如 PEiD、CFF Explorer、IDA Pro 和 OllyDbg,这些工具帮助我们进行反向工程。本章将探讨并介绍更多我们可以使用和选择的工具。工具的选择取决于所需的分析。例如,如果一个文件被识别为 ELF 文件类型,我们就需要使用适合分析 Linux 可执行文件的工具。
本章涵盖了 Windows 和 Linux 的工具,按静态分析和动态分析分类。市场上有很多可用的工具——不要仅仅局限于本书讨论的工具。
在本章中,您将实现以下学习目标:
-
设置工具
-
理解 Windows 和 Linux 的静态及动态工具
-
理解支持工具
分析环境
在逆向工程中,环境的设置对结果至关重要。我们需要一个沙箱环境,在其中可以分析和操作文件,而不必担心会破坏某些东西。由于 Microsoft Windows 和 Linux 是最流行的操作系统,我们将在虚拟环境中讨论如何使用这些操作系统。
虚拟机
在第一章中,我们介绍了使用 VirtualBox 作为我们的桌面虚拟化系统。我们选择 VirtualBox 的原因是它是免费的。然而,除了 VirtualBox,选择合适的沙箱软件还需要根据用户的偏好和需求。每种沙箱软件都有其优缺点,因此值得探索市场上提供的各种软件,找出您偏好的软件。以下是一些虚拟化软件的简短列表:
-
VMWare Workstation: 这是一款商业软件,广泛流行。VMWare Workstation 可以从
www.vmware.com下载。 -
VirtualBox: 这是一款免费的开源虚拟化软件。可以从
www.virtualbox.org下载。 -
Qemu(快速仿真器): 这实际上不是虚拟化软件,而是一款仿真器。虚拟化软件利用 CPU 的虚拟化功能,但使用真实的 CPU 资源来实现,而仿真器只是模仿 CPU 及其资源。也就是说,运行在虚拟化环境中的操作系统使用的是实际 CPU,而在仿真环境中运行的操作系统使用的是模仿的 CPU。Qemu 模块可以从 Linux 标准仓库安装。它有 Windows 和 macOS 版本,可以从
www.qemu.org下载。 -
Bochs: 一款仅限于模拟 x86 CPU 架构的仿真器。它作为开源软件发布,通常用于调试小型磁盘镜像的主引导记录(MBR)。详细信息请参见
bochs.sourceforge.net。 -
Microsoft Hyper-V: Microsoft Windows 版本中的虚拟化功能,包括 Windows 10。通过以下菜单激活它:
- Parallels: 一款商业虚拟化程序,主要设计用于在 macOS 主机上运行 Windows。有关此软件的更多信息,请访问
www.parallels.com/。
模拟器的优势在于可以模拟其他 CPU 架构,如 ARM。与虚拟化软件不同,模拟器依赖于裸机的虚拟化管理程序。缺点是可能会有性能上的拖慢,因为每个模拟的指令都需要解释执行。
Windows
推荐在 32 位或 64 位的 Windows 10 系统上进行分析,或使用提供的最新版本。至少,Windows 7 仍然可以使用,因为它轻量且为运行可执行文件提供了稳定的环境。尽可能选择最受欢迎、最广泛使用的 Windows 版本将是最好的选择。选择 XP 等旧版本可能不会非常有用,除非我们要逆向的程序仅为 Windows XP 构建。
在撰写本文时,我们可以通过两种方式获取 Windows 用于分析**:**
-
从
www.microsoft.com/en-us/software-download/windows10下载的安装程序或 ISO 镜像安装 Windows 10。 -
部署用于测试旧版本 Edge 和 Internet Explorer 的 Windows 设备。该设备可以从**
developer.microsoft.com/en-us/microsoft-edge/tools/vms**下载。
这些下载没有安装任何许可证,并且会在短时间内到期。对于前面列表中的第二个选项,在部署完设备后,最好在运行虚拟机之前先拍一个初始快照。恢复到这个初始快照应将过期时间重置为设备部署时的时间。之后也应创建更多快照,包含配置更新和已安装的工具。
Linux
由于 Linux 是开源的,因此可以轻松下载。流行的系统通常是从 Debian 或 Red Hat 系统派生的。但由于大多数为分析开发的工具是基于 Debian 系统构建的,我们选择了 Lubuntu 作为我们的分析环境。
Lubuntu是 Ubuntu 的轻量版**。**
然而,我们不会把基于 Red Hat 的系统从我们的列表中排除。如果一个程序只设计在 Red Hat-based 系统上运行,我们应该在 Red Hat-based 系统上进行动态逆向和调试。如前所述,逆向工程不仅需要适合目标的工具,还需要适合的环境。
Lubuntu 可从 lubuntu.net 下载。但如果您更喜欢使用 Ubuntu,可以从 www.ubuntu.com 下载安装程序。
信息收集工具
确定我们正在处理的内容可以进一步为我们做准备。例如,如果识别出一个文件是 Windows 可执行文件,那么我们可以准备相应的 Windows 可执行文件工具。信息收集工具帮助我们确定文件类型及其属性。收集的信息成为分析配置文件的一部分。这些工具通常被分类为文件类型识别、哈希计算、文本字符串收集和监控工具。
文件类型信息
这些工具收集有关文件的基本信息。收集的数据包括文件名、文件大小、文件类型及特定于文件类型的属性。这些工具的结果使分析人员能够规划如何分析文件:
-
PEiD: 用于识别文件类型、压缩器和编译器的工具。该工具适用于 Windows 系统。虽然不再维护,但仍然非常有用。
-
TrID: 类似于 PEiD 的命令行工具。该工具有 Windows 和 Linux 版本。它可以读取各种文件类型的社区驱动签名数据库。
-
CFF Explorer: 主要用于读取和编辑 PE 格式文件的工具。它在 Windows 系统下运行,并具有诸如列出进程和将进程转储到文件的功能。还可用于重建进程转储。
-
PE Explorer: 用于读取和编辑 PE 文件结构的另一种工具。它还可以解包多种可执行压缩程序,如 UPX、Upack 和 NSPack。PE Explorer 仅在 Windows 系统中运行。
-
Detect-it-Easy (DiE): 可从
github.com/horsicq/Detect-It-Easy下载,DiE 是一个开源工具,使用社区驱动的一组算法签名来识别文件。该工具提供了 Windows 和 Linux 版本。 -
ExifTool: 该工具最初设计用于读取和编辑具有 EXIF 文件格式的图像文件的元数据。后来扩展了对其他文件格式的支持,包括 PE 文件。ExifTool 可在 Windows 和 Linux 上使用,并可从
sno.phy.queensu.ca/~phil/exiftool/下载。
哈希识别
信息收集还包括通过其哈希标识文件。哈希不仅有助于验证传输的文件,还常被用作文件分析配置文件的唯一标识:
-
Quickhash: 这是一个开源工具,可在 Windows、Linux 和 macOS 上生成任何文件的 MD5、SHA1、SHA256 和 SHA512。可从
quickhash-gui.org/下载。 -
HashTab: 该工具在 Windows 系统中运行,并可以集成为文件属性信息的选项卡。它可以计算 MD5、SHA1 和其他几种哈希算法。
-
7-zip: 这个工具实际上是一个文件归档工具,但它有一个扩展工具,可以启用计算文件的 MD5、SHA1、SHA256 等哈希值。
字符串
文本字符串收集工具主要用于快速识别程序可能使用的函数或消息。并非所有文本字符串都一定会被程序使用。程序流程仍然依赖于程序中设置的条件。然而,文件中的字符串位置可以作为分析人员可以追踪的标记:
-
SysInternals Suite's strings: 这是一个 Windows 下的命令行工具,用于显示任何类型文件中的文本字符串列表。
-
BinText: 这是一个基于 GUI 的 Windows 工具,可以显示任何给定文件的 ASCII 和 Unicode 文本字符串。
监控工具
无需手动深入挖掘程序的算法,只需运行程序即可获得大量关于其行为的信息。监控工具通常通过在常见或特定的系统库函数中放置传感器来工作,然后记录使用的参数。使用监控工具是快速生成程序初步行为分析的方式:
-
SysInternals Suite's Procmon 或 Process Monitor: 仅在 Windows 上运行,这是一个实时监控工具,用于监控进程、线程、文件系统和注册表事件。它可以从
docs.microsoft.com/en-us/sysinternals/downloads/procmon下载,并且是 SysInternals Suite 套件的一部分。 -
API Monitor: 这个强大的工具通过监控程序运行时的 API 调用来帮助逆向工程。分析人员需要设置工具需要挂钩的 API。一旦 API 被挂钩,所有使用该 API 的用户模式进程都会被记录。API Monitor 可以从
www.rohitab.com/apimonitor下载。 -
CaptureBAT: 除了 Process Monitor 的功能外,这个命令行工具还能够监控网络流量。
默认命令行工具
操作系统自带了一些有用的工具,这些工具在没有第三方工具的情况下非常有用:
-
strings: 这是一个 Linux 命令,用于列出给定文件中找到的字符串。
-
md5sum: 这是一个 Linux 命令,用于计算给定文件的 MD5 哈希值。
-
file: 这是在 Linux 中用于识别文件的命令行工具。它使用 libmagic 库。
反汇编器
反汇编器是用于查看从高级语言或相同低级语言编译的程序低级代码的工具。作为分析的一部分,死列和识别代码块有助于构建程序的行为。然后,可以更容易地识别只需要彻底调试的代码块,而不需要运行整个程序代码:
-
IDA Pro: 这是软件安全行业常用的工具,用于反汇编基于 x86 和 ARM 架构的各种低级语言。它拥有广泛的功能列表,能够生成代码的图形化流程,显示代码块和分支。它还支持脚本编写,可以用来解析代码并将其反汇编成更有意义的信息。IDA Pro 有一个扩展插件,名为 Hex-Rays,能够将汇编代码识别为等效的 C 源代码或语法。IDA Pro 的免费版本可以从**
www.hex-rays.com/products/ida/support/download_freeware.shtml**下载。 -
Radare: 适用于 Windows、Linux 和 macOS 的开源工具,可以显示给定程序的反汇编结果。它有一个命令行界面视图,但也有现成的插件可以通过计算机的浏览器显示它。Radare 的源代码可以从**
github.com/radare/radare2下载并自行构建。有关如何安装二进制文件的信息可以在其网站上找到,网址为rada.re**。 -
Capstone: 这是一个开源的反汇编和反编译引擎。许多反汇编和反编译工具,如 Snowman,都使用该引擎。关于此工具的信息可以在**
www.capstone-engine.org/**找到。 -
Hopper: 一款适用于 Linux 和 macOS 操作系统的反汇编工具。它的界面与 IDA Pro 相似,且能够使用 GDB 进行调试。
-
BEYE: 也被称为 Binary EYE,这是一款十六进制查看和编辑工具,新增了反汇编视图模式。BEYE 适用于 Windows 和 Linux 系统,可以从**
sourceforge.net/projects/beye/**下载。 -
HIEW: 也叫 Hacker's View,类似于 BEYE,但对 PE 文件有更好的信息输出。HIEW 的付费版本支持更多的文件类型和机器架构。
调试器
当使用调试工具时,这意味着我们处于分析的代码跟踪阶段。调试器用于逐步执行程序应该执行的每条指令。在调试过程中,可以识别出内存、磁盘、网络和设备中的实际交互和变化:
-
x86dbg: 这是一款 Windows 用户模式下的调试器。它是开源的,能够调试 32 位和 64 位程序,且可以接受用户编写的插件。源代码可以从**
github.com/x64dbg下载,构建版本可以从x64dbg.com**下载。 -
IDA Pro: 付费版本的 IDA Pro 能够使用相同的反汇编界面进行调试。当你想查看解密代码的图形视图时,它非常有用。
-
OllyDebug: 一款流行的 Windows 调试器,由于其可移植性和丰富的功能而广受欢迎。它可以容纳用户编写的插件,增加例如解压加载的可执行压缩文件(通过到达原始入口点)和内存转储等功能。Ollydebug 可以从
www.ollydbg.de/下载。 -
Immunity Debugger: 这个程序的界面看起来像是 OllyDebug 的一个高度改进版本。它支持 Python 和其他工具的插件。Immunity Debugger 可以从 Immunity, Inc. 的网站
www.immunityinc.com/products/debugger/下载。旧版本可以在github.com/kbandla/ImmunityDebugger/找到。 -
Windbg: 由微软开发的调试器。界面相当简单,但可以配置为显示逆向工程师需要的各种信息。它能够被设置为远程调试设备驱动程序、内核级软件,甚至整个 Microsoft 操作系统。
-
GDB: 也被称为 GNU 调试器,GDB 最初是为 Linux 和其他一些操作系统开发的调试器。它不仅能够调试低级语言,还能用于调试 C、C++ 和 Java 等高级语言。GDB 也可以在 Windows 上使用。GDB 使用命令行界面,但也有现成的 GUI 程序可以使用 GDB 提供更为详细的信息展示。
-
Radare: Radare 也有一个随附的调试器。它还可以通过远程使用 GDB 进行远程调试。其界面基于命令行,但也具有集成的可视化视图。其开发者还通过浏览器创建了一个更好的可视化界面。基本上,相较于 GDB,Radare 更受青睐。它主要为 Linux 构建,但也提供 Windows 和 macOS 的编译二进制文件。
反编译器
反汇编器用于显示编译后高级程序的低级代码,而反编译器则尝试显示程序的高级源代码。这些工具通过识别与高级程序中对应语法匹配的低级代码块来工作。预计这些工具无法展示原始程序的源代码样式,但尽管如此,它们通过提供更好的伪代码视图,帮助加快分析过程。
-
Snowman: 这是一个 C 和 C++ 反编译器。它可以作为独立工具运行,也可以作为 IDA Pro 插件运行。源代码可以在
github.com/yegord/snowman找到,已编译的二进制文件可以从derevenets.com/下载。它适用于 Windows 和 Linux。 -
Hex-Rays: 这也是一个 C 和 C++ 反编译器,并作为 IDA Pro 的插件运行。它作为 IDA Pro 的一部分进行商业销售。用户应该期待它能提供比 Snowman 更好的反编译输出。
-
**dotPeek: **这是 Jetbrains 提供的一款免费的 .NET 反编译器。可以从
www.jetbrains.com/decompiler/下载。 -
iLSpy: 这是一个开源的 .NET 反编译器。源代码和预编译的二进制文件可以在
github.com/icsharpcode/ILSpy找到。
网络工具
以下是用于监控网络的工具列表:
-
tcpdump: 这是一个基于 Linux 的工具,用于捕获网络流量。可以从默认的仓库中安装。
-
**Wireshark: **这款工具能够监控网络流量。进出网络流量,包括数据包信息和数据,都会实时记录。Wireshark 原名 Ethereal,支持 Windows、Linux 和 macOS 系统,可以从
www.wireshark.org/下载。 -
**mitmproxy: **也称为中间人代理。顾名思义,它作为代理设置,从而能够控制和监控网络流量,在数据被发送到外部或内部程序接收之前。
-
inetsim: 本质上,这个工具模拟网络和互联网连接,从而捕获程序外部发送的任何网络流量。它非常适用于分析恶意软件,防止其向外发送数据,同时了解它连接到哪里,以及它试图发送哪些数据。
编辑工具
有时我们需要修改程序的内容以使其正常工作,或者验证代码的行为。修改文件中的数据也可能改变代码流,其中可能会发生条件指令。更改指令还可以绕过反调试技巧:
-
HxD Hex Editor: 这是一个 Windows 二进制文件查看器和编辑器。你可以使用它查看文件的二进制内容。
-
Bless: 这是一个 Linux 二进制文件查看器和编辑器。
-
Notepad++: 这是一个 Windows 文本编辑器,但也可以读取二进制文件,尽管用十六进制数字读取二进制文件需要一个十六进制编辑插件。尽管如此,由于其支持的语言种类丰富,包括 Visual Basic 和 JavaScript,它仍然适用于阅读和分析脚本。
-
BEYE: 一个有用的工具,可以查看和编辑任何类型的文件。BEYE 支持 Windows 和 Linux 系统。
-
**HIEW: **这款软件的价值所在是它能够通过汇编语言实时加密。
攻击工具
有时我们需要自己构造数据包,让程序认为它正在接收来自网络的实时数据。尽管这些工具主要用于生成利用的网络数据包进行渗透测试,但它们也可以用于逆向工程:
-
Metasploit (
www.metasploit.com/): 这是一个包含脚本的框架,能够生成利用包并发送到目标进行渗透测试。脚本是模块化的,用户可以开发自己的脚本。 -
ExploitPack (
exploitpack.com/):它的概念与 Metasploit 相同,但由不同的研究小组维护。
自动化工具
有时,我们必须开发自己的程序进行分析。例如,如果程序包含解密算法,我们可以开发一个独立的程序,运行相同的算法,这可能适用于具有相同解密算法的类似程序。如果我们想识别我们正在分析的文件的变种,我们可以通过以下之一来自动识别进入的文件:
-
Python: 这种脚本语言因其在多个平台上的可用性而受到欢迎。它在 Linux 操作系统中是预装的;Windows 的编译二进制文件可以从
www.python.org/下载。 -
Yara: 由 VirusTotal 的开发者提供的工具和语言。它能够搜索文件内容中的一组二进制或文本特征。其最常见的应用是搜索受损系统中的恶意软件残留。
-
Visual Studio: 微软的编程和构建程序软件。当反编译程序需要进行图形化调试时,逆向工程师可以使用它。例如,我们可以使用 Visual Studio 调试反编译后的 C# 程序,而不是尝试理解每个反汇编 C# 代码的 p-code。
软件法医工具
逆向工程包括分析程序执行后的行为。这涉及从内存和磁盘镜像中收集和确定对象与事件。使用这些工具,我们可以分析操作系统的挂起状态,并分析程序在运行内存中仍然在执行的过程。
这里列出了可以下载的不同法医软件:
-
Digital Forensics Framework (
github.com/arxsys/dff) -
开放计算机法医架构
https://github.com/DNPA/OcfaLib
https://github.com/DNPA/OcfaModules
https://github.com/DNPA/OcfaDocs
https://github.com/DNPA/OcfaJavaLib
-
CAINE (
www.caine-live.net/) -
X-Ways Forensics Disk Tools (
www.x-ways.net/forensics/) -
SleuthKit (
www.sleuthkit.org/) -
LibForensics (
code.google.com/archive/p/libforensics/) -
Volatility (
github.com/volatilityfoundation):
在恶意软件分析中,Volatility 是其中一个非常流行的开源软件。它能够读取虚拟机的挂起状态。此类工具的优势在于,像 rootkit 等恶意软件,通常会试图隐藏自己免受用户领域的监视,但可以通过内存取证工具进行提取。
-
BulkExtractor (
downloads.digitalcorpora.org/downloads/bulk_extractor/) -
PlainSight (
www.plainsight.info/index.html) -
Helix3 (
www.e-fense.com/products.php) -
Xplico (
www.xplico.org/)
自动化动态分析
这些工具用于通过在封闭沙箱中运行程序来自动收集信息。
-
Cuckoo: 这是一款使用 Python 编写的软件,部署在基于 Debian 的操作系统上。通常,Cuckoo 安装在宿主的 Ubuntu 系统中,并将文件发送到 VMWare 或 VirtualBox 沙箱客户端进行分析。它的发展由社区驱动,因此有许多开源插件可供下载。
-
ThreatAnalyzer: 商业化销售的 ThreatAnalyzer,之前称为 CWSandbox,在反病毒社区中非常流行,因其能够分析恶意软件并返回非常有用的信息。由于用户可以开发自己的规则,ThreatAnalyzer 作为一个后端系统,可以用来确定提交的文件是否包含恶意行为。
-
Joe Sandbox: 这是另一款商业工具,它展示了提交的程序在执行过程中所执行的活动的有意义信息。
-
Buster Sandbox Analyzer (BSA): BSA 的设置与前面提到的三个工具不同。它不需要客户端沙箱,而是安装在沙箱环境中。该工具的概念是分配磁盘空间供程序运行。运行后,所有在此空间中发生的事件都会被记录并在之后恢复。尽管如此,仍然建议在封闭环境中使用 BSA。
-
Regshot: 这是一个用于捕获磁盘和注册表快照的工具。运行程序后,用户可以拍摄第二次快照。比较这两次快照的差异,从而显示系统中所做的更改。Regshot 应在封闭环境中运行。
在线服务网站
目前已有一些在线服务可以帮助我们进行反向分析。
-
VirusTotal: 该工具提交文件或 URL,并与各种安全程序的检测结果进行交叉参考。结果能够帮助我们判断该文件是否确实为恶意文件。它还可以展示一些文件信息,如 SHA256、MD5、文件大小以及任何指示信息。
-
Malwr: 提交到此处的文件将被提交到后端的 Cuckoo 系统进行分析。
-
Falcon Sandbox: 这也被称为混合分析,是一个由 Payload Security 开发的在线自动化分析系统。Cuckoo 和混合分析的结果揭示了相似的行为,但一个可能比另一个提供更多信息。这可能取决于客户端沙箱的设置。例如,如果沙箱中没有安装.NET 框架,那么提交的.NET 可执行文件将无法按预期运行。
-
whois.domaintools.com: 这是一个显示域名或 URL 的 whois 信息的网站。当你试图确定一个程序正在连接哪个国家或州时,这个工具可能会派上用场。
-
robtex.com: 一个类似 whois 的网站,它显示给定网站的历史信息以及其连接的图形化树状图。
-
debuggex.com: 这是一个在线正则表达式服务,你可以在这里测试你的正则表达式语法。在开发脚本或阅读包含正则表达式的脚本或代码时,这个工具非常有用。
向这些在线网站提交文件或 URL 意味着你会将信息分享给它们。最好在提交之前先征得文件或 URL 所有者的许可。
总结
在本章中,我们列出了一些用于逆向工程的工具。我们尝试根据工具的用途对其进行分类。但正如我们选择使用每一款软件一样,逆向工程师的工具选择取决于它们所包含的功能、用户友好性,以及最重要的,是否具备执行任务所需的功能。我们已经介绍了可以用于静态分析的工具,包括二进制查看器和反汇编工具。我们还列出了可以用于 Windows 和 Linux 的有用调试工具。
从这个列表中,我个人推荐 HIEW、x86dbg、IDA Pro、Snowman 和 iLSpy 用于 Windows 平台下 PE 二进制可执行文件的分析。而在 Linux 平台上,BEYE、Radare、GDB 和 IDA Pro 非常适合分析 ELF 文件。
我们还介绍了一些在线服务,这些服务可以帮助我们获取更多关于从分析中提取的网站的信息。我们还介绍了在处理大量文件时可以自动化分析的系统。此外,我们列出了几款法医工具,这些工具可以用来分析挂起的内存。
如同往常一样,这些工具各有优缺点,最终选择哪一个将取决于用户和所需的分析类型。这些工具各自拥有独特的功能和使用舒适度。在接下来的章节中,我们将使用这些工具的组合。我们可能不会使用所有工具,但会选择那些能够完成分析的工具。
在下一章中,我们将在 Linux 平台上进行逆向工程时,学习更多工具。