Ghidra入門:自作のJavaクラスを解析し、戻り値42の根拠を読む

コンパイル済みのファイルを見ても、文字の並びだけでは処理を追いにくいことがあるで。Ghidraは、逆アセンブル・逆コンパイル・参照の調査などをまとめて行う、NSAが公開する解析ツールや。今回は自分で作った小さなJavaクラスを読み込み、「42を返す」という処理を解析結果から確かめるで。

今回のゴールと環境

Windows 11 x64とPowerShell 7で、Ghidra 12.2を画面なしのHeadlessモードから使うで。ゴールは、自作のAnswer.classを静的に解析し、逆コンパイル結果のreturn 0x2a;を元ソースのreturn 42;と対応付けることや。今回はJavaバイトコードが題材で、WindowsのネイティブEXEや、対象の実行・デバッグは扱わへんで。

実行確認:2026年10月25日、Windows 11 x64・PowerShell 7.6.5・Ghidra 12.2・Temurin JDK 25.0.4.1+1で、コンパイル、Headless解析、下記スクリプト、逆コンパイル結果まで確認したで。OSからの新規インストールやGhidraのGUI操作を一通り実測したという意味やないで。

向いてるのは、コンパイル後のコードの読み方を覚えたい人や、自作ソフトの調査手順を小さく試したい人や。AIの契約やAPIキーは要らへん。本体の公式目安はRAM 4GB・Ghidraの配置用に1GBで、JDKやZIPを保管する空き容量は別に必要やで。

1.PowerShellと作業場所

スタートメニューで「PowerShell 7」を開いてな。まだなければ、端末で次を実行してからPowerShell 7を開き直すで。Microsoftの導入案内も確認してな。

winget install --id Microsoft.PowerShell --source winget

次で練習フォルダーを作るで。以降のコマンドは、同じPowerShell 7の画面で順番に実行するんや。$ghidraWorkなどの変数もそこで引き継ぐで。

$ghidraWork = Join-Path $env:USERPROFILE 'github-practice\ghidra'
New-Item -ItemType Directory -Force $ghidraWork | Out-Null
Set-Location $ghidraWork
$PSVersionTable.PSVersion
Get-Location

今回は半角英数字で空白のないパスを使って確認したで。本検証では日本語を含む配置先からバッチを起動できへんかったため、英数字のフォルダーへ置いて確認したんや。ユーザー名に日本語や空白がある場合は、$ghidraWorkを自分が書き込める英数字の場所へ変更してな。同名の練習データがあるなら、別名のフォルダーで始めるで。

2.GhidraとJDKの準備

Ghidra 12.2では64-bitのJDK 25を使うで。古いGhidra向けの「JDK 21」の案内と混ぜへんようにしてな。Ghidraは公式の配布ZIPを展開して使う。Source codeのZIPを代わりに取る必要はないで。次は版を固定した公式配布ファイルの取得や。通信量はZIPだけでGhidraが約570MB、JDKが約141MBになるで。

Invoke-WebRequest -Uri 'https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.2_build/ghidra_12.2_PUBLIC_20261016.zip' -OutFile ghidra.zip
Invoke-WebRequest -Uri 'https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.4.1%2B1/OpenJDK25U-jdk_x64_windows_hotspot_25.0.4.1_1.zip' -OutFile jdk.zip

取得後はSHA-256を確認するで。違っていたら展開せず、配布元とダウンロードの完了を確かめてな。

if ((Get-FileHash .\ghidra.zip -Algorithm SHA256).Hash.ToLower() -ne '2c2cd776db3c1f6a097ba418922378ac3649cadd5e844a6f5f088ae628a7be67') {
  throw 'Ghidra ZIPのハッシュが一致してへんで。'
}
if ((Get-FileHash .\jdk.zip -Algorithm SHA256).Hash.ToLower() -ne '00c847d804f4a78e9f04f2683faf14fed898535b177b7fc704486cb0284e9283') {
  throw 'JDK ZIPのハッシュが一致してへんで。'
}

エラーがなければ、次で展開して使う場所を設定するで。既存のGhidraの上へ重ねて展開せず、この練習専用の新しいフォルダーを使ってな。

