精通逆向工程-全-

精通逆向工程(全)

原文:annas-archive.org/md5/4edb9a969a22ca6c6dd931a00e8c6024

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

逆向工程是一种用于分析软件、发现其弱点并加强防御的工具。黑客使用逆向工程作为工具来暴露安全漏洞和可疑的隐私实践。本书将帮助您掌握使用逆向工程的技巧。

本书适合的人群

如果您是安全工程师、分析师或系统程序员,并且想使用逆向工程来改善软件和硬件,那么本书非常适合您。如果您是想探索和学习逆向工程的开发者,本书对您也非常有用。

为了最大化地利用本书

  • 具备一些编程/脚本编写知识会是一个额外的加分项。

  • 了解信息安全和 x86 汇编语言将是一个优势。

  • 使用的操作系统:Windows 和 Linux(版本将取决于 VirtualBox 的要求)

  • 至少四核处理器,4 GB 内存,和 250 GB 硬盘空间。

  • 您可能需要提前从微软下载虚拟机,因为这些文件可能需要一些时间才能下载。请参阅开发者页面:developer.microsoft.com/en-us/microsoft-edge/tools/vms/

下载示例代码文件

您可以从您的帐户下载本书的示例代码文件,网址是www.packt.com。如果您在其他地方购买了本书,可以访问www.packt.com/support,并注册以直接将文件发送到您的电子邮箱。

您可以按照以下步骤下载代码文件:

  1. 登录或注册到www.packt.com

  2. 选择“SUPPORT”选项卡。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

下载文件后,请确保使用以下最新版本解压或提取文件夹:

  • 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-errata,选择您的书籍,点击错误提交表格链接并填写详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法副本,请提供给我们位置地址或网站名称将不胜感激。请通过链接联系我们,链接为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,且系统已连接互联网:

  1. 下载并安装 VirtualBox 并运行它。VirtualBox 有适用于 Windows 和 Linux 的安装程序。下载 Windows 7 32 位镜像,如下所示:

  1. 从微软网站下载的镜像是压缩的,应先解压。在 VirtualBox 中,点击文件|导入虚拟机。您将看到一个对话框,允许我们导入 Windows 7 32 位镜像。

  2. 只需浏览并选择从 ZIP 文件解压出来的 OVA 文件,然后点击“下一步”,如下所示:

  1. 在继续之前,可以更改设置。默认 RAM 设置为 4096 MB。分配更多的 RAM 并设置更多的 CPU 核心时,运行或调试时的性能会有所提升。然而,增加的 RAM 会使得创建镜像快照时消耗相同量的磁盘空间。这意味着,如果我们分配了 1GB 的 RAM,创建一个快照也会消耗至少 1GB 的磁盘空间。我们将 RAM 设置为 2048 MB,这对于我们来说是一个合理的工作量:

  1. 点击“导入”,它应该开始生成虚拟磁盘镜像。一旦完成,我们需要创建第一个快照。建议在关闭状态下创建快照,因为此时消耗的磁盘空间最小。找到“快照”标签,然后点击“创建”。填写快照名称和快照描述字段,然后点击“确定”按钮。这将快速创建您的第一个快照。

在开机状态下,虚拟机中的 RAM 和修改后的磁盘空间总和等于快照所消耗的总磁盘空间。

  1. 点击“启动”以开始运行 Windows 7 镜像。您应该会看到如下窗口。如果系统要求输入密码,默认密码是Passw0rd!

此时,网络设置为 NAT。这意味着虚拟机所需的任何网络资源将使用主机计算机的 IP 地址。虚拟机的 IP 地址来自 VirtualBox 的虚拟 DHCP 服务。记住,虚拟机中的所有网络通信都将使用主机计算机的 IP 地址。

由于我们无法防止某些恶意软件将信息发送到网络,以便将信息返回到我们的虚拟机,因此需要注意的是,一些 ISP 可能会监控常见的恶意软件行为。最好查看与他们的合同,并在必要时做出决定。

我们的大部分逆向工程都涉及恶意软件,截至目前,攻击者通常以 Windows 系统为目标。我们的设置使用的是 Microsoft Windows 7 32 位。你可以自由选择使用其他版本。我们建议安装 32 位版本的 Microsoft Windows,因为它在低级调试时更容易追踪虚拟地址和物理地址。

样本

我们将构建自己的程序来验证和理解低级代码的工作原理。

它的行为和外观。以下是我们将用来构建程序的软件列表:

如果你对恶意软件感兴趣,可以从以下网站获取样本:

总结

逆向工程已经存在多年,并且一直是一项有用的技术,用于了解事物的运作方式。在软件行业中,逆向工程有助于验证和修复代码流和结构。通过这些任务获得的信息可以提高软件、网络基础设施以及人类意识的各个方面的安全性。作为反恶意软件行业的核心技能要求,逆向工程有助于创建检测和修复信息;这些信息也被用来构建保护措施,保障机构的服务器安全。它还被当局和法医专家用来追踪犯罪集团。

有一些基本步骤可以帮助建立逆向工程的信息。一旦分析师获得原作者的批准进行逆向工程,他们可以从静态分析、动态分析开始,然后进行低级分析。接下来是报告软件的概述和细节。

在进行分析时,会使用各种工具,包括静态分析工具、反汇编器、反编译器、调试器和系统监控工具。在对恶意软件进行逆向工程时,最好在一个没有或有限访问网络的环境中使用这些工具,以避免个人用途或工作中的网络被入侵。这可以防止你的基础设施被破坏。恶意软件应该得到妥善处理,我们列出了几种防止意外双击的方式。

然而,恶意软件分析仍然需要互联网,以便进一步了解恶意软件的工作原理及其行为。可能会有一些法律问题,需要你查阅所在国家的法律以及当地互联网服务提供商(ISP)的政策,以确保你不会违反其中的任何规定。

分析实验室设置的核心要求是目标操作系统能够恢复到未修改的状态。

恶意软件样本可以从以下链接获取:github.com/PacktPublishing/Mastering-Reverse-Engineering/tree/master/tools。这些样本将在本书中贯穿使用。

现在我们已经完成了基本设置,让我们开始逆向工程的旅程吧。

第二章:隐藏组件的识别与提取

目前,逆向工程最常见的应用是针对恶意软件。像其他任何软件一样,恶意软件也有其安装过程。不同之处在于它不会请求用户的安装许可。恶意软件甚至不会安装到Program files文件夹中,那里是其他合法应用程序的安装位置。相反,它倾向于将恶意软件文件安装到用户不常访问的文件夹中,从而避免被注意到。然而,有些恶意软件会被注意到,并在几乎所有显眼的文件夹中生成其副本,比如桌面。它的目的是让用户通过意外的双击或好奇心来执行其副本。这就是我们通常所说的恶意软件持久性。

持久性是指恶意软件持续在后台运行。在本章中,我们将指出恶意软件通常用来实现持久性的技术方法。我们还将解释恶意软件文件存储的常见位置。还将展示恶意软件的主要行为及一些能够识别恶意软件如何在系统中安装的工具。理解恶意软件的传播方式将帮助逆向工程师解释攻击者是如何成功入侵系统的。

本章我们将学习以下内容:

  • 操作系统环境基础

  • 典型的恶意软件行为:

    • 恶意软件传递

    • 恶意软件持久性

    • 恶意软件有效载荷

  • 用于识别隐藏组件的工具

技术要求

讨论将使用 Windows 环境。我们将使用在前一章节中创建的虚拟机设置。此外,您还需要下载并安装以下软件:SysInternals 套件(docs.microsoft.com/en-us/sysinternals/downloads/sysinternals-suite)。

操作系统环境

进行逆向工程要求分析人员了解正在逆向的软件运行的环境。软件在操作系统中运行所需的主要部分是内存和文件系统。在 Windows 操作系统中,除了内存和文件系统,微软还引入了注册表系统,实际上这些数据存储在被保护的文件中,称为注册表蜂巢。

文件系统

文件系统是直接将数据存储到物理硬盘驱动器的地方。这些文件系统管理文件和目录在磁盘上的存储方式。各种磁盘文件系统有自己高效读写数据的变体。

有不同的磁盘文件系统,如FATNTFSex2ex3XFSAPFS。Windows 常用的文件系统是 FAT32NTFS。文件系统中存储了关于目录路径和文件的信息,包括文件名、文件大小、日期戳和权限。

以下截图显示了存储在文件系统中的 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 的键名将列出仅运行一次的程序。如果恶意软件继续将其文件路径放置在 RunOnceRunOnceExRunServicesOnce 键下,它可能仍会持续存在。

加载和运行值

以下注册表值,在其相应的注册表项下,将在任何用户登录时触发执行:

  • HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Windows

    • Load = <文件路径>

    • Run = <文件路径>

BootExecute 值

  • HKEY_LOCAL_MACHINE\SYSTEM\ControlSetXXX\Control\Session Manager

    • XXXControlSetXXX 中是一个三位数,通常是 ControlSet001ControlSet002ControlSet003

    • 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 Folders

    • Startup = [启动文件夹路径]
  • HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders

    • Startup = [启动文件夹路径]
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders

    • Common 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 已安装并启用

    • CLSIDHKEY_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文件包含有关自动执行文件的信息。此文件由存储在光盘中的软件安装程序使用,这样当磁盘插入时,它会自动运行安装程序。这一功能被恶意软件滥用,步骤如下:

  1. 将恶意软件文件的副本投放到可移动驱动器中

  2. 随着恶意软件副本的投放,它生成一个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 图标。它还使用具有欺骗性的文件名,如INVOICENew FolderScandalExposePamelaConfidential等。以下截图展示了实际恶意软件模仿已知文档的例子:

注意,突出显示的伪造 PDF 文件实际上是一个应用程序。

有效载荷——其中的恶意内容

攻击者开发恶意软件是有目的的,通常是为了对目标造成伤害,可能是出于仇恨、娱乐、金钱或,可能是政治原因。以下是一些在实际情况中见过的典型恶意软件有效载荷:

  • 为赎金加密文件

  • 删除所有文件

  • 格式化驱动器

  • 完全访问系统和网络

  • 偷取账户和密码

  • 偷取文档、图片和视频

  • 更改特定配置和设置

  • 将计算机转变为代理服务器

  • 安装cryptocoin矿工

  • 持续打开网站——广告或色情网站

  • 安装更多恶意软件

  • 安装广告软件

逆向工程师在报告中列出的一个结论是有效载荷。这决定了恶意软件在安装后对计算机的实际影响。

工具

识别与恶意软件相关的注册表项、已丢弃的文件和正在运行的进程需要工具。我们可以使用现有的工具来提取这些对象。我们应该考虑两种分析事件:恶意软件执行后的分析和恶意软件执行前的分析。由于本章的目标是提取组件,我们将讨论能够帮助我们找到可疑文件的工具。关于我们提取可疑恶意软件后的分析工具将在后续章节中讨论。

当系统已经被攻破时,分析员需要使用可以识别可疑文件的工具。每个可疑文件将进一步分析。首先,我们可以基于持久性来识别它。

  1. 列出所有进程及其相关的文件信息

  2. 从已知的注册表持久性路径列表中,查找包含文件路径的条目

  3. 提取可疑文件

上述步骤可能需要使用微软 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。有了这些工具在手,我们应该能够列出我们怀疑的文件。然而,就像其他任何任务一样,通过实践和经验,我们将能够更快地掌握识别技能。

进一步阅读

第三章:低级语言

任何反向工程师在开始之前需要掌握的主要知识是汇编语言。理解汇编语言就像学习反向工程的 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

有符号数

二进制数可以按有符号或无符号进行构造。对于有符号数或整数,最高有效位决定了数值的符号。这要求二进制数有明确的大小,比如 BYTEWORDDWORDQWORDBYTE 大小为 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. 反转 1 和 0,使得 1 变为 0,0 变为 1,例如,1010b 变为 0101b。这一步叫做 1 的补码。

  2. 将前一步的结果加 1,例如,0101b + 1b = 0110b

要写出 -63 的二进制等价表示,假设它是一个 BYTE,我们只取第 0 位到第 7 位:

  1. 使用前述方法转换为二进制:
63 = 0111111b
  1. 做“1 的补码”如下:
0111111b -> 1000000b
  1. 在前面的结果上加 1,得到“2 的补码”结果:
1000000b + 1 = 1000001b
  1. 由于这是一个负数,所以将最高有效位设置为 1:
11000001b = -63

下面是如何写下负二进制数的十进制表示:

  1. 请注意,符号位是 1,因此为负号:
10111011b
  1. 先做“1 的补码”,然后加 1:
  01000100b
+        1b 
  01000101b
  1. 将结果转换为十进制,并将负号放在最前面,因为这是一个负数:
- 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”访问(EAXEBXECXEDXESPEIPEFLAGS)。64 位模式也是如此,可以通过前缀“R”访问(RAXRBXRCXRDXRSPRIP)。

内存被划分为不同的段,例如代码段、栈段、数据段和其他段。段寄存器用于标识这些段的起始位置,如下所示:

  • 栈段(SS)

  • 代码段(CS)

  • 数据段(DS)

  • 扩展段(ES)

  • F 段(FS)

  • G 段(GS)

当程序加载时,操作系统会将可执行文件映射到内存。可执行文件包含有关数据如何映射到相应段的信息。代码段包含可执行代码。数据段包含数据字节,如常量、字符串和全局变量。栈段被分配用于存储运行时函数变量和其他处理过的数据。扩展段与数据段类似,但此空间通常用于在变量之间移动数据。一些 16 位操作系统,如 DOS,由于每个段只分配 64KB 的内存,因此使用 SS、CS、DS 和 ES。然而,在现代操作系统(32 位及更高系统)中,这四个段被设置在同一内存空间中,而 FS 和 GS 分别指向进程和线程信息。

内存寻址

一段数据的起始位置,即存储在内存中的一系列字节,可以通过其内存地址来定位。存储在内存中的每个字节都有一个内存地址,用来标识它的位置。当用户执行程序时,系统会读取可执行文件,然后将其映射到一个分配的内存地址。可执行文件包含了如何进行映射的信息,确保所有的可执行代码都在代码段内,所有初始化的数据都在数据段内,未初始化的数据则在 BSS 段内。代码段中的代码指令可以通过内存地址访问数据段中的数据,这些地址可以是硬编码的。数据也可以是一个地址列表,指向另一组数据。

字节序

在读取或写入数据到内存时,我们使用寄存器或内存以BYTEWORDDWORD甚至QWORD的形式来处理它们。根据平台或程序,数据可能以小端或大端格式进行读取。

在小端格式中,当数据块被读取为DWORD时,它会被反转。我们以以下数据为例:

AA BB CC DD

当文件或内存中的数据以小端格式呈现时,它将在DWORD值中读取为DDCCBBAAh。这种字节序在 Windows 应用程序中很常见。

在大端系统中,相同的数据块将被读取为AABBCCDDh。使用大端格式的优势在于读取流式数据时,例如文件、串行数据和网络流。

使用小端格式读取的优势在于,读取的地址保持固定,无论它是作为BYTEWORD还是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 和操作数值 80000000hMOV 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, 0xaabbccdd0xaabbccdd 的值放入 eax 寄存器。

mov eax, edxedx 寄存器中的数据值放入 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 ptrword ptrdword 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] 开始,这是一个包含两个整数 xystruct Test 数组。for 循环语句取 y 的值和 ystruct test 元素中的指针地址。

假设测试数组的基址在 EBX 中,for-loop计数器iECX中,整数为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)中,OFSFCF标志会受到影响。我们来看一些指令的使用示例。

add eax, ecxecx寄存器中的值加到eax中的值,eax中存储的是eaxecx相加的结果。

让我们通过以下示例来看它是如何设置OFSFCF标志的:

mov ecx, 0x0fffffff
mov ebx, 0x0fffffff
add ecx, ebx

寄存器是 DWORD 类型。ecxebx寄存器被设置为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 = 0SF = 1OF = 1。假设ecxebx都是无符号数,CF标志不会被设置。假设ecxebx都是有符号数且都是正数,则OF标志会被设置。由于最高有效位变为1SF标志也被设置。

那么,两个负数相加会怎么样呢?我们来考虑以下示例:

mov ecx, 0x80000000
mov ebx, 0x80000000
add ecx, ebx

基本上,我们正在将ecxebx相加,这两个寄存器的值为0x80000000(-2,147,483,648),结果变为零(0)。CF = 1SF = 0OF = 1。由于结果的最高有效位(MSB)是 0,因此SF标志没有被设置。将ecxebx的最高有效位相加肯定会超出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:AXEDX:EAX(长整型或 QWORD)中:

mov eax, 0x80000000
mov ecx, 2
mul ecx

存储在 eax 中的乘积为零 (0),而 edx 现在包含 0x00000001SF =0CF = 1OF = 1

对于除法,被除数放入 AXDX:AXEDX:EAX 中,除法操作后,商放入 ALAXEAX 中。余数存储在 AHDXEDX 中。

其他有符号操作

NEG

此操作执行二进制补码操作。

以以下示例为例:NEG EAXNEG dword ptr [00403000]

如果 EAX01h,它将变为 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 等于 10111010bBAh)且 AH 等于 11101101bEDh),则 AL 变为 10101000bA8h)。

OR

该操作如果两个比特都是 0,则将比特设置为 0,否则将比特设置为 1

以以下为例:OR AL, AH

如果 AL 等于 10111010bBAh)且 AH 等于 11101100bECh),则 AL 变为 11111110bFEh)。

XOR

该操作如果两个比特相等,则将比特设置为 0,否则设置为 1

以以下为例:XOR EAX, EAX

对相同的值进行 XOR 操作将结果变为 0。因此,EAX 变为 0

XOR AH, AL

如果 AH 为 100010b22h)且 AL 为 1101011b6Bh),则 AH 变为 1001001b49h)。

SHL/SAL

该操作将位向左移动。

以以下为例:SHL AL, 3

如果 AL11011101bDDh),将其向左移动 3 位后,AL 等于 11101000bE8h)。

SHR/SAR

该操作将位向右移动。

以以下为例:SHR AL, 3

如果 AL11011101bDDh),将其向右移动 3 位后,AL 等于 011011b1Bh)。

ROL

该操作将位向左旋转。

以以下为例:ROL AL, 3

如果 AL11011101bDDh),将其向左旋转 3 位后,AL 等于 11101110bEEh)。

ROR

该操作将位向右旋转。

以以下为例:ROR AL, 3

如果 AL11011101bDDh),将其向右旋转 3 位后,AL 等于 10111011bBBh)。

控制流

程序的美妙之处在于,我们可以根据条件和状态执行多种不同的行为。例如,我们可以让某个任务重复执行,直到计数器达到定义的最大值。在 C 语言编程中,程序的流控制由诸如 if-then-elsefor-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 寄存器中:

栈由两个常见指令控制:PUSHPOP

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 位构建):EAXECXEDXEBXEBPESPEBPESIEDIPUSHA适用于 16 位操作数,而PUSHAD适用于 32 位操作数。不过,二者可能是同义的,取决于当前的操作数大小。

POPA/POPAD 都将所有通用寄存器从栈中弹出,并按与PUSHA/PUSHAD存储顺序的相反顺序恢复。

PUSHFEFLAGS压入栈中。

POPFEFLAGS从栈中弹出。

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

NASMNetwide 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的声明。预期声明的函数用于调用printfExitProcess API 函数,这些函数将在idata部分讨论。

该程序中有三个内建部分:datacodeidata部分。这里的部分名称被标记为.data.code.idata。每个部分的权限也被指示为可读可写可执行data部分是放置整数和文本字符串的地方,并使用定义字节(db)指令列出。code部分是执行指令代码的地方。idata部分是导入的 API 函数声明的地方。

下一行,我们看到数据部分被定义为可写部分:

section '.data' data readable writeable

程序的.data部分包含两个常量变量,messagemsgformat。这两个文本字符串是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]

在数据部分,地址messagemsgformat作为调用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 函数,kernel32ExitProcessmsvcrtprintf

 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 之后

我们遇到了对 printfExitProcess 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_32WININETURLMONNETAPI32 是包含与网络和互联网通信相关的函数的库。

常见 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

|

runcontinue 使调试器连续执行指令,直到程序终止、遇到错误,或遇到手动设置的断点。

设置 断点 是让调试器中断已设置为自由运行的代码的一种方法。例如,如果我在以下代码的地址 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 有一个内置的命令,称为 filefile 基于 libmagic 库,能够识别各种文件格式的文件类型:

MASTIFF

MASTIFF 是一个静态分析框架。它可以在 Linux 和 Mac 上运行。作为一个框架,静态分析基于 MASTIFF 作者和社区提供的插件。

这些插件包括以下内容:

trid:这是一个用于识别文件类型的工具。

