
GoogleのGemma 4 E4BローカルAIモデルを使用してGhidraでWindowsのクラックミーをリバースエンジニアリングするためのステップバイステップのチュートリアル。ローカル推論のセットアップと自動的な関数/変数のリネーミングを含む。
Googleがリリースした新しいGemma E4Bオープンウェイトローカルモデルを試していたところ、ローカルオフラインでのリバースエンジニアリングシナリオで非常に成果が上がっていることに驚きました。このチュートリアルでは、ローカルAIが多くの基本的なリバースエンジニアリングタスクに十分対応できるようになったこと、そしてこれから急速に改善が進むであろうことを広めるために書いています。
新しいバイナリをリバースする上で最も退屈な部分の1つは、最初の段階で、重要な関数や変数について何の手がかりもないことです。リバーサーは、文字列参照を見る、バイナリ差分、類似関数のマッチングなど、着手するための多くのテクニックを持っています。
AIはここで非常に役立ち、私はOpenAI APIを使ってバイナリに注釈を付けたり、デコンパイラの出力を整理したりすることに個人的に大きな成功を収めてきました。しかし、これらのAPIを使用するにはいくつかの欠点があります。
コスト - デコンパイルと逆アセンブルは大量のトークンを生成します。APIはトークンごとに課金されるため、大きなバイナリを分析するにはかなりの金額がかかる可能性があります。毎週更新される多数のバイナリを含む大規模なターゲットを扱う場合、これらのコストはすぐに膨らみ、趣味でリバースエンジニアリングをする人には手が出せません。
プライバシー - リモートAPIを使用する場合、AIのホストはあなたが何をしているかを把握できます。これはプロフェッショナルなシナリオでは受け入れられない場合があります。
制御 - リモートAPIに依存している場合、どのモデルが提供されているかやモデルの品質を制御できません。ミッションクリティカルな作業でそれらに依存している場合、遅くなったりダウンしたり必要なときに使えなくなったり、出力品質が低下して役に立たなくなったりする可能性があります。
独自のローカルAIモデルを実行することで、これらの問題点のいくつかに対処できます。
コスト - ホスト型サービスに依存するよりも、ローカルモデルを実行する方がはるかに安価な場合があります。ローカルモデルはおそらく優良プロバイダーのものより小さく低速ですが、十分に優れており、妥当な時間で動作するなら、特に大量のデータを簡単なタスクで処理する場合には、独自モデルを実行してお金を節約することは理にかなっています。
プライバシー - モデルをローカルで実行する場合、ネットワークコールは発生せず、プライバシーを完全に制御できます。自分のマシンで何にモデルを使っているかを誰も見ることはできません。
制御 - オープンウェイトモデルの素晴らしさは、誰もそれを奪うことができないことです。OpenAIやAnthropicは、いつか価格引き上げやAPIの明示的な削除によって、SotAモデルを利用できなくするかもしれません。しかし、オープンウェイトモデルを使えば、良くも悪くも、自分の運命を自分でコントロールできます。
ローカルAIモデルには欠点もあります。
サイズ - モデルが大きいほど、インテリジェントになります。しかし、ほとんどの大規模モデルはコンシューマーハードウェアに収まりません。そのため、ローカルモデルを実行している場合、おそらくSotA(最先端)モデルの10分の1から100分の1の大きさのモデルを実行していることになります。このサイズの減少はモデルの知能の低下に直結し、ChatGPT/CodexやClaudeのようなSotAモデルで当然と思われている多くのタスクには不向きになります。
速度 - ローカルモデルはおそらく、AI APIを使用する場合よりもマシン上で低速に動作します。これもコンシューマーハードウェアの制限と、APIプロバイダーが行うことができるトリックの一部が一般的には利用できないことによるものです。
設定 - ローカルモデルの実行は、中古のノートパソコンでLinuxを動かそうとするのと、Apple Storeで新品のMacBook Airを買うのとの違いに似ています。CodexやClaude Codeの体験はAIにおけるApple Storeの体験です。ローカルAIモデルの体験は、ガレージでフェドーラ帽をかぶった男がガタガタのコンピューターを叩いて動かそうとしているようなものです。最低限、以下のことを気にしなければなりません。
これは簡単な道のりではなく、多くの人が諦めて、ローカルAIモデルはそのタスクに適していないと思い込んでしまいます。なぜなら、適切なハードウェア/モデル/設定/プロンプト/ハーネスの組み合わせを見つけられなかったからです。多くの場合、彼らの言う通りですが、このチュートリアルがローカルモデルがどこまで進歩したか、リバースエンジニアリングにどのように役立つかを示し、人々にローカルAIに挑戦するきっかけを与えることを願っています。
(アーカイブパスワードは crackmes.one です)。元のリンクが切れた場合に備えて、リポジトリ内に代替リンクをホストしています。
Ghidra 12.04 を逆アセンブルとデコンパイルに使用します。これを使用するには OpenJDK 21 をインストールする必要があります: https://github.com/nationalsecurityagency/ghidra
このチュートリアルを作成している間、私はClaude Codeでヴァイブコーディングして、AIで関数や変数の名前を変更するGhidraプラグインを作成しました。プラグインはこちらからダウンロードできます: https://github.com/markoglasgow/Ghidra_FastAIRenamer_Plugin
インストールするには、ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip ファイルを ${GHIDRA_HOME}\Extensions\Ghidra に移動し、${GHIDRA_HOME}\ghidraRun.bat を実行してGhidraを起動します。プラグインを有効にするには、初期画面のトップメニューで File -> Install Extensions を選択し、プラグインブラウザで FastAIRenamerPlugin の横にあるチェックボックスをオンにして、Ok をクリックします。Ghidraは再起動を促すので、すぐに実行してください。
プラグインを設定するには、次回Ghidraが起動したら、トップメニューで Tools -> Run Tool -> CodeBrowser に移動します。Ghidraが「新しい拡張機能が検出されました。設定しますか?」と表示するので、「はい」をクリックし、再度 FastAIRenamerPlugin の横のチェックボックスをオンにして、Okをクリックします。CodeBrowserが開いたら、トップメニューで Window -> Fast AI Renamer をクリックし、次に Config ボタンをクリックします。ここでAIモデルを設定できます。完了したら、プラグインウィンドウと空のCodeBrowserウィンドウを閉じます。
注: プラグインの読み込みに問題がある場合は、Ghidraでデベロッパーモードを有効にする必要があるかもしれません(File -> Configure -> Developerの横のチェックボックス)。
注: プラグインが読み込まれているかどうかは、CodeBrowserで File -> Configure -> Ghidra Core -> 青い設定ボタンをクリック -> "FastAIRenamer" でフィルター -> 名前の横のチェックボックスがオンになっていることを確認 で確認できます。
注: プラグインをアンインストールするには、最初にCodeBrowserを開き、File -> Configure -> Ghidra Core -> 青い設定ボタンをクリック -> "FastAIRenamer" でフィルター -> チェックを外す -> ok。CodeBrowserを閉じ、初期のGhidraウィンドウで File -> Install Extensions -> "FastAIRenamer" のチェックを外す。最後にGhidraを閉じ、${GHIDRA_HOME}\Extensions\Ghidra から ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip を削除します。拡張機能が削除されたことを確認するには、次回Ghidraを実行したときに、初期画面で Help -> Runtime Information -> Extension Points -> "FastAIRenamer" でフィルター し、何も表示されないことを確認します。ふぅ。
最後に、Visual Studioまたは他のC++開発環境がセットアップされていることを確認し、適宜クラックミーの解決策をヴァイブコーディングできるようにしてください。
ローカルAIのセットアップは、ハードウェアと利用可能な資金によって大きく異なります。私の場合:
私は Nvidia GTX 3080 を使用しており、Cuda 13.2 がインストールされています。Windowsターミナルで nvcc --version を実行するとCudaのバージョンを確認できます。
私は bartowski の https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF/blob/main/google_gemma-4-E4B-it-Q8_0.gguf 量子化モデルを使用しています。迷信かもしれませんが、分析タスクでは可能な限り量子化を少なくし、できれば完全に避けるようにしています。
推論サーバーとして llama.cpp の特定のバージョン llama-b8893-bin-win-cuda-13.1-x64 を使用しています。
llama.cpp を以下のCLI設定で実行しています:```
..\llama-b8893-bin-win-cuda-13.1-x64\llama-server.exe ^
--port 8090 ^
--threads 12 ^
--n-gpu-layers 256 ^
--no-mmap ^
--model "google_gemma-4-E4B-it-Q8_0.gguf" ^
--ctx-size 32768 ^
--temp 1.0 ^
--top-k 64 ^
--top-p 0.95 ^
--offline
これにより `75 tokens/sec` が得られます。これはローカルAIとしてはかなり高速です。
## ハードウェアがなくても大丈夫
利用可能なハードウェアがない、または設定に問題があるが、それでもこのチュートリアルに沿って進めたい場合は、OpenRouterにログインして無料のAPIのいずれかを使用できます。
https://openrouter.ai/models/?q=free
Googleは現在、`Gemma 4 31B` と `Gemma 4 26B-A4B` を(期間限定で)無料で提供しています。
https://openrouter.ai/google/gemma-4-31b-it:free
https://openrouter.ai/google/gemma-4-26b-a4b-it:free
...これらの無料APIは遅く、信頼性が低く、送信したすべてのデータはおそらく内部分析に記録され、次のトレーニング実行に使用されることに留意してください。それでも、このチュートリアルの目的には、これらのAPIで十分に追従できるはずです。
GhidraプラグインがOpenRouterログインを使用するように設定するには、プラグイン設定を開き、次のように入力します。```
Base URL: https://openrouter.ai/api/
API Key: <your OpenRouter API Key>
Model Name: qwen/qwen3-235b-a22b-2507
この例では、qwen3 モデルに対してプラグインを実行します こちら
crackmepls.exe を解凍して実行すると、標準的なログイン画面が表示されます。任意のパスワードを入力すると、「Access Denied」というメッセージが表示されます。```
User: marko
Pass: 123
Access denied
Ghidraを開くには、`ghidraRun.bat`を実行してください。ツールバーで`File -> New Project`を選択し、`Non-Shared Project`が選択されたまま`Next >>`をクリックし、空のプロジェクトディレクトリを選択してプロジェクト名を入力します。その後`Finish`をクリックします。
次に、`File -> Import File`をクリックし、`crackmepls.exe`を選択してプロジェクトに追加します。Ghidraがファイルの詳細をポップアップ表示し、それが`x86:LE:64:default:windows`向けの`Portable Executable (PE)`ファイルであることを通知します。何も変更せずに`OK`をクリックして受け入れます。しばらく待つと、さらにファイルの詳細がポップアップ表示されるので、再度`OK`をクリックして受け入れます。最後に、プロジェクト内の`crackmepls.exe`をダブルクリックしてコードブラウザを開き、逆アセンブルを開始します。
最初に、Ghidraが「crackmepls.exe has not been analyzed. Would you like to analyze it now?」というメッセージを表示します。`Yes`をクリックし、次のウィンドウで`Analyze`ボタンをクリックし、Ghidraがバイナリ内のすべての関数を特定、逆アセンブル、逆コンパイルするのを待ちます。分析中にPDBファイルが見つからないといったエラーが1~2件表示されることがありますが、`Ok`をクリックして無視してください。
Ghidraによるファイルの分析が完了すると、以下のような画面が表示されるはずです。

これは標準的なMSVCエントリポイントです。`FUN_14000200c`をダブルクリックし、スクロールダウンします。
逆コンパイラウィンドウに`__p___argv`、`__p___argc`への参照があり、それらをパラメータとして受け取る関数呼び出し`FUN_140001290`が表示されるはずです。これがcrackmeの`main()`関数と思われるので、ダブルクリックします。

関数`FUN_140001290`内に入ったら、逆コンパイラウィンドウで少しスクロールダウンします。文字列`User:`と`Pass:`への参照、および`basic_istream`(入力ストリーム)と`basic_ostream`(出力ストリーム)への参照が表示されます。

これらの文字列は、最初にcrackmeを実行したときに表示されたprint文と入力文と一致するため、crackmeの`main()`関数内にいることがわかります。
ここからが、従来のリバースエンジニアリングの面倒な作業の始まりです。バイナリが何をしているのかを分析し、crackmeを解くために、すべての変数名と関数呼び出しを意味のある名前に変更していく必要があります。かつてリバースエンジニアは逆アセンブリ上で手作業でこれを行っていましたが、幸いなことにテクノロジーと税金によって、スクリプト化してAIと連携できる便利な逆コンパイラが手に入りました。
そこで、実際の作業をする代わりに、楽をして、頭を休めて、ローカルAIにすべての作業を任せることにしましょう。
このチュートリアルのセットアップ中に、関数や変数の名前変更を支援するGhidraプラグインをインストールしているはずです。今こそそれを使う時です。Ghidraの上部ツールバーで`Window`をクリックし、`Fast AI Renamer`を選択してプラグインを開きます。

プラグインのUIが表示されます。これはいくつかのボタンとテキストエリアで構成されています。まず`Config`ボタンをクリックし、ローカルAIと通信するための設定が正しく行われていることを確認します。私のコンピュータでの設定は以下の通りです。

`Save`をクリックして設定ウィンドウを閉じ、次に`Rename Variables`ボタンをクリックします。ドラゴンが表示された進行状況ウィンドウがポップアップし、コンピュータがローカルAIモデルを実行して逆コンパイラウィンドウ内のすべての変数の名前を変更するため、少し負荷がかかる音が聞こえるかもしれません。

AIの処理が完了すると(私のマシンでは約10~20秒)、プラグインのテキストエリアに名前変更された変数の説明が表示され、逆コンパイラウィンドウ内の変数名が実際に変更されます。

すべてのAIモデル、特に小型のローカルモデルは本質的に信頼性が低いため、この最後のステップでエラーが発生する可能性があります。これについては後ほど再度触れます。それまでは、`Rename Variables`ボタンを再度クリックしてAIを再実行し、エラーがなく満足のいく結果が得られるまで続けることができます。
## Crackmeを解く
ここまでで、逆コンパイラの出力は見やすくマークアップされ、すべての変数名が意味のあるものに変更されています。従来であれば、リバースエンジニアはこのコードを読み、Crackmeの解決策の草案を作成することができました。しかし、今回はローカルAIにcrackmeを解かせてみようと思います。今日は頭を使わないことにしたのですから。
まず、`Rename ALL Functions and Variables`ボタンをクリックして、AIにバイナリ全体のマークアップを完了させました。これにより、このcrackme関数内で使用されているすべての関数呼び出しに名前が割り当てられ、逆コンパイラの出力がさらにきれいになります。私のマシンではこれに約15分かかりました。
`Local AI Setup`で記載したCLIパラメータで`llama.cpp`を使用している場合、ブラウザで[http://localhost:8090/](http://localhost:8090/)にアクセスすると、ローカルモデルとのチャットUIを開くことができます。
ここで、crackmeの関数の逆コンパイラ出力を入力し(プラグインUIの`Decompile Function`ボタンをクリックするか、右側の逆コンパイラウィンドウからコピーして取得できます)、AIに解決策をコードとして書くよう依頼しました。
======================================================================
======================================================================
======================================================================
crackmeの解決策を書くのを手伝ってほしい。以下はcrackmeの関数の逆コンパイラ一覧です。ユーザーがユーザー名とパスワードを入力し、正しいパスワードが入力された場合にアクセスが許可されます。パスワードは関数内で計算されます。```
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
/* **Reasoning:**
The function takes user input (a username/input and a password) via standard input. It processes
the user input by calculating a complex, custom checksum/hash. It then compares the provided
password input against a target buffer (likely a stored hash or secret). Finally, it determines
and outputs whether "Access granted" or "Access denied," indicating the function serves as an
authentication routine. */
undefined8 authenticate_user(undefined8 param_1,undefined8 param_2,undefined8 max_len)
{
uint user_checksum;
int iVar1;
undefined8 ****temp_free_ptr;
ulonglong user_input_byte_index;
char *result_message;
undefined1 *status_buffer;
undefined8 ****data_buffer_ptr;
ulonglong pass_input_length_1;
bool access_granted;
undefined8 uStack_d0;
undefined1 status_buffer_small [8];
undefined1 status_buffer_large [32];
undefined8 ***allocated_ptrs [2];
size_t compare_length;
ulonglong ptr_metadata_size;
undefined8 ***user_input_buffer;
undefined8 uStack_78;
ulonglong user_input_length;
ulonglong user_input_length_param;
undefined8 ***pass_input_buffer;
undefined8 uStack_58;
size_t pass_input_length;
ulonglong pass_input_length_param;
ulonglong checksum_seed;
code *code_pointer;
undefined8 ***pass_input_buffer_ptr;
undefined8 ***user_input_buffer_ptr;
ulonglong user_input_length_1;
status_buffer = status_buffer_small;
checksum_seed = MAGIC_VALUE_1 ^ (ulonglong)status_buffer_small;
user_input_byte_index = 0;
uStack_78 = 0;
user_input_length = 0;
user_input_length_param = 0xf;
user_input_buffer = (undefined8 ****)0x0;
uStack_58 = 0;
pass_input_length = 0;
pass_input_length_param = 0xf;
pass_input_buffer = (undefined8 ****)0x0;
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,"User: ");
extract_token_from_stream((basic_istream<char,struct_std::char_traits<char>_> *)cin_exref,
(longlong *)&user_input_buffer,max_len);
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,"Pass: ");
extract_token_from_stream((basic_istream<char,struct_std::char_traits<char>_> *)cin_exref,
(longlong *)&pass_input_buffer,max_len);
user_input_length_1 = user_input_length_param;
user_input_buffer_ptr = user_input_buffer;
user_checksum = 0;
pass_input_length_1 = user_input_byte_index;
if (user_input_length != 0) {
do {
temp_free_ptr = &user_input_buffer;
if (0xf < user_input_length_param) {
temp_free_ptr = (undefined8 ****)user_input_buffer;
}
user_checksum =
((int)user_input_byte_index + 1) *
(int)*(char *)((longlong)temp_free_ptr + user_input_byte_index) +
(int)pass_input_length_1;
user_checksum = user_checksum * 8 ^ user_checksum;
user_input_byte_index = user_input_byte_index + 1;
pass_input_length_1 = (ulonglong)user_checksum;
} while (user_input_byte_index < user_input_length);
}
int_to_string_dynamic(allocated_ptrs,user_checksum * 0x539 ^ 0x5a5a);
pass_input_length_1 = pass_input_length_param;
pass_input_buffer_ptr = pass_input_buffer;
temp_free_ptr = &pass_input_buffer;
if (0xf < pass_input_length_param) {
temp_free_ptr = (undefined8 ****)pass_input_buffer;
}
data_buffer_ptr = allocated_ptrs;
if (0xf < ptr_metadata_size) {
data_buffer_ptr = (undefined8 ****)allocated_ptrs[0];
}
if (compare_length == pass_input_length) {
if (compare_length == 0) {
access_granted = true;
}
else {
iVar1 = memcmp(data_buffer_ptr,temp_free_ptr,compare_length);
access_granted = iVar1 == 0;
}
}
else {
access_granted = false;
}
if (0xf < ptr_metadata_size) {
temp_free_ptr = (undefined8 ****)allocated_ptrs[0];
status_buffer = status_buffer_small;
if (0xfff < ptr_metadata_size + 1) {
temp_free_ptr = (undefined8 ****)allocated_ptrs[0][-1];
data_buffer_ptr =
(undefined8 ****)((longlong)allocated_ptrs[0] + (-8 - (longlong)temp_free_ptr));
status_buffer = status_buffer_small;
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
temp_free_ptr = data_buffer_ptr;
status_buffer = status_buffer_large;
}
}
*(undefined8 *)(status_buffer + -8) = 0x140001424;
free(temp_free_ptr);
}
result_message = "Access granted\n";
if (!access_granted) {
result_message = "Access denied\n";
}
*(undefined8 *)(status_buffer + -8) = 0x140001443;
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,result_message);
if (0xf < pass_input_length_1) {
temp_free_ptr = (undefined8 ****)pass_input_buffer_ptr;
if (0xfff < pass_input_length_1 + 1) {
temp_free_ptr = (undefined8 ****)pass_input_buffer_ptr[-1];
data_buffer_ptr =
(undefined8 ****)((longlong)pass_input_buffer_ptr + (-8 - (longlong)temp_free_ptr));
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
status_buffer = status_buffer + 8;
temp_free_ptr = data_buffer_ptr;
}
}
*(undefined8 *)(status_buffer + -8) = 0x14000147d;
free(temp_free_ptr);
}
if (0xf < user_input_length_1) {
temp_free_ptr = (undefined8 ****)user_input_buffer_ptr;
if (0xfff < user_input_length_1 + 1) {
temp_free_ptr = (undefined8 ****)user_input_buffer_ptr[-1];
data_buffer_ptr =
(undefined8 ****)((longlong)user_input_buffer_ptr + (-8 - (longlong)temp_free_ptr));
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
status_buffer = status_buffer + 8;
temp_free_ptr = data_buffer_ptr;
}
}
*(undefined8 *)(status_buffer + -8) = 0x1400014b7;
free(temp_free_ptr);
}
*(undefined8 *)(status_buffer + -8) = 0x1400014c9;
return 0;
}
デコンパイラのリストを読み、その後、入力されたユーザー名に対応する正しいパスワードを計算するために、以下の私のC++コードを完成させてください。``` #include #include #include #include
int main() { std::string username; std::cout << "User: "; std::cin >> username;
uint32_t password_value = 0;
std::cout << "Pass: " << password_value << "\n";
return 0;
}
## 生成されたソリューション
最初にソリューションを生成した時は、それは間違っていました。このリポジトリ内の [tut1.txt](https://github.com/markoglasgow/gemma_crackme_tutorial/blob/master/tut1.txt) にその結果を含めています。一見すると、そのソリューションは _正しい_ ように見えますが、試してみると失敗します。AIは基本的に信頼性が低く、これはその完璧な例です。
とはいえ、新しいチャットを開き、上記のプロンプトをコピー&ペーストして、AIに新しいソリューションを生成させるよう試みることはできます。私の場合、2回目の試行で動作するソリューションを得ることができました。```
#include <cstdio>
#include <cstdint>
#include <string>
#include <iostream>
#include <sstream>
#include <iomanip>
/**
* @brief Simulates the custom checksum calculation defined in the crackme function.
*
* @param username The input string (username).
* @return int The final calculated user_checksum.
*/
int calculate_username_checksum(const std::string& username) {
// user_checksum is treated as an 'int' (32-bit signed integer) during the core calculation.
int user_checksum = 0;
// pass_input_length_1 acts as the state variable and is a ulonglong (64-bit).
// It starts at 0, as pass_input_length_1 = user_input_byte_index (which is 0) initially.
uint64_t state_pass_length_1 = 0;
for (size_t i = 0; i < username.length(); ++i) {
// i = user_input_byte_index
// Step 1: Calculate the intermediate value T1.
// The decompiler implies this calculation happens using standard int math
// (truncating/wrapping if intermediate values exceed 32 bits).
// (int)user_input_byte_index + 1
int index_term = (int)(i + 1);
// (int)*(char *)((longlong)temp_free_ptr + user_input_byte_index)
// This is just the ASCII value of the character.
int char_term = (int)username[i];
// (int)pass_input_length_1
// The state is cast down to an int for this arithmetic operation.
int state_term = (int)state_pass_length_1;
// T1 = ((int)user_input_byte_index + 1) * (int)char + (int)state
int intermediate_t1 = index_term * char_term + state_term;
// Step 2: Apply the XOR/Multiplication hash function.
// user_checksum = user_checksum * 8 ^ user_checksum;
// Note: The original code structure suggests the update happens here,
// but since we are recalculating the hash step-by-step, we use the definition:
// user_checksum = T1 * 8 ^ T1;
// Since the original code overwrites user_checksum completely with the hash
// of the current character, we use the T1 derived above.
int current_checksum = intermediate_t1;
current_checksum = current_checksum * 8 ^ current_checksum;
// Update the final running checksum
user_checksum = current_checksum;
// Step 3: Update the state variable for the next iteration.
state_pass_length_1 = (uint64_t)user_checksum;
}
return user_checksum;
}
int main()
{
std::string username;
std::cout << "User: ";
std::cin >> username;
// 1. Calculate the final checksum using the username
int final_user_checksum = calculate_username_checksum(username);
// 2. Calculate the final hash/secret value
// FinalHashValue = user_checksum * 0x539 ^ 0x5a5a
// Note: The arithmetic here is done on the final 32-bit 'int' checksum.
uint32_t final_hash_value = (uint32_t)final_user_checksum * 0x539 ^ 0x5a5a;
// 3. Convert the final hash value into a string (as implied by int_to_string_dynamic)
// Since the password needs to match the memory contents (memcmp), it must be the string representation.
std::stringstream ss;
ss << final_hash_value;
std::string required_password = ss.str();
// We output the required password string.
std::cout << "Pass: " << required_password << "\n";
return 0;
}
このソリューションを生成したチャットを、solution_chat.html としてこのリポジトリに含めています。そこには思考の流れも含まれており、ローカルで動作するこの小さなモデルとしてはかなりクールだと思いました。
とにかく、Visual Studio でソリューションをコンパイルし、実行して有効なユーザー名/パスワードの組み合わせを生成できます。そのユーザー名/パスワードを crackme に入力し、解けたことを確認します。