Expand-Archive -LiteralPath .\ghidra.zip -DestinationPath .\tools
Expand-Archive -LiteralPath .\jdk.zip -DestinationPath .\tools
$env:JAVA_HOME = (Resolve-Path '.\tools\jdk-25.0.4.1+1').Path
$ghidraHome = (Resolve-Path '.\tools\ghidra_12.2_PUBLIC').Path
$env:GHIDRA_HEADLESS_JAVA_OPTIONS = "-Dapplication.settingsdir=$ghidraWork\settings -Dapplication.cachedir=$ghidraWork\cache"
& "$env:JAVA_HOME\bin\java.exe" -version
& "$env:JAVA_HOME\bin\javac.exe" -version

両方に25系の版が表示されたら、この練習のJDKが使えてるで。Ghidraの設定とキャッシュも練習用の場所に分けた。JAVA_HOMEなどは今のPowerShell内だけの設定で、Windowsのシステム設定を書き換える操作やないで。Pythonで動かすPyGhidraやDebuggerの準備は、今回のJava方式とは別や。

3.題材と読取スクリプトの作成

ここは準備で、まだGhidraの解析は実行せえへんで。次で、42を返すメソッドを持つ自作ソースと、Ghidraに結果を表示させる自作スクリプトを保存するんや。

@'
public class Answer {
    public static int answer() {
        return 42;
    }
}
'@ | Set-Content .\Answer.java -Encoding UTF8

続くReadAnswer.javaは解析対象やなく、Ghidraの中で動く補助スクリプトや。見つかった関数名を表示し、指定した関数の逆コンパイル結果を出すで。ブロック全体をコピーして実行してな。

@'
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;

public class ReadAnswer extends GhidraScript {
    @Override
    public void run() throws Exception {
        if (getScriptArgs().length != 1) {
            throw new Exception("Pass one generated function name to this script");
        }
        String wantedName = getScriptArgs()[0];
        Function selected = null;
        FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
        while (functions.hasNext()) {
            Function function = functions.next();
            println("SAMPLE_FUNCTION " + function.getName());
            if (function.getName().equals(wantedName)) {
                selected = function;
            }
        }
        if (selected == null) {
            throw new Exception("The answer function was not found");
        }
        DecompInterface decompiler = new DecompInterface();
        try {
            if (!decompiler.openProgram(currentProgram)) {
                throw new Exception(decompiler.getLastMessage());
            }
            DecompileResults result = decompiler.decompileFunction(selected, 30, monitor);
            if (!result.decompileCompleted()) {
                throw new Exception(result.getErrorMessage());
            }
            println("SAMPLE_DECOMPILE_BEGIN");
            println(result.getDecompiledFunction().getC());
            println("SAMPLE_DECOMPILE_END");
        } finally {
            decompiler.dispose();
        }
    }
}
'@ | Set-Content .\ReadAnswer.java -Encoding UTF8

次はソースからAnswer.classを作る、コンパイルの操作や。--release 17は題材のクラスファイルの形式をJava 17向けにする指定で、Ghidra自体を動かすJDK 25の指定とは別やで。

& "$env:JAVA_HOME\bin\javac.exe" --release 17 -g .\Answer.java
if ($LASTEXITCODE -ne 0) { throw 'コンパイルのエラーを先に直してな。' }
Get-Item .\Answer.class
New-Item -ItemType Directory -Force .\projects | Out-Null

準備の成功の目安:Answer.java、Answer.class、ReadAnswer.javaとprojectsフォルダーができることや。javacはコンパイルであって、answer()を実行する操作やないで。

4.解析の実行

ここからGhidraで解析するで。同じ練習フォルダーで次を実行してな。JavaPracticeは練習用のプロジェクト名や。初回はスクリプトのコンパイルなども行われる。結果と診断はファイルへ保存するから、端末に何も出ない間もすぐ閉じんようにしてな。