ssdeepssdeep 是一个模糊哈希计算器。模糊哈希,或称为上下文触发的分段哈希(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模块能够显示导入的库和函数,如下所示:

HxDHIEW是本章中使用的流行二进制编辑器;HxD更为流行,是免费的,可以轻松地用于对文件进行二进制编辑。更多信息和下载链接可以在mh-nexus.de/en/hxd/找到。如果你尝试使用HxD,你会看到类似于此屏幕截图的内容:

另一个有用的十六进制编辑工具是HIEW(黑客视图)。演示版和免费版能够解析PE头。该工具还可以显示导出和导入的 API 函数:

静态导入的模块、库和函数是我们可以预期程序访问的线索。例如,考虑到如果PE文件导入了KERNEL32.DLL库,那么我们应该预期文件包含核心 API,这些 API 可能会访问文件、进程和线程,或者动态加载其他库并导入函数。以下是我们应当注意的一些常见库:

  • ADVAPI32.DLL:此库包含将访问注册表的函数。

  • MSVCRXX.DLL(其中 XX 是版本号。示例包括MSVCRT.DLLMSVCR80.DLL)——此文件包含 Microsoft Visual C 运行时函数。这直接告诉我们该程序是使用 Visual C 编译的。

  • WININET.DLL:此库包含访问互联网的函数。

  • USER32.DLL:此库包含与显示在显示器上的任何内容相关的窗口控制函数,如对话框、显示消息框和定位窗口框等。

  • NTDLL.DLL:此库包含直接与内核系统交互的原生函数。KERNEL32.DLL和像USER32.DLLWININET.DLLADVAPI32.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(如 mallocVirtualAlloc)进行:

上面的截图是 jbtest.exe 刚刚在 32 位 Windows 中加载时的映射视图。这里是一个更具描述性的标准布局,展示了程序在 Windows 中虚拟分配空间中的结构:

进程和线程监控

监控进程和线程,尤其是那些由我们分析的文件创建的线程,告诉我们比表面上看起来的更多行为。一个进程可以创建多个线程,这意味着它可能在同时执行多个行为。一个创建的进程意味着一个新程序刚刚被执行。

在 Windows 中,进程的终止、创建和打开可以通过第三方工具(如 Process Monitor)进行监控。尽管有内置的工具,如任务管理器,能够显示进程信息,但一些第三方工具可以提供更详细的关于进程及其线程的信息。

网络流量

服务器和客户端计算机之间传输的数据只有在动态分析过程中才能看到。在传输过程中捕获的数据包将帮助分析员了解程序向服务器发送了什么数据,以及服务器如何响应接收到的任何数据。

流行的工具,如 Wireshark 和 Fiddler,用于捕获数据包并将其存储为 pcap 文件。在 Linux 中,tcpdump 工具通常用于执行相同的操作。

监控系统变化

对于 Windows,我们需要监控三个方面:内存、磁盘和注册表。文件监控工具会监视创建、修改或删除的文件和目录。另一方面,注册表监控工具会监视创建、更新或删除的注册表键、值和数据。我们可以使用诸如 FileMonRegMon 的工具来完成这项工作。

执行后的差异

比较在执行文件之前和之后拍摄的快照之间的差异,能够显示所有系统变化。这种分析方法无法识别发生在两者之间的任何事件。它对于找出软件安装程序如何安装程序非常有用。因此,差异结果在手动卸载软件时尤其有用。这里使用的工具是 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 资源管理器的右键菜单中:

它可以显示文件的CRCMD5SHA1SHA-256SHA-512SHA-384,如下所示:

MD5: 38b55d2148f2b782163a3a92095435af
SHA1: d3bdb435d37f843bf68560025aa77239df7ebb36
CRC: 0bfe57ff
SHA256: 810c0ac30aa69248a41c175813ede941c79f27ddce68a91054a741460246e0ae
SHA512: a870b7b9d6cc4d86799d6db56bc6f8ad811fb6298737e26a52a706b33be6fe7a8993f9acdbe7fe1308f9dbf61aa1dd7a95015bab72b5c6af7b7359850036890e
SHA384: b0425bb66c1d327d7819f13647dc50cf2214bf00e5fb89de63bcb442535860e13516de870cbf07237cf04d739ba6ae72

通常,我们只会使用MD5SHA1SHA256

我们不应忘记通过简单的文件属性检查查看文件的大小和创建时间:

修改日期在文件实际编译时更为相关。创建日期是文件写入或复制到现在目录时的日期。这意味着当文件首次创建时,创建日期和修改日期是相同的。

为了静态分析文件的行为,我们将使用一个叫做 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 函数:

  1. vsprintf_s

  2. MessageBoxA

  3. InternetOpenA

  4. InternetConnectW

  5. InternetOpenUrlA

  6. memset

  7. InternetReadFile

  8. InternetCloseHandle

  9. strcpy_s

  10. CreateFileA

  11. WriteFile

  12. CloseHandle

  13. RegCreateKeyExW

  14. RegSetValueExA

利用我们在第三章《低级语言》中学到的知识,试着跟踪代码并推测文件在不执行的情况下会做什么。为了帮助你,这里是程序的预期行为:

  1. 根据当前系统时间显示不同的消息。消息可能为以下之一:

    • Good Morning

    • Good Afternoon

    • Good Evening

    • `Nice Night`

  2. 从互联网读取文件内容,解密内容,并将其保存到名为 File.txt 的文件中。

  3. 创建一个注册表键 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 用于分析

这些下载没有安装任何许可证,并且会在短时间内到期。对于前面列表中的第二个选项,在部署完设备后,最好在运行虚拟机之前拍一个初始快照。恢复到这个初始快照应将过期时间重置为设备部署时的时间。之后也应创建更多快照,包含配置更新和已安装的工具。

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。

软件法医工具

逆向工程包括分析程序执行后的行为。这涉及从内存和磁盘镜像中收集和确定对象与事件。使用这些工具,我们可以分析操作系统的挂起状态,并分析程序在运行内存中仍然在执行的过程。

这里列出了可以下载的不同法医软件:

github.com/DNPA/OcfaArch

                    https://github.com/DNPA/OcfaLib

                    https://github.com/DNPA/OcfaModules

                    https://github.com/DNPA/OcfaDocs

                    https://github.com/DNPA/OcfaJavaLib

在恶意软件分析中,Volatility 是其中一个非常流行的开源软件。它能够读取虚拟机的挂起状态。此类工具的优势在于,像 rootkit 等恶意软件,通常会试图隐藏自己免受用户领域的监视,但可以通过内存取证工具进行提取。

自动化动态分析

这些工具用于通过在封闭沙箱中运行程序来自动收集信息。

  • 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 平台上进行逆向工程时,学习更多工具。

第六章:在 Linux 平台上的逆向工程

很多我们的工具在 Linux 中都运行得很好。在上一章中,我们介绍了一些已经默认内置的 Linux 命令行工具。Linux 也已经安装了 Python 脚本语言。在本章中,我们将讨论一个用于分析 Linux 文件和托管 Windows 沙箱客户端的良好设置。

我们将通过探索逆向工具学习如何逆向一个 ELF 文件。我们将通过设置一个 Windows 沙箱客户端、在其中运行程序并监控来自沙箱的网络流量来结束本章。

并不是所有人都喜欢使用 Linux。Linux 是一个开源系统。它是一项将伴随我们的技术。作为逆向工程师,任何技术都不应该成为障碍,学会这项技术永远不会太晚。关于 Linux 系统的基础知识可以轻松地在互联网上找到。本章尽可能详细地描述了安装和执行所需内容的步骤,确保你能跟上。

本章将讨论以下内容:

  • 理解 Linux 可执行文件

  • 逆向一个 ELF 文件

  • Linux 中的虚拟化 – 在 Linux 主机下分析 Windows 可执行文件

  • 网络流量监控

设置

本章讨论了 Linux 逆向工程,因此我们需要进行 Linux 设置。对于逆向工程,建议在裸机上部署 Linux。由于大多数已开发的分析工具基于 Debian,因此我们使用 32 位 Ubuntu Desktop。 我选择 Ubuntu 是因为它有一个强大的社区。正因如此,大多数问题可能已经有了解决方案,或者解决方案很容易获得。

为什么要在裸机上构建我们的设置?它是我们沙箱客户端的更好主机,尤其是在监控网络流量时。它还在正确处理 Windows 恶意软件方面具有优势,可以防止由于恶意软件执行而导致的安全问题。

你可以访问 www.ubuntu.com/ 获取 Ubuntu 安装程序的 ISO 文件。该网站包含了安装指南。如需更多帮助,可以访问社区论坛 ubuntuforums.org/

“裸机”是指直接在硬件上执行代码的计算机。它通常用来指代硬件,而不是虚拟机。

Linux 可执行文件 – hello world

首先,让我们创建一个 hello world 程序。在此之前,我们需要确保已经安装了构建该程序所需的工具。打开一个终端(终端是 Linux 中类似于 Windows 命令提示符的工具),并输入以下命令。这可能需要你输入超级用户密码:

sudo apt install gcc

C 程序编译器,*gcc, *通常预装在 Linux 中。

打开任何文本编辑器,输入以下代码,并将其保存为 *hello.c*

#include <stdio.h>
void main(void)
{
    printf ("hello world!\n");
}

你可以通过在终端中运行 vi 来使用 vim 作为文本编辑器。

要编译并运行程序,请使用以下命令:

hello 文件是我们用于显示控制台消息的 Linux 可执行文件。

现在,开始对这个程序进行逆向分析。

dlroW olleH

作为一种良好实践,逆向分析程序的过程应从正确的识别开始。让我们从 file 命令开始:

它是一个 32 位 ELF 文件类型。ELF 文件是 Linux 平台的原生可执行文件。

下一站,让我们快速查看文本字符串,使用 strings 命令:

这个命令将产生类似以下的输出:

/lib/ld-linux.so.2
libc.so.6
_IO_stdin_used
puts
__libc_start_main
__gmon_start__
GLIBC_2.0
PTRh
UWVS
t$,U
[^_]
hello world!
;*2$"(
GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609
crtstuff.c
__JCR_LIST__
deregister_tm_clones
__do_global_dtors_aux
completed.7209
__do_global_dtors_aux_fini_array_entry
frame_dummy
__frame_dummy_init_array_entry
hello.c
__FRAME_END__
__JCR_END__
__init_array_end
_DYNAMIC
__init_array_start
__GNU_EH_FRAME_HDR
_GLOBAL_OFFSET_TABLE_
__libc_csu_fini
_ITM_deregisterTMCloneTable
__x86.get_pc_thunk.bx
_edata
__data_start
puts@@GLIBC_2.0
__gmon_start__
__dso_handle
_IO_stdin_used
__libc_start_main@@GLIBC_2.0
__libc_csu_init
_fp_hw
__bss_start
main
_Jv_RegisterClasses
__TMC_END__
_ITM_registerTMCloneTable
.symtab
.strtab
.shstrtab
.interp
.note.ABI-tag
.note.gnu.build-id
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rel.dyn
.rel.plt
.init
.plt.got
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.jcr
.dynamic
.got.plt
.data
.bss
.comment

字符串按文件开头的顺序列出。列表的前两部分包含了我们的消息和编译器信息。前两行还显示了程序使用的库:

/lib/ld-linux.so.2
libc.so.6

列表的最后部分包含了文件各个区段的名称。我们只知道一些文本片段,它们被放入我们的 C 代码中。其余的则是由编译器本身放入的,作为其准备并结束代码优雅执行的一部分。

在 Linux 中,反汇编只是一个命令行的事情。使用 objdump 命令的 -d 参数,我们应该能够显示可执行代码的反汇编。你可能需要通过以下命令将输出结果写入文件:

objdump -d hello > disassembly.asm

输出文件 disassembly.asm 应包含以下代码:

如果你注意到,反汇编语法与我们学过的 Intel 汇编语言格式不同。我们在这里看到的是 AT&T 反汇编语法。要获取 Intel 语法,我们需要使用 -M intel 参数,如下所示:

objdump -M intel -d hello > disassembly.asm

输出应该给我们以下的反汇编结果:

结果显示了每个函数的反汇编代码。总的来说,从可执行部分有 15 个函数:

Disassembly of section .init:
080482a8 <_init>:

Disassembly of section .plt:
080482d0 <puts@plt-0x10>:
080482e0 <puts@plt>:
080482f0 <__libc_start_main@plt>:

Disassembly of section .plt.got:
08048300 <.plt.got>:

Disassembly of section .text:
08048310 <_start>:
08048340 <__x86.get_pc_thunk.bx>:
08048350 <deregister_tm_clones>:
08048380 <register_tm_clones>:
080483c0 <__do_global_dtors_aux>:
080483e0 <frame_dummy>:
0804840b <main>:
08048440 <__libc_csu_init>:
080484a0 <__libc_csu_fini>:

Disassembly of section .fini:
080484a4 <_fini>:

我们代码的反汇编通常位于 .text 区段。由于这是一个由 GCC 编译的程序,我们可以跳过所有初始化代码,直接进入 main 函数,那里存放着我们的代码:

我已经标出了 puts 的 API 调用。puts API 也是 printf 的一个变种。GCC 足够聪明,选择了 puts 而不是 printf,原因是该字符串没有被解释为 C 风格的 格式化字符串。格式化字符串或 formatter 包含控制字符,这些字符用 % 符号表示,例如 %d 表示整数,%s 表示字符串。实际上,puts 用于非格式化字符串,而 printf 用于格式化字符串。

到目前为止,我们收集了什么信息?

假设我们对源代码没有任何了解,这是我们迄今为止收集到的信息:

  • 该文件是一个 32 位 ELF 可执行文件。

  • 它是使用 GCC 编译的。

  • 它有 15 个可执行函数,包括 main() 函数。

  • 代码使用了常见的 Linux 库:libc.sold-linux.so

  • 根据反汇编代码,预计该程序只是显示一条消息。

  • 程序预计会使用puts显示消息。

动态分析

现在让我们进行一些动态分析。请记住,动态分析应在沙箱环境中进行。Linux 中通常预安装了一些可以用来显示更详细信息的工具。在这次逆向工程中,我们将介绍ltracestracegdb

这是ltrace的使用方法:

ltrace的输出显示了程序执行的可读代码。ltrace记录了程序调用和接收的库函数。它调用了puts来显示一条消息。当程序终止时,它还收到了一个退出状态*13*

地址*0x804840b*也是反汇编结果中列出的main函数的地址。

strace是我们可以使用的另一种工具,但它会记录系统调用。下面是我们在 hello world 程序上运行strace的结果:

strace记录了所有发生的系统调用,从程序被系统执行开始。execve是记录的第一个系统调用。调用execve会运行由其函数参数中的文件名指向的程序。openread是用来读取文件的系统调用。mmap2mprotectbrk负责内存活动,如分配、权限和段边界设置。

puts的代码内部,它最终会执行一个write系统调用。write通常会将数据写入它所指向的对象。通常,它用于写入文件。在这个例子中,write的第一个参数值为11的值表示STDOUT,这是控制台输出的句柄。第二个参数是消息,因此它将消息写入STDOUT

进一步调试

首先,我们需要通过运行以下命令安装gdb

sudo apt install gdb

安装应该是这样的:

然后,使用gdb来调试hello程序,如下所示:

gdb ./hello

gdb可以通过命令进行控制。这些命令在在线文档中有详细列出,但只需输入help就可以帮助我们掌握基础。

你还可以使用gdb显示指定函数的反汇编,通过disass命令。例如,让我们看看如果我们使用disass main命令会发生什么:

然后,我们再次获得了以 AT&T 语法表示的反汇编。要将gdb设置为使用 Intel 语法,请使用以下命令:

set disassembly-flavor intel

这应该会给我们 Intel 汇编语言语法,如下所示:

要在main函数处设置断点,命令是b *main

请注意,星号 (***) 指定了程序中的地址位置。

设置断点后,我们可以使用 run 命令运行程序。我们应该最终到达 main 函数的地址:

要获取当前寄存器的值,请输入 info registers。由于我们处于 32 位环境中,因此会使用扩展寄存器(即 EAX、ECX、EDX、EBX 和 EIP)。如果是 64 位环境,寄存器会以 R 为前缀(即 RAX、RCX、RDX、RBX 和 RIP)。

现在我们已经进入了主函数,我们可以逐步执行每条指令(使用 stepi 命令)并跳过指令(使用 nexti 命令)。通常,我们会跟随 info registers 命令,查看哪些值发生了变化。

stepinexti 的简写命令分别是 sini

继续输入 sidisass main,直到你看到包含 call 0x80482e0 <puts@plt> 的行。你应该会得到以下 disassinfo registers 的结果:

左侧的 => 指示了指令指针所在的位置。寄存器应如下所示:

puts 函数被调用之前,我们可以检查栈中推入的值。我们可以通过 x/8x $esp 查看:

x 命令用于显示指定地址的内存转储。语法为 x/FMT ADDRESS。FMT 有三个部分:重复次数、格式字母和大小字母。你可以通过 help x 查看更多关于 x 命令的信息。x/8x $esp 会从 esp 寄存器指向的地址处显示 8 个 DWORD 十六进制值。由于地址空间是 32 位的,因此默认的大小字母是 DWORD

puts 期望一个单一的参数。因此,我们只关注在 0x080484c0 栈位置推送的第一个值。我们预计该参数应该是一个消息存放的地址。因此,输入 x/s 命令应该给出消息的内容,如下所示:

接下来,我们需要对调用指令行进行跳过(ni)。这应该会显示以下消息:

但是,如果你使用了 si,指令指针将位于 puts 包装代码中。我们仍然可以使用 until 命令回到我们离开的地方,简写为 u。只使用 until 命令将步进一条指令。你需要指定停止的位置地址。这就像是一个临时的断点。记得在地址前加上星号:

剩下的 6 行代码会在进入 main 函数后恢复 ebpesp 的值,然后通过 ret 返回。记住,调用指令会在跳转到函数地址之前,将返回地址存储在栈顶。ret 指令将读取 esp 寄存器指向的返回值。

espebp 的值应在执行 ret 指令之前恢复。通常,函数开始时会设置自己的栈帧,以便与函数的局部变量一起使用。

下面是展示给定地址指令执行后 espebpecx 寄存器值变化的表格。

请注意,栈由 esp 寄存器表示,栈从高地址开始,随着数据的存储,地址逐渐下降。

地址 指令 esp ebp ecx 备注
0x0804840b lea ecx,[esp+0x04] 0xbffff08c 0 0xbffff090 进入 main 后的初始值。[0xbffff08c] = 0xb7e21637,这是返回地址。
0x0804840f and esp,0xfffffff0 0xbffff080 0 0xbffff090 将栈对齐到 16 字节边界。实际上,这将 esp 减去 0xc。
0x08048412 push DWORD PTR [ecx-0x4] 0xbffff07c 0 0xbffff090 [0xbffff07c] = 0xb7e21637,ecx - 4 = 0xbffff08c 指向返回地址。现在返回地址被放置在两个栈地址中。
0x08048415 push ebp 0xbffff078 0 0xbffff090 开始设置栈帧。[0xbffff078] = 0
0x08048416 mov ebp,esp 0xbffff078 0xbffff078 0xbffff090 保存 esp。
0x08048418 push ecx 0xbffff074 0xbffff078 0xbffff090 保存 ecx。[0xbffff074] = 0xbffff090。
0x08048419 sub esp,0x4 0xbffff070 0xbffff078 0xbffff090 为栈帧分配 4 字节空间。
0x0804841c sub esp,0xc 0xbffff064 0xbffff078 0xbffff090 为栈帧分配额外的 12 字节空间。
0x0804841f push 0x80484c0 0xbffff060 0xbffff078 0xbffff090 [0xbffff060] = 0x080484c0,[0x080484c0] = "hello world!"
0x08048424 call 0x80482e0 <puts@plt> 0xbffff060 0xbffff078 0xffffffff 调用后栈没有变化。
0x08048429 add esp,0x10 0xbffff070 0xbffff078 0xffffffff 将 0x10 加到 esp,减少栈帧大小。
0x0804842c nop 0xbffff070 0xbffff078 0xffffffff 无操作
0x0804842d mov ecx,DWORD PTR [ebp-0x4] 0xbffff070 0xbffff078 0xbffff090 恢复调用前的 ecx 值。

| 0x08048430 | leave | 0xbffff07c | 0 | 0xbffff090 | leave 相当于 mov esp, ebp。 |

pop ebp |

0x08048431 lea esp,[ecx-0x4] 0xbffff08c 0 0xbffff090 ecx - 4 = 0xbffff08c,[0xbffff08c] = 0xb7e21637,恢复了 esp 的地址。
0x08048434 ret - - - 返回到 0xb7e21637。

你可以继续探索ret后的清理代码,或者通过使用continue或它的缩写c让程序最终结束,如下所示:

一个更好的调试器

在进行更多 Linux 可执行文件逆向操作之前,让我们先探索更多工具。gdb看起来可以,但如果我们能使用可视化调试工具进行交互式调试会更好。在第五章,工具的使用部分,我们介绍了 Radare,作为一个既能进行反汇编也能进行调试的工具。所以,让我们感受一下使用 Radare 的体验。

设置

Radare 已经是第二个版本。要安装它,你需要git从 GitHub 仓库进行安装,步骤如下:

git clone https://github.com/radare/radare2.git

安装说明写在README文件中。根据写作时的建议,可以通过运行sys/install.shsys/user.sh的 shell 脚本在终端中安装Radare2

Radare2 中的 Hello World

除了反汇编器和调试器,Radare2还包含了一堆工具。大多数都是静态分析工具。

要获取 hello world 二进制文件的MD5哈希值,可以使用rabin2

通过使用ls命令和rahash2,我们能够确定以下信息:

filesize: 7348 bytes
time stamp: July 12 21:26 of this year
md5: 799554478cf399e5f87b37fcaf1c2ae6
sha256: 90085dacc7fc863a2606f8ab77b049532bf454badefcdd326459585bea4dfb29

rabin2是另一个可以从文件中提取静态信息的工具,例如文件类型、头信息、部分内容和字符串。

首先通过使用rabin2 -I hello命令获取文件类型:

bintypeclasshascodeos 字段表明该文件是一个可执行的 32 位 ELF 文件,并且可以在 Linux 上运行。archbitsendianmachine 表示该文件是用 x86 代码构建的。此外,lang字段表明该文件是从 C 语言编译而来的。这些信息无疑将帮助我们在反汇编和调试时做好准备。

要列出导入的函数,我们使用rabin2 -i hello

我们关注的有两个全局函数:puts__libc_start_main。如我们所讨论,puts用于打印消息。__libc_start_main是一个初始化堆栈帧、设置寄存器和一些数据结构、设置错误处理并最终调用main()函数的函数。

要获取 ELF 头信息,可以使用rabin2 -H hello

如果我们只对从数据段中找到的字符串感兴趣,可以使用rabin2 -z hello命令:

使用rabin2,我们获得了关于文件的额外信息,如下所示:

filetype: 32-bit elf file and has executable code for Linux
architecture: x86 Intel
functions: imports puts and has a main function
notable strings: hello world!

现在让我们尝试使用radare2调试器。从终端控制台,你可以使用radare2的缩写r2,或者直接使用radare2,并将-d <file>作为参数:

这将带你进入radare2控制台。在方括号中,地址表示当前eip的位置。这不是 hello 程序的入口点,而是动态加载器中的一个地址。与gdb类似,你需要输入命令。要调出帮助,只需使用?,它将显示如下命令列表:

我们首先使用aaa命令。此命令分析代码中的函数调用、标志、引用,并尝试生成有意义的函数名称:

使用V!命令将控制台设置为可视模式。在此模式下,我们应该能够在交互式查看寄存器和栈的同时调试程序。输入:应该会显示命令控制台。按Enter键将使我们返回可视模式。输入V?以显示更多可视模式命令。最好将终端窗口最大化,以便更好地查看调试器:

在命令控制台中,输入db entry0。这将设置一个断点,位于我们程序的入口地址。但由于我们也知道该程序有一个主函数,你还可以输入db sym.entry,在主函数处设置断点。

在可视模式中,你可以使用默认提供的这些按键开始实际的调试:

| F2 toggle breakpoint
| F4 run to cursor
| F7 single step
| F8 step over
| F9 continue

通过设置入口点和主函数的断点,按F9运行程序。我们应该会到达入口点地址。

你需要通过重新打开radare2的可视模式来刷新它,以查看更改。为此,只需按q两次退出可视模式。但在再次运行V!之前,你需要通过使用s eip命令来查找当前的eip

再次按F9应该将你带到程序的主函数。记得刷新可视模式:

F7F8可以跟踪程序的执行,同时查看栈和寄存器的变化。在0x0804840b这一行地址左侧的字母b表示该地址已设置断点。

到目前为止,我们已经了解了基本的命令和按键。随时探索其他命令,你肯定能获取更多信息,并学到一些简单的技巧来分析文件。

密码是什么?

既然我们已经知道如何进行“Unix 风格”调试,让我们尝试密码程序。你可以从github.com/PacktPublishing/Mastering-Reverse-Engineering/raw/master/ch6/passcode下载密码程序。

尝试获取一些静态信息。以下是你可以使用的命令列表:

ls -l passcode
rahash2 -a md5,sha256 passcode
rabin2 -I passcode
rabin2 -i passcode
rabin2 -H passcode
rabin2 -z passcode

此时,我们正在寻找的信息如下:

  • 文件大小:7,520 字节

  • MD5 哈希:b365e87a6e532d68909fb19494168bed

  • SHA256 哈希:68d6db63b69a7a55948e9d25065350c8e1ace9cd81e55a102bd42cc7fc527d8f

  • 文件类型:ELF

    • 32 位 x86 Intel

    • 编译后的 C 代码中有一些显著的导入函数:printfputsstrlen__isoc99_scanf

  • 显著的字符串如下:

    • 输入密码:

    • 正确的密码!

    • 密码错误!

现在,为了进行快速动态分析,让我们使用ltrace ./passcode

我们尝试了几个密码,但没有一个返回“正确密码!”该文件甚至没有字符串列表中的任何提示供我们使用。让我们尝试strace

包含read(0, asdf123的那一行是密码被手动输入的地方。之后的代码进入退出流程。让我们基于反汇编代码做一个死名单活动,但这次我们将使用radare2的图形视图。请打开radare2,并使用radare2 -d passcode命令。在radare2控制台中,使用以下命令序列:

aaa
s sym.main
VVV

这些应该打开来自main函数的反汇编代码块的图形表示。向下滚动,你应该能看到条件分支,其中绿色线条表示true,红色线条表示false流程。继续向下滚动,直到看到Correct password!文本字符串。我们将从这里向后工作:

0x80485d3块中,显示Correct password!字符串,我们看到该消息是通过puts函数显示的。进入该块的是来自0x80485c7块的红线。在0x80485c7块中,local_418h的值与0x2de(即十进制的 734)进行了比较。为了跳转到Correct password!块,值应该等于 734。如果我们尝试反编译 C 代码,它应该类似于下面这样:

...
if (local_418h == 734)
    puts("Correct password!)
...

向上滚动查看红线的来源:

从这个图形来看,存在一个循环,退出循环需要local_414h的值大于或等于local_410h的值。循环会跳转到0x80485c7块。在0x8048582块中,local_418hlocal_414h的值都被初始化为 0。这些值在0x80485b9块中进行比较。

检查0x8048598块时,有三个需要关注的变量:local_40chlocal_414hlocal_418h。如果我们为这个块编写伪代码,它应该是这样的:

eax = byte at address [local_40ch + local_414h]
add eax to local_418h 
increment local_414h

local_414h似乎是指向local_40c指向的数据的指针。local_418从 0 开始,每次添加local_40ch中的一个字节。从概览来看,这里似乎发生了一个校验和算法:

...
// unknown variables for now are local_40ch and local_410h
int local_418h = 0;
for (int local_414h = 0; local_414h < local_410h; local_414++)
{
    local_418h += local_40ch[local_414h];
}

if (local_418h == 734)
    puts("Correct password!)
...

让我们进一步向上滚动,找出local_40chlocal_410h应该是什么:

这是主块。这里有三个命名的函数:

  • printf()

  • scanf()

  • strlen()

这里使用了local_40chlocal_410hlocal_40chscanf的第二个参数,而0x80486b1地址中的数据应该包含期望的格式。local_40ch包含输入的缓冲区内容。要检索0x80486b1处的数据,只需输入冒号(:),输入s 0x80486b1,然后返回可视模式。再次按q查看数据:

local_40ch中数据的长度被识别并存储在local_410h中。local_410h中的值与 7 进行比较。如果相等,程序会沿着红线进入0x8048582块,即校验和循环的开始。如果不相等,程序会沿着绿线进入0x80485e5块,其中包含显示“密码错误!”的代码。

总结来说,代码可能是这样的:

...
printf ("Enter password: ");
scanf ("%s", local_40ch);
local_410h = strlen(local_40ch);

if (local_410h != 7)
    puts ("Incorrect password!);
else
{
    int local_418h = 0;
    for (int local_414h = 0; local_414h < local_410h; local_414++)
    {
        local_418h += local_40ch[local_414h];
    }

    if (local_418h == 734)
        puts("Correct password!)
}

输入的密码应该有7 个字符,并且密码中所有字符的总和应该等于 734。因此,密码可以是任何内容,只要满足给定的条件即可。

使用 ASCII 表,我们可以确定每个字符的等效值。如果 7 个字符的总和为 734,我们只需将 734 除以 7,这样得到的结果是 104,或者 0x68,余数为 6。我们可以将余数 6 分配给 6 个字符,得到如下字符集:

十进制 十六进制 ASCII 字符
105 0x69 i
105 0x69 i
105 0x69 i
105 0x69 i
105 0x69 i
105 0x69 i
104 0x68 h

让我们尝试密码*iiiiiih**hiiiiii*,如下面所示:

网络流量分析

这次,我们将编写一个接收网络连接并返回一些数据的程序。我们将使用可在github.com/PacktPublishing/Mastering-Reverse-Engineering/raw/master/ch6/server下载的文件。下载完成后,通过终端执行该文件,命令如下:

该程序是一个等待连接到端口9999的服务器程序。要进行测试,请打开浏览器,使用服务器运行机器的 IP 地址和端口。例如,如果您在自己的机器上尝试,可以使用127.0.0.1:9999。您可能会看到如下输出:

要了解网络流量,我们需要使用诸如tcpdump之类的工具来捕获一些网络数据包。tcpdump通常在 Linux 发行版中预安装。打开另一个终端并使用以下命令:

sudo tcpdump -i lo 'port 9999'  -w captured.pcap

下面是所用参数的简要解释:

-i lo 使用 loopback 网络接口。我们在这里使用它,因为我们计划在本地访问该服务器。

'port 9999',带单引号的部分,仅筛选使用端口 9999 的报文。

-w captured.pcap将数据包写入名为captured.pcap的 PCAP 文件中。

一旦tcpdump开始监听数据,尝试通过浏览器访问127.0.0.1:9999来连接服务器。如果你希望从服务器所在机器外部连接,则重新运行tcpdump,不要加上-i lo参数。这样会使用默认的网络接口。而且,你需要使用默认网络接口的 IP 地址,而不是127.0.0.1来进行访问。

要停止tcpdump,只需通过Ctrl + C中断它。

要以人类可读的形式查看captured.pcap文件的内容,可以使用以下命令:

sudo tcpdump -X -r captured.pcap > captured.log

此命令应将tcpdump的输出重定向到captured.log-X参数以十六进制和 ASCII 形式显示数据包的内容。-r captured.pcap表示从PCAP文件captured.pcap中读取数据。打开captured.log文件时,应该类似以下内容:

在继续之前,让我们先了解一下两种最常见的网络协议,传输控制协议TCP)和用户数据报协议UDP)。TCP 是一种网络传输协议,它建立了发送方和接收方之间的通信。通信开始时进行三次握手,发送方向接收方发送 SYN 标志,接收方再向发送方发送 SYN 和 ACK 标志,最后发送方再发送 ACK 标志给接收方,从而开启通信。发送方和接收方之间的进一步数据交换是以分段的方式进行的。每个段都有一个 20 字节的 TCP 头部,包含了发送方和接收方的 IP 地址以及当前的状态标志。接下来是数据的大小和数据本身。UDP 使用较短的头部,因为它只发送数据,并不需要接收方的确认。UDP 不需要进行三次握手。UDP 的主要目的是持续不断地将数据发送给接收方。虽然 TCP 在数据交换方面更可靠,但 UDP 的数据发送速度更快,因为没有必要的开销。UDP 通常用于通过文件传输协议传输大量数据,而 TCP 则用于需要数据完整性的通信。

在前面的截图中,第 1 行到第 15 行显示了一个 TCP 三次握手。第一次从本地主机端口55704(客户端)到本地主机端口9999(服务器)的连接是一个 SYN,标志位为S。然后,服务器响应了一个S.标志,表示 SYN 和 ACK。最后是一个 ACK,用 .表示。这时,客户端端口 55704 是一个临时端口。临时端口是系统为客户端连接生成的端口。服务器端口9999在服务器程序中是固定的。

在第 16 行到第 23 行,我们可以看到服务器到客户端的实际响应数据。服务器返回一个包含 55 个字符的数据,包含字符串"You have connected to the Genie. Nothing to see here."和 2 个换行(0x0A)字符。55 个字符字符串前的数据是数据包的头部,包含有关数据包的信息。解析后的数据包头部内容在第 16 行中描述。TCP 标志为P.,表示 PUSH 和 ACK。数据包头部结构中的信息已在 TCP 和 UDP 规范中进行了文档化。你可以从RFC 675tools.ietf.org/html/rfc675)和RFC 768tools.ietf.org/html/rfc768)开始查找这些规范。为了加速处理过程,我们可以使用 Wireshark,稍后会讨论它,来帮助我们解析数据包信息。

在第24行到第28行,FIN 和 ACK 标志,格式为F.,从服务器发送到客户端,表示服务器正在关闭连接。第 29 行到第 33 行是一个 ACK 响应,.,确认连接正在关闭。

捕获和查看图形化信息的更好工具是Wireshark。之前称为Ethereal,Wireshark 具有与tcpdump相同的功能。Wireshark 可以从www.wireshark.org/手动下载和安装。也可以通过以下apt命令进行安装:

sudo apt install wireshark-qt

捕获网络数据包需要 root 权限才能访问网络接口。这也是我们在运行tcpdump时使用sudo的原因。使用Wireshark时也是如此。因此,要在 Linux 中执行Wireshark,我们使用以下命令:

sudo wireshark

除了捕获流量并实时显示外,您还可以在Wireshark中打开并查看 PCAP 文件:

要开始捕获,双击接口列表中的any。这基本上会同时捕获默认网络接口和回环接口lo的流量。你将看到连续的网络流量数据包行。Wireshark 有一个显示过滤器,可以最小化我们看到的所有噪音。对于我们的练习,在过滤器字段中输入以下显示过滤器:

tcp.port == 9999

这应该只显示使用9999端口的 TCP 数据包。你还可以尝试其他过滤器。更多内容请参考 Wireshark 的手册页面。

点击数据包后,可以查看解析的信息,这将帮助你更好地理解数据包字段,如下图所示:

Wireshark 对标准数据包有广泛的知识。这使得 Wireshark 成为每个分析师必备的工具。

概要

本章我们讨论了已经内置在 Linux 系统中的逆向工程工具。基于 Debian 的操作系统,如 Ubuntu,由于广泛的社区支持和可用的工具,常被用于逆向工程目的。我们更多地关注了如何分析 Linux 原生可执行文件——ELF 文件。我们首先使用 GCC 将一个 C 程序源代码编译成 ELF 可执行文件。接着,我们使用静态信息收集工具,包括lsfilestringsobjdump,对可执行文件进行了分析。然后我们使用ltracestrace进行动态分析。之后我们使用gdb调试程序,展示了 Intel 汇编语言的语法。

我们还介绍并探讨了radare2工具包。我们使用了rahash2rabin2来收集静态信息,并使用radare2在交互式视图中进行反汇编和调试。网络分析工具也没有被忽视,我们使用了tcpdumpWireshark

在信息安全领域,待分析的大多数文件都是基于微软 Windows 的可执行文件,我们将在下一章进行讨论。虽然在行业中我们可能不会遇到太多 Linux 文件的分析,但了解如何进行分析一定会在任务需要时派上用场。

深入阅读

本章使用的文件和源代码可以在github.com/PacktPublishing/Mastering-Reverse-Engineering/tree/master/ch6找到。

第七章:Windows 平台的逆向工程

由于 Windows 是全球最流行的操作系统之一,网络世界中的大多数软件都为其编写。这其中包括恶意软件。

本章聚焦于 Windows 本地可执行文件 PE 文件的分析,并通过文件分析直接进行演变,即收集静态信息并执行动态分析。我们将深入了解 PE 文件如何与 Windows 操作系统交互。以下主题将在本章中进行讲解:

  • 分析 Windows PE 文件

  • 工具

  • 静态分析

  • 动态分析

技术要求

本章需要读者具备 Windows 环境及其管理的知识。读者还应了解如何在命令提示符中使用命令。本章的第一部分要求读者具备使用 Visual Studio 或类似软件构建和编译 C 程序的基本知识。

Hello World

Windows 环境中的程序通过使用 Windows API 与系统进行通信。这些 API 是围绕文件系统、内存管理(包括进程、栈和分配)、注册表、网络通信等构建的。在逆向工程方面,广泛覆盖这些 API 及其库模块,在通过低级语言等效视角理解程序的工作方式时具有很大优势。因此,开始探索 API 及其库的最佳方式是自己开发一些程序。

开发者使用的高级语言有很多,比如 C、C++、C# 和 Visual Basic。C、C++ 和 Visual Basic(本地)编译为可执行文件,直接执行 x86 语言的指令。C# 和 Visual Basic(p-code)通常会被编译成使用解释器的形式,解释器将 p-code 转换为实际的 x86 指令。本章将重点讨论从 C/C++ 和汇编语言编译的可执行二进制文件。目标是更好地理解使用 Windows API 的程序行为。

对于本章,我们选择使用 Visual Studio Community 版来构建 C/C++ 程序。Visual Studio 是广泛用于构建 Microsoft Windows 程序的工具。由于它也是微软的产品,已包含编译程序所需的兼容库。你可以从 visualstudio.microsoft.com/downloads/ 下载并安装 Visual Studio Community 版。

这些程序既不有害也不恶意。以下 C 编程活动可以在裸机上使用 Visual Studio 完成。如果你计划在 Windows 虚拟机上安装 Visual Studio,根据本书的编写时间,Visual Studio 2017 Community 版的推荐系统要求如下:

  • 1.8 GHz 双核

  • 4 GB 的内存

  • 130 GB 的磁盘空间

这些系统要求可以在docs.microsoft.com/en-us/visualstudio/productinfo/vs2017-system-requirements-vs找到。你可能需要执行一些 Windows 更新,并安装.NET 框架。也可以从我们之前下载的 Windows 7 安装包中安装,下载链接为developer.microsoft.com/en-us/microsoft-edge/tools/vms/。请访问微软 Visual Studio 网站,了解新版的要求。

有许多 Visual Studio 的替代工具,它们有较小的系统要求,例如 Bloodshed Dev C++、Zeus IDE 和 Eclipse。然而,这些 IDE 中的一些可能不是最新的,或者可能需要正确设置编译器及其依赖项。

学习 API

我们将在此跳过Hello World,因为我们在前面的章节已经做过了。相反,我们将研究以下示例程序:

  • 将键盘记录器保存到filez

  • 枚举注册表键并打印输出

  • 列出进程并打印输出

  • 加密数据并将其存储到文件中

  • 解密加密文件

  • 监听端口9999并在连接时发送回一个消息

这些程序的源代码可以在github.com/PacktPublishing/Mastering-Reverse-Engineering/tree/master/ch7找到。可以随意使用这些程序,添加自己的代码,甚至创建自己的版本。这里的目标是让你学习这些 API 如何协同工作。

确定程序行为的关键之一是学习如何使用 API。每个 API 的使用方法都在微软开发者网络(MSDN)文档库中有记录。我们即将查看的程序只是程序行为的示例。我们利用这些 API 在这些行为的基础上进行扩展。我们在这里的目标是学习这些 API 的使用方法以及它们如何相互交互。

作为一名逆向工程师,读者应当并且要求使用 MSDN 或其他资源进一步了解 API 的工作原理。可以在 MSDN 文档库中搜索 API 名称,网址为msdn.microsoft.com

键盘记录器

键盘记录器是一个记录用户按键的程序。日志通常保存在一个文件中。这里使用的核心 API 是GetAsyncKeyState。每个可以从键盘或鼠标按下的按钮都有一个被称为虚拟键代码的分配 ID。指定虚拟键代码后,GetAsyncKeyState会提供关于该键是否被按下的信息。

这个程序的源代码可以在github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch7/keylogger.cpp找到。

为了使键盘记录功能正常工作,我们需要检查每个虚拟键码的状态,并将它们放入一个循环中。一旦识别到一个按键被按下,虚拟键码就会被存储到文件中。以下代码实现了这一功能:

  while (true) {
 for (char i = 1; i <= 255; i++) {
 if (GetAsyncKeyState(i) & 1) {
 sprintf_s(lpBuffer, "\\x%02x", i);
 LogFile(lpBuffer, (char*)"log.txt");
 }
 }

LogFile 是一个函数,接受两个参数:它写入的数据和日志文件的文件路径。lpBuffer 包含数据,并通过 sprintf_s API 格式化为 \\x%02x。因此,格式会将任何数字转换为两位数的十六进制字符串。数字 9 会变成 \x09,数字 106 会变成 \x6a

我们只需要三个 Windows API 函数来实现将数据存储到日志文件中——CreateFileWriteFileCloseHandle——如下面的代码所示:

void LogFile(char* lpBuffer, LPCSTR fname) {

  BOOL bErrorFlag;
  DWORD dwBytesWritten;

  HANDLE hFile = CreateFileA(fname, FILE_APPEND_DATA, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  bErrorFlag = WriteFile(hFile, lpBuffer, strlen(lpBuffer), &dwBytesWritten, NULL);
  CloseHandle(hFile);

  return;_
}

CreateFileA 用于根据文件名和文件的使用方式创建或打开一个新文件。由于这个练习的目的是不断记录按键的虚拟键码,我们需要以追加模式打开文件(FILE_APPEND_DATA)。返回的文件句柄存储在 hFile 中,并被 WriteFile 使用。lpBuffer 包含格式化的虚拟键码。WriteFile 需要的参数之一是要写入的数据大小。这里使用了 strlen API 来确定数据的长度。最后,使用 CloseHandle 关闭文件句柄。关闭文件句柄是很重要的,这样文件才会可供使用。

有不同的键盘变体,旨在适应用户的语言。因此,不同的键盘可能具有不同的虚拟键码。在程序开始时,我们使用 GetKeyboardLayoutNameA(lpBuffer) 来识别正在使用的键盘类型。在读取日志时,将使用键盘类型作为参考,以正确识别哪些键被按下。

regenum

如下所述,regenum 程序旨在枚举给定注册表项中的所有值和数据。API 所需的参数取决于前一个 API 的结果。就像我们在键盘记录器程序中能够写入数据到文件一样,注册表枚举的 API 也需要一个句柄。在这种情况下,RegEnumValueARegQueryValueExA API 使用的是注册表项的句柄。

这个程序的源代码可以在 github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch7/regenum.cpp 找到。

int main()
{
 LPCSTR lpSubKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
 HKEY hkResult;
 DWORD dwIndex;
 char ValueName[1024];
 char ValueData[1024];
 DWORD cchValueName;
 DWORD result;
 DWORD dType;
 DWORD dataSize;
 HKEY hKey = HKEY_LOCAL_MACHINE;

 if (RegOpenKeyExA(hKey, lpSubKey, 0, KEY_READ, &hkResult) == ERROR_SUCCESS)
 {
 printf("HKEY_LOCAL_MACHINE\\%s\n", lpSubKey);
 dwIndex = 0;
 result = ERROR_SUCCESS;
 while (result == ERROR_SUCCESS)
 {
 cchValueName = 1024;
 result = RegEnumValueA(hkResult, dwIndex, (char *)&ValueName, &cchValueName, NULL, NULL, NULL, NULL);
 if (result == ERROR_SUCCESS)
 {
 RegQueryValueExA(hkResult, ValueName, NULL, &dType, (unsigned char *)&ValueData, &dataSize);
 if (strlen(ValueName) == 0)
 sprintf((char*)&ValueName, "%s", "(Default)");
 printf("%s: %s\n", ValueName, ValueData);
 }
 dwIndex++;
 }
 RegCloseKey(hkResult);
 }
 return 0;
}

枚举从通过 RegOpenKeyExA 获取注册表项的句柄开始。成功的返回值应该是非零的,而输出应该显示存储在 hkResult 中的句柄。这里要访问的注册表项是 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run

hkResult中的句柄由RegEnumValueA使用,用于开始枚举注册表键下的每个注册表值。后续对RegEnumValueA的调用将返回下一个注册表值条目。因此,这段代码被放在循环中,直到返回ERROR_SUCCESS结果为止。ERROR_SUCCESS结果表示成功检索到注册表值。

对于每个注册表值,都会调用RegQueryValueExA。记住,我们只获取了注册表值,但没有获取其对应的数据。通过使用RegQueryValueExA,我们应该能够获取到注册表数据。

最后,我们需要通过使用RegCloseKey来关闭句柄。

这里使用的其他 API 包括printfstrlensprintfprintf在程序中用于将目标注册表键、值和数据打印到命令行控制台。strlen用于获取文本字符串的长度。每个注册表键都有一个默认值。由于RegEnumValueA将返回ERROR_SUCCEPantf,我们可以将ValueName变量替换为一个名为(Default)的字符串:

processlist

类似于枚举注册表值的方式,列出进程也基于相同的概念。由于实时进程变化快速,需要获取进程列表的快照。该快照包含快照创建时的进程信息列表。可以使用CreateToolhelp32Snapshot来获取快照。结果存储在hSnapshot中,它是快照句柄。

要开始枚举列表,使用Process32First来获取列表中的第一个进程信息。该信息存储在pe32变量中,类型为PROCESSENTRY32。通过调用Process32Next来检索后续的进程信息。当处理完列表后,最终使用CloseHandle

再次使用printf来打印出可执行文件名和进程 ID:

int main()
{
  HANDLE hSnapshot;
  PROCESSENTRY32 pe32;

  hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  pe32.dwSize = sizeof(PROCESSENTRY32);

  if (Process32First(hSnapshot, &pe32))
  {
    printf("\nexecutable [pid]\n");
    do
    {
      printf("%ls [%d]\n", pe32.szExeFile, pe32.th32ProcessID);
    } while (Process32Next(hSnapshot, &pe32));
    CloseHandle(hSnapshot);
  }
    return 0;
}

该程序的源代码可以在github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch7/processlist.cpp找到。

加密和解密文件

勒索软件已成为全球传播的最流行恶意软件之一,其核心要素是能够加密文件。

在这些加密和解密程序中,我们将学习一些用于加密和解密的基本 API。

用于加密的 API 是CryptEncrypt,而CryptDecrypt用于解密。然而,这些 API 至少需要一个加密密钥的句柄。为了获得加密密钥的句柄,需要先获得加密服务提供商CSP)的句柄。从本质上讲,在调用CryptEncryptCryptDecrypt之前,必须先调用一些 API 来设置将要使用的算法。

在我们的程序中,CryptAcquireContextA用于从 CSP 获取一个CryptoAPI密钥容器句柄。在这个 API 中,算法 AES 被指定。加密将使用的密钥由用户定义的密码控制,该密码设置在password[]字符串中。为了获取派生密钥的句柄,使用了CryptCreateHashCryptHashDataCryptDeriveKey这些 API,并将用户定义的password传递给CryptHashData。要加密并赋值给buffer变量的数据,会传递给CryptEncrypt。最终加密后的数据会被写入同一数据缓冲区,并在此过程中覆盖原数据:

int main()
{
  unsigned char buffer[1024] = "Hello World!";
  unsigned char password[] = "this0is0quite0a0long0cryptographic0key";
  DWORD dwDataLen;
  BOOL Final;

  HCRYPTPROV hProv;

  printf("message: %s\n", buffer);
  if (CryptAcquireContextA(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT))
  {
    HCRYPTHASH hHash;
    if (CryptCreateHash(hProv, CALG_SHA_256, NULL, NULL, &hHash))
    {
      if (CryptHashData(hHash, password, strlen((char*)password), NULL))
      {
        HCRYPTKEY hKey;
        if (CryptDeriveKey(hProv, CALG_AES_128, hHash, NULL, &hKey))_
        {
          Final = true;
          dwDataLen = strlen((char*)buffer);
          if (CryptEncrypt(hKey, NULL, Final, NULL, (unsigned char*)&buffer, &dwDataLen, 1024))
          {
            printf("saving encrypted buffer to message.enc");
            LogFile(buffer, dwDataLen, (char*)"message.enc");
          }
          printf("%d\n", GetLastError());
          CryptDestroyKey(hKey);
        }
      }
      CryptDestroyHash(hHash);
    }
    CryptReleaseContext(hProv, 0);
  }
  return 0;
}

使用修改后的LogFile函数,该函数现在包括写入数据的大小,已将加密数据存储在message.enc文件中:

void LogFile(unsigned char* lpBuffer, DWORD buflen, LPCSTR fname) {

  BOOL bErrorFlag;
  DWORD dwBytesWritten;

  DeleteFileA(fname);

  HANDLE hFile = CreateFileA(fname, FILE_ALL_ACCESS, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  bErrorFlag = WriteFile(hFile, lpBuffer, buflen, &dwBytesWritten, NULL);
  CloseHandle(hFile);

  Sleep(10);

  return;
}

为了优雅地关闭CryptoAPI句柄,使用了CryptDestroyKeyCryptDestroyHashCryptReleaseContext

加密后的消息Hello World!现在会变成这样:

解密消息的方法是使用相同的CryptoAPI,但这次使用CryptDecrypt。这时,message.enc的内容会被读入数据缓冲区,解密后存储在message.dec中。CryptoAPI 的使用方式与获取密钥句柄时相同。缓冲区的长度应存储在dwDataLen中,初始值应为缓冲区的最大长度:

int main()
{
  unsigned char buffer[1024];
  unsigned char password[] = "this0is0quite0a0long0cryptographic0key";
  DWORD dwDataLen;
  BOOL Final;

  DWORD buflen;
  char fname[] = "message.enc";
  HANDLE hFile = CreateFileA(fname, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  ReadFile(hFile, buffer, 1024, &buflen, NULL);
  CloseHandle(hFile);

  HCRYPTPROV hProv;

  if (CryptAcquireContextA(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT))
  {
    HCRYPTHASH hHash;
    if (CryptCreateHash(hProv, CALG_SHA_256, NULL, NULL, &hHash))
    {
      if (CryptHashData(hHash, password, strlen((char*)password), NULL))
      {
        HCRYPTKEY hKey;
        if (CryptDeriveKey(hProv, CALG_AES_128, hHash, NULL, &hKey))
        {
          Final = true;
          dwDataLen = buflen;
          if ( CryptDecrypt(hKey, NULL, Final, NULL, (unsigned char*)&buffer, &dwDataLen) )
          {
            printf("decrypted message: %s\n", buffer);
            printf("saving decrypted message to message.dec");
            LogFile(buffer, dwDataLen, (char*)"message.dec");
          }
          printf("%d\n", GetLastError());
          CryptDestroyKey(hKey);
        }
      }
      CryptDestroyHash(hHash);
    }
    CryptReleaseContext(hProv, 0);
  }
  return 0;
}

加密和解密程序的源代码可以在以下链接中找到:

加密:github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch7/encfile.cpp

解密:github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch7/decfile.cpp

服务器

第六章,Linux 平台上的逆向工程中,我们学习了如何使用套接字 API 来控制客户端和服务器之间的网络通信。相同的代码也可以在 Windows 操作系统中实现。对于 Windows,使用套接字 API 之前,需要通过WSAStartupAPI 初始化套接字库。与 Linux 的函数相比,不再使用write,而是使用send来向客户端发送数据。同时,关于close,它在 Windows 中对应的是closesocket,用于释放套接字句柄。

这是一个图示,展示了服务器和客户端通常如何通过使用套接字 API 进行通信。请注意,下面图示中显示的函数是 Windows API 函数:

socket 函数用于初始化一个套接字连接。完成连接后,通过 closesocket 函数关闭通信。服务器要求我们将程序与一个网络端口 bind 绑定。listenaccept 函数用于等待客户端连接。sendrecv 函数用于服务器和客户端之间的数据传输。send 用于发送数据,而 recv 用于接收数据。最后,closesocket 用于终止传输。以下代码显示了一个实际的服务器端程序 C 源代码,它接受连接并回复 You have connected to the Genie. Nothing to see here.


int main()
{
 int listenfd = 0, connfd = 0;
 struct sockaddr_in serv_addr;
 struct sockaddr_in ctl_addr;
 int addrlen;
 char sendBuff[1025];

 WSADATA WSAData;

 if (WSAStartup(MAKEWORD(2, 2), &WSAData) == 0)
 {
     listenfd = socket(AF_INET, SOCK_STREAM, 0);
     if (listenfd != INVALID_SOCKET)
     {
         memset(&serv_addr, '0', sizeof(serv_addr));
         memset(sendBuff, '0', sizeof(sendBuff));
         serv_addr.sin_family = AF_INET;
         serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
         serv_addr.sin_port = htons(9999);
         if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == 0)
         {
             if (listen(listenfd, SOMAXCONN) == 0)
             {
                 printf("Genie is waiting for connections to port 9999.\n");
                 while (1)
                 {
                     addrlen = sizeof(ctl_addr);
                     connfd = accept(listenfd, (struct sockaddr*)&ctl_addr, &addrlen);
                     if (connfd != INVALID_SOCKET)
                     {
                         printf("%s has connected.\n", inet_ntoa(ctl_addr.sin_addr));

                         snprintf(sendBuff, sizeof(sendBuff), "You have connected to the Genie. Nothing to see here.\n\n");
                         send(connfd, sendBuff, strlen(sendBuff), 0);
                         closesocket(connfd);
                     }
                 }
             }
         }
     closesocket(listenfd);
     }
 WSACleanup();
 }
 return 0;
}

该程序的源代码可以在 github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch7/server.cpp 上找到。

密码是什么?

在这一部分,我们将对 passcode.exe 程序进行逆向工程。作为练习,我们将通过使用静态和动态分析工具来收集我们需要的信息。我们将使用前几章介绍的一些 Windows 工具。不要仅限于我们在这里使用的工具,实际上有很多其他工具也能完成相同的任务。用于分析该程序的操作系统环境是一个 Windows 10、32 位、2 GB 内存、2 核处理器的虚拟机环境。

静态分析

除了文件名,你还需要知道的第二个信息是文件的哈希值。我们可以使用 Quickhash (quickhash-gui.org/) 来帮助完成这个任务。在使用 Quickhash 打开 passcode.exe 文件后,我们可以获得各种算法的哈希计算。以下截图显示了 passcode.exe 文件的 SHA256 哈希值:

该文件的扩展名为 .exe。这使得我们首先想到使用用于分析 Windows 可执行文件的工具。但是,为了确保它确实是一个 Windows 可执行文件,我们可以使用 TriD 来获取文件类型。TrID (mark0.net/soft-trid-e.html) 是基于命令行的工具,应在命令提示符下运行。我们还需要从 mark0.net/download/triddefs.zip 下载并解压 TrID 的定义文件。在下面的截图中,我们使用了 dirtrid。通过使用 dir 获取目录列表,我们得到了文件的时间戳和文件大小。使用 trid 工具后,我们能够识别 passcode.exe 是什么类型的文件:

现在我们已经验证它是一个 Windows 可执行文件,使用 CFF Explorer 应该能给我们更多的文件结构细节。从 ntcore.com/ 下载并安装 CFF Explorer。打开后,你会看到以下界面:

TrID 和 CFF Explorer 都将该文件识别为 Windows 可执行文件,但它们的识别结果不一致。这可能会令人困惑,因为 TrID 将该文件识别为 Win64 可执行文件,而 CFF Explorer 将其识别为 可移植可执行文件 32。这需要从 PE 头文件本身识别机器类型。PE 文件的头文件参考可以在 www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx 查看。

我们可以使用 CFF Explorer 的 Hex Editor 查看二进制文件。第一列显示文件偏移量,中间列显示二进制的十六进制表示,最右边一列显示可打印字符:

文件以 MZ 魔术头(即 0x4d5a)开始,表示这是一个 Microsoft 可执行文件。在文件偏移量 0x3c 处,DWORD 值(按小端格式读取)为 0x00000080。这就是 PE 头部所在的文件偏移位置。PE 头部以 DWORD0x00004550PE 开头,后面跟随两个空字节。接下来是一个 WORD 值,告诉你程序可以运行的机器类型。在本程序中,我们得到 0x014c,这相当于 IMAGE_FILE_MACHINE_I386,意味着它可以在 Intel 386(32 位微处理器)及更高版本的处理器上运行,也可以在其他兼容的处理器上运行。

此时,我们已经知道的信息如下:

Filename:  passcode.exe
Filesize:  16,766 bytes
MD5:  5D984DB6FA89BA90CF487BAE0C5DB300
SHA256:  A5A981EDC9D4933AEEE888FC2B32CA9E0E59B8945C78C9CBD84085AB8D616568
File Type: Windows PE 32-bit
Compiler: MingWin32 - Dev C++

为了更好地了解文件,我们将其在沙盒中运行。

快速运行

从虚拟机中打开 Windows 沙盒,然后将 passcode.exe 的副本拖放并运行:

程序要求输入密码。猜测密码后,程序突然关闭。从这一事件中,我们获得的信息如下:

  • 第一条信息是关于程序要求输入密码的。

  • 第二条信息是程序打开了命令提示符。

这只是意味着程序应该在命令提示符下运行。

死亡列表

对于密码,我们可能能在文件本身的文本字符串中找到它。为了从文件中获取字符串列表,我们需要使用 SysInternal Suite 的 Strings 工具(docs.microsoft.com/en-us/sysinternals/downloads/strings)。Strings 是一个基于控制台的工具,输出的字符串列表将打印到控制台上。

该程序的源代码可以在 github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch7/passcode.c 找到。

我们应该通过运行 strings.exe passcode.exe > strings.txt 将输出重定向到文本文件:

尽管如此,当我们尝试字符串时仍然得到错误密码。也就是说,字符串确实显示了一个正确消息很可能会显示correct password. bye!。列表还显示了程序使用的许多 API。但是,知道这是使用 MingWin-Dev C++编译的,大部分使用的 API 可能是程序的初始化的一部分。

使用 IDA Pro 32 位反编译器对文件进行反汇编,我们可以看到主函数的代码。您可以从github.com/PacktPublishing/Mastering-Reverse-Engineering/tree/master/tools/Disassembler%20Tools下载并安装 IDA Pro。由于我们在 Windows 32 位环境中工作,请安装 32 位的idafree50.exe文件。这些安装程序是从官方 IDA Pro 网站获取的,并托管在我们的 GitHub 存储库中以确保可用性。

这个文件是一个 PE 文件,或者可移植可执行文件。应该以可移植可执行文件的形式打开,以读取 PE 文件的可执行代码。如果使用 MS-DOS 可执行文件打开,结果代码将是 16 位 MS-DOS 存根:

IDA Pro 能够识别主函数。它位于地址0x004012B8。向下滚动到图形概览,显示了块的分支情况,可能会让你了解程序代码在执行时的流程。要查看纯汇编代码,即没有图形表示,只需切换到文本视图模式:

由于这是一个 C 编译代码,我们只需要关注_main函数的分析。我们将尝试从分析中生成伪代码。将收集的信息包括 API,因为它们在代码流程中使用,使跳转分支的条件,以及使用的变量。程序中可能会注入一些特定的编译器代码,我们可能需要识别并跳过:

快速检查函数sub_401850sub_4014F0,我们可以看到这里使用了_atexit API。atexit API 用于设置程序正常终止后将执行的代码。atexit和类似的 API 通常由高级编译器使用来运行清理代码。这些清理代码通常设计用于防止可能的内存泄漏,关闭已打开但未使用的句柄,释放已分配的内存,和/或为了优雅退出重新调整堆栈和堆:

_atexit中使用的参数指向sub_401450,包含清理代码。

接下来,我们将调用 printf 函数。在汇编语言中,调用 API 需要将其参数按照顺序放置在栈顶。我们通常使用 push 指令将数据存入栈中。这段代码正是做了同样的事情。如果你右击 [esp+88h+var_88],会弹出一个下拉菜单,显示可能的变量结构列表。可以将指令行理解为 mov dword ptr [esp], offset aWhatIsThePassw

这与 push offset aWhatIsThePassw 做的事情相同。方括号用于定义一个数据容器。在这个例子中,esp 是容器的地址,容器中存储的是 "what is the password?" 的地址。使用 pushmov 之间有区别。在 push 指令中,栈指针 esp 会被递减。总体而言,printf 得到了它需要的参数,用来将信息显示到控制台。

下一个 API 是 scanfscanf 需要两个参数:输入格式和存储输入的地址。第一个参数位于栈顶,应该是输入格式,后面是存储输入的地址。修改后的变量结构应该是这样的:

给定的格式是 "%30[0-9a-zA-Z ]",这意味着 scanf 只会从输入的开头读取 30 个字符,并且只会接受方括号内的第一个字符集。接受的字符仅限于 "0" 到 "9"、"a" 到 "z"、"A" 到 "Z" 和空格字符。此类型的输入格式用于防止超过 30 个字符的输入。它还用于防止其余代码处理非字母数字字符,空格字符除外。

第二个参数,位于 [esp+4],应该是存储输入的地址。追溯回来,eax 寄存器的值设置为 [ebp+var_28]。我们只需注意,var_28 存储的地址是输入的密码。

strlen API 紧接其后,且只需要一个参数。追溯 eax 寄存器的值,var_28,即输入的密码,它是 strlen 将要使用的字符串。字符串的最终长度将存储在 eax 寄存器中。字符串大小与 11h17 进行比较。在 cmp 之后,通常会有一个条件跳转。使用了 jnz 指令。如果比较结果为,则跟随红线。如果条件为,则跟随绿线。蓝线则直接跳到下一个代码块,如下所示:

跟随红线表示字符串长度为 17。此时,我们的伪代码如下:

main()
{
    printf("what is the password? ");
    scanf("%30[0-9a-zA-Z ]", &password);
    password_size = strlen(password);
    if (password_size == 17)
    { ... }
    else
    { ... }
}

如果密码的长度不是 17,很可能会显示错误密码。 让我们首先跟随绿色路径:

绿线进入 loc_4013F4 块,随后是结束 _main 函数的 loc_401400 块。 loc_4013F4 处的指令是对 sub_401290 的调用。 该函数包含显示错误密码消息的代码。 请注意,许多行指向 loc_4013F4

下面是使用错误密码功能构建伪代码的延续:

wrong_password()
{
    printf("wrong password. try again!\n");
}

main()
{
    printf("what is the password? ");
    scanf("%30[0-9a-zA-Z ]", &password);
    password_size = strlen(password);
    if (password_size == 17)
    { ... }
    else
    {
        wrong_password();
    }
}

在逆向工程中的一个好技巧是尽可能找到最短的退出路径。 然而,这需要实践和经验。 这使得更容易描绘代码的整体结构。

现在,让我们分析 17 个字符长度的代码的其余部分。 让我们跟踪分支指令,并根据条件向后工作:

jle 的条件是对 var_60 和 0 的比较。 var_60 的值为 5,来自 var_5c。 这促使代码的方向沿着红线进行,如下所示:

放大一点,我们正在查看的代码实际上是一个具有两个退出点的循环。 第一个退出点是 var_60 的值小于或等于 0 的条件。 第二个退出点是寄存器 eax 指向的字节不应等于 65h 的条件。 如果进一步检查循环中的变量,可以看到 var_60 的初始值为 5var_60 的值在 loc_401373 块中递减。 这意味着循环将迭代 5 次。

我们还可以在循环中看到 var_8var_5c。 但是,自主代码的开始以来,var_8 从未被设置。 var_5c 也不是作为变量使用,而是作为计算地址的一部分。 IDA Pro 帮助识别了可能作为 main 函数堆栈帧一部分使用的变量,并将其基础设置为 ebp 寄存器中。 这次,我们可能需要通过仅在循环代码中选择给出的列表中的结构来取消对 var_8var_5c 的变量识别。 这可以通过右键单击变量名称来完成:

因此,计算 eax 中的值时,我们从 lea 指令行开始。存储到 edx 中的值是从 ebp 中减去 8 后得到的差值。这里的 lea 并不会获取 ebp-8 中存储的值,不同于使用 mov 指令时的行为。存储在 ebp 中的值是进入 main 函数后 esp 寄存器中的值。这使得 ebp 成为堆栈帧的基址。引用堆栈帧中的变量需要使用 ebp。记住,堆栈是通过从高地址向低地址递减来使用的。这就是为什么从 ebp 寄存器引用时需要相对减法的原因:

现在,在 add 指令行中,要存储到 edx 中的值将是 edx 和从计算地址中存储的值的总和。这个计算出的地址是 eax*4-5Cheax 是来自 var_60 的值,包含从 50 递减的值。但由于当 var_60 达到 0 时循环结束,这行中的 eax 只会有从 51 的值。计算所有五个地址时,应该得到以下输出:

[ebp+5*4-5ch] -> [ebp-48h] = 10h
[ebp+4*4-5ch] -> [ebp-4Ch] = 0eh
[ebp+3*4-5ch] -> [ebp-50h] = 7
[ebp+2*4-5ch] -> [ebp-54h] = 5
[ebp+1*4-5ch] -> [ebp-58h] = 3

也正因为如此,在调用第一个 printf 函数之前,这些堆栈帧地址中存储的值就已设置。在此时,给定 eax51 的值,edx 应该具有以下结果值:

eax = 5;  edx = ebp-8+10h;  edx = ebp+8
eax = 4;  edx = ebp-8+0eh;  edx = ebp+6
eax = 3;  edx = ebp-8+7;    edx = ebp-1
eax = 2;  edx = ebp-8+5;    edx = ebp-3
eax = 1;  edx = ebp-8+3;    edx = ebp-5

edx 的结果值随后通过 mov 指令存储到 eax 中。然而,在这之后,eax 会被减去 20h

from eax = 5;  eax = ebp+8-20h;  eax = ebp-18h
from eax = 4;  eax = ebp+6-20h;  eax = ebp-1ah
from eax = 3;  eax = ebp-1-20h;  eax = ebp-21h
from eax = 5;  eax = ebp-3-20h;  eax = ebp-23h
from eax = 5;  eax = ebp-5-20h;  eax = ebp-25h

接下来的两行代码是循环的第二个退出条件。cmp 指令将 65heax 指向的地址中存储的值进行比较。65h 的等效 ASCII 字符是 "e"。如果 eax 指向的地址中的值与 65h 不匹配,代码将退出循环。如果发生不匹配,跟随红色线条会调用 sub_401290,这恰好是错误密码函数。与字符 "e" 进行比较的地址必须是输入字符串的一部分。

如果我们将堆栈帧绘制成一个表格,它看起来可能是这样的:

0 1 2 3 4 5 6 7 8 9 A B C D E F
-60h 03 00 00 00 05 00 00 00
-50h 07 00 00 00 0e 00 00 00 10 00 00 00
-40h
-30h X X X e X e X e
-20h X X X X X X e X e
-10h
ebp

我们需要考虑 scanf 将输入的密码存储在 ebp-var_28ebp-28 中。知道正确密码恰好有 17 个字符,我们用 X 标记了这些输入位置。我们还需要设置那些应该与 "e" 匹配的地址以继续。记住,字符串从偏移量 0 开始,而不是 1

现在我们对循环很熟悉了,那么我们的伪代码到目前为止应该是这个样子:

wrong_password()
{
    printf("wrong password. try again!\n");
}

main()
{
    e_locations[] = [3, 5, 7, 0eh, 10h];
    printf("what is the password? ");
    scanf("%30[0-9a-zA-Z ]", &password);
    password_size = strlen(password);
    if (password_size == 17)
    {

        for (i = 5; i >= 0; i--)
            if (password[e_locations[i]] != 'e')
            {
                wrong_password();
                goto goodbye;
            }
        ...
    }
    else
    {
        wrong_password();
    }
goodbye:
}

在循环之后,我们会看到另一个使用strcmp的块,这次我们校正了一些变量结构,以更好地了解我们的栈帧可能是什么样子:

前两条指令从ebp-1Ahebp-25h读取DWORD值,并用于计算二进制 AND。查看我们的栈帧,这两个位置都在输入密码字符串区域内。最终再次使用二进制 AND 处理结果值和0FFFFFFh。最终值存储在ebp-2Ch。然后使用strcmp比较存储在ebp-2Ch处的值与字符串"ere"。如果字符串比较不匹配,绿线会进入错误密码代码块。

使用AND指令和0FFFFFFh意味着只限于 3 个字符。对来自密码字符串的两个DWORD使用AND将意味着两者应该相等,至少在 3 个字符上。因此,ebp-1Ahebp-25h应包含"ere":

0 1 2 3 4 5 6 7 8 9 A B C D E F
-60h 03 00 00 00 05 00 00 00
-50h 07 00 00 00 0e 00 00 00 10 00 00 00
-40h
-30h e r e X X X e r e X e
-20h X X X X X X e r e
-10h
ebp

让我们继续下一个代码集,按照红线操作:

所有的绿线都指向错误密码代码块。因此,为了继续前进,我们必须遵循与红线相关的条件。在前面截图的第一个代码块中,使用XOR指令验证ebp-1Ehebp-22h处的字符是否相等。第二个块将来自相同偏移量ebp-1Ehebp-22h的字符值相加。总和应为40h。在这种情况下,字符应具有 ASCII 值20h,即空格字符。

第三块从ebp-28h读取DWORD值,然后使用 AND 指令仅取前 3 个字符。结果与647541h进行比较。如果转换为 ASCII 字符,读作"duA"。

第四个块执行与第三个相同的方法,但从ebp-1Dh中取出DWORD,并将其与636146h或"caF"进行比较。

最后一个块从ebp-20h读取一个 WORD 值,并将其与7473h或"ts"进行比较。

把这些写到我们的栈帧表中应该用小端法完成:

0 1 2 3 4 5 6 7 8 9 A B C D E F
-60h 03 00 00 00 05 00 00 00
-50h 07 00 00 00 0e 00 00 00 10 00 00 00
-40h
-30h e r e A u d e r e e
-20h s t F a c e r e
-10h
ebp

密码应该是 "Audere est Facere"。如果成功,它应该会运行正确的密码函数:

为了完成我们的伪代码,我们需要计算从 ebp-28h 开始的字符串相对偏移量。ebp-28h 是密码字符串的偏移量,值为 0,而字符串中的最后一个偏移量,即偏移量 16,应位于 ebp-18h

wrong_password()
{
    printf("\nwrong password. try again!\n");
}

correct_password()
{
    printf("\ncorrect password. bye!\n");
}

main()
{
    e_locations[] = [3, 5, 7, 0eh, 10h];
    printf("what is the password? ");
    scanf("%30[0-9a-zA-Z ]", &password);
    password_size = strlen(password);
    if (password_size == 17)
    {
        for (i = 5; i >= 0; i--)
            if (password[e_locations[i]] != 'e')
            {
                wrong_password();
                goto goodbye;
            }
        if ( (password[6] ^ password[10]) == 0 )   // ^ means XOR
            if ( (password[6] + password[10]) == 0x40 )
                if ( ( *(password+0) & 0x0FFFFFF ) == 'duA' )
                    if ( ( *(password+11) & 0x0FFFFFF ) == 'caF' )
                        if ( ( *(password+8) & 0x0FFFF ) == 'ts' )
                        {
                            correct_password();
                            goto goodbye
                        }
    }
    wrong_password();
goodbye:
}

使用调试器进行动态分析

没有什么比验证我们在静态分析中假设的内容更好的了。只需运行程序并输入密码,任务就完成了:

死列表与调试程序同样重要。两者可以同时进行。调试有助于加速死列表过程,因为它也能同时验证。对于本次练习,我们将通过使用 x32dbg 重新分析 passcode.exe,下载地址为 x64dbg.com

x32dbg 中打开 passcode.exe 后,注册 EIP 时会位于一个较高的内存区域。这绝对不在 passcode.exe 映像的任何部分:

为了绕过这个问题,点击“选项->首选项”,然后在“事件”标签下,取消选中 系统断点

点击保存按钮,然后使用“调试->重启”或按 Ctrl + F2。这将重启程序,但现在 EIP 应该会停在 PE 文件的入口点地址:

由于我们也知道 main 函数的地址,我们需要在该地址设置一个断点并让程序运行(*F9*)。为此,在命令框中输入以下内容:

bp 004012b8

运行后,EIP 应该停在 main 函数的地址。我们能看到与死列表时相同的一段代码:

F7F8 是进入单步执行和单步跳过的快捷键。点击调试菜单,你应该可以看到分配给调试命令的快捷键。继续尝试这些命令;如果弄乱了,随时可以重启。

使用调试器的好处是你应该能轻松看到堆栈帧。有五个内存转储窗口组成堆栈帧。我们来使用转储 2 来展示堆栈帧。执行两步指令,让 ebp 设置为堆栈帧的基址。在左侧窗格的寄存器列表中,右击寄存器 EBP,然后选择“跟随转储->转储 2”。这将把转储 2 显示出来。由于堆栈是从较高地址向下移动的,你需要将滚动条向上滚动,显示堆栈帧中的初始数据:

这是输入 scanf 后相同的栈帧。另外,在 scanf 期间,您需要切换到命令提示符窗口以输入密码,然后再切换回来。以下截图中还包括了栈窗口,位于右侧窗格:

即使在调试器中,我们也可以随时更改输入字符串的内容,从而强制程序继续执行,直到正确的密码条件出现。我们所需要做的就是右键点击 Dump 窗口中的字节,并选择修改值 例如,在比较 65h ("e") 和寄存器 eax 指向地址中存储的值的循环中,在执行 cmp 指令之前,我们可以更改该地址处的值。

在下面的截图中,地址 0060FF20h(EAX)处存储的值正在从 35h 修改为 65h

也可以通过右键点击字节进行二进制编辑,然后选择 Binary->Edit 来进行相同的修改。

如果我们输入了正确的密码,这里应该是我们最终的结果:

反编译器

如果伪代码能够自动提供给我们,那可能会更容易。确实存在一些工具,可能能帮助我们实现这一点。我们来尝试反编译 passcode.exegithub.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch7/passcode.exe)使用 Snowman 的独立版本(derevenets.com/)。打开文件后,点击 View->Inspector。这将显示一个包含程序解析函数的框。寻找函数定义 _main,选择它以显示与汇编语言等效的伪代码。此时,左侧窗格会突出显示汇编语言行,中央窗格则显示伪代码:

截至撰写本书时,输出的 C 源代码可能有所帮助,但并非所有代码都正确反编译。例如,比较 "e" 的循环未能正确反编译。输出显示为一个 while 循环,但我们预期 v10 变量的值应该是从密码字符串中计算出的偏移量读取的。然而,大部分代码应该能够在某种程度上帮助我们理解程序的工作方式。该反编译器引擎是开源的(www.capstone-engine.org/),因此不应期待过多支持,因为它并非时刻可用。

好消息是,已经有更强大的反编译工具,例如 HexRays。大多数机构以及一些进行逆向工程的独立分析师和研究人员愿意为这些反编译工具付费。对大多数逆向工程师来说,HexRays 性价比非常高。

这是passcode.exe的 HexRays 反编译版本:

反编译工具在不断发展,因为这些工具可以加快分析速度。它们并不能完美地反编译,但应该接近源代码。

总结

在本章中,我们介绍了逆向工程,首先通过学习 API 在功能程序中的使用来开始。接着,我们使用静态和动态分析工具对程序进行了反向分析。

总体来说,Windows 平台上有很多可用的逆向工具。这些工具也包含了大量的关于如何在特定逆向场景中使用它们的信息和研究。逆向工程主要是通过获取来自互联网的资源,以及你已经掌握的知识,我们已经做到了这一点。

进一步阅读

第八章:沙盒 - 作为逆向工程组成部分的虚拟化

在之前的章节中,我们使用了虚拟化软件,特别是 VirtualBox 或 VMware,来设置 Linux 和 Windows 环境进行分析。虚拟化工作得很好,因为这些虚拟化软件仅支持 x86 架构。虚拟化是逆向工程中非常有用的组件。事实上,大多数软件都是在 x86 架构下构建的。虚拟化通过虚拟机监控器使用主机计算机的 CPU 资源。

不幸的是,还有其他不支持虚拟化的 CPU 架构。VirtualBox 和 VMware 不支持这些架构。如果我们被给定了一个非 x86 可执行文件来处理,而我们所有的设备都只安装了 x86 操作系统,怎么办?嗯,这并不会阻止我们进行逆向工程。

为了解决这个问题,我们将使用模拟器。模拟器早在虚拟机监控器引入之前就已经存在。模拟器本质上是模拟一个 CPU 机器。把它当作一台新机器,运行在非 x86 架构上的操作系统可以被部署。然后,我们就可以运行原生的可执行文件。

在本章中,我们将学习如何使用 QEMU 部署非 x86 操作系统。我们还将学习如何使用 Bochs 模拟 x86 计算机的启动过程。

模拟

模拟的魅力在于它可以欺骗操作系统,让操作系统认为它在某种 CPU 架构上运行。缺点是性能明显较慢,因为几乎每一条指令都需要解释执行。简要说明 CPU,有两种 CPU 架构设计:复杂指令集计算CISC)和简化指令集计算RISC)。在汇编编程中,CISC 只需要少量指令。例如,一个单一的算术指令,如 MUL,会在其内部执行更低级的指令。而在 RISC 中,低级程序需要仔细优化。实际上,CISC 的优点在于需要较少的内存空间,但每条指令的执行时间较长。另一方面,RISC 由于以简化的方式执行指令,因此具有更好的性能。然而,如果代码没有得到适当的优化,针对 RISC 构建的程序可能无法达到预期的执行速度,且可能会占用较多空间。高级编译器应该能够优化 RISC 的低级代码。

这里有一个简短的 CPU 架构列表,按照 CISC 和 RISC 分类:

  • CISC:

    • 摩托罗拉 68000

    • x86

    • z/Architecture

  • RISC:

    • ARM

    • ETRAX CRIS

    • DEC Alpha

    • LatticeMico32

    • MIPS

    • MicroBlaze

    • Nios II

    • OpenRISC

    • PowerPC

    • SPARC

    • SuperH

    • 惠普 PA-RISC

    • 英飞凌 TriCore

    • UNICORE

    • Xtensa

在 CISC 和 RISC 架构中,x86 和 ARM 都非常流行。x86 由 Intel 和 AMD 的计算机使用,目的是减少程序使用的指令数。新的设备,如智能手机和其他移动设备,采用 ARM 架构,因为它具有低功耗和高性能的优势。

本章讨论的目的是在 x86 机器上模拟 ARM 架构。我们选择 ARM 架构,因为它目前是手持设备中最常用的处理器。

在 x86 主机上模拟 Windows 和 Linux

我们解释了在虚拟机上安装操作系统时,它遵循主机机器的架构。例如,Windows x86 版本只能安装在安装在 x86 机器上的虚拟机上。

许多 Linux 操作系统,包括 Arch Linux、Debian、Fedora 和 Ubuntu,都支持在 ARM 处理器上运行。另一方面,Windows RT 和 Windows Mobile 是为使用 ARM CPU 的设备构建的。

由于我们在使用 x86 处理器的 PC 上工作,分析一个非 x86 架构的可执行文件仍然遵循相同的静态和动态分析逆向工程概念。唯一的不同是,我们需要为可执行文件运行设置环境,并学习可以在这个模拟环境中使用的工具。

模拟器

我们将介绍两种最流行的模拟器:QEMU(快速模拟器)和 Bochs。

QEMU 因其支持多种架构(包括 x86 和 ARM)而被认为是最广泛使用的模拟器。它还可以安装在 Windows、Linux 和 macOS 上。QEMU 通过命令行使用,但也有可用的 GUI 工具,如 virt-manager,可以帮助设置和管理来宾操作系统镜像。然而,virt-manager 仅适用于 Linux 主机。

Bochs 是另一种模拟器,但仅支持 x86 架构。值得一提的是,这个模拟器用于调试内存引导记录MBR)代码。

