mcpField
P
pyghidra-scripting

pyghidra-scripting

github.com/cyberkaidaセキュリティ88587 回インストール1 日前に更新
リポジトリ

pyghidra-scripting について

ReVaのMCPツールを使い、PyGhidra経由でGhidraセッション内のPythonスクリプトを作成、編集、実行、確認する方法を扱うスキルです。独自のバイナリ解析を行う分析者向けです。

主な機能

  1. 01プログラム上でPythonスクリプトを実行
  2. 02スクリプトの一覧表示、読み取り、保存、編集
  3. 03PyGhidra経由でGhidra Java APIを利用
  4. 04トランザクションと実行制限への対応

活用シーン

  1. 01関数や命令に独自条件を適用
  2. 02複数のGhidra API呼び出しを一括実行
  3. 03解析スクリプトの保存と改訂

MCP 対応

ReVaは` scripts`プロバイダーを通じて、スクリプト実行とファイル操作用の5つの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 プロバイダーで 5 つの MCP ツールを公開しており、アシスタントは ReVa がサービスを提供しているのと同じ Ghidra セッション内で、Python スクリプトの作成、編集、実行、確認を行えます。Python コードは PyGhidra(CPython 3 + JPype)上で実行されるため、FlatProgramAPI、DecompInterface、FunctionManager をはじめ、Ghidra の Java API をすべて利用できます。

このスキルでは、これらのツールを使う場面、run-script に送る Python の構成方法、実際に使う際の落とし穴について説明します。

5 つのツールの概要

ツール目的使用する場面
run-scriptプログラムに対して Python を実行します。インラインの code、または scriptPath / scriptName を使います。専用の ReVa MCP ツールでは必要な処理ができない場合や、プログラムに対して一度に簡潔な処理を行いたい場合。
list-scripts登録済みディレクトリ内のスクリプトを一覧表示します。名前やパスで絞り込みでき、ページ分けにも対応しています。新しいスクリプトを書く前に、すでに保存されているものを確認する場合。
read-scriptoffset / limit と truncated フラグを指定して、スクリプトを cat -n 形式で表示します。編集前に既存のスクリプトを読む場合。行番号付きの出力を基準に edit-script で編集します。
write-script新しい .py ファイルを作成します(overwrite: true で全体を上書きできます)。ユーザーが書き込み可能なディレクトリのみ対象です。再利用するスクリプトを保存する場合。インラインコードをまとめるだけの目的では使わず、1 回限りの使用には code を指定して run-script を使います。
edit-scriptold_string → new_string の置換を行います。old_string が複数箇所に一致すると、replace_all: true が指定されていない限りエラーになります。既存のスクリプトを反復して編集する場合。先に read-script と組み合わせて行の前後関係を確認します。

使用する場面

run-script は、ほかの方法では対応できない処理に使います。次の場合に使用してください。

  • 既存の一覧表示/検索ツールでは適切に絞り込めない条件で、すべての関数/シンボル/命令に対して独自の判定処理を実行する必要がある場合。
  • 複数の Ghidra API 呼び出しを、何度も MCP を呼び出さずに 1 つの処理にまとめる必要がある場合。
  • ReVa が公開していない Ghidra API を使いたい場合。たとえば、DecompInterface の低レベル機能、PCode 解析、カスタムの AddressSet 演算、データフロー API の直接利用、BinaryReader などです。
  • 試作中の場合。動作し、再利用できるようになったら write-script で保存してください。

専用の ReVa MCP ツールですでに処理できる場合は、run-script を使わないでください。専用ツールはスキーマが安定しており、自由形式の stdout よりも構造化された出力を扱いやすいためです。一般的な処理は、逆コンパイラー/関数/文字列/シンボル/クロスリファレンスの各ツールで対応できます。

実行環境の条件:PyGhidra のみ

run-script が動作するのは、Ghidra が PyGhidra 経由で起動された場合のみです。

  • ✅ mcp-reva(Claude CLI / stdio モード)
  • ✅ pyghidra-gui(PyGhidra 経由で起動した GUI モード)
  • ✅ reva_headless_server.py(PyGhidra 経由のヘッドレスモード)
  • ❌ 通常の ghidraRun — PyGhidra が組み込まれていないため、起動方法の案内とともに PyGhidraNotAvailableException エラーが返されます。

run-script の呼び出しでこのエラーが返ってきた場合は、再試行を続けず、ユーザーに再起動方法を案内してください。ほかの 4 つのツール(list-scripts、read-script、write-script、edit-script)は通常のファイル操作であるため、どのモードでも動作します。

run-script の実行契約

