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()、FunctionManager、Function。我个人习惯把这三个对象称为「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)来控制顺序。
下面这张图展示了分析器的执行流程:
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,简直是为自动化逆向量身定做的。
公众号:蓝海资料掘金营,微信deep3321