在不熟悉的环境中进行分析

在这里,逆向工程概念是相同的。然而,工具的可用性是有限的。静态分析仍然可以在 x86 环境中进行,但当我们需要执行文件时,它将需要沙箱模拟。

最好在模拟环境中本地调试本地可执行文件。但如果本地调试条件有限,另一种选择是进行远程调试。对于 Windows,最常用的远程调试工具是 Windbg 和 IDA Pro。对于 Linux,我们通常使用 GDB。

分析 ARM 编译的可执行文件与分析 x86 可执行文件的过程差别不大。我们遵循与 x86 相同的步骤:

  1. 学习 ARM 低级语言

  2. 使用反汇编工具进行死机列表分析

  3. 在操作系统环境中调试程序

学习 ARM 低级语言的方式与我们学习 x86 指令的方式相同。我们只需要理解内存地址空间、通用寄存器、特殊寄存器、栈和语言语法。这还包括如何调用 API 函数。

可以使用 IDA Pro 等工具,以及其他 ARM 反汇编工具,来显示本地 ARM 可执行文件的 ARM 反汇编代码。

QEMU 中的 Linux ARM 客户机

Linux ARM 可以安装在 QEMU 的 ARM CPU 客户机中,该客户机运行在 Windows 系统的 x86 CPU 上。那么,让我们直接开始部署 Arch Linux ARM 吧。由于有很多可供下载的资源,运行 Arch Linux 实例作为 QEMU 客户机并不难。为了演示,我们将使用一个预先安装的 Arch Linux 镜像并在 QEMU 中运行它。准备下载以下文件:

在本书中,我们将在 Windows 主机上安装 QEMU。在安装过程中,注意 QEMU 的安装位置。这一点尤其重要,因为 QEMU 的路径将在后续使用。

archlinuxarm-29-04-2012.img.zip 的镜像文件解压到一个新目录中,并将 zImage-devtmpfs 复制到同一目录下。

打开镜像和内核文件所在目录的命令行。然后,执行以下命令:

"c:\Program Files\qemu\qemu-system-arm.exe" -M versatilepb -cpu arm1136-r2 -hda archlinuxarm-29-04-2012.img -kernel zImage-devtmpfs -m 192 -append "root=/dev/sda2" -vga std -net nic -net user

在这里,将 C:\Program Files\qemu 更改为 QEMU 安装的路径。这应该会启动 QEMU 并运行 Arch Linux,如下所示:

现在,使用以下凭据登录:

alarmpi login: root
Password: root

你可以像使用常规的 Linux 控制台一样进行操作。Arch Linux 是一款由 Raspberry Pi 爱好者安装的流行操作系统。

使用 Bochs 进行 MBR 调试

当我们开启计算机时,首先执行的代码来自 BIOS(基本输入输出系统),它是嵌入在 CPU 中的程序。它执行一个开机自检(POST),以确保连接的硬件正常工作。BIOS 将主引导记录(MBR)加载到内存中,然后将代码执行传递下去。主引导记录(MBR)是从指定启动磁盘的第一个磁盘扇区读取的。MBR 包含引导加载程序,负责加载操作系统。

例如,如果我们想要调试给定的 MBR 镜像,我们可以使用一个名为 Bochs 的模拟器来进行调试。Bochs 可以从 bochs.sourceforge.net/ 下载。

为了测试这个,我们提供了一个可以从 github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch8/mbrdemo.zip 下载的磁盘镜像。这个 ZIP 压缩包解压后约为 10MB,文件中包含 mre.bin 磁盘镜像和将传递给 Bochs 的 bochsrc 配置文件。

如果我们使用 IDA Pro 打开 mre.bin,应该能够静态分析 MBR 代码。MBR 几乎总是从 0x7c00 地址开始。它是一个 16 位代码,使用硬件中断来控制计算机。

在 IDA Pro 中加载文件时,请确保将加载偏移量更改为0x7c00,如下面的截图所示:

当询问反汇编模式时,选择 16 位模式。由于一切仍然是未定义的,我们需要将数据转换为代码。选择第一个字节代码,右键单击以打开上下文菜单,然后选择 Code,如下所示:

当转换为反汇编代码时,我们可以看到 IDA Pro 也能识别中断函数及其使用方式。以下截图展示了 16 位的反汇编代码,以及使用中断 13h 从磁盘扇区读取数据:

要使用 Bochs 调试 MBR,我们必须确保 bochsrc 文件中包含以下行:

display_library: win32, options="gui_debug"

这一行启用了 Bochs 图形界面调试器的使用。

如果我们有不同的磁盘镜像,可以在 at0-master 行中更改磁盘镜像文件的文件名。在这个演示中,磁盘镜像的文件名是 mre.bin

ata0-master: type=disk, path="mre.bin", mode=flat

要模拟该磁盘镜像,执行以下命令:

set $BXSHARE=C:\Program Files (x86)\Bochs-2.6.8
"C:\Program Files (x86)\Bochs-2.6.8\bochsdbg.exe" -q -f bochsrc

你可能需要将 C:\Program files (x86)\Bochs-2.6.8 更改为你安装 Bochs 的路径。请注意,对于 $BXSHARE 环境变量,没有引号。

这里 Bochs 是在 Windows 环境下安装的。如果在 Linux 环境下工作,可以更改路径。

一旦运行,控制台将会显示日志输出,如下所示:

这将打开调试控制台,界面应如下面的截图所示:

另一个显示输出的窗口也应该出现:

MBR 代码从 0x7c00 地址开始。我们需要在 0x7c00 设置一个断点。Bochs 的图形界面有一个命令行,我们可以在这里设置指定地址的断点。它位于窗口的底部。请参阅下面截图中高亮的区域:

要在 0x7c00 设置断点,请输入 lb 0x7c00。要查看命令列表,请输入 help。常用的命令如下:

c             Continue/Run
Ctrl-C        Break current execution
s [count]     Step.  count is the number of instructions to step
lb address    Set breakpoint at address
bpe n         Enable breakpoint where n is the breakpoint number
bpd n         Disable breakpoint where n is the breakpoint number
del n         Delete breakpoint where n is the breakpoint number
info break    To list the breakpoints and its respective numbers 

GUI 界面还将键盘按键与命令进行了映射。选择命令菜单查看这些按键。

按下F5继续代码执行,直到它到达0x7c00处的 MBR 代码。我们现在应该能看到与在 IDA Pro 中看到的相同的反汇编代码。然后我们可以开始按F11逐步调试每一行指令:

在某个时刻,代码将进入无限循环状态。如果我们查看输出窗口,最终结果应该会显示相同的消息,如以下截图所示:

总结

在本章中,我们了解到,即使文件不是 Windows 或 Linux x86 本地可执行文件,我们仍然可以分析非 x86 可执行文件。仅通过静态分析,我们可以分析一个文件,而无需进行动态分析,尽管我们仍然需要参考资料来理解非 x86 架构的低级语言,这些架构被分类为 RISC 或 CISC。正如我们学习 x86 汇编语言一样,像 ARM 汇编语言这样的语言也可以通过相同的概念来学习。

然而,通过实际代码执行,使用动态分析仍然可以证明分析的有效性。为此,我们需要设置一个可以本地运行可执行文件的环境。我们介绍了一种名为 QEMU 的仿真工具,它可以为我们完成这项工作。它支持多种架构,包括 ARM。今天,使用 ARM 架构的最流行操作系统之一是 Arch Linux。这个操作系统通常由树莓派爱好者部署。

我们还学习了如何调试从磁盘镜像中提取的 MBR 代码。通过使用 Bochs,一个能够模拟 x86 系统启动顺序的工具,我们能够演示如何加载和调试使用硬件中断的 16 位代码。此外,一些勒索软件采用了可以注入或替换 MBR 为恶意代码的功能。通过我们在本章学到的内容,没有什么可以阻止我们逆向这些代码片段。

进一步阅读

第九章:二进制混淆技术

二进制混淆是一种使程序代码难以理解或逆向的技术。它也用于隐藏数据,以防止数据轻易被查看。它可以被归类为一种逆向防护技术,通过增加逆向处理时间来提高难度。混淆还可以使用加密和解密算法,以及其硬编码或代码生成的密码密钥。

本章将讨论数据和代码如何被混淆。我们将展示如何在示例中应用混淆,包括简单的 XOR、简单的算术运算、在堆栈中构建数据,以及关于多态性和变形代码的讨论。

在恶意软件的世界里,二进制混淆是病毒常用的一种技术,旨在击败基于签名的防病毒软件。当病毒感染文件时,它会通过多态性或变形来混淆其代码。

本章将实现以下学习目标:

  • 确定正在堆栈上组装的数据

  • 确定数据在使用前是否经过 XOR 或解混淆

  • 修改文本或其他段中的数据,并在堆上组装

堆栈上的数据组装

堆栈是一个内存空间,可以在其中存储任何数据。可以使用堆栈指针寄存器来访问堆栈(对于 32 位地址空间,使用 ESP 寄存器)。让我们考虑以下代码片段的示例:

push 0
push 21646c72h
push 6f57206fh
push 6c6c6548h
mov eax, esp
push 74h
push 6B636150h
mov edx, esp
push 0
push eax
push edx
push 0
mov eax, <user32.MessageBoxA>
call eax

这最终会显示以下消息框:

在没有引用可见文本字符串的情况下,为什么会发生这种情况?在调用MessageBoxA函数之前,堆栈看起来是这样的:

这些 push 指令将以 null 终止的消息文本组装到堆栈中。

push 0
push 21646c72h
push 6f57206fh
push 6c6c6548h

另一字符串是通过这些 push 指令组装的:

push 74h
push 6B636150h

实际上,堆栈转储将如下所示。

每次字符串组装后,寄存器 ESP 的值会存储到 EAX 中,然后是 EDX。也就是说,EAX 指向第一个字符串的地址,EDX 指向第二个组装字符串的地址。

MessageBoxA接受四个参数。第二个参数是消息文本,第三个参数是标题文本。从上面的堆栈转储中,字符串位于地址0x22FE500x22FE54

push 0
push eax
push edx
push 0
mov eax, <user32.MessageBoxA>

MessageBoxA已经具备了所需的所有参数。尽管字符串是在堆栈上组装的,但只要数据可以访问,就可以使用它。

代码组装

代码方面也可以使用相同的概念。这是另一个代码片段:

push c3
push 57006a52
push 50006ad4
push 8b6b6361
push 5068746a
push c48b6c6c
push 6548686f
push 57206f68
push 21646c72
push 68006a5f
mov eax, esp
call eax
mov eax, <user32.MessageBoxA>
call eax

这会产生与之前相同的消息框。不同之处在于,这段代码将opcode字节推送到堆栈中,并将代码执行传递给它。在进入第一个call eax指令之后,堆栈将如下所示:

记住,栈顶的值应包含call指令设置的返回地址。到目前为止,我们的指令指针应该在这里:

pop edi指令将返回地址存储到EDI寄存器中。组装消息文本的同一指令集在这里也被使用。最后,执行push edi,然后是ret指令,应该会返回到返回地址。

结果栈应该是这样的:

接下来是一些调用MessageBoxA的指令。

在栈中运行代码的这种技术被许多恶意软件采用,包括软件漏洞利用。作为防止恶意软件代码执行的措施,一些操作系统已经发布了安全更新,以禁止栈中代码的执行。

加密数据识别

杀毒软件的主要功能之一是通过签名检测恶意软件。签名是恶意软件特有的一组字节序列。虽然现在这种检测技术被认为对杀毒软件不再有效,但它仍然在文件检测中扮演着重要角色,尤其是在操作系统断网时。

简单的签名检测可以通过加密恶意软件的数据和/或代码轻松破解。这样,新的签名将从加密数据的独特部分生成。攻击者可以简单地使用不同的密钥重新加密相同的恶意软件,从而生成另一个签名。但恶意软件的行为依然保持不变。

当然,杀毒软件已经做出了很大改进来应对这种技术,使得签名检测成为过去的技术。

另一方面,这是一种混淆技术,增加了逆向软件的额外时间消耗。在静态分析下,识别加密数据和解密例程能让我们了解在分析过程中应该预期什么,尤其是在调试时。首先,我们将查看几个代码片段。

循环代码

通过检查在循环中运行的代码,可以轻松识别解密过程:

  mov ecx, 0x10
  mov esi, 0x00402000
loc_00401000:
  mov al, [esi]
  sub al, 0x20
  mov [esi], al
  inc esi
  dec ecx
  jnz loc_00401000

这个循环代码由条件跳转控制。要识别解密或加密代码,它应该有一个源地址和一个目的地址。在这段代码中,源地址从0x00402000开始,目的地址也位于相同的地址。数据中的每个字节都会被算法修改。在这个例子中,算法是从字节中减去0x20。只有在0x10字节数据被修改时,循环才会结束。0x20被确定为加密/解密密钥。

算法可以有所不同,可以使用标准算术和二进制运算,或仅使用标准算术。只要源数据在循环中被修改并写入目的地,我们就可以说已识别出加密例程。

简单算术