$projectDir = (Resolve-Path .\projects).Path
$classFile = (Resolve-Path .\Answer.class).Path
& "$ghidraHome\support\analyzeHeadless.bat" "$projectDir" JavaPractice -import "$classFile" -scriptPath "$ghidraWork" -postScript ReadAnswer.java answer_int -log .\analysis.log 1> .\analysis-output.log 2> .\analysis-error.log
$ghidraExit = $LASTEXITCODE
"Ghidraの終了コード: $ghidraExit"
Get-Content .\analysis-output.log -Tail 30
指定意味
"$projectDir" JavaPractice保存先フォルダーと、Ghidraのプロジェクト名や。
-import "$classFile"解析対象はコンパイル済みのクラスや。元ソースや補助スクリプトを指定する欄やないで。
-scriptPath "$ghidraWork"補助スクリプトを、今いるフォルダーから探すで。
-postScript ReadAnswer.java answer_int自動解析の後で補助スクリプトを実行する。answer_intはそのスクリプトへ渡す関数名で、Answer.classの起動引数やないで。
-log .\analysis.logGhidraのログを保存するで。
1>/2>PowerShellが標準出力と標準エラーをそれぞれ保存する指定や。

この処理はクラスファイルを読み込んで解析するもので、題材のanswer()をアプリとして実行するものやないで。

5.出力の読み方

ログには、読み込んだ関数名と、実際の逆コンパイル結果が出るで。必要な周辺だけを表示するなら、次を実行してな。

Select-String -Path .\analysis-output.log -Pattern 'SAMPLE_FUNCTION'
Select-String -Path .\analysis-output.log -Pattern 'SAMPLE_DECOMPILE_BEGIN' -Context 0,17

実測した関数名:ログの日時・接頭辞は省略してるで。

SAMPLE_FUNCTION <init>_void
SAMPLE_FUNCTION answer_int

実測した逆コンパイル結果の抜粋:ログの接頭辞、フラグのコメント、余分な空行を除いて掲載してるで。元ソースをコピーしたものやなく、GhidraがAnswer.classから得た表現や。

int answer_int(void)
{
  return 0x2a;
}
表示読み方
<init>_voidこのクラスのコンストラクターを表す関数名や。今回読むanswer()とは別やで。
answer_int今回の読み込みでGhidraが付けた関数名や。補助スクリプトに渡した名前と一致するで。別のファイルにも必ず同じ名前があるわけやないで。
int/voidこの結果では、整数を返し、引数なしの関数として表現されてるで。
return 0x2a;0xは16進数。2aは10進数の42や。元ソースのreturn 42;と対応するで。

Ghidraの表示はC風の表現や。元のJavaソースの書き方や変数名を、そのまま完全復元したことにはならへんで。今回はソースを自分で持っているから、解析結果との対応を確かめる練習にできるんや。

[Convert]::ToInt32('2a', 16)

このPowerShellの出力は42や。ただし、これも数値表現の変換であって、解析対象を実行した結果やないで。

成功の目安:終了コードだけで判断せず、次で完了マーカーがTrue、スクリプトエラーがFalseになるか確認してな。そのうえでreturn 0x2a;と元ソースを照合できたら今回のゴールや。

Select-String -Path .\analysis-output.log -SimpleMatch 'SAMPLE_DECOMPILE_END' -Quiet
Select-String -Path .\analysis-output.log -SimpleMatch 'ERROR REPORT SCRIPT ERROR' -Quiet

検証中には補助スクリプトの関数名を間違えて、Ghidraの終了コードが0でもスクリプトだけ失敗するケースがあったで。だから、終了コード0だけで成果が出たと扱わず、結果と完了マーカーまで見るんや。

コンパイルと解析の流れ

自分のPC:コンパイルしたファイルを静的に調べるAnswer.javajavacAnswer.classReadAnswer.javaGhidraログへ出力自作の元ソースreturn 42;コンパイルする解析する対象Javaバイトコード解析を手伝うスクリプト解析対象とは別の役割クラスを読み込む関数を逆コンパイルint answer_int(void)return 0x2a;プロジェクトも projects 内へ保存入力生成.classを取り込むGhidra内で実行表示
この記事の独自図解やで。上段は題材のコンパイル、下段はGhidraでの解析や。Answer.classは解析対象、ReadAnswer.javaは結果を取り出す補助スクリプトで、役割が違うんや。今回の処理はPC内で行い、ZIPのダウンロード通信や外部AIとは別の話やで。
そのまま見る場合今回の読み方
クラスファイルは、そのままではソースのように読めへん。Ghidraの出力から戻り値を見て、自分の元ソースと照合するで。

