关于 pyghidra-scripting
这项技能介绍如何通过 ReVa 的 MCP 工具,在 Ghidra 会话中借助 PyGhidra 创建、编辑、运行和查看 Python 脚本,适用于需要自定义二进制分析的分析人员。
主要功能
- 01在程序上运行 Python 脚本
- 02列出、读取、写入和编辑脚本
- 03通过 PyGhidra 使用 Ghidra Java API
- 04处理事务与执行限制
适用场景
- 01按自定义条件筛选函数或指令
- 02一次组合调用多个 Ghidra API
- 03保存并迭代分析脚本
MCP 相关
ReVa 的 `scripts` 提供程序包含五个 MCP 工具,用于脚本执行和文件操作。`run-script` 可在 ReVa 服务的 Ghidra 会话中执行代码。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cyberkaida/ reverse-engineering-assistant --skill pyghidra-scripting安装后在 Claude Code 中输入/pyghidra-scripting确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/decompiler-pcode.mdflat-api.mdjpype-interop.mdpersistent-scripts.mdrecipes.md详细介绍
通过 ReVa 使用 PyGhidra 编写脚本
ReVa 在 scripts 提供程序下公开了五个 MCP 工具,允许助手在 ReVa 正在服务的同一个 Ghidra 会话中编写、编辑、运行和检查 Python 脚本。Python 代码通过 PyGhidra(CPython 3 + JPype)运行,因此可以使用完整的 Ghidra Java API——包括 FlatProgramAPI、DecompInterface、FunctionManager 等。
本技能介绍何时使用这些工具、如何组织发送给 run-script 的 Python 代码,以及实践中容易踩到的坑。
五个工具一览
| 工具 | 用途 | 适用场景 |
|---|---|---|
run-script | 对程序执行 Python。可以内联提供 code,也可以通过 scriptPath / scriptName 指定脚本。 | ReVa 专用 MCP 工具无法满足需求,或你想快速遍历程序一次时。 |
list-scripts | 枚举已注册目录中的脚本。可按名称 / 路径筛选;支持分页。 | 编写新脚本之前,查找已有的已保存脚本。 |
read-script | 以 cat -n 的形式查看脚本,支持 offset / limit,并返回 truncated 标记。 | 编辑现有脚本之前先阅读脚本。带行号的输出可用于 edit-script 的行匹配。 |
write-script | 创建新的 .py 文件(或使用 overwrite: true 完整覆盖)。仅限用户可写目录。 | 保存可复用脚本。不要仅为了封装内联代码而使用此工具——一次性执行请使用带 code 的 run-script。 |
edit-script | 将 old_string 替换为 new_string。如果 old_string 匹配多个位置且未设置 replace_all: true,则会报错。 | 迭代修改现有脚本。先配合 read-script 查看行上下文。 |
适用场景
run-script 是应急方案。以下情况可以使用:
- 你需要对每个函数 / 符号 / 指令运行自定义谓词,而现有的列表 / 查找工具无法按所需方式筛选。
- 你需要将多个 Ghidra API 调用合并为一次操作,否则就要进行多次 MCP 往返。
- 你想使用 ReVa 未公开的 Ghidra API,例如
DecompInterface的底层功能、PCode分析、自定义AddressSet运算、直接使用数据流 API、BinaryReader等。 - 你正在进行原型开发;确认可行且可复用后,用
write-script保存。
如果 ReVa 已有专用 MCP 工具可以完成任务,就不要使用 run-script——这些工具具有稳定的架构和结构化输出,比自由格式的 stdout 更容易分析。反编译器 / 函数 / 字符串 / 符号 / 交叉引用工具可以覆盖常见需求。
运行时限制:仅支持 PyGhidra
只有在 通过 PyGhidra 启动 Ghidra 时,run-script 才能正常工作:
- ✅
mcp-reva(Claude CLI / stdio 模式) - ✅
pyghidra-gui(通过 PyGhidra 启动的 GUI 模式) - ✅
reva_headless_server.py(通过 PyGhidra 启动的无头模式) - ❌ 普通的
ghidraRun——未接入 PyGhidra;你会收到包含启动指导的PyGhidraNotAvailableException错误响应。
如果 run-script 调用返回此错误,正确的做法是告诉用户如何重新启动(不要继续重试)。另外四个工具(list-scripts、read-script、write-script、edit-script)在所有模式下都可用,因为它们只是普通的文件操作。
run-script 的执行约定
这是你的内联 code(或已保存脚本)运行时遵循的约定。请牢记这些内容——大多数故障都是因为其中某项理解有误。
预先绑定的全局变量。 脚本作为 Ghidra PyGhidra 脚本运行,因此标准的 GhidraScript 全局变量已定义:currentProgram、currentAddress、currentSelection、currentHighlight、monitor、state,以及 FlatProgramAPI 辅助函数,例如 toAddr、getFunctionAt、getFunctionContaining、getSymbolAt、getInstructionAt、getDataAt、getReferencesTo、getReferencesFrom、createLabel、setEOLComment、find、findBytes、clearListing。你无需导入或设置这些内容。currentProgram 是由 programPath 参数指定的程序。
不会自动开启事务。 与从 Ghidra GUI 内运行的 GhidraScripts 不同,ReVa 的 run-script 会刻意不在你的代码外开启事务。任何修改(重命名、重新指定类型、添加注释或标签、创建函数等)都必须手动包装:
tx = currentProgram.startTransaction("Describe the edit")
try:
# ... do stuff that mutates state ...
currentProgram.endTransaction(tx, True) # commit
except Exception:
currentProgram.endTransaction(tx, False) # roll back
raise
这是有意为之——如果脚本本身已处理事务,工具层自动包装会造成嵌套事务陷阱。只读脚本(打印、计数、转储)完全无需事务。
内联标头。 传入 code 时,工具会自动在前面添加 # @runtime PyGhidra\n。不要自行添加。
取消采用协作式机制。 只有代码向监视器让出控制权时,配置的超时(默认为 60s,可通过每次调用的 timeoutSeconds 覆盖)才会触发。在长循环中,应定期调用 monitor.isCancelled() 并退出:
fm = currentProgram.getFunctionManager()
for func in fm.getFunctions(True):
if monitor.isCancelled():
break
# ... work ...
如果 Python 循环持续紧密运行而不检查监视器,就会超过超时时间。此时结果会返回 timedOut: true 和部分输出,但 Ghidra 会话可能仍会忙碌片刻。
输出会被捕获并限制大小。 默认情况下,stdout 和 stderr 各自最多为 64K 字符(可通过 SCRIPT_OUTPUT_CHAR_LIMIT 配置)。超过上限时,结果会包含 stdoutTruncated: true 或 stderrTruncated: true。请针对 LLM 消费者设计输出——简洁、结构化、每条记录占一行。若下游解析很重要,请使用 JSON:
import json
results = []
for func in currentProgram.getFunctionManager().getFunctions(True):
if monitor.isCancelled(): break
if some_predicate(func):
results.append({"addr": str(func.getEntryPoint()), "name": func.getName()})
print(json.dumps(results))
结果架构。 run-script 返回:
{
"success": true|false,
"programPath": "/binary.exe",
"stdout": "...",
"stderr": "...",
"stdoutTruncated": false,
"stderrTruncated": false,
"durationMs": 1234,
"timedOut": false,
"scriptSource": {"type": "inline|path|name", "value": "..."},
"error": "ClassName: message" // only when the executor itself threw
}
如果脚本引发 Python 异常(通过 stderr 中的 Traceback (most recent call last) 标记检测)、触发超时,或执行器抛出异常,则 success 为 false。查看 stderr 以了解实际的回溯信息——Python 异常不会以 MCP 错误的形式出现,而是通过 stderr 返回,且 success: false。
可在脚本中调用的内容
脚本可以使用完整的 Ghidra/PyGhidra 接口。最常用的入口包括:
# Program structure
prog = currentProgram # the Program object
listing = prog.getListing() # CodeUnits, data, comments
memory = prog.getMemory() # blocks, bytes
fm = prog.getFunctionManager() # functions
st = prog.getSymbolTable() # symbols/labels
rm = prog.getReferenceManager() # xrefs
dtm = prog.getDataTypeManager() # data types
# Flat-API one-liners (already global)
addr = toAddr(0x401000)
func = getFunctionAt(addr) or getFunctionContaining(addr)
sym = getSymbolAt(addr)
instr = getInstructionAt(addr)
xrefs_to = getReferencesTo(addr)
# Java classes — import as normal Python after PyGhidra is up (it is, when run-script runs)
from ghidra.program.model.symbol import SourceType
from ghidra.app.decompiler import DecompInterface, DecompileOptions
其他内容请参阅 references/flat-api.md 中按类别整理的 Flat-API 速查表,以及 references/jpype-interop.md 中通过 Python 调用 Java 的惯用写法(数组、迭代器、符号扩展、类型存根)。
工作流模式
内联一次性执行(最常见)
run-script(programPath="/bin", code="""
fm = currentProgram.getFunctionManager()
total = 0
huge = []
for f in fm.getFunctions(True):
if monitor.isCancelled(): break
size = f.getBody().getNumAddresses()
total += size
if size > 4096:
huge.append((str(f.getEntryPoint()), f.getName(), size))
print(f'total bytes: {total}')
for addr, name, size in huge:
print(f'{addr} {size:6d} {name}')
""")
读取 stdout、得出结论,然后继续。不会保留任何内容。
先迭代,再保存
- 使用内联
code通过run-script发送草稿。 - 运行成功后,使用
scriptName="MyAnalysis.py"和最终的code调用write-script。(选择一个描述性名称——list-scripts会返回该名称。) - 下次调用时,使用
scriptName="MyAnalysis.py"调用run-script。
原地编辑已保存的脚本
- 调用
list-scripts(或记住名称)。 - 调用
read-script——记下要修改内容附近的行号。 - 使用
old_string设置为一段可唯一识别的内容调用edit-script(加入足够的上下文,确保只匹配一处)。如果存在歧义,除非传入replace_all: true,否则工具会报错。 - 使用
run-script(scriptName=...)重新运行。
相比重写整个文件,优先使用 edit-script——它会保留脚本的其余内容不变,也方便用户以差异形式检查修改。
常见问题
以下是最常见的“为什么我的脚本行为异常”。
忘记事务
未调用 startTransaction 就执行修改会抛出 IllegalStateException: not in transaction。如果 run-script 调用执行了修改,而你在 stderr 中看到此错误,原因就在这里。将修改操作代码块放入事务中——参见上文的约定部分。
缺少 monitor.isCancelled() 检查
不会检查监视器的紧密循环会悄无声息地超出超时时间——timedOut 标志仍会翻转,但你的脚本可能已经造成了损害。对于遍历每个函数/指令/地址的任何循环,都务必检查监视器。
Java Iterator 与 Python 可迭代对象
由于 JPype,大多数 Ghidra 迭代器都可在 Python 中迭代,但有少数例外(尤其是某些访问器返回的 SymbolIterator 和较旧的 ReferenceIterator),它们只提供 hasNext()/next()。如果 for x in it: 循环没有报错却不产生任何结果,请改用 while it.hasNext(): x = it.next()。FunctionManager.getFunctions(True) 可直接迭代。
Java byte 是有符号的
MemoryBlock.getBytes(addr, byte[] buf) 会将数据填入 Java byte[]。返回值范围为 -128..127。如果需要 0..255,请使用 b & 0xff 进行掩码处理。分配缓冲区:import jpype; buf = jpype.JByte[16]。
print() 的输出会显示在 stdout 中
脚本的 print() 会被重定向到捕获的 stdout 写入器——输出就是通过这种方式返回的。不要使用你期望能将内容刷新到可见位置的日志框架;约定的方式是使用 print(...)。
构造反编译器的开销很大
如果在一个脚本中反编译的函数超过少数几个,请创建一次 DecompInterface 并重复使用。务必调用 dispose()。每个函数的超时时间为 30s;如果每次调用都重新构造反编译器,那么处理 100 个函数很容易超出 run-script 的总体超时时间。
from ghidra.app.decompiler import DecompInterface
decomp = DecompInterface()
decomp.openProgram(currentProgram)
try:
for f in currentProgram.getFunctionManager().getFunctions(True):
if monitor.isCancelled(): break
res = decomp.decompileFunction(f, 30, monitor)
if res.decompileCompleted():
# process res.getDecompiledFunction().getC()
pass
finally:
decomp.dispose()
script 路径必须位于已注册的脚本目录中
run-script(scriptPath=...) 会拒绝 Ghidra 已注册脚本目录(用户目录、系统目录、捆绑包目录)之外的路径。read-script 也一样。写入文件时,路径还必须位于可写目录中(用户目录,通常是 ~/ghidra_scripts/)。系统目录为只读。
内联 code 和已保存脚本都会绕过 Ghidra 的 # @category UI 元数据
run-script(code=...) 会写入一个临时文件,其中只有 # @runtime PyGhidra 标头。已保存脚本中的 # @category、# @menupath、# @keybinding 标头对 Ghidra GUI 有效,但会被 run-script 忽略——如果希望脚本出现在 Script Manager 中,请保留这些标头,否则可以删除。
接下来读什么
这些参考文件与此技能打包在一起。只加载相关文件。
-
references/flat-api.md — 程序检查接口的分类速查表:
FlatProgramAPI单行用法(toAddr、getFunctionAt、createLabel、setEOLComment、引用、注释、书签、搜索)、用于限制迭代范围的AddressSet运算、导入项/外部项/跳板(isThunk、getCallingFunctions、ExternalManager)、数据类型与结构体(StructureDataType、DataTypeManager、createData),以及FlatDecompilerAPI基础知识。在编写用于检查或修改程序结构的内联code时加载。 -
references/decompiler-pcode.md — 反编译器的内部表示:
HighFunction、LocalSymbolMap、HighSymbol/HighVariable/HighParam、Varnode定义-使用链、PcodeOp操作码(COPY/LOAD/CALL/MULTIEQUAL/PTRSUB/…)、用于批量处理的ParallelDecompiler、用于保存反编译器派生重命名的HighFunctionDBUtil。当问题是“追溯这个值的来源”“这个参数来自哪里”“哪些函数看起来像是让反编译器困惑了”“把这个iVar3重命名为更有用的名称”,或任何需要的信息超出 C 源代码字符串的任务时加载。 -
references/jpype-interop.md — Python ↔ Java 边界说明:数组(
jpype.JByte[16])、符号扩展、可变参数、重载解析、异常类型、getter/setter ↔ 属性、包名冲突、类型存根(在TYPE_CHECKING下使用from ghidra.ghidra_builtins import *)。出现 JPype 错误,或需要进行超出简单for x in it:迭代的操作时加载。 -
references/recipes.md — 可复制粘贴的内联
code片段,涵盖常见操作路径:使用谓词迭代函数、带调用者上下文的符号交叉引用、按正则表达式批量重命名、识别跳板的导入项调用者、反编译并搜索、附带引用者的字符串转储、调用图遍历、plate/EOL 注释、原始字节读取,以及更复杂的模式:定义结构体并应用到内存、使用正确的存储设置函数签名、通过 Varnode 定义-使用链追溯 argN、统计每个函数中的寄存器访问、使用ParallelDecompiler批量反编译、使用EmulatorHelper进行字符串去混淆、递归遍历交叉引用,以及通过HighFunctionDBUtil保存反编译器派生的重命名。用户询问“如何做 X”且 X 属于常见操作路径时加载。 -
references/persistent-scripts.md — 已保存脚本的约定:命名、文件存放位置、GhidraScript 标头(
# @category、# @runtime)、如何在复制脚本前查找现有脚本、典型的编辑周期。用户想要保存/重用/迭代脚本,而不是一次性运行时加载。
工作方式
以下几点有助于让通过 ReVa 使用 PyGhidra 更顺畅:
- 优先使用专用的 ReVa 工具。
run-script功能强大,但形式自由;结构化工具(反编译器、函数、字符串、xrefs,…)拥有更可靠的架构,助手也更容易据此推理。只有当结构化工具无法满足问题需求时,才使用run-script。 - 输出结构化结果。 如果
run-script的结果将再次交由大模型处理,优先使用 JSON 或固定列,而不是散文。每个流的上限为 64K — 请据此设计。 - 缩小事务范围。 每个逻辑编辑开启一个事务,不要用一个事务包裹整个脚本。较小的事务可在 Ghidra 的历史记录中干净地撤销,也更能经受错误。
- 使用
edit-script迭代,而不是write-script。 已保存脚本后,针对性编辑能生成便于审查的差异;完整覆盖则不然。 - 先读再改。 先使用
read-script查看当前状态,并挑选唯一的old_string片段。盲目编辑几乎总会选到过短的片段,导致遗漏或匹配多行。