除了使用按位操作外,基本的数学运算也可以使用。如果加法有一个对应的减法运算,我们可以使用加法对文件进行加密,并使用减法解密,反之亦然。以下代码演示了使用加法进行解密:

 mov ecx, 0x10
 mov esi, 0x00402000
loc_00401000:
 mov al, [esi]
 add al, 0x10
 mov [esi], al
 inc esi
 dec ecx
 jnz loc_00401000

字节值的美妙之处在于它们可以作为有符号数字处理,例如,给定这组加密信息:

data = 0x00, 0x01, 0x02, 0x0a, 0x10, 0x1A, 0xFE, 0xFF
 key = 0x11
 encrypt algorithm = byte subtraction
 decrypt algorithm = byte addition

每个字节都减去0x11后,加密数据将如下所示:

encrypted data = 0xEF, 0xF0, 0xF1, 0xF9, 0xFF, 0x09, 0xED, 0xEE

为了恢复它,我们需要加回之前减去的相同值0x11

decrypted data = 0x00, 0x01, 0x02, 0x0a, 0x10, 0x1A, 0xFE, 0xFF

如果我们查看前面字节在无符号和有符号形式下的等效十进制值,数据将如下所示:

data (unsigned) = 0, 1, 2, 10, 16, 26, 254, 255
data (signed) = 0, 1, 2, 10, 16, 26, -2, -1

这是以十进制值显示的加密数据:

encrypted data (unsigned) = 239, 240, 241, 249, 255, 9, 237, 238
encrypted data (signed) = -17, -16, -15, -7, -1, 9, -19, -18

总结一下,如果我们使用基本的算术运算,我们应该以值的有符号形式来看待它。

简单的 XOR 解密

XOR 是软件加密中最常用的操作符。如果我们要修改前面代码片段中的代码算法,它会变成这样:

 mov ecx, 0x10
 mov esi, 0x00402000
loc_00401000:
 mov al, [esi]
 xor al, 0x20
 mov [esi], al
 inc esi
 dec ecx
 jnz loc_00401000

它之所以受欢迎,是因为相同的算法可以用来加密和解密数据。使用相同的密钥,XOR可以恢复原始数据。与使用SUB时不同,数据恢复的对应操作需要使用ADD算法。

这里有一个快速演示:

Encryption using the key 0x20:
  data:  0x46 = 01000110b
   key:  0x20 = 00100000b
0x46 XOR 0x20 = 01100110b = 0x66

Decryption using the same key:
  data:  0x66 = 01100110b
   key:  0x20 = 00100000b
0x66 XOR 0x20 = 01000110b = 0x46

在其他内存区域装配数据

可以在进程的图像空间之外的其他内存区域执行数据。类似于如何在堆栈空间执行代码,堆和新分配的内存空间等内存区域,可以用来操控数据并执行代码。这是一种不仅恶意软件,甚至合法应用程序也常用的技术。

访问堆需要调用 API,如HeapAlloc(Windows)或通常的malloc(Windows 和 Linux)。每个创建的进程都会分配一个默认的堆空间。Heap通常用于请求一小块内存空间。堆的最大大小在操作系统之间有所不同。如果请求的内存空间大小无法适配当前堆空间,HeapAllocmalloc会内部调用VirtualAlloc(Windows)或sbrk(Linux)函数。这些函数会直接向操作系统的内存管理器请求内存空间。

分配的内存空间有明确的访问权限。就像程序的各个段一样,它们通常有读取、写入和执行权限。如果该区域需要执行代码,则应设置读取和执行权限。

查看以下代码片段,它实现了将数据解密到堆空间:

                call GetProcessHeap
                push 1000h             ; dwBytes
                mov edi, eax
                push 8 ; dwFlags
                push edi               ; hHeap
                call HeapAlloc
                push 1BEh              ; Size
                mov esi, eax
                push offset unk_403018 ; Src
                push esi               ; Dst
                call memcpy
                add esp, 0Ch
                xor ecx, ecx
                nop
loc_401030:
                xor byte ptr [ecx+esi], 58h
                inc ecx
                cmp ecx, 1BEh
                jl short loc_401030

该代码分配了1000h字节的堆空间,然后将1BEh字节的数据从地址0x00403018复制到分配的堆空间中。解密循环可以在这段代码中轻松识别出来。

算法使用XOR,密钥值为58h。数据大小为1BEh,数据直接更新到相同的已分配堆空间中。迭代由ECX寄存器控制,而加密数据的位置(在堆地址处)存储在ESI寄存器中。

让我们看看在调试工具的帮助下,哪些内容被解密。

使用 x86dbg 解密

上述代码片段来自HeapDemo.exe文件。你可以从github.com/PacktPublishing/Mastering-Reverse-Engineering/tree/master/ch9下载该文件。开始使用x86dbg调试该文件吧。该截图展示了在x86dbg中加载文件后的WinMain函数反汇编代码:

从可执行文件的代码入口点开始,我们遇到了通过GetProcessHeapRtlAllocateHeap API 进行的堆分配。接着使用了_memcpy函数,它将0x1BE字节的数据从heapdemo.enc指定的地址复制过来。让我们来看看heapdemo.enc的内存转储。为此,右键点击push <heapdemo.enc>,然后选择“Follow in Dump”。点击给定地址,而不是选中的地址。这应该会改变当前聚焦的Dump窗口中的内容:

这应该是下一行代码将在循环中解密的数据。我们还应该能在执行_memcpy后,在已分配的堆空间中看到相同的加密数据。已分配堆空间的地址应仍然存储在寄存器ESI中。在显示寄存器和标志列表的窗口中右键点击ESI寄存器的值,然后选择“Follow in Dump”。这将显示堆地址空间中相同的数据内容。下图显示的转储是加密数据:

接下来是有趣的部分——解密。在查看堆的转储时,继续执行调试步骤。你应该注意到随着xor byte ptr ds:[ecx+esi], 58指令的执行,值会发生变化:

由于逐步调试这些字节 0x1BE 次会非常繁琐,我们可以简单地在jl指令后的行设置断点,然后按F9继续运行指令。这应该会生成这个解密后的转储:

继续调试代码;它的结束通过清理分配的堆并退出进程来完成。使用HeapFree API 来释放已分配的堆。通常,使用ExitProcess API 来退出程序。这次,它使用GetCurrentProcessTerminateProcess来执行这一操作。

其他混淆技术

我们讨论的混淆技术是基于使用简单的加密方法来隐藏实际的字符串和代码。然而,还有其他方法可以混淆代码。只要有阻止数据和代码轻易提取与分析的概念,混淆就会发生。让我们再讨论一些混淆技术。

控制流平坦化混淆

控制流平坦化的目的是让简单的代码看起来像一组复杂的条件跳转。我们来考虑这段简单的代码:

    cmp byte ptr [esi], 0x20
    jz loc_00EB100C
    mov eax, 0
    jmp loc_00EB1011
loc_00EB100C:
    mov eax, 1
loc_00EB1011:
    test eax, eax
    ret

当使用控制流平坦化方法进行混淆时,代码将像这样:

    mov ecx, 1
    mov ebx, 0                ; initial value of control variable
loc_00EB100A:
    test ecx, ecx
    jz loc_00EB103C           ; jump will never happen, an endless loop
loc_00EB100E:
    cmp ebx, 0                ; is control variable equal to 0?
    jnz loc_00EB102B
loc_00EB1013:
    cmp byte ptr [esi], 0x20
    jnz loc_00EB1024
loc_00EB1018:
    mov eax, 0
    mov ebx, 2
    jmp loc_00EB103E
loc_00EB1024:
    mov ebx, 1                ; set control variable to 1
    jmp loc_00EB103E
loc_00EB102B:
    cmp ebx, 1                ; is control variable equal to 1?
    jnz loc_00EB103C
loc_00EB1030:
    mov eax, 1
    mov ebx, 2                ; set control variable to 2
    jmp loc_00EB103E
loc_00EB103C:
    jmp loc_00EB1040          ; exit loop
loc_00EB103E:
    jmp loc_00EB100A          ; loop back
loc_00EB1040:
    test eax, eax
    ret

混淆后的代码最终将与原始代码产生相同的结果。在控制流平坦化混淆中,代码的流动是由一个控制变量引导的。在前面的代码中,控制变量是EBX寄存器。为了直观地查看差异,下面是原始代码的样子:

这是应用混淆后的代码样子:

代码被放置在一个循环中,并通过控制变量EBX寄存器中的值来控制。每个代码块都有一个 ID。在离开第一个代码块之前,控制变量会设置为第二个代码块的 ID。流再一次循环,进入第二个代码块,离开之前会设置为第三个代码块的 ID。这个过程会持续下去,直到执行到最后一个代码块。代码块中的条件可以设置控制变量,以选择下一个要跳转的代码块。在我们之前的代码中,循环只会执行两次就结束。

看看前面的两个图表,我们可以看到一个简单的代码在混淆后如何变得复杂。作为一个逆向工程师,挑战在于如何将复杂的代码还原为更易理解的代码。这里的技巧是识别是否存在一个控制变量。

垃圾代码插入

垃圾代码插入是一种廉价的让代码看起来复杂的方法。代码中简单地插入一些实际上没有任何作用的代码或代码序列。在下面的代码片段中,尝试识别所有的垃圾代码:

    mov eax, [esi]
    pushad
    popad
    xor eax, ffff0000h
    nop
    call loc_004017f
    shr eax, 4
    add ebx, 34h
    sub ebx, 34h
    push eax
    ror eax, 5
    and eax, 0ffffh
    pop eax
    jmp loc_0040180
loc_004017f:
    ret

去除垃圾代码后,代码应该简化成这样:

    mov eax, [esi]
    xor eax, ffff0000h
    shr eax, 4
    jmp loc_0040180

很多恶意软件使用这种技术快速生成其自身代码的变种。它可能增加代码的大小,但结果是使其无法被基于签名的反恶意软件检测到。

使用形态变换引擎的代码混淆

一个程序可以用不同的方式编码。要“增加一个变量的值”意味着给它加一。在汇编语言中,INC EAX也等同于ADD EAX, 1。用等效指令替换相同指令或指令集的概念与形态变换相关。

这里有几个可以互换的代码示例:

|

mov eax, 78h

|

push 78h
pop eax

|

|

mov cl, 4
mul cl

|

shl eax, 2

|

|

jmp 00401000h

|

push 00401000h
ret

|

|

xchg eax, edx

|

xor eax, edx
xor edx, eax
xor eax, edx

|

|

rol eax, 7

|

push ebx
mov ebx, eax
shl eax, 7
shr ebx, 25
or eax, ebx
pop ebx

|

|

push 1234h

|

sub esp, 4
mov [esp], 1234h

|

这个概念最初出现在计算机病毒中,这些病毒能够感染具有不同代际的文件。引入这一概念的计算机病毒包括 Zmist、Ghost、Zperm 和 Regswap。这些病毒中的变形引擎面临的挑战是使感染的文件依然像原始文件一样正常工作,并防止它们被破坏。

那么,变形代码和多态代码有何不同呢?首先,这两种技术都是为了阻止反病毒软件检测多个代际的恶意软件。反病毒软件通常通过签名来检测恶意软件。这些签名是恶意文件中的独特字节序列。为了防止反病毒软件进一步检测,使用加密来隐藏整个病毒代码或其中的部分代码。桩代码负责解密病毒的自加密代码。以下图示展示了多态病毒的文件代际表示:

如我们所见,桩代码通常带有相同的代码,但密钥发生变化。这使得加密后的代码与前一代有所不同。在前面的图示中,我们通过改变加密代码的颜色来表示这种差异。如果代码涉及解密和加密,它可以被称为多态代码。一些反病毒软件使用代码仿真或添加特定的解密算法来解密病毒代码,从而使签名得以匹配并进行检测。

对于变形代码,不涉及加密。这个概念是用不同的代码替换原有代码,且实现相同的行为。每一代病毒代码都会发生变化。多态代码很容易被识别,因为它有固定的桩代码。但是,变形代码的识别几乎是不可能的,因为它看起来就像一段普通的代码。以下是变形代码的文件代际表示:

所有这些变形代际都会产生相同的结果,保持其代码序列不变。反病毒软件的签名很难检测到变形病毒,因为代码本身会发生变化。变形代码只能通过比较两个变体来识别。在变形病毒中,生成新代码的过程涉及变形引擎,该引擎与代码本身一起存在。即使是引擎的代码行本身也可以被修改。

动态库加载

在静态分析过程中,我们可以立即看到可供程序使用的导入函数。可能在导入表中只看到两个 API 函数,但程序却使用了几十个 API。在 Windows 系统中,这两个 API 函数是LoadLibraryGetProcAddress,而在 Linux 系统中,则是dlopendlsym

LoadLibrary 只需要目标 API 函数所在库的名称。GetProcAddress 负责从库中检索该 API 名称对应的 API 函数的地址。库加载完成后,程序就可以通过 API 的地址来调用 API 函数。

以下代码片段演示了如何进行动态库加载。最终代码会显示一个 "hello world 消息框:


; code in the .text section
push 00403000h
call LoadLibrary
push 00403010h
push eax
call GetProcAddress
push 0
push 00403030h
push 00403020h
push 0
call eax              ; USER32!MessageBoxA

; data in the .data section
00403000h "USER32.DLL", 0
00403010h "MessageBoxA", 0
00403020h "Hello World!", 0
00403030h "Packt Demo", 0

一些程序会加密文本字符串,包括 API 函数的名称,并在运行时解密,之后才进行动态导入。这可以防止像 StringsBinText 这样的工具列出程序可能使用的 API。分析人员在进行调试时,可以看到这些加载的函数。

使用 PEB 信息

进程环境块PEB)包含关于正在运行的进程的有用信息。这包括为进程加载的模块列表、结构化错误处理程序SEH)链,甚至程序的命令行参数。在这里,混淆技术直接从 PEB 读取这些信息,而不是使用如 GetCommandLineIsDebuggerPresent 等 API 函数。

例如,IsDebuggerPresent API 包含以下代码:

单独使用以下代码将返回10的值,保存在EAX寄存器中。它位于 FS 段中,其中包含PEB线程信息块TIB)。这段代码显示调试标志可以在PEB的偏移量2处找到。

mov eax, large fs:30h
movzx eax, byte ptr [eax+2]

混淆的实现方式有很多种。它可以根据开发者的创造力来实现。只要隐蔽显而易见的目标存在,它就能让逆向工程师难以分析二进制文件。对各种混淆技术的更好理解,肯定能帮助我们克服在逆向过程中分析复杂代码的难题。

总结

在本章中,我们了解了混淆的含义。作为一种隐藏数据的手段,简单的加密学是最常用的技术之一。识别简单的解密算法需要寻找密码密钥、待解密数据和数据的大小。在识别这些解密参数后,我们只需要在解密代码的退出点设置断点。我们还可以通过调试工具的内存转储来监控解密后的代码。

我们列举了一些混淆中使用的方法,比如控制流扁平化、垃圾代码插入、形态变换代码、动态导入 API 函数以及直接访问进程信息块。识别混淆代码和数据有助于我们克服分析复杂代码的难题。混淆技术的引入是为了隐藏信息。

在下一章,我们将继续介绍相同的概念,特别是我们将探讨它们是如何通过使用 Packer 工具和加密技术在可执行文件中实现的。

第十章:打包和加密

作为我们学习混淆的延续,我们现在将介绍一组工具,这些工具被分类用于防止软件被逆向工程。使用这些工具(如打包器和加密器)的结果是将原始可执行文件转换成一个新的版本,而新版本的文件仍然完全保持原有的代码行为流。根据所使用的工具,我们将讨论转换后的可执行文件会是什么样子,以及转换后的文件是如何执行的。

我们选择了 UPX 工具来演示打包器如何在低级别上工作,并展示可以用来反向工程的技术。

互联网上有很多免费的打包器,通常被恶意作者用来打包他们的软件(如 fsg、yoda、aspack),但为了简便起见,我们将重点介绍最简单的 UPX。

本章将以 Windows 作为我们的环境,并使用x86DbgOllyDbg进行调试。我们还将展示如何使用 Volatility 工具。我们会涉及脚本语言中的混淆,并使用一些 Cyber Chef 来解密数据。

本章将涵盖以下主题:

  • 使用 UPX 工具解包

  • 识别解包存根,并使用调试器设置断点以提取内存

  • 转储内存,提取在内存中执行的程序

  • 使用可执行文件中的密钥识别和解密段

回顾原生可执行文件如何被操作系统加载

为了更好地理解打包程序如何修改文件,我们先快速回顾一下操作系统如何加载可执行文件。原生可执行文件通常被称为 Windows 的 PE 文件和 Linux 的 ELF 文件。这些文件被编译成低级格式;也就是说,使用类似于x86指令的汇编语言。每个可执行文件都由头部、代码段、数据段和其他相关部分组成。代码段包含实际的低级指令代码,而数据段包含代码使用的实际数据。头部包含关于文件、各个段以及文件如何映射为内存中的进程的信息。以下图示展示了这一过程:

头部信息可以分为原始信息和虚拟信息。原始信息包含关于物理文件的相关信息,如文件偏移量和大小。偏移量是相对于文件偏移量 0 的。虚拟信息则包含关于进程中内存偏移的相关信息,虚拟偏移通常是相对于图像基址的,图像基址是进程映像在内存中的起始位置。图像基址是操作系统分配的进程空间中的一个地址。基本上,头部信息告诉我们操作系统应如何将文件(原始)及其各个部分映射到内存(虚拟)。此外,每个部分都有一个属性,告诉我们该部分是否可以用于读取、写入或执行。在第四章,静态与动态逆向分析中,我们在“进程的内存区域与映射”一节中展示了如何将原始文件映射到虚拟内存空间。下图显示了当磁盘上的文件(左)映射到虚拟内存空间(右)时的样子:

包含代码所需函数的库或模块也列在文件的一个部分中,该部分可以在代码和数据部分之外的其他部分看到。这部分称为导入表。它是一个 API 函数及其所属库的列表。文件映射后,操作系统在相同的进程空间中加载所有库。这些库的加载方式与可执行文件相同,但位于同一进程空间的较高内存区域。关于库加载位置的更多信息,请参阅第四章](1017358e-f842-4115-8779-f721299bbe3c.xhtml),静态与动态逆向分析中的“进程的内存区域与映射”部分。

当所有内容都正确映射并加载后,操作系统从头部信息中读取入口点地址,然后将代码执行传递到该地址。

文件中还有其他部分会使操作系统以特殊方式运行。例如,文件资源部分中包含的图标就是一个例子,它们会在文件资源管理器中显示。文件还可以包含数字签名,作为指示文件是否允许在操作系统中运行的标志。CFF Explorer 工具应该能帮助我们查看头部信息及这些部分,如下图所示:

到目前为止,我们已经涵盖了基础内容,但所有这些结构都已由微软和 Linux 社区进行良好的文档化。Windows PE 文件的结构可以在以下链接中找到:docs.microsoft.com/en-us/windows/desktop/debug/pe-format。而 Linux ELF 文件的结构可以在以下链接中找到:refspecs.linuxbase.org/elf/elf.pdf.

打包器、加密器、混淆器、保护器和自解压文件(SFX)

可执行文件可以通过打包、加密和混淆来保护其代码,但仍然保持可执行,且程序本身完好无损。这些技术主要旨在防止程序被反向工程。规则是,如果原始程序能够正常运行,那么它是可以被反向的。接下来我们将定义术语“宿主”或“原始程序”,指的是在文件被打包、加密、混淆或保护之前的可执行文件、数据或代码。

打包器或压缩器

打包器,也称为压缩器,是用于将宿主文件压缩为更小文件的工具。压缩数据的概念帮助我们减少传输数据时所需的时间。在混淆方面,压缩后的数据通常不会显示完整的可读文本。

在下图中,左侧窗格显示了压缩前的代码的二进制和数据,而右侧则显示其压缩后的形式。注意,压缩后的文本字符串并不完全显示出来:

由于代码和数据现在已被压缩,执行文件时需要一个解压缩的代码。这个代码被称为解压缩代码段。

在下图中,左侧显示的是文件的原始结构,其中程序的入口点位于代码段。一个可能的打包版本将会有一个新的结构(右侧),其中入口点位于解压缩代码段。

当打包的可执行文件被执行时,首先运行的是代码段,然后将代码执行权交给解压后的代码。文件头中的入口点应指向代码段的地址。

打包器减少了部分段的大小,因此必须修改文件头中的值。文件头中各段的原始位置和大小会被修改。事实上,一些打包器会将文件视为一个包含代码和数据的大段。诀窍是将这个大段设置为可读、可写且可执行。然而,这可能会带来错误处理不当的风险,尤其是当代码不小心写入一个应为只读的区域,或执行代码时访问了一个应为不可执行的区域。

打包文件的最终结果是保留宿主的行为完整,同时使打包文件的大小变小。

加密器

通过加密进行的混淆是由加密程序完成的。打包程序压缩段,而加密程序则加密段。与打包程序类似,加密程序也有一个存根用于解密加密的代码和数据。因此,加密程序可能会增加宿主文件的大小。

以下图像展示了一个由Yoda Crypter加密的文件:

段偏移量和大小已被保留,但已加密。存根被放置在一个新添加的名为*yC*的段中。如果我们比较原始操作码字节和加密后的字节,就会注意到操作码字节中有零字节分布。这是一个可以用来识别加密字节的特征。

打包程序和加密程序的另一个特征是它们如何导入 API 函数。使用 CFF Explorer 查看导入目录时,我们只看到两个导入的 API:LoadLibraryGetProcAddress。这两个函数都来自Kernel32.DLL,并且注意到它的名称使用了混合字符大小写:KeRnEl32.Dll,如下所示:

仅通过这两个 API 函数,它所需的每个功能都可以动态加载。

以下图像展示了GetProcAddress API:

以下图像展示了LoadLibrary API:

看存根时,我们预计它会包含一个包含解密算法的循环代码。以下图像展示了Yoda Crypter使用的解密算法:

混淆器

混淆器也被归类为代码修改器,它们在保留程序流程的同时更改代码的结构。在前一章中,我们介绍了控制流扁平化(CFF)技术。CFF 技术将小段代码转换为在循环中运行,并通过控制标志进行控制。然而,混淆不仅限于 CFF 技术。编译后的文件结构也可以被修改,特别是对于基于伪代码执行的程序,如 Visual Basic 和.NET 编译程序。

混淆的主要技术之一是使函数名变得模糊不清或加密,使反编译器无法正确识别函数。此类高阶混淆工具的例子有ObfuscarCryptoObfuscatorDotfuscator

变量名的重命名,使用随机生成的文本字符串,转换代码文本为十六进制文本,以及将文本分割供代码拼接,是用于脚本(如 JavaScript 和 Visual Basic 脚本)的一些混淆技术。

以下截图展示了一个使用在线混淆工具的混淆 JavaScript 代码示例:

原始代码在左侧,混淆后的版本在右侧。

保护程序

保护工具通过结合使用打包器和加密器,以及其他反调试特性来保护软件。受保护的软件通常有多层解压和解密过程,可能会使用像blowfishsha512bcrypt这样的加密算法。一些复杂的保护工具甚至使用自己的代码虚拟化技术,这类似于伪代码的概念。保护工具通常是商业销售的,并用于防止盗版。

Windows 可执行文件保护工具的示例包括ThemidaVMProtectEnigmaAsprotect

SFX 自解压归档

我们通常使用 ZIP 和 RAR 来归档文件。但是,你知道这些归档文件可以转换为自解压执行文件(SFX)吗?这些工具的目的在于轻松地为任何需要多个文件的软件生成安装程序,比如主程序及其依赖的库模块。SFX 归档中嵌入了一个 SFX 脚本。该脚本负责指示文件要解压到哪些目录。如下图所示:

通常,SFX 具有可以执行以下操作的脚本功能:

  • 提取归档文件

  • 从提取的文件中运行文件

  • 从系统中运行任何文件

  • 删除文件

  • 创建注册表项

  • 从互联网访问网站

  • 创建文件

基本上,它几乎可以做任何常规程序能对系统做的事情。SFX 工具的示例包括Winzip SFXRARSFXNSIS

解包

在这一阶段,使用x86dbg,我们将解包一个已压缩的可执行文件。在这个调试会话中,我们将解包一个 UPX 打包的文件。我们的目标是达到原始主机的入口点。除了这个 UPX 压缩文件外,我们在 GitHub 页面上还提供了可以用于练习的压缩样本。

UPX 工具

eXecutables的终极打包器,也称为 UPX,可以从upx.github.io/下载。该工具本身可以打包 Windows 可执行文件。它还能够恢复或解包 UPX 打包的文件。为了展示其功能,我们使用该工具对文件original.exe进行了操作,如下所示:

注意,在被打包后,原文件的大小已经减少。

通过打包器进行调试

打包器对文件做了重大修改,特别是在 PE 文件头中。为了更好地理解打包器如何工作,让我们比较主机和打包后的可执行文件版本。使用 CFF 工具,我们将检查头部的差异。

上图显示了原始版本和 UPX 压缩版本之间的 NT 头差异:

这里唯一的区别是节的数量,从四个减少到三个,如下例所示:

在前面的示例中的可选头比较中,变化如下:

  • SizeOfCode: 0x0C00 到 0x1000

  • SizeOfInitializedData: 0x0e000x5000

  • AddressOfEntryPoint: 0x157e0x6b90

  • BaseOfCode: 0x10000x6000

  • BaseOfData: 0x20000x7000

  • SizeOfImage: 0x50000x8000

  • SizeOfHeaders: 0x04000x1000

  • CheckSum: 0x4a920

下图展示了原始和 UPX 压缩后版本的数据目录表之间的对比。

前面的示例展示了数据目录中的变化:

  • 导入目录 RVA: 0x234c0x71b4

  • 导入目录大小: 0x00780x017c

  • 资源目录 RVA: 0x40000x7000

  • 资源目录大小: 0x01b00x01b4

  • 调试目录 RVA: 0x21100

  • 调试目录大小: 0x001c0

  • 配置目录 RVA: 0x22400x6d20

  • 配置目录大小: 0x400x48

  • 导入地址目录 RVA: 0x20000

  • 导入地址目录大小: 0xf40

下面的图片展示了原始程序和 UPX 压缩后版本之间头部节区的对比。

前面的示例展示了在 UPX 压缩版本中,原始节区头部几乎所有信息都发生了变化。原始和虚拟偏移量、大小及特性都已变化。

对于 UPX0 节区,Characteristics 字段中位标志的含义在下面的示例中列出:

以下示例显示,虽然导入的 API 函数数量减少,但原始的静态导入库文件依然保持不变:

下图展示了将要为 KERNEL32.dll 导入的 API 函数。它们拥有完全不同的 API 函数:

至于资源目录的内容,似乎大小没有变化,唯一的变化是偏移量,以下示例中可以看到这一点:

以下列表展示了在压缩文件中基于哪些特征所做的更改:

  • 有三个节区,即 UPX0UPx1.rsrc

    • UPX0具有虚拟节区属性,但没有原始节区属性。这仅意味着该节区将由操作系统分配,但不会从文件中映射数据到该节区。该节区设置了读、写和执行标志。

    • 入口点地址位于 UPX1 节区内。存根应位于此节区内,并且压缩的代码和数据也应存放在此处。

    • .rsrc 节区似乎保留了其内容。保留资源节区仍能提供操作系统文件浏览器读取的正确图标和程序详细信息。

  • 由于打包程序具有自己结构,导致节区发生了重大变化,像 BaseOfCodeBaseOfData 这样的头部字段已完全修改。

  • 虚拟大小是基于SectionAlignment对齐的。例如,.rsrc的虚拟大小最初为0x1b0,通过与SectionAlignment对齐,使其变为0x1000

  • 由于打包器插入了一个存根,ImageSize已经增加。

入口点是ImageBaseAddressOfEntryPoint之和。原始入口点位于0x0040157e。该地址位于UPX0区段范围内,UPX00x00401000开始,大小为0x5000。存根位于打包文件的入口点,位于UPX1区段内。我们期望的结果是,打包器解压代码,动态导入 API 函数,最后将代码执行传递给原始入口点。为了加快调试,我们应该寻找一条或一组指令,将执行传递到0x0040157e,即原始入口点。

让我们通过在x86dbg中打开upxed.exe来观察这一过程。我们从入口点0x00406b90开始,如下图所示:

操作系统将文件映射到内存,并且所有虚拟区段也都已分配。第一条指令使用pushad保存所有初始标志状态。如果它保存了所有标志,那么在跳转到原始入口点之前,应该恢复这些标志。接下来的指令将地址0x00406000存储到寄存器esi中。这个地址是UPX1区段的起始位置,压缩数据就在这里。下一行将0x00401000存储到寄存器edi中。可以清楚地看出,压缩数据将从esi解压到edi。开启调试后,解压代码位于0x00406b910x00406c5d之间。