別のファイルを調べるとき

別のJavaクラスなら-importの対象を変えるだけでなく、SAMPLE_FUNCTIONに表示された関数名に合わせてスクリプトの引数も変えるで。オーバーロードやコンパイル条件で名前・結果が変わることがあるんや。

EXE・ELF・rawバイナリでは、ファイル形式やCPU、読み込みアドレス、関数の入口など別の条件が関わるで。今回のanswer_intをそのまま使わへんようにしてな。公式のHeadless案内か、展開先のsupport\analyzeHeadlessREADME.htmlで各指定を確認するんや。

REAなどの外部ツールからGhidraを呼ぶ場合は、その連携ツール側の対応版も確認してな。Ghidra単体で使える最新版と、連携側が対応する版は別の話やで。

トラブルシューティング

困ったこと確認すること
Javaがない・対応JDKが見つからない同じPowerShellで$env:JAVA_HOMEを表示し、& "$env:JAVA_HOME\bin\java.exe" -versionが25系か確認してな。JREだけではなくJDKを使うで。
The system cannot find the path specifiedGhidraとJDKの展開先と、今いる場所を確認するで。本検証では日本語パスからの起動に失敗し、英数字の配置先で先へ進めた。
Felix frameworkのdata dirエラー既存キャッシュを削除して済ませず、「2.GhidraとJDKの準備」のapplication.settingsdirとapplication.cachedirを練習用フォルダーに設定したか確認してな。本検証では設定先を分けて実行できたで。
関数が見つからないSAMPLE_FUNCTIONの行を見て、スクリプトへ渡した名前が一致するか確認してな。answerではなく、この例の実測ではanswer_intや。
プロジェクト内に同名ファイルがある再開なら下の-processを使うで。最初から比較したいなら別のプロジェクト名にしてな。
終了コード0なのに結果がないログのERROR REPORT SCRIPT ERRORと、SAMPLE_DECOMPILE_ENDを確認してな。analysis-error.logにも診断が残るで。

終了と再開

Headless処理が終われば、そのプロセスは終了するで。プロジェクトはprojects内のJavaPractice.gprとJavaPractice.repに残る。ログや元ソースとは別の保存物やで。PowerShellを閉じた後に再開するなら、練習フォルダーへ移動し、$ghidraWork、$env:JAVA_HOME、$ghidraHome、$env:GHIDRA_HEADLESS_JAVA_OPTIONSの設定をやり直してな。

すでに取り込んだクラスを使うときは、-importの代わりに-processで保存済みの名前を指定するで。次の再開用の書式は公式仕様からの案内で、今回の実測出力は上の初回取り込みで取得したものや。

$projectDir = (Resolve-Path .\projects).Path
& "$ghidraHome\support\analyzeHeadless.bat" "$projectDir" JavaPractice -process Answer.class -scriptPath "$ghidraWork" -postScript ReadAnswer.java answer_int

Ghidraの画面でも見たくなったら、展開先のghidraRun.batを起動してFile → Open ProjectからJavaPractice.gprを開く方法があるで。このGUI操作は今回未実機確認や。片付けるときはGhidraを閉じ、必要なログとプロジェクトの両方を退避してから、練習用フォルダーをエクスプローラーでゴミ箱へ移してな。

公式資料と利用条件

Ghidra本体はApache License 2.0や。同梱コンポーネントは個別の条件があるから、配布物のlicensesなども確認してな。今回使ったTemurin JDKはOpenJDKのGPLv2+Classpath Exceptionなどの条件を持ち、Ghidra本体のライセンスとは別や。解析するファイルの権利も別なので、まずはこの記事の自作題材で試してな。

記事の確認日:2026年10月25日。題材・補助スクリプト・手順図は独自制作。出力は上記環境での実測から必要な部分を抜粋したで。記事の選定にはHysen Labsの過去30日間のスター純増ランキング(2026年9月24日〜10月24日、追跡対象12,567件の範囲、20位)を参照した。GitHub全体の順位とは断定せえへんで。データの出典:Hysen Labs、CC BY 4.0。ダウンロードCSVは前の期間やったため、この選定には新しい期間を明示したHTMLを使ったで。