mcpField
P
pyghidra-scripting

pyghidra-scripting

github.com/cyberkaida安全88587 次安装1 天前更新
代码仓库

关于 pyghidra-scripting

这项技能介绍如何通过 ReVa 的 MCP 工具,在 Ghidra 会话中借助 PyGhidra 创建、编辑、运行和查看 Python 脚本,适用于需要自定义二进制分析的分析人员。

主要功能

  1. 01在程序上运行 Python 脚本
  2. 02列出、读取、写入和编辑脚本
  3. 03通过 PyGhidra 使用 Ghidra Java API
  4. 04处理事务与执行限制

适用场景

  1. 01按自定义条件筛选函数或指令
  2. 02一次组合调用多个 Ghidra API
  3. 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.md
flat-api.md
jpype-interop.md
persistent-scripts.md
recipes.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、得出结论,然后继续。不会保留任何内容。

先迭代,再保存
  1. 使用内联 code 通过 run-script 发送草稿。
  2. 运行成功后,使用 scriptName="MyAnalysis.py" 和最终的 code 调用 write-script。(选择一个描述性名称——list-scripts 会返回该名称。)
  3. 下次调用时,使用 scriptName="MyAnalysis.py" 调用 run-script。
原地编辑已保存的脚本
  1. 调用 list-scripts(或记住名称)。
  2. 调用 read-script——记下要修改内容附近的行号。
  3. 使用 old_string 设置为一段可唯一识别的内容调用 edit-script(加入足够的上下文,确保只匹配一处)。如果存在歧义,除非传入 replace_all: true,否则工具会报错。
  4. 使用 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 片段。盲目编辑几乎总会选到过短的片段,导致遗漏或匹配多行。

评论

支持 Markdown