6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

レガシーアプリを Ghidraで逆コンパイルして Blazor WebAssemblyで再構築した話

はじめに

顧客から昔作ってくれたアプリを更新してほしいって話が来た。
まぁ、よくある話

話を聞いていくと

  • 開発者が辞めている
  • ソースも逸失
  • exe と、操作説明書はある

ここから、再現してほしいけど、どの程度でできそうか?

まぁ、そこそこ面倒だなぁって話

とはいえ、今は心強い味方である GitHub Copilot がいるので、いけんじゃね?と思った

ということで、Ghidraを使ってC++/CEFベースのWindowsアプリケーションを逆コンパイルし、Blazor WebAssembly + PWAとして再構築したプロジェクトの記録を残す。

今回、著作権は、顧客が保持しているので問題ありません。
同様のことをされる場合は、お気を付けください

いきなりの躓き:「.NET アプリだと思ってたら、ネイティブだった 🤣」

当初の想定:

  • 「CEFを使ってる」→ きっと.NETアプリだろう
  • .NETならILSpyやdnSpyで簡単に逆コンパイルできる
  • C#のソースがそのまま復元できるはず

現実:

PS> dumpbin /headers checker.exe

Microsoft (R) COFF/PE Dumper Version 14.00
     14C machine (x86)
       3 number of sections
4AC8F5D8 time date stamp  # ← 2009年頃のビルド
       0 file pointer to symbol table
       0 number of symbols
      E0 size of optional header
     10F characteristics
           Executable
           Line numbers stripped
           32 bit word machine

# .NET ヘッダーが見つからない → ネイティブアプリ!

つまり、C++のネイティブアプリだった。 🤯

.NET と C++ の見分け方

  • .NET: ILSpyで開ける、mscorlib.dll 依存
  • C++: PE構造のみ、msvcrt.dll 依存、.NETヘッダーなし

最初にdumpbinで確認すると時間の節約になる。

問題点

  • ソースコードが散逸していた
  • Windowsにロックインされている
  • C++/CEF/MFC
  • The author has gone!

目標

  • ブラウザで動作するWebアプリ化
  • PWAでオフライン対応
  • クロスプラットフォーム化
  • .NET 9 での再構築

