97、自动化逆向(二):Ghidra脚本编写、自定义分析器

上一讲我们聊了Ghidra的基本操作和插件开发。说实话,那只是开胃菜。真正让Ghidra变得可怕(或者说强大)的,是它的脚本系统和自定义分析器。我当年第一次用IDA写IDC脚本时,差点被那语法逼疯。后来转到Ghidra,发现用Python写脚本简直是在做慈善——舒服太多了。

这一讲,我们深入两个核心能力:脚本编写自定义分析器。前者让你批量处理二进制文件,后者让你在反编译过程中注入自己的分析逻辑。

为什么需要脚本和分析器?

你想想看,每次逆向一个固件,都要手动标记字符串、识别加密函数、定位关键跳转……重复劳动多了,人就容易出错。我有个习惯:任何重复超过3次的操作,必须写成脚本

Ghidra的脚本系统允许你用Python或Java直接操作程序的AST(抽象语法树)、函数、基本块、指令。而自定义分析器则是在Ghidra加载二进制文件时,自动执行你的分析逻辑——比如自动识别某种自定义加密算法,或者自动标记特定的API调用模式。

核心区别:脚本是手动触发的,分析器是自动运行的。两者互补,缺一不可。

Ghidra脚本编写实战

1. 脚本环境与基础API

Ghidra的Python脚本基于Jython(Java上的Python实现)。嗯,这里要注意:不是CPython,所以有些标准库用不了(比如numpy、requests)。但Ghidra自己封装了一套强大的API,足够你折腾了。

先看一个最简单的脚本——遍历所有函数并打印名称:

# 获取当前程序
program = getCurrentProgram()
# 获取函数管理器
func_mgr = program.getFunctionManager()
# 遍历所有函数
for func in func_mgr.getFunctions(True):
    print(f"函数名: {func.getName()}, 地址: {func.getEntryPoint()}")

这段代码虽然简单,但包含了最常用的三个对象:getCurrentProgram()FunctionManagerFunction。我个人习惯把这三个对象称为「Ghidra脚本三件套」。

2. 实战:批量提取字符串引用

我在分析一个IoT固件时,遇到过一个问题:固件里有一千多个字符串,但很多是加密存储的。我需要找到所有调用解密函数的代码位置。手动找?那得找到猴年马月。

于是写了这个脚本:

from ghidra.program.model.listing import Function
from ghidra.program.model.address import Address

# 目标解密函数名称
DECRYPT_FUNC_NAME = "decrypt_string"

# 获取当前程序
program = getCurrentProgram()
func_mgr = program.getFunctionManager()

# 找到解密函数
decrypt_func = func_mgr.getFunction(
    program.getSymbolTable().getSymbol(DECRYPT_FUNC_NAME).getAddress()
)

if decrypt_func is None:
    print(f"未找到函数: {DECRYPT_FUNC_NAME}")
    exit(1)

# 获取所有调用该函数的位置
calls = getReferencesTo(decrypt_func.getEntryPoint())

print(f"找到 {len(calls)} 处调用:")
for ref in calls:
    caller_addr = ref.getFromAddress()
    caller_func = func_mgr.getFunctionContaining(caller_addr)
    print(f"  调用地址: {caller_addr}, 所在函数: {caller_func.getName()}")

这个脚本的核心是getReferencesTo()函数,它能反向查找所有引用某个地址的位置。说白了,就是「谁调用了这个函数」。

避坑指南:我曾经在分析一个混淆过的固件时,发现解密函数被内联了。这时候getReferencesTo()就找不到调用了。解决办法是先做一遍「恢复内联」的反编译优化,再跑脚本。

3. 脚本与GUI交互

有时候你需要让用户选择一些参数。Ghidra提供了askChoice()askString()等交互函数:

# 让用户选择分析模式
mode = askChoice("选择模式", "请选择分析模式:", 
                 ["快速扫描", "深度分析", "自定义"], "快速扫描")

if mode == "快速扫描":
    print("执行快速扫描...")