0x00406c62处放置断点之前,设置一个地址为0x00401000的转储窗口。这将帮助我们查看主机的解压部分。一直运行代码直到0x00406c62,应完成解压过程。下图展示了这一过程:

下一组指令修复使用相对跳转地址的调用指令。该代码从0x00406c65运行到0x00406c94。只需放置另一个断点,或者使用“运行直到”选项,选择在0x00406c96这一行,便可通过这段修复调用的代码循环。

接下来的几行是打包器动态加载主机使用的 API 函数的部分。代码将0x00405000存储到寄存器edi中。这个地址包含了数据,可以在其中找到原始模块的名称列表以及与每个模块相关的 API 函数名称。

对于每个模块名称,它使用LoadLibraryA来加载主机稍后将使用的库。下图展示了这一过程:

加载模块后,它使用 GetProcAddress 获取主机将使用的 API 地址,如以下截图所示:

每个检索到的 API 地址都存储在主机的导入表中,位于 0x00402000。将函数地址恢复到相同的导入表地址应该可以让主机正常调用 API。在 0x00406cde 处设置断点应执行动态导入例程。

下一例程将设置映射头部的访问权限为只读,防止其被写入或执行代码,如以下截图所示:

VirtualProtect 用于设置内存访问标志,并且还需要四个参数。以下代码显示了根据 MSDN 的参数:

BOOL WINAPI VirtualProtect(
  _In_  LPVOID lpAddress,
  _In_  SIZE_T dwSize,
  _In_  DWORD  flNewProtect,
  _Out_ PDWORD lpflOldProtect
);

第一次调用 VirtualProtect 时,lpAddress 设置为 0x00400000dwSize 设置为 0x1000 字节,保护标志设置为 4。值 4 表示 PAGE_READWRITE 常量。之后的 VirtualProtect 调用将保护标志设置为 PAGE_READONLY。如以下截图所示:

记住,在代码开始时,我们遇到了一个 pushad 指令。此时,我们正处于其对立指令 popad 处。这很可能是执行将传递给原始入口点的部分。查看 0x00406D1B 处的 jmp 指令,地址跳转到 UPX0 区段中的某个地址。根据我们的主机打包比较,原始入口点确实位于 0x0040157e

到达原始入口点应结束调试打包程序代码。

从内存中转储进程

打包文件的数据无法直接看到,但如果让它运行,所有内容都应解包到其进程空间中。我们的目标是生成一个解包状态下的文件版本。为此,我们需要转储整个内存,然后将可执行文件的进程映像提取到文件中。

使用 VirtualBox 进行内存转储

我们将使用 Volatility 从暂停的 VirtualBox 映像中转储进程。首先,我们需要了解如何转储 VirtualBox 映像:

  1. 启用 VirtualBox 的调试菜单:

    • 对于 Windows VirtualBox 主机:

      • 输入一个名为 VBOX_GUI_DBG_ENABLED 的新环境变量,并将其设置为 true。如以下截图所示:

    • 对于 Linux 主机:

      • 以 root 用户身份编辑 /etc/environment

      • 添加一个新的条目 VBOX_GUI_DBG_ENABLED=true

      • 执行命令:source /etc/environment

      • 如果 VirtualBox 已经打开,请重新启动

  1. 在 Windows 客户机中运行打包的可执行文件。我们将从我们的 GitHub 页面运行 upxed.exe

  2. 在 VBoxDbg 控制台中,执行以下命令将整个内存转储保存到文件中。注意,pgmphystofile 命令前应该加上一个点,如下所示:

    .pgmphystofile memory.dmp
    
  3. memory.dmp 是文件名,并存储在登录用户的主目录中。它是 Windows 中的%userprofile%文件夹,Linux 中的~/文件夹。

接下来,我们将使用 Volatility 解析内存转储并提取我们需要的数据。

使用 Volatility 将进程提取到文件

Volatility 可以从www.volatilityfoundation.org/releases下载。在这一部分中,我们的 VirtualBox 主机运行的是 Linux Ubuntu 系统。这里展示的 Volatility 命令参数,在 Windows 中使用时也应该是一样的。

首先,我们需要使用 Volatility 的imageinfo参数来识别确切的操作系统版本,以下是一些示例:

vol -f ~/memory.dmp imageinfo

再次说明,~/memory.dmp是我们刚刚转储的内存文件路径。结果应显示已识别的操作系统配置文件列表。对于 Windows 7 SP1 32 位,我们将使用Win7SP1x86作为后续Volatility命令的配置文件。

接下来,我们需要列出正在运行的进程并识别哪个是我们的打包可执行文件。为了列出运行的进程,我们将使用pslist参数,如以下示例所示:

volatility --profile=Win7SP1x86 -f ~/memory.dmp pslist

查看上一张截图中第二列的最后一行,我们发现upxed.exe。我们需要记下进程 ID(PID),它的值是2656。现在我们已经获取了打包可执行文件的 PID,我们可以使用procdump参数将进程导出为文件,如以下代码所示:

volatility --profile=Win7SP1x86 -f ~/memory.dmp procdump -D dump/ -p 2656

procdump将把进程可执行文件保存在-D参数设置的dump/文件夹中,如下图所示:

Volatility 有很多功能可供选择。请随意探索这些参数,它们可能有助于适应分析情况。

解包后的可执行文件怎么样?

现在我们从 Volatility 获取了一个可执行文件,将其在我们的 Windows 虚拟沙盒中运行,得到以下信息:

请记住,打包的可执行文件有自己的 PE 头和存根,而不是原始主机的。头部、存根和压缩数据被直接映射到进程空间。每个 API 函数都是动态导入的。即使代码和数据已经解压,头部中设置的入口点仍然是打包可执行文件的,而不是原始主机的。

幸运的是,x86dbg有一个插件叫做 Scylla。在到达原始入口点后,这意味着我们已经进入解包状态,我们可以将正在调试的进程重建为一个全新的可执行文件。这个新的可执行文件已经解包,可以单独执行。

这仍然要求我们调试打包后的可执行文件,直到我们到达原始入口点(OEP)。一旦到达 OEP,从插件下拉菜单中打开 Scylla。这应该会打开 Scylla 窗口,如以下示例所示:

当前活动进程已经设置为upxed.exe进程。OEP 也已经设置为指令指针所在的位置。接下来要做的是点击 IAT Autosearch,让 Scylla 解析进程空间并定位最可能的导入表。这将填充 IAT 信息框中的 VA 和Size字段,显示可能的导入表位置和大小。点击Get Imports,让 Scylla 扫描已导入的库和 API 函数。如下图所示:

展开其中一个库,它会显示出它找到的 API 函数。现在,在 Dump 框架下,点击 Dump 按钮。这会弹出一个对话框,询问保存可执行文件的位置。这只是将可执行文件的进程转储出来。我们仍然需要应用 IAT 信息和导入。点击 Fix Dump 并打开转储的可执行文件。这会生成一个新的文件,并在文件名后附加_SCY,如下图所示:

运行这个新的可执行文件应该会给我们与原始主机行为相同的结果。

在 Volatility 中,我们没有足够的信息来重建可执行文件。然而,使用x86dbg和 Scylla,尽管需要我们绕过打包器的调试,我们仍然能够得到一个重建后的可执行文件。

其他文件类型

如今,网站通常将二进制数据转换为可打印的 ASCII 文本,以便网站开发人员轻松地将这些数据与 HTML 脚本一起嵌入。其他网站则将数据转换成不容易被人类读取的形式。在本节中,我们将目标是解码那些已被隐藏的不可直观理解的数据。在第十三章 反向工程各种文件类型中,我们将处理如何反向工程除了 Windows 和 Linux 可执行文件之外的其他文件类型。在此之前,我们将仅解码明显的数据。

让我们去浏览器并访问www.google.com,在写作时(我们存储了该页面的源代码副本,见github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch10/google_page_source.txt),查看源代码时会显示一部分包含b64编码文本,如下图所示:

使用 Cyberchef 工具,这个工具可以帮助解码多种编码数据,包括 base 64,我们可以将这些数据转化为我们能理解的内容。只需将 base-64 数据复制并粘贴到输入框中,然后双击 From Base64。这样应该会在输出框中显示解码后的二进制内容,如下图所示:

请注意,输出开头写有 PNG。这很可能是一个 PNG 图像文件。此外,如果我们仔细查看源代码,还可以看到在 base-64 编码数据之前也有标明数据类型,如以下示例所示:

data:image/png;base64

如果我们点击磁盘图标,我们可以将输出数据保存到文件并命名为 .png 扩展名。这样我们就能查看图像,如以下截图所示:

Cyberchef 工具支持其他编码类型。如果我们遇到类似的编码文本,互联网提供了所有可用的工具来帮助我们。

总结

逆向工程就是如何在正确的情境中使用工具。即使是经过压缩、加密和混淆的可执行文件,隐藏的信息仍然可以被提取出来。

在本章中,我们介绍了如何通过打包工具、加密工具、混淆器、保护器甚至自解压工具隐藏数据的各种概念。我们遇到了一个由 UPX 工具生成的打包文件,并且我们仍然能够通过调试器进行逆向工程。通过注意指令指针的位置,我们可以判断自己是否已经到达原始入口点。通常来说,如果指令指针已经跳转到不同的区域,我们可以认为自己已经到达了原始入口点。

使用另一种查看程序解包状态的方案,我们使用 Volatility 结合来自 VirtualBox 虚拟机的内存转储,并提取了我们刚刚运行的可执行文件的进程。通过 Scylla 工具,我们还能够重建解包后的可执行文件状态。

本章最后我们介绍了 CyberChef 工具,它能够解码流行的编码数据,如 base-64。这个工具可能在我们遇到编码数据时非常有用,不仅在网站上的脚本中,在我们遇到的每个可执行文件中都可能出现。

在下一章中,我们将继续我们的旅程,通过识别恶意软件执行的恶意行为。

第十一章:反分析技巧

反调试、反虚拟机(VM)、反仿真和反转储是一些试图阻止分析的技巧。在本章中,我们将尝试展示这些反分析方法的概念。为了帮助我们识别这些代码,我们将解释这些概念并展示实际的反汇编代码。能够识别这些技巧将帮助我们避开它们。通过初步的静态分析,我们将能够跳过这些代码。

在本章中,我们将实现以下学习成果:

  • 识别反分析技巧

  • 学习如何克服反分析技巧

反调试技巧

反调试技巧的目的是确保代码在没有调试器干预的情况下运行。假设我们有一个程序,其中包含反调试代码。该程序的行为就像它没有反调试代码一样运行。然而,当程序被调试时,情况就不同了。在调试时,我们会遇到直接退出程序或跳转到不合理代码的情况。这个过程在下图中有所说明:

开发反调试代码需要了解程序和系统的特征,无论是在正常运行还是在被调试时。例如,进程环境块PEB)包含一个标志,该标志在程序在调试器下运行时被设置。另一个常见的技巧是使用 结构化异常处理程序SEH)来继续在调试时强制产生错误异常的代码。为了更好地理解这些技巧的工作原理,我们来更详细地讨论这些技巧。

IsDebuggerPresent

IsDebuggerPresent 是一个 Kernel32 API 函数,它简单地告诉我们程序是否处于调试器下运行。结果存储在 eax 寄存器中,值为真(1)或假(0)。使用时,代码大致如下所示:

call IsDebuggerPresent
test eax, eax
jz notdebugged

同样的概念适用于 CheckRemoteDebuggerPresent API。不同之处在于,它检查的是另一个进程还是自身进程是否正在被调试。CheckRemoteDebuggerPresent 需要两个参数:一个进程句柄和一个输出变量,告诉我们该进程是否正在被调试。以下代码检查其自身进程是否正在被调试:

call GetCurrentProcess
push edi
push eax
call CheckRemoteDebuggerPresent
cmp dword ptr [edi], 1
jz beingdebugged

GetCurrentProcess API 用于检索正在运行的进程句柄。它通常返回一个 -10xFFFFFFFF)值,这是它自身进程的句柄。edi 寄存器应该是一个变量地址,用于存储 CheckRemoteDebuggerPresent 的输出结果。

PEB 中的调试标志

线程是执行的基本单位。进程本身作为线程实体运行,能够在同一进程空间中触发多个线程。当前正在运行的线程信息存储在线程环境块(TEB)中。TEB 也叫线程信息块(TIB),其中包含线程 ID、结构化错误处理框架、堆栈基地址和限制、以及指向有关线程所在进程的信息的地址。关于进程的信息存储在进程环境块(PEB)中。

PEB 包含诸如指向列出已加载模块的表的指针、用于运行进程的命令行参数、从 PE 头部提取的信息,以及是否被调试等信息。TIB 和 PEB 结构由微软在 https://docs.microsoft.com/en-us/windows/desktop/api/winternl/ 中记录。

PEB有可以用来识别进程是否正在被调试的字段:BeingDebuggedNtGlobalFlag标志。在PEB中,它们位于以下位置:

偏移量 信息
0x02 BeingDebugged(为真时为 1) - BYTE
0x68 GlobalNTFlag(通常在调试时为 0x70) - DWORD

在内部,IsDebuggerPresent使用以下代码:

让我们检查一下IsDebuggerPresent代码的运行情况:

mov eax, dword ptr fs:[18]

上述行从线程信息块TIB)中检索线程环境块TEB)的地址。FS段包含TIBTEB地址存储在TIB的偏移量0x18处。TIB存储在eax寄存器中。

下一行获取PEB地址并将其存储在eax寄存器中。PEB地址位于TEB的偏移量0x30处:

mov eax, dword ptr ds:[eax+30]

PEB偏移量2处的字节包含布尔值10,表示进程是否正在被调试:

movzx eax, byte ptr ds:[eax+2]

如果我们想创建自己的函数,并且应用了GlobalNTFlag,那么我们可以将代码写成这样:

mov eax, dword ptr fs:[18]
mov eax, dword ptr ds:[eax+0x30]
mov eax, dword ptr ds:[eax+0x68]
cmp eax, 0x70
setz al
and eax, 1

上述代码的前三行基本上是从PEB的偏移量0x68中检索GlobalNTFlag

接下来的cmp指令会在eax的值等于0x70时将零标志设置为1

cmp eax, 0x70

setz指令会根据ZF的值将al寄存器设置为01

setz al

最后,and指令将仅保留eax寄存器的第一个位,从而清除寄存器,但保留一个值,值为10,用于表示真假:

and eax, 1

NtQueryInformationProcess获取调试器信息

使用NtQueryInformationProcess函数查询进程信息为我们提供了另一种识别进程是否在调试中的方法。根据MSDNNtQueryInformationProcess的语法声明如下:

NTSTATUS WINAPI NtQueryInformationProcess(
  _In_       HANDLE ProcessHandle,
  _In_       PROCESSINFOCLASS ProcessInformationClass,
  _Out_      PVOID ProcessInformation,
  _In_       ULONG ProcessInformationLength,
  _Out_opt_  PULONG ReturnLength
);

有关此函数的更多信息,请参见 docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntqueryinformationprocess

根据第二个参数 PROCESSINFOCLASS 提供的 ID,返回具体的信息。PROCESSINFOCLASS 是一个列举的 ID 列表,我们希望查询的 ID 包含在内。为了确定进程是否正在被调试,我们需要以下 ID:

  • ProcessDebugPort (7)

  • ProcessDebugObjectHandle (30)

  • ProcessDebugFlags (31)

本质上,如果第三个参数 ProcessInformation 填充后的输出结果为非零值,则意味着该进程正在被调试。

定时技巧

通常,从地址 A 到地址 B 执行一段程序所需的时间不超过一秒钟。但如果这些指令正在被调试,人工调试可能需要每行约一秒钟。从地址 A 调试到地址 B 至少需要几秒钟。

本质上,这个概念就像一个计时器。如果几行代码执行的时间过长,技巧就会认为程序正在被调试。

定时技巧可以作为一种反调试方法应用于任何编程语言。设置计时器只需要一个能够读取时间的函数。以下是一些定时技巧在 x86 汇编中的实现示例:

rdtsc
mov ebx, eax
nop
nop
nop
nop
nop
nop
nop
nop
rdtsc
sub eax, ebx
cmp eax, 0x100000
jg exit

在 x86 处理器中意味着 读取时间戳计数器 (RDTSC)。每次处理器重置(无论是硬重置还是开机)时,时间戳计数器都会被重置为 0。时间戳计数器在每个处理器时钟周期中递增。在之前的 RDTSC 代码段中,第一个 RDTSC 指令的结果存储在 ebx 寄存器中。经过一系列的 nop 指令后,存储在 ebx 中的值与第二个 RDTSC 指令的结果相减。这就是计算第一和第二次 TSC 的差值。如果差值大于 0x100000,则跳转至退出。如果程序没有被逐行调试,差值应该约小于 0x500

另一方面,GetSystemTimeGetLocalTime 这两个可以获取时间的 API 函数,也可以用来实现定时技巧。为了识别这些技巧,代码必须包含两个时间获取函数。

通过 SEH 传递代码执行

最流行的反调试技巧之一是通过 SEH 传递代码执行。它是 Windows 计算机病毒中常用的技巧。但在讨论该技巧如何用于反调试之前,我们先简要了解一下 SEH 的工作原理。

异常通常由错误引发,例如从无法访问的内存区域读取字节,或像除以零这样简单的操作。它们也可以由调试器中断引发,INT 3INT 1。当异常发生时,系统会跳转到异常处理程序。通常,异常处理程序的工作是处理错误。

通常,这项工作会给出一个错误消息通知,导致程序优雅地终止。从编程的角度来看,这就是 try-except 或 try-catch 处理。以下是 Python 编程中异常处理的示例:

try:
    print("Hello World!")

except:
    print("Hello Error!")

一个 SEH 记录包含两个元素:异常处理程序的地址和下一个 SEH 记录的地址。下一个 SEH 记录包含指向下一个 SEH 记录的地址。总体来说,SEH 记录是彼此相连的,这被称为 SEH 链。如果当前处理程序无法处理该异常,则下一个处理程序将接管。如果 SEH 记录耗尽,可能会导致程序崩溃。这个过程如下所示:

如我们所见,最后一个 SEH 记录在 SEH 记录指针字段中包含一个 -1(对于 32 位地址空间为 0xFFFFFFFF)的值。

现在我们知道 SEH 是如何工作的,那么如何将其用于反调试呢?使用我们的 try-except Python 代码,滥用它可能会像这样:

x = 1
try:
    x = x / 0
    print("This message will not show up!")
except:
    print("Hello World!")

我们所做的是强制引发一个错误(准确地说是除以零的错误)以引发异常。异常处理程序会显示 Hello World! 消息。那么,在 x86 汇编语言中它是如何工作的呢?

为了设置我们的新 SEH,我们首先需要找出当前 SEH 的位置。对于每个进程,Windows 操作系统会设置一个 SEH 链。当前的 SEH 记录可以从 TIB 的偏移量 0 获取,如 FS 段寄存器所示。

以下汇编代码将当前 SEH 记录的地址获取到 eax 寄存器:

mov eax, dword ptr FS:[0]

要更改处理程序,我们只需用我们的 SEH 记录更改当前 SEH 记录的地址,即 FS:[0]。假设处理代码的地址为 0x00401000,而当前的 SEH 记录位于 0x00200000,并且包含以下值:

下一个 SEH 记录 0xFFFFFFFF
当前处理程序地址 0x78000000

接下来要做的是构建我们的 SEH 记录,并将其存储在栈中。通过 FS:[0] 返回 0x00200000 的值,而我们的处理程序位于 0x00401000,这里是从栈中构建 SEH 记录的一种方式:

push 0x00401000
push dword ptr FS:[0]

栈的状态应该类似于以下样子:

ESP 0x00200000
ESP+4 0x00401000

我们需要做的就是将 FS:[0] 的值更新为该 SEH 记录的地址,这就是 ESP 寄存器的值(即栈顶):

mov dword ptr FS:[0], esp

前面的代码应该将我们的 SEH 添加到 SEH 链中。

引发异常

接下来要做的是开发一段代码,强制引发一个异常。我们有几种已知的方法来实现这一点:

  • 使用调试断点(INT 3 / INT 1)

  • 访问不可访问的内存空间

  • 除零错误

SEH 反调试技巧的目的是将调试分析引导到一个错误。这使得分析师试图追溯可能导致错误的原因,从而浪费时间。而且,如果分析师熟悉 SEH,他就能很容易地找到处理程序所在的位置并在那里设置断点。

步骤调试之所以有效,是因为 Interrupt 1,而断点是通过 Interrupt 3 设置的。当代码执行遇到 INT 3 指令时,会发生调试异常。要调用 Interrupt 1 异常,必须首先设置陷阱标志。

当读取不可访问的内存时,会发生读取错误。已经有已知的内存区域,例如内核空间,这些区域不允许从用户模式进程直接访问。这些区域大多数被 PAGE_GUARD 标志保护。可以通过 VirtualAllocVirtualProtect 函数设置 PAGE_GUARD 标志。这意味着我们可以创建自己的不可访问内存区域。通常,进程空间中的 0 偏移区域是不可访问的。以下代码行将导致访问违规异常:

mov al, [0]

在数学中,实际的除以零操作是一个无限的任务。系统会明确识别此类错误并引发异常。以下是一个示例代码行:

mov eax, 1
xor cl, cl
div cl

前述代码的作用是将 eax 寄存器设置为 1,将 cl 寄存器设置为 0,然后用 cl 除以 eax,从而引发除零异常。

一个典型的 SEH 设置

基于我们所学的内容,让我们利用常规的代码流程,然后使用 SEH 作为反调试技巧。以下代码将是我们的原始代码:

push eax
mov eax, 0x12345678
mov ebx, 0x87654321
and eax, ebx
pop eax

在添加 SEH 反调试技巧后,代码看起来大致如下:

    mov eax, dword ptr FS:[0]
    push 0x00401000
    push eax
    mov dword ptr FS:[0], esp
    mov al, [0]

RDTSC (with CPUID to force a VM Exit)

VMM instructions i.e. VMCALL

VMEXIT
0x00401000:
    push eax
    mov eax, 0x12345678
    mov ebx, 0x87654321
    and eax, ebx
    pop eax

我们在这里做的是手动设置 SEH。幸运的是,Windows 还提供了一个可以设置异常处理程序的功能,称为向量化异常处理程序。注册新处理程序的 API 是 AddVectoredExceptionHandler。实现此功能的 C 语言源代码可以在 docs.microsoft.com/en-us/windows/desktop/debug/using-a-vectored-exception-handler 找到。

反虚拟机技巧

这个技巧的目的是当它检测到程序正在虚拟化环境中运行时退出程序。识别虚拟机环境的最典型方法是检查计算机中是否安装了特定的虚拟化软件痕迹。这些痕迹可能位于注册表或正在运行的服务中。我们列出了一些可以用来识别虚拟机内运行的特定痕迹。

虚拟机运行进程名称

程序确定自己是否在虚拟机中的最简单方法是识别运行进程的已知文件名。以下是每种流行虚拟机软件的列表:

Virtualbox VMWare QEMU Parallels VirtualPC

| vboxtray.exe vboxservice.exe

vboxcontrol.exe | vmtoolsd.exe vmwaretray.exe

vmwareuser

VGAuthService.exe

vmacthlp.exe | qemu-ga.exe | prl_cc.exe prl_tools.exe | vmsrvc.exe vmusrvc.exe |

虚拟机文件和目录的存在

确定至少存在一个虚拟机软件的文件,可以判断该程序是否正在虚拟机中运行。下表列出了可以用来识别程序是否在 VirtualBox 或 VMware 客户机中运行的文件:

VirtualBox VMWare

| %programfiles%\oracle\virtualbox guest additions system32\drivers\VBoxGuest.sys

system32\drivers\VBoxMouse.sys

system32\drivers\VBoxSF.sys

system32\drivers\VBoxVideo.sys

system32\vboxdisp.dll

system32\vboxhook.dll

system32\vboxmrxnp.dll

system32\vboxogl.dll

system32\vboxoglarrayspu.dll

system32\vboxoglcrutil.dll

system32\vboxoglerrorspu.dll

system32\vboxoglfeedbackspu.dll

system32\vboxoglpackspu.dll

system32\vboxoglpassthroughspu.dll | %programfiles%\VMWare system32\drivers\vm3dmp.sys

system32\drivers\vmci.sys

system32\drivers\vmhgfs.sys

system32\drivers\vmmemctl.sys

system32\drivers\vmmouse.sys

system32\drivers\vmrawdsk.sys

system32\drivers\vmusbmouse.sys |

默认 MAC 地址

虚拟机默认 MAC 地址的前三个十六进制数字也可以用来识别。但当然,如果 MAC 地址被更改,这些方法就不适用了:

VirtualBox VMWare Parallels

| 08:00:27 | 00:05:69 00:0C:29

00:1C:14

00:50:56 | 00:1C:42 |

虚拟机创建的注册表项

软件的信息和配置通常保存在注册表中。这同样适用于虚拟机客户机软件,它会创建注册表项。以下是 VirtualBox 创建的注册表项的简短列表:

HARDWARE\ACPI\DSDT\VBOX__ 
HARDWARE\ACPI\FADT\VBOX__ 
HARDWARE\ACPI\RSDT\VBOX__ 
SOFTWARE\Oracle\VirtualBox Guest Additions 
SYSTEM\ControlSet001\Services\VBoxGuest 
SYSTEM\ControlSet001\Services\VBoxMouse 
SYSTEM\ControlSet001\Services\VBoxService 
SYSTEM\ControlSet001\Services\VBoxSF 
SYSTEM\ControlSet001\Services\VBoxVideo 

以下是已知来自 VMWare 的注册表项:

SOFTWARE\VMware, Inc.\VMware Tools 

使用 Wine 模拟的 Linux 有如下注册表项:

SOFTWARE\Wine

也可以通过注册表识别出 Microsoft 的 Hyper-V:

SOFTWARE\Microsoft\Virtual Machine\Guest

虚拟机设备

这些是虚拟机创建的虚拟设备。以下是 VirtualBox 和 VMWare 创建的可访问设备:

VirtualBox VMWare

| \\.\VBoxGuest \\.\VBoxTrayIPC

\\.\VBoxMiniRdrDN | \\.\HGFS \\.\vmci |

CPUID 结果

CPUID 是一条 x86 指令,用于返回正在运行的处理器的信息。在执行该指令之前,需要指定信息类型,这些信息被称为“叶子”,并存储在寄存器 EAX 中。根据叶子的不同,它会在寄存器 EAX、EBX、ECX 和 EDX 中返回值。每个寄存器中存储的每一位都可以指示某个 CPU 特性是否可用。关于返回的 CPU 信息的详细内容,可以查看 en.wikipedia.org/wiki/CPUID

CPUID 返回的信息之一是一个标志,它表示系统是否在超虚拟机监控程序(Hypervisor)上运行。超虚拟机监控程序是一个 CPU 功能,支持运行虚拟机(VM)客户机。对于反虚拟机检测,如果启用此标志,意味着进程运行在虚拟机客户机中。

以下 x86 代码检查是否启用了超虚拟机监控程序标志:

mov eax, 1
cpuid
bt ecx, 31
jc inhypervisor

前面的代码从 CPUID 第 1 项获取信息。ecx 寄存器中的第 31 位结果被放置在进位标志中。如果该位被设置为 1,则表示系统在超虚拟机监控程序上运行。

除了超虚拟机监控程序的信息外,一些特定的虚拟机软件可以通过来宾操作系统识别。CPUID 指令可以返回一个唯一的字符串 ID,以识别客户机所运行的虚拟机软件。以下代码检查是否运行在 VMWare 客户机中:

mov eax, 0x40000000
cpuid
cmp ebx, 'awMV'
jne exit
cmp ecx, 'MVer'
jne exit
cmp edx, 'eraw'
jne exit

ebxecxedx 寄存器的值拼接在一起时,它会显示为 VMwareVMware。以下是其他虚拟机软件使用的已知字符串 ID 列表:

VirtualBox 4.x VMware Hyper-V KVM Xen
VBoxVBoxVBox VMwareVMware Microsoft Hv KVMKVMKVM XenVMMXenVMM

反仿真技巧

反仿真或反自动化分析是程序用来防止在代码执行过程中继续推进的一种方法,前提是它识别到自己正在被分析。程序的行为可以使用自动化分析工具如 Cuckoo Sandbox、Hybrid Analysis 和 ThreatAnalyzer 来记录和分析。这些技巧的核心在于能够确定程序运行的系统是由用户控制的,并且是用户设置的。