gg
このチュートリアル全体で何度も見てきたように、AI は本質的に信頼できません。以下の 2 つのスクリーンショットを見てください。


最初のスクリーンショットはチュートリアルの前の方のもので、2 番目のスクリーンショットは同じステップで、AI によるリネームを別のタイミングで再実行したものです。逆コンパイラウィンドウで、上のスクリーンショットではパスワード入力バッファが &pass_input_buffer とラベル付けされているのに対し、下のスクリーンショットでは同じバッファが &username_buffer_1 とラベル付けされていることに注目してください。下のスクリーンショットでは、AI が私たちに嘘をついており、その嘘を「幻覚」と呼びます。
すべての AI は嘘をつき、幻覚を起こします。モデルが小さいほど、幻覚の頻度は高くなります。モデルが大きいほど、運が良くなり、幻覚は少なくなります。支払う金額が多いほど、運が良くなります。ただし、幻覚に対して免疫のある AI モデルは存在せず、これを決して忘れてはなりません。
AI は、出力を検証できるタスク、または完璧な精度が必要ないほど重要度の低いタスクに最適です。リバースエンジニアリングの場合、AI は未知のバイナリをすばやく覗き見し、面倒な手作業を大幅に節約するのに役立ちます。しかし、AI が間違ったリネームを行った例で見たように、誤った方向に導く可能性もあります。AI が生成した結果は常に注意深く扱い、可能であれば常に検証してください。
AI を最適に活用する方法は、その結果を自動的に検証することです。たとえば、AI にコードを書かせる場合、手作業で作成したテストスイートに対して AI の出力をテストするスクリプトがあると便利です。crackme のソリューションを AI に生成させる場合、数千の異なるユーザー名に対して適切なパスワードを生成するソリューションを自動的にテストするテストスイートを作成し、テストに合格したソリューションを受け入れることができます。そして、AI をループで実行し、テストに合格するソリューションを書くまで続けます。
この crackme は、比較的小規模なモデル(Gemma 4 E4B)で解きました。このモデルは 80 億のパラメータを持ち、数百億から数兆のパラメータを持つ SotA モデルとは異なります。小規模なモデルは推論が十分にできず、幻覚の頻度が高くなり、コンテキストウィンドウがいっぱいになるとすぐに役に立たなくなります。
実際のリバースエンジニアリングの課題では、小規模モデルのコンテキストウィンドウよりも大きな関数や、その限られた知能では推論するには複雑すぎるロジックを持つ関数をリバースエンジニアリングする必要があるかもしれません。モデルを使用する際には、その限界をよく理解しておく必要があり、どこでどのように失敗するかを事前に把握しておく必要があります。
コンシューマ向けハードウェアがモデルアーキテクチャとともに進歩するにつれて、より大規模で優れたモデルをローカルで実行できるようになり、さらに高度なリバースエンジニアリングタスクもローカルモデルで実現可能になるでしょう。
私が作成した Ghidra プラグイン「Fast AI Renamer」は、現在、分析する関数ごとにサマリを作成し、作業中のプロジェクトの C およびアセンブリのリストを一括エクスポートする機能を備えています。各関数のサマリの埋め込みを計算し、それをベクトルデータベースまたは SQLite にインデックス化するツールを作成したいと考えています。そうすれば、データベースにクエリを実行して、自然言語クエリに従って関数を見つけることができます。
Ghidra プラグインは現在、リネームにマルチターン会話を使用しない単純なワークフローを使用しており、タスクを実行するために AI の出力から CSV を解析することに依存しています。このように実装したのは、小規模な AI モデルがエージェンティックワークフローに固有の長い会話や複数のツール呼び出しを処理できるとは思わなかったからです。私の考えが間違っている可能性もあり、AI へのプロンプト方法の代替案を探るべきです。それでも、小規模なローカルモデルでは、コンテキストウィンドウのサイズを可能な限り制限するのが最善だと思います。
Ghidra プラグインがプロンプトインジェクションに対してどの程度脆弱かはわかりません。セキュリティ専門家は、マルウェアや悪意のあるバイナリの分析にこれを使用する際には注意する必要があります。一般に、これはすべての AI 搭載リバースエンジニアリングツールに当てはまります。これらのツールは、認証されていない localhost の MCP サーバーや、コード実行を達成できる機密機能に使用可能なツールを備えている可能性があります。注意してください。
リバースエンジニアリングに小規模な汎用モデルを使用していることを考えると、より大規模でよりインテリジェントなモデルから蒸留した会話でファインチューニングすることにより、そのパフォーマンスをどの程度向上させることができるのでしょうか?
最後に、Ghidra プラグインの C/asm エクスポート機能は Claude Code とよく連携し、Claude Code + Sonnet を使用して、この crackme の正しいソリューションを初回で書くことができました。crackme の注釈付き逆コンパイル結果をそのままフィードし、ソリューションを見つけるように指示しただけです。Claude Code が、逆コンパイラの出力テキストファイルを通じてバイナリを分析するのに優れ、分析を実行するために Ghidra への MCP サーバーを経由する必要がないようにするためのスキルを書けるかどうか疑問に思っています。たとえば、 スキルを Claude Code に導入し、バイナリの仮想メモリ内のバイトを検査できるようにすることで、逆アセンブリ/逆コンパイラのリストから何らかのデータが参照されている場合、Ghidra を経由せずにそのメモリの内容を確認できるようにすることが考えられます。
crackme を作成してくださった Kryptos 様、およびホスティングしてくださった Crackmes.one 様に感謝します。crackmes.de の後を引き継いでくださったことに感謝します(RIP)。
優れたローカルモデルをリリースしてくださった Google に感謝します。
私の税金を使ってクールなリバースエンジニアリングツールを作ってくださった NSA に感謝し、Ryan Kurtz 様には Ghidra の Github での絶え間ないサポートに感謝します。