elif mode == "深度分析":
    print("执行深度分析(可能需要几分钟)...")
else:
    custom_param = askString("自定义参数", "请输入参数:")
    print(f"自定义模式,参数: {custom_param}")

嗯,这里要注意:交互函数只能在Ghidra的脚本面板中运行,如果你用命令行模式(headless)跑脚本,这些函数会直接报错。所以写脚本时最好加个判断:

if isRunningHeadless():
    # 命令行模式,使用默认参数
    mode = "快速扫描"
else:
    # GUI模式,让用户选择
    mode = askChoice(...)

自定义分析器开发

1. 分析器的工作原理

Ghidra的分析器(Analyzer)是在程序加载后、反编译前执行的一系列插件。每个分析器可以修改程序数据库(添加注释、标记函数、创建数据结构等)。

分析器的执行顺序很重要。比如「识别x86函数边界」这个分析器必须在「识别调用约定」之前运行。Ghidra通过优先级(Priority)来控制顺序。

下面这张图展示了分析器的执行流程:

Ghidra 自定义分析器执行流程 1. 程序加载完成 2. 内置分析器执行(函数识别、调用约定等) 3. 自定义分析器执行(你的逻辑) 4. 反编译与展示 优先级控制 BEFORE / AFTER 分析器可以 修改程序数据库 添加注释 标记函数 创建数据结构

2. 编写第一个自定义分析器

创建一个分析器需要继承Analyzer接口,并实现几个关键方法。下面是一个实战例子——自动识别并标记所有调用memcpy的缓冲区溢出风险点:

from ghidra.app.services import Analyzer
from ghidra.app.analyzers import AnalyzerType
from ghidra.program.model.listing import Function
from ghidra.util.task import TaskMonitor

class MemcpyOverflowAnalyzer(Analyzer):
    
    def __init__(self):
        super().__init__()
        self.name = "Memcpy溢出检测器"
        self.analyzer_type = AnalyzerType.BYTE_ANALYZER
        self.priority = "BEFORE"  # 在反编译之前运行
    
    def getDefaultEnablement(self, program):
        # 默认启用
        return True
    
    def canAnalyze(self, program):
        # 只分析PE和ELF格式
        fmt = program.getExecutableFormat()
        return "PE" in fmt or "ELF" in fmt
    
    def added(self, program, set, monitor, task_monitor):
        # 主分析逻辑
        func_mgr = program.getFunctionManager()
        
        # 找到memcpy函数
        memcpy = func_mgr.getFunction(
            program.getSymbolTable().getSymbol("memcpy").getAddress()
        )
        
        if memcpy is None:
            return True
        
        # 获取所有调用memcpy的位置
        refs = getReferencesTo(memcpy.getEntryPoint())
        
        for ref in refs:
            caller_addr = ref.getFromAddress()
            caller_func = func_mgr.getFunctionContaining(caller_addr)
            
            # 在调用位置添加高亮注释
            listing = program.getListing()
            code_unit = listing.getCodeUnitAt(caller_addr)
            code_unit.setComment(
                0,  # 前置注释
                f"[溢出风险] 调用memcpy,请检查长度参数!"
            )
            
            # 在函数上添加标签
            if caller_func:
                caller_func.setComment(
                    f"包含memcpy调用,可能存在缓冲区溢出"
                )
        
        return True
注意:分析器中的added()方法可能会被多次调用(比如用户手动重新分析)。所以你的逻辑必须是幂等的——多次执行不会产生副作用。我踩过这个坑:第一次运行加了注释,第二次运行又加了一遍,结果注释重复了。

3. 分析器的优先级与依赖

Ghidra的分析器系统支持复杂的依赖关系。你可以指定你的分析器在某个内置分析器之后运行:

# 在分析器中声明依赖
self.supports_analysis = True
self.analysis_priority = "AFTER"
self.analysis_dependencies = ["Function Start Analyzer", "Call Convention Analyzer"]