以下是一些区分用户控制的环境与自动化分析控制的系统之间差异的事项:

  • 用户控制的系统具有鼠标移动。

  • 用户控制的系统可能包含一个对话框,等待用户向下滚动然后点击按钮。

  • 自动化分析系统的设置具有以下特点:

    • 物理内存不足

    • 磁盘空间过小

    • 磁盘上的可用空间几乎耗尽

    • CPU 数量只有一个

    • 屏幕分辨率过小

简单地设置一个需要用户手动输入的任务即可确定程序是否在用户控制的环境中运行。类似于反虚拟机检测,虚拟机客户机的设置将尽可能使用最低的资源要求,以免占用虚拟机主机的计算机资源。

另一个反分析技巧是检测是否运行分析工具。这些工具包括以下内容:

  • OllyDBG(ollydbg.exe

  • WinDbg(windbg.exe

  • IDA Pro(ida.exeidag.exeida64.exeidag64.exe

  • SysInternals 套件工具,包括以下内容:

    • 进程浏览器(procexp.exe

    • 进程监视器(procmon.exe

    • Regmon(regmon.exe

    • Filemon(filemon.exe

    • TCPView(tcpview.exe

    • Autoruns(autoruns.exeautorunsc.exe

  • Wireshark(wireshark.exe

规避这些技巧的一种方法是通过自动化分析进行反制。例如,可以模拟鼠标移动,甚至读取对话框窗口属性,滚动和点击按钮。一个简单的反分析技巧是重命名我们用来监视行为的工具。

反转储技巧

这种方法并不会停止将内存转储到文件中。这个技巧实际上是通过让逆向工程师不容易理解转储的数据来起作用。以下是一些应用示例:

  • PE 头的部分内容已经被修改,因此进程转储会显示错误的属性。

  • PEB 的部分内容,如 SizeOfImage,已经被修改,因此进程转储工具会转储错误的数据。

  • 转储对于查看解密后的数据非常有用。反转储技巧会在使用后重新加密解密的代码或数据。

为了克服这个技巧,我们可以识别或跳过修改数据的代码。对于重新加密的情况,我们也可以跳过重新加密的代码,使其保持在解密状态。

总结

恶意软件通过加入新技术来规避防病毒软件和逆向工程。这些技术包括进程空洞化、进程注入、进程替换、反调试和反分析。进程空洞化和进程替换技术基本上是用恶意程序替换合法进程的映像,从而伪装成合法进程。另一方面,进程注入技术则是将代码插入并在远程进程空间中运行。

反调试、反分析以及本章讨论的其他技巧是逆向工程的障碍。但了解这些技巧的概念能帮助我们克服它们。通过静态分析和死列表方法,我们可以识别并跳过这些复杂代码,或者在 SEH 的情况下,在异常处理程序处设置断点。

我们讨论了反调试技巧,以及它通过错误导致异常并将剩余代码停留在异常处理程序中的技术。我们还讨论了其他技巧,包括反虚拟机和反仿真技巧,这些技巧能够识别出当前环境是分析环境。

在下一章中,我们将运用这里学到的知识,进行一个可执行文件的逆向工程分析。

第十二章:Windows 可执行文件的实用逆向工程

逆向工程在处理恶意软件分析时非常常见。在本章中,我们将查看一个可执行程序,并使用我们目前所学的工具确定其实际行为流程。我们将直接从静态分析进入动态分析。这要求我们设置好实验环境,以便更容易跟进分析过程。

本章要分析的目标文件具有在实际恶意软件中看到的行为。无论文件是否恶意软件,我们在分析时都必须小心地在封闭环境中处理每个文件。让我们开始进行一些逆向工程。

本章将涵盖以下主题:

  • 实用的静态分析

  • 实用的动态分析

准备事项

我们即将分析的文件可以从github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch12/whatami.zip下载。它是一个密码保护的压缩文件,密码是"infected",不带引号。

我们需要准备好 Windows 实验室环境。本章讨论的分析将程序运行在一个 VirtualBox 虚拟机中,虚拟机上运行 Windows 10 32 位操作系统。还需要准备以下工具:

在分析过程中,我们可能需要其他工具。如果你发现有更方便的工具,可以随时使用它们。

初步静态分析

为了帮助我们进行静态信息收集,以下是我们需要获取的信息列表:

  • 文件属性(名称、大小、其他信息)

  • 哈希值(MD5、SHA1)

  • 文件类型(包括头信息)

  • 字符串

  • 死名单(标记需要信息的地方)

在初步分析的最后,我们需要总结所有获取的信息。

初始文件信息

为了获取文件名、文件大小、哈希值、文件类型和其他关于文件的信息,我们将使用 CFF Explorer。当打开文件时,可能会遇到一个错误消息,如下截图所示:

此错误是由 MS Windows 的病毒防护功能引起的。由于我们处于一个沙箱环境中(在虚拟化的客户环境下),禁用此功能应该是可以的。禁用此功能在生产环境中可能会暴露计算机被恶意软件攻击的风险。

要在 Windows 中禁用此功能,请选择“开始”->“设置”->“Windows 安全”->“病毒和威胁防护”->“病毒和威胁防护设置”。然后关闭实时保护。你还可以关闭云传递保护和自动样本提交,以防止任何安全设置阻止程序可能执行的操作。

以下截图显示了禁用实时保护后的状态:

使用 CFF Explorer 打开文件可以揭示很多信息,包括文件被 UPX 打包的打包者信息:

根据前面的结果,我们可以列出以下文件信息:

文件名 whatami.exe
文件大小 28,672 字节
MD5 F4723E35D83B10AD72EC32D2ECC61091
SHA-1 4A1E8A976F1515CE3F7F86F814B1235B7D18A231
文件类型 Win32 PE 文件 - 使用 UPX v3.0 打包

我们将需要下载 UPX 工具并尝试解压文件。UPX 工具可以从 upx.github.io/ 下载。使用 UPX,使用 "-d" 选项解压文件,方法如下:

upx -d whatami.exe

解压文件后的结果,如下所示,告诉我们文件最初的大小为 73,728 字节:

所以,如果我们重新打开文件在 CFF Explorer 中,我们的文件信息表将现在包含以下内容:

文件名 whatami.exe
文件大小 73,728 字节
MD5 18F86337C492E834B1771CC57FB2175D
SHA-1 C8601593E7DC27D97EFC29CBFF90612A265A248E
文件类型 Win32 PE 文件 - 由 Microsoft Visual C++ 8 编译

让我们看看使用 SysInternals 的 strings 工具可以找到哪些值得注意的字符串。Strings 是一个命令行工具,只需将文件名作为工具的参数传递,并将输出重定向到文件。以下是使用方法:

strings.exe whatami.exe > filestrings.txt

通过去除噪音字符串或与分析无关的文本,我们获得了以下内容:

!This program cannot be run in DOS mode.
Rich
.text
`.rdata
@.data
.rsrc
hey
how did you get here?
calc
ntdll.dll
NtUnmapViewOfSection
KERNEL32.DLL
MSVCR80.dll
USER32.dll
Sleep
FindResourceW
LoadResource
LockResource
SizeofResource
VirtualAlloc
FreeResource
IsDebuggerPresent
ExitProcess
CreateProcessA
GetThreadContext
ReadProcessMemory
GetModuleHandleA
GetProcAddress
VirtualAllocEx
WriteProcessMemory
SetThreadContext
ResumeThread
GetCurrentProcess
GetSystemTimeAsFileTime
GetCurrentProcessId
GetCurrentThreadId
GetTickCount
QueryPerformanceCounter
SetUnhandledExceptionFilter
TerminateProcess
GetStartupInfoW
UnhandledExceptionFilter
InterlockedCompareExchange
InterlockedExchange
_XcptFilter
exit
_wcmdln
_initterm
_initterm_e
_configthreadlocale
__setusermatherr
_adjust_fdiv
__p__commode
__p__fmode
_encode_pointer
__set_app_type
_crt_debugger_hook
?terminate@@YAXXZ
_unlock
__dllonexit
_lock
_onexit
_decode_pointer
_except_handler4_common
_invoke_watson
_controlfp_s
_exit
_cexit
_amsg_exit
??2@YAPAXI@Z
memset
__wgetmainargs
memcpy
UpdateWindow
ShowWindow
CreateWindowExW
RegisterClassExW
LoadStringW
MessageBoxA
WHATAMI
t<assembly  manifestVersion="1.0">
  <dependency>
    <dependentAssembly>
      <assemblyIdentity type="win32" name="Microsoft.VC80.CRT" version="8.0.50727.6195" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity>
    </dependentAssembly>
  </dependency>
</assembly>PAD

我们突出了多个文本字符串。因此,我们可能会期望通过使用MessageBoxA函数弹出多个消息。使用像LoadResourceLockResource这样的 API 时,我们也可能会遇到处理资源部分数据的代码。在看到像CreateProcessResumeThread这样的 API 后,可能会调用一个挂起的进程。使用IsDebuggerPresent API 时,也可能会遇到反调试技术。程序可能已经被编译成使用基于 GUI 的代码,通过CreateWindowExWRegisterClassExW,但我们没有看到窗口消息循环函数:GetMessageTranslateMessageDispatchMessage

这些都只是我们在进一步分析后可以更好理解的假设。现在,让我们尝试使用 IDA Pro 对该文件进行死列举。

死列举

在 IDA Pro 中打开whatami.exe后,自动分析识别出了WinMain函数。在接下来的截图中,我们可以看到将要执行的前三个 API 是LoadStringWRegisterClassExWCreateWindowEx

当执行CreateWindowExW时,窗口的属性来自RegisterClassExW设置的配置。ClassName,作为窗口的名称,是通过LoadStringW从文件的文本字符串资源中获取的。然而,我们在这里关心的只是lpfnWindProc指向的代码。当执行CreateWindowExW时,lpfnWndProc参数指向的代码将被执行。

在继续之前,先看看sub_4010C0。我们来看看CreateWindowExW之后的代码:

上面的截图显示,在CreateWindowExW之后,ShowWindowUpdateWindow是可能被执行的唯一 API。然而,确实没有预期中的窗口消息 API 来处理窗口活动。这使得我们假设程序的意图只是运行lpfnWndProc参数指向的地址处的代码。

双击dword_4010C0,即lpfnWndProc的地址,将显示一组 IDA Pro 尚未正确分析的字节。由于我们确定这个区域应该是代码,因此我们需要告诉 IDA Pro 它是代码。通过在地址0x004010C0按下'c',IDA Pro 将开始将字节转换为可读的汇编语言代码。当 IDA Pro 询问我们是否将其转换为代码时,选择

向下滚动,我们将在0x004011a0处遇到另一个无法识别的代码。只需执行相同的步骤:

再往下滚动会看到一些无法再转换的数据。这应该是代码的最后一部分。让我们告诉 IDA Pro 将这段代码处理为一个函数。操作方法是高亮选中从0x004010C00x004011C0的行,右键点击高亮部分,然后选择“创建函数...”将这段代码变成一个函数。

将代码转化为函数可以帮助我们的死链表查看代码的图形视图。为此,右键点击并选择图形视图。下图显示了该函数的第一组代码。我们关心的是rdtsccpuid指令的使用方式:

第十一章,与 POC 恶意软件的识别中,在反调试技巧下,我们讨论了rdtsc被用作时间计算技巧。差异是在第二次rdtsc后计算的。在以下代码中,预期的持续时间应该小于或等于0x10000,即65,536个周期。如果我们能通过这个时间技巧,就会弹出一个消息框。

第 1 个叶子(设置在寄存器eax中)被传递给第一次执行cpuid指令。再次在第十一章中,cpuid可以用于反虚拟机技巧。结果被放置在寄存器 eax 中。接着是三条xor指令,最终交换eaxecx寄存器的值。

xor ecx, eax
xor eax, ecx
xor ecx, eax

bt指令将第 31 位(0x1F)移动到进位标志。如果第 31 位被设置,则意味着我们正在一个超级管理程序环境中运行。在后续的调试过程中,我们需要注意这一行。我们希望使结果中第 31 位被设置为0

这之后可能会紧接着用xor ecx, 20h检查第 5 位。如果第 5 位被设置,意味着 VMX(虚拟机扩展)指令可用。如果 VMX 指令可用,则意味着系统能够运行虚拟化。通常,VMX 仅在主机虚拟机上可用,程序可以假设它正在物理机上运行。对于位运算,如果ecx的第 5 位被设置,xor 20h应该使其归零。但如果ecx寄存器的其他位被设置,ecx寄存器的值就不会是零。我们在调试过程中也要特别注意这一点。

这里展示了两种主要的技巧——一个是时间技巧,另一个是反虚拟机技巧。总体而言,如果我们推测我们分析的内容,程序可以走两个方向:一个是loc_4010EF处的循环,它没有意义,另一个是MessageBoxA代码。

如果我们仔细看,会发现整个反调试和反虚拟机技巧都被pushapopa指令包围。实际上,我们可以跳过整个技巧代码,直接跳到MessageBoxA代码,正如下面的截图所示:

MessageBoxA代码后面是读取RCDATA0x0A)资源类型,且该资源的序号名称为0x88136)的函数。使用 CFF Explorer,点击 Resource Editor 并展开 RCData。我们应该能够看到正在读取的数据,如下截图所示:

数据通过 memcpy 被复制到使用 VirtualAlloc 分配的内存空间中。分配的大小是 RCData 属性中指示的大小。可以通过在 CFF Explorer 中展开 Resource Directory 中的 RCData 来查看大小。复制的数据地址被存储在 edi 寄存器中。

我们还看到 IsDebuggerPresent 被使用了,这是另一种反调试技巧。跟随绿色线条最终会到达 ExitProcess

以下截图是红线的去向:

loc_4011A0 处的循环似乎在解密数据。记住,数据的地址保存在寄存器 edi 中。解密算法使用 ror 0x0c(向右旋转 12 位)。解密后,数据地址被存储到寄存器 eax 中,然后调用 sub_4011D0 函数。

了解解密数据的位置和大小后,我们应该能够在调试过程中创建一个内存转储。

sub_4011D0 内部,存储在 eax 中的地址被转移到 esi 寄存器,随后转移到 edi 寄存器。然后我们看到一个调用 CreateProcessA 的函数来运行“calc”:

名为“calc”的进程实际上是 Windows 默认的计算器应用程序。CreateProcessA的第六个参数dwCreationFlags在这里是我们关注的重点。值为 4 表示 CREATE_SUSPENDED。计算器以挂起模式作为进程运行,这意味着它并未执行,而是仅仅在计算器自己的进程空间中加载。

如果我们要为 sub_4011D0 创建一个包含 API 函数顺序的框图,可能会得到如下图所示的结果。

这些 API 的顺序展示了一种名为进程空洞(process hollowing)的行为。进程空洞是一种常被恶意软件使用的技术,通过将其代码隐藏在合法进程下方来掩盖其存在。这项技术会创建一个处于挂起状态的进程,然后卸载其内存并用另一个进程映像替换它。在这个案例中,合法进程是计算器(Calculator)。

NtUnmapViewOfSection API 是一个从给定进程空间中卸载或移除 PE 映像布局的函数。这个 API 来自于 NTDLL.DLL 库文件。与使用 LoadLibrary 不同,这里使用了 GetModuleHandleLoadLibrary 用于加载尚未加载的库,而 GetModuleHandle 用于检索已加载库的句柄。在这种情况下,程序假设 NTDLL.DLL 已经被加载。

以下截图展示了获取NtUnmapViewOfSection函数地址的反汇编代码:

资源部分的 RCData 解密数据会传递给 sub_4011D0。每次调用WriteProcessMemory都会从解密数据中读取数据块。鉴于此,我们预期解密数据应该是一个 Win32 PE 文件的内容。

总结一下,代码最初会创建一个窗口。然而,已注册的窗口属性几乎为空,只有回调函数 WndprocWndproc 回调是窗口创建时最初执行的代码。因此,使用 RegisterClassExCreateWindow API 创建窗口实际上只是用来传递代码执行。换句话说,整个窗口创建过程只是 jmp 指令的简单等效。

这是另一张概述 Wndproc 回调中代码流的图示:

Wndproc 代码的第一部分,我们遇到了反调试(通过 rdtsc 做时间技巧)和反虚拟机(cpuid 位 31 和 5)技巧。一旦我们通过这些,弹出了一个消息框。资源中的 RCData 数据被复制到一个分配的内存中。我们又遇到了一个使用 IsDebuggerPresent API 的反调试技巧。数据被解密并传递给一个使用计算器的进程劫持代码。

我们接下来的分析目标是通过进程劫持执行的解密镜像。我们将直接开始调试。

调试

我们将使用 x86dbg 进行调试会话。记住,我们已经使用 UPX 解压了文件。最好打开解压后的版本,而不是原始的 whatami.exe 文件。打开压缩的文件也是可以的,但我们将需要调试 UPX 打包的代码。

与 IDA Pro 不同,x86dbg 无法识别 WinMain 函数,也就是程序的实际起始点。此外,打开文件后,指令指针可能还会处于 NTDLL 内存空间。为了避免在启动时进入 NTDLL 区域,我们可能需要在 x86dbg 中进行一些简单的配置更改。

选择“选项”->“首选项”。在“事件”标签下,取消勾选“系统断点”和“TLS 回调”。点击保存按钮,然后选择“调试”->“重启”。这应该将我们带到 whatami.exe 的入口点,地址为:0x004016B8

既然我们已经通过 IDA Pro 知道了 WinMain 的地址,我们可以直接在该地址设置断点。WinMain 地址是 0x00401000。按下 CTRL+G,然后输入 0x00401000,再按 F2 设置断点,最后按 F9 运行程序。

这是此时我们应该达到的位置的截图:

在静态分析中,我们观察到使用了RegisterClassExWCreateWindowExW来设置 WndProc 作为窗口处理程序,其中包含更多有趣的代码。请在 WndProc 地址0x004010c0处设置断点,然后按 F9 键。这将带我们进入如下截图,其中包含反调试和反虚拟机代码:

我们在这里突出了反调试和反虚拟机代码。代码从pushad指令开始,到popad指令结束。我们可以做的是跳过这些反调试和反虚拟机代码。按 F7 或 F8,直到我们到达地址0x004010C9。选择0x00401108这一行(即popad之后的行),然后右键单击它以弹出上下文菜单。选择“Set New Origin Here”。这样,指令指针(寄存器 EIP)就会定位到这个地址。

现在我们应该到达了使用MessageBoxA函数显示以下消息的代码。继续按F8,直到出现以下消息:

你需要点击“确定”按钮才能继续调试。接下来的代码将从资源部分获取RCData。继续按F8,直到我们到达0x0040117D这一行,调用memcpy。仔细观察传递给memcpy的三个参数,寄存器 edi 应包含要复制数据的源地址,寄存器eax应包含目标地址,而寄存器esi应包含要复制的数据大小。为了查看目标将包含的内存内容,在右侧窗格中选择EDI的值,然后右键单击它以显示上下文菜单。选择“Follow in Dump”。现在我们应该能够查看 Dump 1 的内存空间,如下图所示:

F8键继续执行memcpy。以下截图显示了当前的位置:

继续按F8键,直到我们到达调用IsDebuggerPresent之后的行(0x00401192)。寄存器EAX应该被设置为1,表示“True”值。我们需要将其改为“False”,即零值。为此,双击寄存器EAX的值,然后将 1 改为 0。这样,代码就不会直接跳到ExitProcess调用。

接下来的代码将是解密例程。左侧窗格中的箭头显示了一个loopback代码。该算法使用ror指令。继续按F8键,同时观察 Dump 1。我们可以逐渐看到数据被解密,从一个MZ头开始。你可以在地址0x004011B7处设置断点,解密代码结束并完全解密的数据将显示如下:

解密的数据是一个大小为0x0D000(53,248 字节)的Win32 PE 文件。我们可以在这里做的是将此解密的内存转储到文件中。要执行此操作,请单击内存映射选项卡或选择视图->内存映射。这显示了进程内存空间,其中包含内存部分的地址和其相应大小。在我们的情况下,解密数据的内存地址是0x001B000。这个地址可能因分析而异。选择大小为0x00D000的解密数据的内存地址,右键单击以显示上下文菜单,然后选择转储内存到文件。请参考以下示例:

保存文件并使用 CFF Explorer 打开它。这为我们提供了以下文件信息:

文件大小 53,248 字节
MD5 DD073CBC4BE74CF1BD0379BA468AE950
SHA-1 90068FF0C1C1D0A5D0AF2B3CC2430A77EF1B7FC4
文件类型 Win32 PE 文件 - 由 Microsoft Visual C++ 8 编译

此外,查看导入目录显示了四个库模块:KERNEL32ADVAPI32WS2_32URLMON。以下 CFF Explorer 截图显示从ADVAPI32导入了注册表和密码学 API:

存在WS2_32表示程序可能使用网络套接字函数。从URLMON导入的单个 API 是URLDownloadToFile。我们预计会下载一个文件。

回到我们的调试会话,还剩下两个调用指令。其中一个选择是调用ExitProcess,它将终止当前运行的进程。另一个是调用地址0x004011D0。使用F7进行调试步骤,使调试器进入调用指令。这是执行进程空心化例程的函数。下面的截图是我们在输入0x004011D0后应该所处的位置:

持续按F8,直到调用CreateProcessA之后。打开 Windows 任务管理器,查看进程列表。您应该看到calc.exe处于挂起状态,如下所示:

持续按 F8,直到我们到达调用ResumeThread0x0040138C)的行。发生的是未知 PE 文件刚刚替换了计算器进程的映像。如果我们回顾一下sub_4011D0的块图,我们目前处于该程序的进程空心化行为中。虽然计算器处于挂起模式,但尚未执行任何代码。因此,在ResumeThread行上按下 F8 之前,我们将需要附加挂起的计算器,并在其 WinMain 地址或入口点处设置断点。为此,我们将需要打开另一个x86dbg调试器,然后选择文件->附加,并查找 calc。如果您看不到它,您将需要通过选择文件->重新启动以管理员身份运行。

让我们使用 IDA Pro 来帮助我们确定WinMain的地址。打开 IDA Pro 中的转储内存,并在自动分析后,我们将定位到WinMain函数。切换到文本视图,并记录下WinMain的地址,如下图所示:

x86dbg中,将断点设置在0x004017A0,如以下截图所示:

现在我们准备在ResumeThread这一行按F8键。但在此之前,最好还是创建一个正在运行的虚拟机快照,以防万一出现问题:

到此为止,whatami.exe要执行的唯一 API 是ExitProcess。这意味着我们可以按F9让该进程结束。

在调用ResumeThread后,calc进程将被恢复并开始运行。但是由于未知的镜像处于调试器暂停状态,我们观察到calc镜像仍停留在附加的断点指令指针处。

未知镜像

此时,我们已经在 IDA Pro 中打开了内存转储,并且相同的未知镜像已映射到计算器进程中。我们将使用 IDA Pro 查看反汇编代码,并使用x86dbg进行调试。

x86dbg中,我们已经在未知镜像的WinMain地址处设置了断点。然而,指令指针仍然停留在NTDLL地址处。按F9让其继续,直到我们到达WinMain

详细查看WinMain的反汇编代码时,我们会注意到这里有一个 SEH 反调试机制:

call sub_4017CB跳转到一个子程序,里面有call $+5pop eaxretn指令。call $+5调用下一行。记住,当执行call时,栈顶会包含返回地址。call sub_4017CB会将返回地址0x004017B3存储到栈顶。接着,call $+5会将0x004017D0存储到栈顶。由于pop eax0x004017D0被放入 eax 寄存器。ret指令会返回到0x004017AD地址。随后,地址中存储的值加 2,结果是eax中的地址指向0x004017D2。这一定是正在设置的 SEH 处理程序地址。

我们可以通过 SEH,或者简单地在调试会话中跳过它。跳过它也非常简单,因为我们可以识别pushf/pushapopa/popf指令,并执行与在whatami.exe进程中相同的操作。

通过 SEH 的过程应该也很简单。我们可以在处理程序地址0x004017D2处设置断点,并按F9直到到达处理程序。

我们可以选择其中的任何一个选项。在做出这样的决策时,最好先拍个虚拟机的快照。如果出现问题,我们可以通过恢复虚拟机快照来尝试两个选项。

我们的下一站是sub_401730。以下截图显示了sub_401730中的代码:

通过调试这段代码,可以发现使用了LoadLibraryAGetProcAddress来获取MessageBoxA的地址。之后,它只是显示了一个消息。

下一行代码是一个反自动化分析的技巧。我们可以看到,两个GetTickCount的结果差值正在与值0x0493e0300000进行比较。在GetTickCount的调用之间,还调用了一个 Sleep 函数。

一个 300000 的 Sleep 意味着 5 分钟。通常,自动化分析系统会将较长的 Sleep 时间改为非常短的时间。前面的代码希望确保 5 分钟的时间确实已经过去。作为调试这段代码的分析员,我们可以通过将指令指针设置在jb指令之后,简单地跳过这个技巧。

接下来是调用sub_401500,并传递两个参数:"mcdo.thecyberdung.net"和0x270F9999)。这个例程包含了套接字 API。像之前一样,让我们列出我们将遇到的 API 序列。

对于网络套接字行为,我们需要关注的是gethostbynamehtonssendrecv的参数和结果。同样,在继续之前,建议此时先拍摄一个虚拟机快照。

继续逐步调试,直到我们到达gethostbyname的调用。我们可以通过查看gethostbyname的参数来获取程序连接的服务器。而这个服务器就是"mcdo.thecyberdung.net"。继续调用后,我们可能会遇到gethostbyname的结果问题。寄存器 EAX 中的结果是零。这意味着gethostbyname失败了,因为它未能将"mcdo.thecyberdung.net"解析为一个 IP 地址。我们需要做的是设置FakeNet来模拟互联网。恢复虚拟机快照,以便回到执行WSAStartup之前的状态。

在运行FakeNet之前,通过从 VirtualBox 菜单中选择“机器->设置->网络”来断开电缆。展开“高级”菜单并取消选中“Cable connected”。我们进行这个操作是为了确保在FakeNet重新配置网络时不会发生干扰。

以下截图显示了FakeNet成功运行。FakeNet可能需要以管理员权限运行。如果发生这种情况,只需以管理员身份运行它:

通过勾选虚拟机网络设置中的“Cable Connected”复选框来恢复电缆连接。为了验证一切正常,打开 Internet Explorer 并访问任何网站。结果页面应类似于以下截图:

现在,我们可以回到gethostbyname地址继续调试。此时,我们应该能在寄存器EAX中获得一个结果,同时FakeNet正在运行。

我们接下来要关注的 API 是htons。它将为我们提供程序将要连接的服务器的网络端口信息。传递给htons的参数存储在寄存器ECX中。这就是将使用的端口号,0x270F或 9999。

继续调试,我们遇到connect函数,实际的连接到服务器和指定端口在此开始。如果连接成功,connect函数会返回零给寄存器EAX。在我们的情况下,这里返回的是-1,说明连接失败。

这样做的原因是,FakeNet 只支持常用且已知的恶意软件端口。幸运的是,我们可以编辑 FakeNet 的配置文件并将端口 9999 添加到列表中。FakeNet 的配置文件FakeNet.cfg位于与 FakeNet 可执行文件相同的目录中。但是在更新此文件之前,我们需要先恢复到WSAStartup调用之前的快照。

使用记事本编辑FakeNet.cfg文件。寻找包含“RawListner”的那一行。如果没有找到,就将以下几行添加到配置文件中。

RawListener Port:9999 UseSSL:No

当这行被添加时,配置文件应该如下所示:

注意我们添加的RawListener行。添加之后,重新启动FakeNet,然后再次调试,直到我们到达connect API。这次我们希望connect函数能成功执行。

继续调试,直到我们到达send函数。send函数的第二个参数(看栈顶第二项)指向要发送的数据的地址。按下F8继续发送数据,并查看FakeNet的命令控制台。

我们已经标出了这个程序与FakeNet之间的通信。请记住,FakeNet在这里是远程服务器的模拟。发送的数据是“OLAH”。

继续调试,直到我们再次遇到sendrecv函数。下一个函数是recv

第二个参数是从服务器接收数据的缓冲区。显然,我们不指望FakeNet返回任何数据。我们可以做的是监控随后处理这个recv缓冲区数据的代码。但是为了让recv调用成功,返回值应该是一个非零值。我们必须在执行recv调用后修改寄存器 EAX 的值,就像下面的截图所示:

接下来的代码行将接收到的数据与一个字符串进行比较。请参见下面的反汇编代码,使用repe cmpsb指令进行字符串比较。该指令比较寄存器ESIEDI指向的地址中的文本字符串。要比较的字节数存储在寄存器ECX中。假定接收到的数据位于寄存器ESI指向的地址。而字符串“jollibee”的地址则存储在寄存器EDI中。我们希望发生的情况是使两个字符串相等。

为了在调试会话中实现这一点,我们需要编辑接收到的数据地址上的字节,并将其设置为正在比较的 9 字符字符串。右键点击寄存器 ESI 的值,弹出上下文菜单,选择“Follow in Dump”。在 Dump 窗口中数据的第一个字节,右键点击并选择“Binary->Edit”。

这会弹出一个对话框(如下所示),在其中我们可以输入字符串“jollibee”:

按 F8 继续比较。这不应跳转到条件跳转指向的地址。继续调试直到我们到达另一个发送函数。再次查看要发送的数据,这是第二个参数指向的地址。然而,无论这是否成功,结果不会被处理。接下来的 API 通过closesocket和 WSACleanup 函数关闭连接,设置EAX1,并从当前函数返回。EAX只会在最后一个发送函数之后被设置为1

我们在下面的反汇编代码中突出显示了var_DBD,以查看在数据发送回服务器后,值1已被存储。

返回到WinMain函数后,最好进行一次虚拟机快照。

继续调试直到我们到达调用地址0x00401280。将有两个参数传递给该函数,值存储在EAXECX寄存器中。数据在Dump 1下被转储,如下所示:

输入函数0x00401280后,我们将只遇到一个 URLDownloadToFile 函数。该函数下载https://raw.githubusercontent.com/PacktPublishing/Mastering-Reverse-Engineering/master/ch12/manginasal并将其存储到名为unknown的文件中,如下截图所示:

这样做后,我们会遇到一个错误,无法下载文件。原因是我们仍处于模拟的互联网环境中。这一次,我们需要连接到真实的互联网。我们必须回到URLDownloadToFile函数之前的快照。

在 FakeNet 控制台中,按下CTRL + C退出工具。为了测试是否能够连接到互联网,请从互联网浏览器访问testmyids.com。结果应该与以下截图类似:

如果无法访问互联网,请检查 VirtualBox 的网络配置和 Windows 的网络设置。

网络连接正常后,程序应该能够成功下载文件。该文件的文件名为unknown。如果我们在 CFF Explorer 中加载此文件,我们将看到这些文件属性:

以下截图展示了通过选择 CFF Explorer 的 Hex Editor 查看文件内容:

该文件似乎是加密的。我们预计接下来的行为会处理这个文件。继续调试,直到我们到达地址0x004012e0。这个函数接受两个参数,一个是存储在EAX寄存器中的地址,另一个是压入栈中的地址。该函数从栈顶接收这些imagine参数字符串,以及从寄存器EAX接收unknown

进入函数后发现正在读取文件"unknown"的内容。读取该文件到新分配的内存空间的反汇编代码如下:

持续按F8直到CloseHandle调用之后。接下来的代码展示了Cryptographic API 的使用。我们在这里再次列出这些 API 的顺序:

.text:0040137A call ds:CryptAcquireContextA
.text:0040139B call ds:CryptCreateHash
.text:004013C8 call ds:CryptHashData
.text:004013EC call ds:CryptDeriveKey
.text:004013FF call sub_401290
.text:0040147B call ds:CryptDecrypt
.text:0040149D call ds:CreateFileA
.text:004014AF call ds:WriteFile
.text:004014B6 call ds:CloseHandle
.text:004014BE call ds:Sleep
.text:004014D9 call ds:CryptDestroyKey
.text:004014E4 call ds:CryptDestroyHash
.text:004014F1 call ds:CryptReleaseContext

根据列表,似乎所有解密的内容都会存储在文件中。我们想要了解的有以下几点:

  • 使用的加密算法

  • 使用的加密密钥

  • 存储数据的文件名称

要识别所使用的算法,我们应该监视CryptAcquireContextA函数中的参数。继续调试直到CryptAcquireContextA。第四个参数dwProvType应该告诉我们使用了哪种算法。dwProvType的值为0x18或 24。关于提供者类型值的列表,我们可以参考docs.microsoft.com/en-us/dotnet/api/system.security.permissions.keycontainerpermissionattribute.providertype。在这种情况下,24 定义为PROV_RSA_AES的值。因此,这里的加密算法使用的是RSA AES

该算法使用的加密密钥应该是CryptHashData函数的第三个参数。请查看以下截图中的CryptHashData函数的第二个参数:

密钥是this0is0quite0a0long0cryptographic0key

对于最后一条信息,我们需要监控CreateFileA,以获取解密数据可能被放置的文件名。在调试到CreateFileA时,我们应该看到第一个参数是输出文件名,"imagine"。CryptDecrypt函数接受加密数据的位置(第五个参数),并在同一位置进行解密。该过程以循环的形式运行,每一片解密后的数据都会附加到"imagine"文件中。

以下截图,IDA Pro 图形视图,显示了解密后的数据被附加到输出文件:

解密过程通过使用CryptDestroyKeyCryptDestroyHashCryptReleaseContext来关闭加密句柄。

好奇吗?让我们使用 CFF Explorer 从"imagine"文件中提取信息:

使用 TrID 工具,我们获得了更有意义的文件类型,如下图所示:

该文件是一个PNG图像文件。

继续调试会话,持续按F8直到到达0x00401180地址的调用。按F7进入此函数。这揭示了此序列中注册表 API 的使用:

.text:004011BF call ds:RegOpenKeyExA
.text:004011E6 call esi ; RegQueryValueExA
.text:004011F3 call edi ; RegCloseKey
.text:00401249 call ds:RegOpenKeyA
.text:0040126A call esi ; RegQueryValueExA
.text:00401271 call edi ; RegCloseKey

基本上,注册表函数仅用于检索注册表中存在的某些值。下面显示的反汇编代码表明,第一个查询从HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice注册表项中检索ProgId的数据值:

如果我们查看注册表,这个位置指向当前登录用户使用的默认互联网浏览器的 ID。以下截图显示了Progid中设置的默认互联网浏览器 ID 的示例,FirefoxURL-308046B0AF4A39CB

对于下一个注册表查询,RegOpenKeyExA打开HKEY_CLASSES_ROOT\FirefoxURL-308046B0AF4A39CB\shell\open\command注册表项,其中FirefoxURL-308046B0AF4A39CB是默认互联网浏览器的 ID:

随后的RegQueryValueExA有第二个参数lpValuename等于zero。请参考以下反汇编:

如果lpValuename等于0,则获取的数据将来自默认值。

查看注册表时,显示为(默认值),如下面所示:

因此,该函数执行的操作是获取默认互联网浏览器的命令行。

以下代码行解析了"imagine"文件的完整文件路径,然后将路径传递给最终函数sub_401000,然后退出进程:

在调试 sub_401000 时,我们遇到了一百多行代码,基本上是在移动测试字符串。但最终的bottomline是,它将使用 CreateProcessA 运行另一个进程。查看将传递给 CreateProcess 的参数时,第二个参数是命令行,它将执行的命令包含了默认浏览器的路径,并将 "imagine" 文件的完整路径作为参数。从以下截图可以看到,我们在 Dump 1 中转储了命令行:

结果是,使用默认的互联网浏览器打开 "imagine" 文件。显示以下截图:

分析总结

以下表格涉及我们发现的文件元素。

原始文件是一个 UPX 压缩的 Win32 可执行文件。

文件名 whatami.exe
文件大小 28,672 字节
MD5 F4723E35D83B10AD72EC32D2ECC61091
SHA-1 4A1E8A976F1515CE3F7F86F814B1235B7D18A231
文件类型 Win32 PE 文件 – 使用 UPX v3.0 压缩

UPX 解压版本为我们提供了关于该文件的新信息:

文件名 whatami.exe
文件大小 73,728 字节
MD5 18F86337C492E834B1771CC57FB2175D
SHA-1 C8601593E7DC27D97EFC29CBFF90612A265A248E
文件类型 Win32 PE 文件 – 由 Microsoft Visual C++ 8 编译

该程序通过进程空洞技术映射了一个未知的 PE 文件。该 PE 文件包含以下信息:

文件大小 53,248 字节
MD5 DD073CBC4BE74CF1BD0379BA468AE950
SHA-1 90068FF0C1C1D0A5D0AF2B3CC2430A77EF1B7FC4
文件类型 Win32 PE 文件 – 由 Microsoft Visual C++ 8 编译

raw.githubusercontent.com/PacktPublishing/Mastering-Reverse-Engineering/master/ch12/manginasal 下载的一个文件被作为未知文件存储。以下是该文件的信息:

文件名 unknown
文件大小 3,008 字节
MD5 05213A14A665E5E2EEC31971A5542D32
SHA-1 7ECCD8EB05A31AB627CDFA6F3CFE4BFFA46E01A1
文件类型 未知文件类型

该未知文件被解密并使用文件名 "imagine" 存储,包含以下文件信息:

文件名 imagine
文件大小 3,007 字节
MD5 7AAF7D965EF8AEE002B8D72AF6855667
SHA-1 4757E071CA2C69F0647537E5D2A6DB8F6F975D49
文件类型 PNG 文件类型

为了回顾它执行的行为,以下是一步步的过程:

  1. 显示消息框:"你是怎么来到这里的?"

  2. 从资源部分解密一个 PE 映像

  3. 使用进程空洞技术将 "calc" 替换为解密后的 PE 映像

  4. 显示消息框:"学习逆向工程很有趣。仅用于教育目的。这不是恶意软件。"

  5. 程序休眠 5 分钟

  6. 检查与 "mcdo.thecyberdung.net:9999" 服务器的连接

  7. raw.githubusercontent.com 下载该文件

  8. 解密下载的文件并将结果输出为 PNG 图像文件。

  9. 获取默认互联网浏览器的路径。

  10. 使用默认的互联网浏览器显示 PNG 图像文件。

总结

逆向工程软件需要时间和耐心。分析一款软件可能需要几天的时间。但随着练习和经验的积累,分析文件所需的时间会有所改善。

在这一章中,我们处理了一个可以使用我们所学工具逆向的文件。在调试器、反汇编器和 CFF Explorer、TriD 等工具的帮助下,我们能够提取文件信息和行为。此外,我们还学习了使用 FakeNet 模拟网络和互联网,当我们为套接字函数生成网络信息时,这对我们非常有用。

有很多障碍,包括反调试技巧。然而,对这些技巧的熟悉使我们能够绕过这些代码。

逆向工程中最重要的技巧之一是不断制作快照,以防遇到障碍。我们可以对每个功能所需的数据进行实验。

再次强调,逆向工程是一项需要耐心的工作,通过保存和加载快照,你可以“作弊”。

进一步阅读

DLL 注入 - en.wikipedia.org/wiki/DLL_injection

进程空洞化 - github.com/m0n0ph1/Process-Hollowing

第十三章:逆向不同类型的文件

到目前为止,我们一直在处理二进制可执行文件。在本章中,我们还将查看代码执行的其他方式。访问网站(HTML)和接收包含文档的电子邮件是恶意软件轻松进入目标系统的一些途径。

在本章中,我们将学习以下主题:

  • 在 HTML 中调试脚本

  • 理解 Office 文档中的宏

  • 执行 PDF 分析

  • SWF 分析

HTML 脚本分析

几乎我们访问的每个网站都包含脚本。最常见的是包含 JavaScript 代码,这些代码通常会在点击网站上的“OK”按钮时触发,或者是在鼠标指针周围游动的那些艺术泡泡和星星。JavaScript 是站点开发者可以使用的最强大工具之一。它可以控制互联网浏览器所包含的元素。

除了 JavaScript,Visual Basic 脚本(VBScripts)也可以嵌入到 HTML 网站中。然而,VBScript 在最近的网络浏览器中已被默认禁用。这是因为 VBScript 在过去曾暴露出许多安全漏洞。此外,JavaScript 是许多互联网浏览器默认使用的语言。

网站的工作有两个方面,即服务器端和客户端。当访问一个网站时,我们看到的是客户端页面。所有后台脚本都在服务器端运行。例如,当访问一个网站时,服务器端程序发送 HTML 内容,包括文本、脚本、图像、Java 小应用程序和 Flash 文件。只有浏览器元素,如 HTML、JavaScript、Java 小应用程序和 SWF Flash,能够被互联网浏览器支持,才是服务器端程序创建并发送的对象。从本质上讲,我们可以分析的就是这些浏览器元素。

幸运的是,脚本是可读的文本文件。我们可以对 HTML 脚本进行静态分析。但像其他代码一样,逆向工程需要我们了解所使用的脚本语言。归根结底,我们需要学习 JavaScript 编程语言的基础。

让我们尝试逆向一个简单的 HTML 文件。你可以通过以下链接下载此 HTML 文件:github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch13/demo_01.html

只有在你有时间的时候才做这个。当逆向 HTML 文件时,建议你设置它的运行方式,像是在网站中查看,而不是作为一个 HTML 文件。

使用文本编辑器,如记事本,我们可以对 HTML 文件进行静态分析。其他文本编辑器,如 Notepad++ (notepad-plus-plus.org/),会更好,因为它可以显示脚本语法的颜色。这有助于我们区分脚本函数和数据,如以下截图所示:

要理解这段代码,互联网上有很多关于 HTML 编程的参考资料。以下是其中一个参考网站:www.w3schools.com/html/default.asp。我们需要关注的是在 script 标签内定义的脚本。在这里共有三个 JavaScript 脚本代码。第一个脚本包含以下代码:

alert("Hello reverser! --from a javascript code");

alert 函数用于显示消息框。消息内容应放在引号内。

第二个脚本包含以下代码:

alert("1 + 2 is equal to");
x = 1
y = 2

再次,脚本显示一个消息,然后将 1 赋值给变量 x,将 2 赋值给变量 y

最后一个脚本包含以下代码:

alert("x + y");

这显示了另一个消息。这次,消息是 xy 变量的和,结果应该是 3。即使脚本代码位于不同的标签中,最后运行的脚本中的变量值也应该会在后续脚本中得到反映。

为了证明这个行为,让我们通过在浏览器中运行该文件来动态分析它。

打开 Internet Explorer。我们也可以使用 Firefox 或 Chrome。将 demo_01.html 文件拖放到 Internet Explorer 中。加载完成后,应该会显示以下消息框:

如果浏览器禁用了 JavaScript 内容的运行,消息可能不会显示。通常会弹出安全提示,询问是否允许运行脚本代码。只需允许脚本运行即可:

随后将弹出以下消息框:

现在页面已经完全加载,按 F12 打开调试器控制台。选择调试器面板。这时应该会显示 HTML 脚本,如下所示:

在调试器中,将断点放在第 3 行,这是第一个 alert 函数。要设置断点,请点击行号左侧的空白区域。这样应该会创建一个红点,表示断点行。以下截图展示了三个脚本及其第一行标记的断点:

通过将焦点放在浏览器页面上并按下 F5 键来刷新浏览器。我们可能会调试 browsertools 脚本,这是一个 Internet Explorer 初始化脚本。以下截图展示了这一过程:

再次按下 F5 键,让调试器继续执行,直到我们到达断点。此时,我们应该已经到达第一个 alert 函数,如下所示:

我们可以按 F11 键进入脚本,或按 F10 键跳过当前行。这样做应该会弹出第一个消息框。继续按 F10 键,跳过接下来的脚本行。下一个脚本是另一个 alert 函数:

以下几行将1赋值给x,将2赋值给y。我们可以通过将这些变量添加到监视列表中来监控这些变量的变化,监视列表位于右侧面板。点击“添加监视”来添加我们可以监控的变量:

最后的函数是另一个alert函数,用于显示xy的和。

让我们尝试用demo_02.html (github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch13/demo_02.html)。

如果我们调试这个,它执行的行为与我们在demo_01.html中遇到的一样。不同之处在于,当我们从文本编辑器查看时,它看起来是被混淆的:

消息被转换为转义格式,使用每个 ASCII 字符的十六进制等价物。在上一章中,我们学习了Cyberchef,这是一种在线工具,可以用来去混淆这些类型的数据。由于这些数据是转义的,我们应该使用unescape操作来解码这些数据。在Cyberchef中,搜索unescape操作,然后将转义数据复制并粘贴到输入窗口中。我们应该得到一个解码后的输出,显示我们在消息中看到的确切文本,像这样:

分析 HTML 脚本并不复杂,尤其是因为几乎所有内容都可以被人类读取。我们需要理解的只是语法和脚本语言的功能。此外,这是使用调试工具动态分析脚本的一种方法,而这些工具在网络浏览器中是可以使用的。

MS Office 宏分析

Microsoft Office 有一种方法可以自动化一些简单的任务,例如创建格式化的表格或插入信头。这叫做 MS Office 宏。MS Office 宏利用了 Visual Basic for Application 语言,它与 Visual Basic 脚本使用相同的语言。然而,这些也可以被滥用来做更多的事情,比如下载文件、创建文件、添加注册表条目,甚至删除文件。

首先,我们需要静态工具来读取信息并从给定的 Office 文件中提取宏源代码。要打开 MS Office 文档,我们需要安装 Microsoft Office。另一个可以使用的工具是 OLE 工具,可以从www.decalage.info/en/python/oletools下载。这些工具集是 Python 脚本,需要在系统上安装 Python 2.7。Python 安装程序可以从www.python.org/下载。

我们要分析的第一个文件是 github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch13/demo_01.doc。在命令行中输入以下代码,使用 olevba.py 分析 demo_01.doc

python olevba.py demo_01.doc

这将提取有关VBA源代码及其源信息:

从上面的截图中我们可以看到,源代码包含两个子程序:autoopen()autoclose()olevba.py 也描述了这些与文档打开和关闭时事件绑定的子程序。

该源包含弹出消息的代码。现在,让我们尝试在 Microsoft Word 中打开文档。通过这样做,我们可能会看到 Microsoft Word 显示有关文档包含代码的安全警告。点击启用内容,以便查看宏可以做什么:

第一个消息立刻出现:

要调试代码,我们需要打开 VBA 编辑器。选择查看 -> 宏,这将打开宏对话框,您可以在其中选择任何宏名称并点击编辑按钮:

我们当前使用的是 Microsoft Office 2013,因此 VBA 编辑器的用户界面在其他版本中可能有所不同。在 VBA 编辑器中,我们现在应该可以看到源代码。按下F9键可以启用或禁用断点。按F8键进行逐步调试。F5用于继续运行代码。我们可以从任何子程序开始调试。选择调试菜单查看更多可用的调试功能:

关闭文档将弹出以下消息框:

现在,尝试分析 demo_02.doc。由于我们将研究如何推导出密码,这将是一个相当大的挑战。

记住,VBA 编辑器是宏开发者的控制台。在这里,宏程序被开发和调试。因此,为了逆向我们正在寻找的内容,我们可以操作源代码。

demo_02.doc的密码可以在本章的摘要部分找到。

PDF 文件分析

PDF 文件已经发展到可以运行特定操作并允许执行 JavaScript。对于 PDF 分析,我们可以提取事件信息并分析 JavaScript 将执行的操作。我们可以使用 Didier Stevens 的 PDF 工具集来帮助分析 PDF。这一工具集是基于 Python 的,因此我们需要安装 Python。PDF 工具可以从 blog.didierstevens.com/programs/pdf-tools/ 下载。如果你访问该网站,可以看到有关每个工具的描述。

让我们尝试使用工具分析 github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch13/demo_01.pdf。使用 pdfid.py 执行以下命令:

python pdfid.py demo_01.pdf

以下截图显示了 pdfiddemo_01.pdf 上的结果:

在这里,我们可以看到它嵌入了 JavaScript 代码。现在让我们尝试使用 pdf-parser.py 文件,以便提取更多信息。PDF 文件中的某些元素可能已压缩,无法读取。pdf-parser 工具能够解压这些流。执行以下命令将 pdf-parser 的输出重定向到 demo_01.log 文件:

python pdf-parser.py demo_01.pdf > demo_01.log

pdf-parser 给出的输出与 demo_01.pdf 的内容基本相同。原因是没有 PDF 对象被解压缩。如果我们仔细查看输出内容,可以轻松识别出脚本代码的位置:

  <<
    /JS (app.alert({cMsg: "Reversing is fun!", cTitle: "Mastering Reverse Engineering"})
    ; )
    /S /JavaScript
  >>

因此,使用 Chrome 作为我们的 PDF 阅读器时,PDF 会显示以下消息框:

要调试 JavaScript,我们需要将其复制到一个单独的 JavaScript 或 HTML 文件中。我们可能还需要修复运行 JavaScript 运算符的语法。PDF 中的 JavaScript 代码可以转换为以下 HTML 代码:

<html>
<script>
    alert("Reversing is fun!", "Mastering Reverse Engineering");
</script>
</html>

SWF 文件分析

ShockWave Flash 文件也可以包含代码。基本上,Flash 文件是合法编写的,按照一系列任务的顺序执行。但就像任何其他代码一样,它也可能被滥用来执行恶意活动。

我们要分析的 SWF 文件可以从 github.com/PacktPublishing/Mastering-Reverse-Engineering/blob/master/ch13/demo01.swf 下载。

在撰写本书时,用于分析 SWF 的主要工具是 JPEXS SWF 反编译器。除此之外,我们先来谈谈其他可以解析 SWF 文件的现有工具。这些工具如下:

  • SWFTools

  • FLASM

  • Flare

  • XXXSWF

SWFTools

SWFTools 是一套用于读取和构建 SWF 文件的工具。它可以从 www.swftools.org/ 下载。要成功安装 SWFTools,应该以管理员身份运行。工具在命令行中使用。这里有两个可以提取 SWF 文件信息的工具:swfdumpswfextract。这是 swfdump 给出的结果:

结果告诉我们该文件是 zlib 压缩的。还有一个名为 MainDOABC 方法。DOABC 的存在也意味着嵌入了动作脚本。使用 HxD,我们可以验证文件是否被压缩。魔法头 CWS 表明 SWF 文件确实是压缩的。未压缩的 SWF 文件以 FWS 魔法字节开头:

另一个工具,swfextract,能够提取嵌入的视频或图像。demo01.swf不包含任何媒体,正如我们从以下截图中看到的:

SWFTools中的其他工具用于从 PDF、图像和视频构建SWF文件。

FLASM

FLASM是一个能够解压和反汇编SWF文件的工具。它可以从nowrap.de/flasm.html下载。我们使用-x参数解压了demo01.swf,并得到了以下输出:

之后,我们使用-d参数反汇编文件,并显示了关于SWF结构的信息:

我们在这里看不到任何反汇编或反编译的动作脚本。

Flare

这是一个能够反编译 ActionScript 代码的工具。它可以从nowrap.de/flare.html下载。然而,它可能无法完全支持AS2AS3代码。只需将SWF文件传递给 Flare 工具,它将生成一个FLR文件。我们可以使用以下命令执行 Flare:

flare.exe demo01.swf

结果保存在demo01.flr中,包含以下输出:

movie 'demo01.swf' {
// flash 32, total frames: 1, frame rate: 30 fps, 800x600 px, compressed, network access alowed

  metadata <rdf:RDF xmlns:rdf=\'http://www.w3.org/1999/02/22-rdf-syntax-ns#\'><rdf:Description rdf:about=\'\' xmlns:dc=\'http://purl.org/dc/elements/1.1\'><dc:format>application/x-shockwave-flash</dc:format><dc:title>Adobe Flex 4 Application</dc:title><dc:description>http://www.adobe.com/products/flex</dc:description><dc:publisher>unknown</dc:publisher><dc:creator>unknown</dc:creator><dc:language>EN</dc:language><dc:date>Oct 29, 2018</dc:date></rdf:Description></rdf:RDF>

  // unknown tag 82 length 706

  // unknown tag 76 length 9
}

它的结果与FLASM相同,没有反汇编任何动作脚本。

XXXSWF

该工具可以从github.com/viper-framework/xxxswf下载。它是一个 Python 脚本,接受以下参数:

Usage: xxxswf.py [options] <file.bad>

Options:
  -h, --help show this help message and exit
  -x, --extract Extracts the embedded SWF(s), names it MD5HASH.swf &
                        saves it in the working dir. No addition args needed
  -y, --yara Scans the SWF(s) with yara. If the SWF(s) is
                        compressed it will be deflated. No addition args
                        needed
  -s, --md5scan Scans the SWF(s) for MD5 signatures. Please see func
                        checkMD5 to define hashes. No addition args needed
  -H, --header Displays the SWFs file header. No addition args needed
  -d, --decompress Deflates compressed SWFS(s)
  -r PATH, --recdir=PATH
                        Will scan a directory for files that contain SWFs.
                        Must provide path in quotes
  -c, --compress Compress SWF using Zlib
  -z, --zcompress Compress SWF using LZMA

我们尝试使用这个工具处理demo01.swf。在使用-H参数后,工具告诉我们该文件已被压缩。然后我们使用-d选项解压了文件,得到了一个解压后的SWF版本,保存在243781cd4047e8774c8125072de4edb1.swf文件中。最后,我们对解压后的文件使用了-H参数:

到目前为止,在没有yaramd5功能的情况下,最有用的功能是它能够搜索嵌入的 Flash 文件。这在检测包含嵌入 SWF 的SWF恶意软件时非常有用。

JPEXS SWF 反编译器

最常用的 SWF 文件分析工具之一是JPEXS SWF 反编译器。夜间版本可以从github.com/jindrapetrik/jpexs-decompiler下载。该工具能够反编译支持AS3ActionScript。以下截图显示了JPEXS控制台:

除了能够反编译,它还具有一个可以与 Adobe Flash Player 的调试器进行设置的界面。安装 JPEXS 后,我们需要从www.adobe.com/support/flashplayer/debug_downloads.html下载flash player projector 内容调试器

打开 JPEXS,然后选择设置->高级设置->路径。接着,浏览到下载的 Flash 可执行文件,填写 Flash Player 投影仪内容调试器路径。完成后点击确定:

这是一个重要的设置,它使我们能够调试反编译后的 ActionScript。你也可以通过从www.adobe.com/support/flashplayer/debug_downloads.html下载 Flash Player 投影仪来填写 Flash Player 投影仪路径。

打开 SWF 文件并展开左侧窗口中的对象树。在scripts对象下选择 Main。这样就会显示反编译后的 ActionScript,如下图所示:

这里是demo01.swf的反编译代码:

package
{
   import flash.display.Sprite;
   import flash.text.TextField;

   public class Main extends Sprite
   {

      public function Main()
      {
         super();
         trace("Hello World!");
         var myText:TextField = new TextField();
         myText.text = "Ahoy there!";
         myText.textColor = 16711680;
         myText.width = 100;
         myText.height = 100;
         addChild(myText);
         var myText2:TextField = new TextField();
         myText2.text = "Reversing is fun!\n--b0yb4w4n9";
         myText.y = 100;
         addChild(myText2);
      }
   }
}

点击调试按钮或Ctrl+F5,这应该会带我们进入调试控制台。在最左边的窗口中,显示的是反编译后的 ActionScript 的字节码等效物。

代码的作用是创建两个 TextFields,包含显示在 SWF 显示空间上的文本。

JPEXS 是一款具有我们希望用来分析 Flash 文件中代码的关键功能的工具。它具备字节码反汇编器、源代码反编译器和调试器。

总结

分析各种文件类型也采用与逆向工程相同的概念。在本章中,我们学习了文件格式所使用的脚本语言。如果我们有兴趣理解文件的头部和结构,我们还可以收集更多的信息。我们还了解到,只要可执行代码可以嵌入到文件中,就一定有方法可以分析它。虽然可能无法轻松进行动态分析,但至少可以进行静态分析。

我们讨论了如何调试嵌入在 HTML 脚本中的 JavaScript。实际上,我们可以分析我们访问的任何网站。我们还了解了可以用来提取 Microsoft Office 文档中宏代码的工具。恰好,我们也可以使用 VBA 编辑器调试这些宏代码。我们还研究了多种工具,用于从 PDF 文件中提取 JavaScript 代码。然后,我们使用 JPEXS 分析了 SWF 文件,这是一个强大的工具,具备反汇编器、反编译器和调试器。

逆向工程软件是一个实践中的概念。我们研究软件是什么以及它是如何工作的。我们还学习了执行文件中代码背后的低级语言。学习这门语言可能需要时间,但从中获得的知识和经验是值得的。

祝你逆向工程愉快!

P.S. demo_02.doc的密码是 burgersteak。

深入阅读

www.w3schools.com/html/default.asp:一个学习 HTML 脚本的优秀教程网站

www.javascriptobfuscator.com - 这是一个可以混淆 JavaScript 代码的在线网站

posted @ 2025-07-07 14:37  绝不原创的飞龙  阅读(164)  评论(0)    收藏  举报