これは、インラインの code(または保存済みスクリプト)が実行される際の契約です。よく理解しておいてください。失敗の多くは、これらのいずれかを誤解したことが原因です。

事前定義済みのグローバル変数。 スクリプトは Ghidra PyGhidra スクリプトとして実行されるため、標準の GhidraScript グローバル変数はすでに定義されています。currentProgram、currentAddress、currentSelection、currentHighlight、monitor、state に加え、toAddr、getFunctionAt、getFunctionContaining、getSymbolAt、getInstructionAt、getDataAt、getReferencesTo、getReferencesFrom、createLabel、setEOLComment、find、findBytes、clearListing などの FlatProgramAPI ヘルパーも使えます。これらをインポートしたり、設定したりする必要はありません。currentProgram は programPath 引数で指定されたプログラムです。

トランザクションは自動で開始されません。 Ghidra の GUI 内から実行する GhidraScript とは異なり、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 を自動的に先頭に追加します。自分で追加しないでください。

キャンセルは協調的です。 設定されたタイムアウト(デフォルトは 60 秒。呼び出しごとに 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 が利用することを想定して、簡潔で構造化された、1 レコード 1 行の出力にしてください。後続処理で解析する必要がある場合は、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 エラーとしては表示されず、success: false とともに stderr に返されます。

スクリプト内から呼び出せるもの

スクリプトでは 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

その他については、カテゴリ別の Flat-API チートシートを references/flat-api.md で、Java を Python から扱う際のイディオム(配列、イテレーター、符号拡張、型スタブ)を references/jpype-interop.md で確認してください。

ワークフローパターン

インラインで一度だけ実行(最も一般的)
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}')
""")

標準出力を読み、結論を導き、次に進みます。何も保存されません。

試行してから保存
  1. run-script にインラインの code を渡してドラフトを実行します。
  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 を呼び出します(周囲の文脈を十分に含め、一致箇所がちょうど 1 つになるようにします)。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(...) が仕様です。

デコンパイラーの構築にはコストがかかる

1 つのスクリプトで数個を超える関数をデコンパイルする場合は、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 — プログラム検査用 API の分類別チートシート: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 のコピー&ペースト可能なスニペット:条件式を使った関数の反復処理、呼び出し元のコンテキストを含むシンボルへの xref、正規表現による一括名前変更、サンクを考慮したインポート関数の呼び出し元、逆コンパイルして grep、参照元付きの文字列ダンプ、呼び出しグラフの走査、プレート/EOL 注釈、バイト列の直接読み取り。さらに、構造体の定義とメモリへの適用、適切なストレージを指定した関数シグネチャの設定、Varnode の定義・使用チェーンを通じた argN の追跡、関数ごとのレジスタアクセス、ParallelDecompiler による一括逆コンパイル、文字列の難読化解除に使う EmulatorHelper、再帰的な xref 走査、HighFunctionDBUtil によるデコンパイラー由来の名前変更の永続化といった、より高度なパターンも扱います。「X を行うにはどうすればよいか」と質問され、X がよく使われる手順である場合に読み込んでください。

  • references/persistent-scripts.md — 保存するスクリプトの規約:命名、ファイルの保存先、GhidraScript ヘッダー(# @category、# @runtime)、既存スクリプトを複製する前に見つける方法、一般的な編集サイクル。スクリプトを一度だけ実行するのではなく、保存/再利用/反復したい場合に読み込んでください。

作業スタイル

PyGhidra を ReVa 経由で使う際に役立つ、いくつかのポイントを紹介します。

  • まず専用の ReVa ツールを優先してください。 run-script は強力ですが、形式が自由です。一方、構造化ツール(デコンパイラー、関数、文字列、xref など)にはスキーマがあり、アシスタントがより確実に推論できます。構造化ツールで質問に対応できない場合にのみ、run-script を使ってください。
  • 構造化された出力を表示してください。 run-script の結果を LLM で再利用する場合は、文章よりも JSON または固定列を優先してください。64K の上限はストリームごとに適用されるため、それを考慮して設計してください。
  • トランザクションの範囲を絞ってください。 スクリプト全体を囲むのではなく、論理的な編集ごとに 1 つ開いてください。小さなトランザクションであれば Ghidra の履歴からきれいに元に戻せ、エラーにもより適切に対処できます。
  • write-script ではなく edit-script で反復してください。 保存済みのスクリプトがある場合、対象を絞った編集ならレビュー可能な差分が作成されますが、全体の上書きではそうなりません。
  • 編集前に読んでください。 まず read-script で現在の状態を確認し、一意な old_string の範囲を選んでください。内容を確認せずに編集すると、範囲が短すぎて変更が反映されなかったり、複数の行に一致したりすることがほとんどです。

コメント