为什么要这样做?举个例子:如果你的分析器需要识别函数参数,那么「调用约定分析器」必须先运行,否则你拿到的参数信息是空的。

4. 调试分析器

分析器不像脚本那样可以随时打断点。我常用的调试方法是:

  • 日志输出:使用task_monitor.setMessage()输出进度信息
  • 临时标记:在分析过程中创建书签(Bookmark),方便事后检查
  • 单元测试:用Ghidra的测试框架加载一个小的测试二进制文件,验证分析结果

这里有个小技巧:在分析器里创建书签:

from ghidra.program.model.listing import BookmarkType

bookmark_mgr = program.getBookmarkManager()
bookmark_mgr.setBookmark(
    address, 
    BookmarkType.ANALYSIS, 
    "自定义分析器", 
    "这里检测到可疑模式"
)

书签会显示在Ghidra的「书签」面板里,比注释更显眼。我个人习惯用红色书签标记高危问题,绿色书签标记已确认的安全操作。

实战:自动化识别自定义加密算法

好了,理论说完了,我们来个综合实战。假设你拿到一个固件,里面有一个自定义的加密算法。特征如下:

  • 使用固定的S盒(256字节)
  • 循环16轮
  • 每轮使用不同的轮密钥

我们要写一个分析器,自动识别这种模式:

class CustomCryptoAnalyzer(Analyzer):
    
    def __init__(self):
        super().__init__()
        self.name = "自定义加密识别器"
        self.S_BOX_SIZE = 256
    
    def added(self, program, set, monitor, task_monitor):
        listing = program.getListing()
        func_mgr = program.getFunctionManager()
        
        # 遍历所有函数
        for func in func_mgr.getFunctions(True):
            # 检查函数大小(加密函数通常不会太小)
            body = func.getBody()
            if body.getNumAddresses() < 50:
                continue
            
            # 检查函数内是否有循环结构
            has_loop = False
            for block in func.getBody().getBlocks():
                if block.getNumAddresses() > 10:
                    has_loop = True
                    break
            
            if not has_loop:
                continue
            
            # 检查函数是否引用了256字节的S盒
            data_mgr = program.getListing()
            data_refs = getReferencesTo(func.getEntryPoint())
            
            s_box_found = False
            for ref in data_refs:
                addr = ref.getFromAddress()
                data = data_mgr.getDataAt(addr)
                if data and data.getNumBytes() == self.S_BOX_SIZE:
                    s_box_found = True
                    break
            
            if s_box_found:
                # 标记为加密函数
                func.setComment("[自动识别] 疑似自定义加密函数")
                bookmark_mgr = program.getBookmarkManager()
                bookmark_mgr.setBookmark(
                    func.getEntryPoint(),
                    BookmarkType.ANALYSIS,
                    "加密函数",
                    f"检测到S盒引用,函数大小: {body.getNumAddresses()}"
                )
        
        return True

这个分析器虽然简单,但已经能抓到大部分自定义加密了。我在一个路由器固件里用这个分析器,一次性标记了12个加密函数,省了我整整一天的手动分析时间。

总结

Ghidra的脚本和分析器系统,说白了就是让你把逆向分析中重复的、机械的工作交给机器去做。你只需要关注那些真正需要人类智慧的判断——比如算法逻辑、漏洞利用思路。

我个人建议的学习路径:先写10个脚本练手(批量提取字符串、标记API调用、统计函数数量等),再尝试写一个自定义分析器。等你熟练了,你会发现Ghidra比IDA在某些方面更灵活——尤其是它的Python API,简直是为自动化逆向量身定做的。

最后提醒:Ghidra的脚本和分析器都支持Java和Python。如果你追求性能(比如处理超大二进制文件),用Java写分析器;如果你追求开发速度,用Python。我一般先用Python写原型,确认逻辑正确后再用Java重写一遍——嗯,这是我在一个2GB的固件分析项目里总结出来的经验。

公众号:蓝海资料掘金营,微信deep3321