レガシーアプリを 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について
- 公式サイト: https://ghidra-sre.org/
- GitHub: https://github.com/NationalSecurityAgency/ghidra
- 完全無料、オープンソース
- Windows/Mac/Linux対応
- 13,000以上の関数を解析可能
フェーズ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関数のみ。
とりあえず、これなら楽そうって感じにはなった ![]()
躓きポイント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
成功! 🎉
まぁ、実際にはここに来る前に長い道のりがあって、あるときふとこの方法を思いついて見つけたってのが真相 ![]()
全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;
}
何をしているか全く分からない。
解決策: 段階的な推測とリファクタリング
-
XPath文字列から機能を推測
-
"//checker/project/kind"→ プロジェクト種別の取得?
-
-
関数呼び出しを追跡
-
FUN_00403800→ XPath評価関数(libxml2) -
FUN_00404250→ XMLノードから整数取得
-
-
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 に感謝 ![]()