首页
社区
课程
招聘
[翻译]基于 Ghidra 脚本的逆向工程任务自动化
发表于: 2025-3-7 16:43 4755

[翻译]基于 Ghidra 脚本的逆向工程任务自动化

2025-3-7 16:43
4755

(译者注:本文是对国外逆向工程师David Álvarez Pérez 所著《Ghidra Software Reverse Engineering for Beginners》第二章《Automating RE Tasks with Ghidra Scripts》主要内容的翻译,版权归原作者所有,翻译如有错误,还请各位读者指正。为熟悉Ghidra操作,文中大部分截图是译者自己截图,与原文有些许差异,如需查看原文可点击文末链接)

Ghidra 脚本允许你在分析二进制文件时自动化逆向工程任务。让我们概述一下如何在“Hello World”程序中使用 CodeBrowser 中的脚本。我们从一个加载到 Ghidra 的 CodeBrowser 中的“Hello World”程序开始,如第 1 章《Ghidra 入门》的“Ghidra 功能概述”部分所述。

正如本章引言中提到的,Ghidra 包含一个真正的脚本库。要访问它,请转到(工具栏的) Window,然后选择 Script Manager。或者,点击下图中突出显示的按钮:

Figure 2.1 – The run script button highlighted in the quick access bar

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

Figure 2.2 – Script Manager

在上图中,当点击 Script Manager 窗口右上角的清单图标时,将显示脚本目录的路径:

Figure 2.3 – Script directories

这是一个非常好的开始,可以尝试使用现有脚本。你可以使用 Ghidra 分析和编辑所有这些脚本。这将帮助你理解它们的工作原理以及如何根据你的需求进行适配。使用下图中突出显示的图标来编辑脚本或创建新脚本:

Figure 2.4 – The edit script and create a new script buttons highlighted in the quick access bar

由于我们正在分析一个仅在屏幕上打印“Hello World”的“Hello World”程序,我们可以选择一个与字符串相关的 Ghidra 脚本,然后看看它如何加快分析速度。正如你在下图中看到的,Python 和 Java 脚本在 Script Manager 中混合在一起:

Figure 2.5 – String-related scripts available in Script Manager

例如,RecursiveStringFinder.py 文件可以通过显示所有函数及其关联的字符串来加快你的分析速度。它之所以能加快分析速度,是因为字符串可以揭示函数的目的,而无需阅读任何一行代码。

让我们执行上述脚本,以“Hello World”程序的 _mainCRTStartup() 函数(译者的样本与书中的不同)作为输入(你需要将光标放在此函数上),同时在脚本控制台中查看输出。

正如你在下图中看到的,RecursiveStringFinder.py 打印了一个根据调用深度缩进的函数列表,每个函数都包含其引用的字符串。

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

Figure 2.6 – Result of running the RecursiveStringFinder.py script over a Hello World program

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

我们修改了这个脚本,在将字符串附加到结果列表时,在 isAnInterestingString()中实现了一个过滤器,以确定是否将其附加到结果列表中。

想象一下,你正在分析的程序代码中寻找 URL,这在分析恶意软件时非常有用,因为它可以揭示攻击者的服务器。你需要做的就是打开 Script Manager 并转到 strings 文件夹(此脚本与字符串相关)。然后,打开 RecursiveStringFinder.py 脚本,并通过实现 isAnInterestingString() 函数向其添加过滤条件。

作为一般规则,在编写脚本之前,请先检查 Ghidra 的脚本库中是否已经存在类似的内容:

这个脚本可以轻松修改为在代码中搜索 URL,这在分析恶意软件时非常有用。你需要做的就是将 isAnInterestingString() 中的条件替换为适当的正则表达式。

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

之前的脚本允许你通过在其前后添加 TODO 字符串来修改程序中引用的字符串。在某些情况下,这个脚本可能很有用。例如,如果你需要解码大量 Base64 编码的字符串或击败某些类似的恶意软件混淆,那么可以修改 translateString() 函数,该函数负责获取输入字符串,应用一些转换,然后返回它。

run() 函数是 Ghidra 脚本的主函数。在本例中,首先将字符串计数器初始化为零,然后对于每个字符串,计数器递增,同时字符串转换被生成并在每次循环迭代中显示。

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

Figure 2.7 – Result of running TranslateStringsScript.java over a Hello World program

我们已经了解了如何使用现有脚本以及如何根据我们的需求进行适配。接下来,我们将学习 Ghidra 脚本类的具体工作原理。

要开发 Ghidra 脚本,你需要点击 Script Manager 菜单中的 Create New Script 选项。然后,你可以选择使用哪种编程语言:

Figure 2.8 – The programming language dialog during new script creation

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

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

Figure 2.9 – Enabling a script to be integrated with Ghidra

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

上述源代码注释会生成以下脚本与 Ghidra 菜单的集成:

Figure 2.10 – Result of integrating a new script with Ghidra

接下来是导入部分,GhidraScript 在其中至关重要,必不可少。所有脚本都必须继承自此类,并实现 run() 方法(该方法是主方法):

所有导入都在 Ghidra 的 JavaDoc 文档中有详细说明;在开发脚本时,你应该参考它。

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

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

currentProgramcurrentAddresscurrentLocationcurrentSelectioncurrentHighlight

如果你想使用 Python 编写脚本,API 与 Java 相同,脚本框架包含一个头部(脚本的其余部分必须用你自己的代码填充),而且它与 Java 的非常相似:

事实上,Java API 通过使用 Jython 包装暴露给 Python,Jython 是 Python 编程语言的一种实现,旨在在 Java 平台上运行。

如果你转到 Window,然后选择 Python,将出现一个 Python 解释器,当按下 Tab 键时,它会自动补全:

Figure 2.11 – The Ghidra Python interpreter autocompletion feature

它还允许你使用 help() 函数查看文档。正如你可能已经注意到的,强烈建议在开发 Ghidra 脚本时打开 Ghidra Python 解释器,以便快速访问文档、测试代码片段等。它非常有用:在本节中,我们介绍了脚本类及其结构,如何查询 Ghidra API 文档以实现它,以及 Python 解释器如何在开发过程中帮助我们。在下一节中,我们将通过编写一个 Ghidra 脚本将其付诸实践。

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

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

然后,我们的脚本需要做的就是获取 Ghidra 中当前光标的位置(currentLocation 变量),然后获取其地址,该地址的指令未定义,用 NOP 指令操作码 0x90 修补字节,并再次反汇编字节。这里的重要工作是在上述 Javadoc 文档中搜索适当的 API 函数:

当然,正如你所知道的,将这段代码翻译成 Python 非常简单,因为如前所述,两种语言的 API 是相同的:

在本节中,我们介绍了如何用两种支持的语言(Java 和 Python)编写一个简单的 Ghidra 脚本。

在本章中,你学习了如何使用现有的 Ghidra 脚本,如何轻松地根据你的需求进行适配,最后,如何为你喜欢的语言开发一个极其简单的脚本作为该主题的入门。

在第 6 章《脚本化恶意软件分析》和第 9 章《脚本化二进制审计》中,你将通过开发和分析应用于恶意软件分析和二进制审计的更复杂脚本来提高 Ghidra 脚本编写技能。

在下一章中,你将学习如何通过将 Ghidra 与 Eclipse IDE 集成来调试 Ghidra,这是一项极其有用且必需的技能,用于扩展 Ghidra 功能以及探索其内部结构。

(资源链接:https://bbs.kanxue.com/thread-264947-1.htm)

def
    isAnInterestingString(string):
    """Returns True if the string is interesting for us"""
return string.startswith("http")
def getStringReferences(insn):
    """Get strings referenced in any/all operands of an instruction, if present"""
    numOperands = insn.getNumOperands()
    found = []
    for i in range(numOperands):
        opRefs = insn.getOperandReferences(i)
        for o in opRefs:
            if o.getReferenceType().isData():
                string = getStringAtAddr(o.getToAddress())
                if string is not None and \
                             isAnInterestingString(string):
                        found.append(
                            StringNode(insn.getMinAddress(),o.getToAddress(),string))
return found
def
    isAnInterestingString(string):
    """Returns True if the string is interesting for us"""
return string.startswith("http")
def getStringReferences(insn):
    """Get strings referenced in any/all operands of an instruction, if present"""
    numOperands = insn.getNumOperands()
    found = []
    for i in range(numOperands):
        opRefs = insn.getOperandReferences(i)
        for o in opRefs:
            if o.getReferenceType().isData():
                string = getStringAtAddr(o.getToAddress())
                if string is not None and \
                             isAnInterestingString(string):
                        found.append(
                            StringNode(insn.getMinAddress(),o.getToAddress(),string))
return found
public class TranslateStringsScript extends GhidraScript {
 
    private String translateString(String s) {
        // customize here
        return "TODO " + s + " TODO";
    }
 
    @Override
    public void run() throws Exception {
        if (currentProgram == null) {
            return;
        }
 
        int count = 0;
 
        monitor.initialize(currentProgram.getListing().getNumDefinedData());
        monitor.setMessage("Translating strings");
 
        for (Data data : DefinedDataIterator.definedStrings(currentProgram, currentSelection)) {
            if (monitor.isCancelled()) {
                break;
            }
 
            StringDataInstance str = StringDataInstance.getStringDataInstance(data);
            String s = str.getStringValue();
 
            if (s != null) {
                TranslationSettingsDefinition.TRANSLATION.setTranslatedValue(data, translateString(s));
                TranslationSettingsDefinition.TRANSLATION.setShowTranslated(data, true);
                count++;
                monitor.incrementProgress(1);
            }
        }
        println("Translated " + count + " strings.");
    }
}
public class TranslateStringsScript extends GhidraScript {
 
    private String translateString(String s) {
        // customize here
        return "TODO " + s + " TODO";
    }
 
    @Override
    public void run() throws Exception {
        if (currentProgram == null) {
            return;
        }
 
        int count = 0;
 
        monitor.initialize(currentProgram.getListing().getNumDefinedData());
        monitor.setMessage("Translating strings");
 
        for (Data data : DefinedDataIterator.definedStrings(currentProgram, currentSelection)) {
            if (monitor.isCancelled()) {
                break;
            }
 
            StringDataInstance str = StringDataInstance.getStringDataInstance(data);
            String s = str.getStringValue();
 
            if (s != null) {
                TranslationSettingsDefinition.TRANSLATION.setTranslatedValue(data, translateString(s));
                TranslationSettingsDefinition.TRANSLATION.setShowTranslated(data, true);
                count++;
                monitor.incrementProgress(1);
            }
        }
        println("Translated " + count + " strings.");
    }

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2025-3-7 19:10 被ZyOrca编辑 ,原因:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回