TL;DR

  • 最初の誤解: .NETアプリだと思ったらC++ネイティブだった
  • Ghidraで.exeを逆コンパイル → 13,328関数から47個のアプリ固有関数を特定
  • パスワード保護されたZIPファイルをバイナリ解析で突破(UD4201109124du
  • 日本語パス問題、変数名推測、XPath散在など実際の躓きポイント8つ
  • GitHub Copilot(Claude 4.5 Sonnet)でリファクタリング
  • 最終的にBlazor WebAssembly(.NET 9)で再実装

フェーズ0: 復元方法の検討

C++ネイティブアプリだと分かったところで、復元方法を検討した。

検討した手法

手法 メリット デメリット 採用
ILSpy / dnSpy .NETなら完璧に復元 C++では使えない
Ghidra 無料、強力、スクリプト可能 学習コストあり
IDA Pro 最高性能 高額(数十万円〜)

結論: Ghidra + PowerShellスクリプトで自動化

なぜGhidraを選んだか

  • 無料・オープンソース
  • ヘッドレスモード(GUI不要)でスクリプト実行可能
  • Pythonスクリプトで関数エクスポート自動化
  • NSAが開発した信頼性

Ghidraについて

フェーズ1: Ghidra解析とバイナリからの情報抽出

解析対象

手元にあったのは2つのexeファイル:

  • checker.exe - メインアプリケーション(2.02 MB)
  • xml2json.exe - データ変換ツール(1.66 MB)

技術スタック(推定):

  • C++ (Visual Studio 2008)
  • MFC(Microsoft Foundation Class)
  • CEF(Chromium Embedded Framework)
  • libxml2(XML処理)

解析の流れ

1. Ghidraプロジェクト作成
2. .exeファイルをインポート
3. 自動解析実行(約20〜40分)
4. 関数一覧から主要ロジックを特定
5. デコンパイル結果をエクスポート

解析結果:

ファイル 総関数数 アプリ固有関数 解析時間
checker.exe 13,328 47個 約40分
xml2json.exe 11,492 約30個 約30分

重要な発見
大半の関数はライブラリ由来(MFC, CEF, libxml2等)。
アプリケーション独自のロジックはわずか47関数のみ。

とりあえず、これなら楽そうって感じにはなった :sweat:

躓きポイント1: 解析が重くて進捗が見えない

問題:
Ghidraのヘッドレスモードで解析すると、ターミナルに何も表示されず、フリーズしたように見える。

解決策:
Ghidraスクリプトにログ出力を追加:

# C:\Tools\ghidra_export.py(抜粋)
from ghidra.program.model.listing import CodeUnit

print("[*] Starting function export...")
functionManager = currentProgram.getFunctionManager()
functions = functionManager.getFunctions(True)

count = 0
total = functionManager.getFunctionCount()

for func in functions:
    count += 1
    if count % 100 == 0:
        print(f"[*] Progress: {count}/{total} functions")
    # 処理...

これで進捗が可視化され、安心して待てるようになった。

躓きポイント2: 日本語パスでGhidraスクリプトがエラー

問題:
プロジェクトパスに日本語が含まれると、Pythonスクリプトで文字化け・クラッシュ。

Error: UnicodeDecodeError: 'ascii' codec can't decode byte 0xe7

解決策:
スクリプトをASCIIのみのパスに配置:

# Before(エラー)
C:\Users\{User}\Documents\日本語ツール名\tools\

# After(成功)
C:\Tools\ghidra_export.py

シンプルだが確実な方法。日本語環境では注意が必要。

主要関数の特定

デコンパイルされた関数名は FUN_00401234 のように無意味。そこで、以下の手がかりから推測:

アドレス 推定名 根拠
004011d0 InitCefBrowserWindow "CEF", "browser" 文字列が出現
00419650 GetTemplatePassword 後述するパスワード文字列が隣接
004035e0 ValidateProjectData XPath文字列 "//checker/project" が出現
0040ebc0 DispatchJavaScriptFunction "eval(", "cef.PJData" 文字列

このあたりで、ビジネスロジックが見えてきた。

フェーズ2: パスワード保護ZIPとの戦い

躓きポイント3: テンプレートファイルが開けない 🔒

アプリはテンプレートファイル(.udt)を読み込んでいる。早速開こうとしたところ...

PS> Expand-Archive common.udt
Expand-Archive : 圧縮 (zip 形式) フォルダー 'common.udt' は無効です。

パスワード付きZIPだった。

パスワード探索作戦

手元にはバイナリしかない。そこで、Ghidraで文字列検索:

# ghidra_find_password.py
from ghidra.program.model.data import StringDataInstance

strings = currentProgram.getListing().getDefinedData(True)
for data in strings:
    if data.hasStringValue():
        value = data.getValue()
        # パスワードっぽい文字列を探す
        if len(value) > 10 and value.isalnum():
            print(f"[+] Found: {value} at {data.getAddress()}")

発見: アドレス 00419650 付近に怪しい文字列

UD4201109124du

試してみると...

PS> 7z x common.udt -pUD4201109124du
Everything is Ok

成功! 🎉

まぁ、実際にはここに来る前に長い道のりがあって、あるときふとこの方法を思いついて見つけたってのが真相 :sweat_smile:

全8種類のテンプレートを一括展開できた。

セキュリティについて
パスワードはアプリ内にハードコードされていた。
セキュリティ的には問題だが、リバースエンジニアリングでは典型的なパターンで助かった

フェーズ3: ソースコードの整理・リファクタリング

躓きポイント4: 変数名が意味不明

逆コンパイルされたコードは構造的には正しいが、変数名が壊滅的:

// Before: Ghidraの出力
void FUN_00403650(void *param_1) {
    int local_c;
    void *local_10;
    local_10 = (void *)FUN_00403800(param_1, "//checker/project/kind");
    if (local_10 != (void *)0x0) {
        local_c = FUN_00404250(local_10);
        return local_c;
    }
    return -1;
}

何をしているか全く分からない。

解決策: 段階的な推測とリファクタリング

  1. XPath文字列から機能を推測

    • "//checker/project/kind" → プロジェクト種別の取得?
  2. 関数呼び出しを追跡

    • FUN_00403800 → XPath評価関数(libxml2)
    • FUN_00404250 → XMLノードから整数取得
  3. GitHub Copilot Chatで命名

    • プロンプト: 「この関数は何をしていますか?適切な名前を提案してください」
// After: リファクタリング済み
int Project::GetProjectKind(void* projectXml) {
    // XPathで //checker/project/kind を取得
    void* kindNode = Xml::EvaluateXPath(projectXml, "//checker/project/kind");
    if (kindNode != nullptr) {
        int kind = Xml::GetIntValue(kindNode);
        return kind;
    }
    return -1; // エラー
}

一気に読みやすくなった。

躓きポイント5: XPathパターンが散在している

コード全体で30箇所以上にXPath文字列が点在:

// バラバラに書かれている
FUN_00403800(data, "//checker/project");
FUN_00403800(data, "//checker/project/kind");
FUN_00403800(data, "//checker/step1_1/user[@id='user1']");
// ... 他27箇所

解決策: 定数に集約

// xpath_constants.hpp
namespace XPath {
    constexpr const char* PROJECT = "//checker/project";
    constexpr const char* PROJECT_KIND = "//checker/project/kind";
    constexpr const char* USER_BY_ID = "//checker/step1_1/user[@id='%s']";
}

// 使用例
Xml::EvaluateXPath(data, XPath::PROJECT_KIND);

これでXPath一覧がひと目で分かるようになった。

躓きポイント6: JavaScript/C++間の連携が複雑

レガシーアプリはCEF(Chromium Embedded Framework)で、JavaScriptとC++が双方向通信していた。

// JavaScript側
cef.PJData = { /* プロジェクトデータ */ };
cef.CallCppFunction("SaveProject");  // C++を呼び出し
// C++側
void DispatchJavaScriptFunction(const char* funcName) {
    if (strcmp(funcName, "SaveProject") == 0) {
        // JavaScriptのcef.PJDataを取得
        // XMLに変換して保存
    }
}

解決策: データフロー図で可視化

Mermaidで連携を図示:

これでBlazor移行時の設計指針が見えた。

GitHub Copilot Chatの活用
リファクタリングには GitHub Copilot Chat(Claude 4.5 Sonnet)を使用。
「この関数の責務は?」「適切な名前は?」といった質問で効率化。

フェーズ4: Blazor WebAssemblyへの移植

C++のロジックを理解できたので、いよいよBlazorで再実装。

アーキテクチャ設計

レガシーのCEF(C++ ⇔ JavaScript)から、Blazor(C#統一)へ:

メリット:

  • 言語統一でバグ減少
  • 型安全性(TypeScript不要)
  • C++/JavaScript間の煩雑な連携が不要

C++からC#への変換

C# 9+の恩恵:

  • LINQ で簡潔に書ける
  • Switch式で見通しが良い
  • Null安全性

技術選定の理由

まぁ、色々書いたけど、単に、Blazor 触りたかっただけです、ハイ 😁

Blazor WebAssembly

  • C#で統一(フロント・バックエンド共通言語)
  • .NET 9の最新機能(Primary Constructor, File-scoped namespace)
  • WebAssemblyで高速動作
  • PWA対応が標準

ASP.NET Core Web API

  • RESTful API が簡単
  • Swagger(OpenAPI)でドキュメント自動生成
  • 依存性注入(DI)がビルトイン

実装のポイント(概要のみ)

詳細なコード実装は割愛するが、要点は以下の通り:

Shared プロジェクト

  • 評価マークのEnum定義(○△×-NA)
  • スコア計算ロジック(C++から移植)
  • データモデル(XML構造をC#クラスに)

Server プロジェクト

  • REST API(テンプレート取得、プロジェクト保存)
  • Swagger(OpenAPI)で自動ドキュメント生成

Primary Constructor の活用
.NET 9 では Primary Constructor が使えるため、DIがシンプル:

public class TemplateService(HttpClient http)
{
    public async Task<List<Template>> GetAllAsync()
        => await http.GetFromJsonAsync<List<Template>>("api/templates") ?? [];
}

Client プロジェクト

  • Blazor Razor コンポーネント(評価マーク選択UI等)
  • PWA(Service Worker)対応

まとめ: 全工程で遭遇した躓きポイント

1. Ghidra解析が重く進捗が見えない

解決: スクリプトにログ出力・進捗表示を追加

2. 日本語パスでGhidraスクリプトがエラー

解決: スクリプトを C:\Tools\ に配置(ASCIIパス)

3. テンプレートファイルがパスワード保護ZIP 🔒

解決: Ghidraでバイナリ内文字列を探索し、パスワード UD4201109124du を発見

4. 変数名が意味不明(param_1, local_10

解決: XPath文字列や関数呼び出しから推測 + GitHub Copilotで命名提案

5. XPathパターンが30箇所以上に散在

解決: 定数ファイルに集約(XPath::PROJECT_KIND 等)

6. JavaScript/C++間の連携が複雑

解決: Mermaidでデータフロー図を作成し、可視化

7. Blazor WASM の静的ファイルが404

解決: Visual Studio 2022でデバッグ実行(VS Codeでは静的Web資産のパス解決に問題あり)

8. API呼び出し失敗で画面が真っ白

解決: フォールバック機能を実装(ローカルデータで代替)

public async Task<List<Template>> GetAllAsync()
{
    try
    {
        return await _http.GetFromJsonAsync<List<Template>>("api/templates")
               ?? GetDefaultTemplates();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"API Error: {ex.Message}");
        return GetDefaultTemplates(); // ローカルデータで代替
    }
}

成果まとめ

Before(レガシーアプリ)

  • プラットフォーム: Windowsのみ
  • 配布: インストーラー必須(約200MB)
  • オフライン: 起動不可
  • 更新: 全ユーザーに再配布が必要
  • 保守: C++/CEF/MFCの知識が必須

After(Blazor WASM + PWA)

  • プラットフォーム: ブラウザで動作(Windows/Mac/Linux/iOS/Android)
  • 配布: URLを共有するだけ(PWAとしてインストール可能)
  • オフライン: Service Workerで動作
  • 更新: サーバー側のみ更新すればOK
  • 保守: C#のみで完結

まとめ

Ghidraを使った逆コンパイルから始まり、Blazor WebAssemblyでのモダンなWebアプリ再構築まで、一連の流れを体験できた。

これもみんな、GitHub Copilot さんがいるからこそ、やってみようってなったわけで、無かったら、Ghidra の使い方でそれなりに時間喰ってただろうし、パスワードがかかってたとことかでやる気が大幅に失せて、それを理由に復活をあきらめてたかもしれない。

いや、ほんと LLM に感謝 :pray:

参考リンク

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?