この記事は CTF Advent Calendar 2022 Day18 の記事として書きました。
Advent Calendar 参加するのは初めてなので楽しみにしてました。
昨日の記事は、Ark さんの「2022のweb問で特におもしろかった問題を紹介」でした。
明日の記事は、Satoooon さんの「CTFTimeの統計を雑に取ってみる」です。
もくじ
今回の記事のテーマ
今回は、バージョン 10.0 から追加された Ghidra のデバッガ機能を使って、Windows と Linux でシンプルな Reversing 問題を解いていきます。
本来は Android の NDK ライブラリを自作のバイナリから dlopen して解析するやつを書きたかったのですが、自環境での検証が上手くいかずに 5 日ほど溶かしてしまったので急遽こちらに内容を変更しました。
バージョン 10.0 から Ghidra にデバッガが実装されるということが話題になってからずいぶん経ちます。
Ghidra のデバッガは、Windows では WinDbg、Linux では GDB に依存して動作しており、ユーザモードアプリケーションの動的解析を行うことができますが、CTF の Writeup などを見ても、使用している例を全く見かけません。
僕自身、まだプレビュー段階のころに一度デバッガを使ってみたものの、正直まだ使いづらいと感じたため、それ以来全く使っていませんでした。
あれからしばらく時間が経ったものの、Ghidra のデバッガに関してはあまり情報を見かけないため、色々いじってみて使い方や感想について書いていこうと思います。
簡単な動的解析の Rev 問を解く形式で、実際に Ghidra のデバッガを試していきます。
今回使用するプログラムについて
今回は、以下のコードから作成したプログラムを使用します。
#include <stdio.h>
#include <string.h>
char flag[30] = {0x4b,0x6a,0x6e,0x6a,0x77,0x70,0x76,0x68,0x6a,0x60,0x6b,0x6a,0x40,0x57,0x45,0x78,0x7a,0x6c,0x76,0x5c,0x74,0x33,0x6d,0x5c,0x67,0x71,0x62,0x22,0x22,0x7e};
int super_secure_checker(char c, int i)
{
if (c == (flag[i]^0x3)) return 1;
else return 0;
}
int main(void)
{
char password[0x100];
printf("Input yout password: ");
scanf("%s", password);
int len = strlen(password);
if (len != 30) {
printf("Wrong!!\n");
return 0;
}
else {
for (int i = 0; i < 30; i++) {
if (super_secure_checker(password[i], i)) {
continue;
}
else {
printf("Wrong!!\n");
return 0;
}
}
}
printf("Correct!!\n");
return 0;
}
コードとしてはシンプルで、以下のステップで動作します。
- ユーザからパスワードを標準入力で受け取る。
- パスワードの長さが 30 文字の場合のみ、先頭から一文字ずつを
super_secure_checker
関数に渡す。 - ハードコードされた値と入力値を 0x3 で XOR した値を比較して、すべての文字が一致する場合は
Correct
を返す。
よくある動的解析やシンボル解析でブルートフォースできるタイプのリバーシング問題です。
また、非常にシンプルな処理なので、super_secure_checker
関数の戻り値のレジスタを毎回書き換えて、1文字ずつレジスタに展開されたパスワードを取得していくことも可能です。
もちろんこれくらいシンプルな処理であれば静的解析でも余裕でパスワードを取得することはできますが、今回は Ghidra のデバッガを使っていくことが目的なので、あえて動的解析で解いていきます。
Linux で Ghidra のデバッガを使う
上記のソースコードをオプションなしの gcc でコンパイルした ELF バイナリを使用して解析を行います。
サンプルプログラムからダウンロードできます。
また、Ghidra はバージョン 10.2.2. を使用しています。
デバッガを起動する
まず、バイナリを読み込んだ後にTool Chest
の真ん中にある虫のようなボタンを押してデバッガを起動します。
次に、デバッガウィンドウの左上の [File]>[Open] から解析対象のファイルをロードします。
ロードが完了すると中央の Listing ウィンドウにディスアセンブル結果が表示されます。
次に、[Debugger] ツールバーから [in GDB locally IN-VM] を選択します。
GDB launch command
は、特に問題なければ既定の/usr/bin/gdb
ままにしておきます。
[Connect] をクリックした後、CommandLine
の設定を確認します。
実行バイナリにコマンドライン引数が必要な場合はここで設定します。
これで[Launch]をクリックするとデバッガが起動して色々な情報が出力されます。
デフォルトでは、中央におなじみの Listing ウィンドウが表示され、右側に Interpreter が表示されています。
Interpreter には、コマンドラインで GDB を起動した場合と同じ出力がされており、GDB コマンドによる操作も可能になっています。(この環境では gdb-peda をセットアップしているため、peda の出力が表示されています)
左ペインには GDB によってデバッグされているプログラムのプロセスに関する情報が表示されます。
ここから設定しているブレークポイントやロードされているモジュールなども確認することができます。
ブレークポイントをセットしてプログラムを実行する
とりあえずデバッガが起動できたので、デコンパイルウィンドウでsuper_secure_checker
関数の呼び出し直後の行を右クリックし、[Toggle Breakpoint] でブレークポイントを設定してみます。
とりあえずSW_EXECUTE
でブレークポイントを設定すると、 Listing ウィンドウでもブレークポイントを設定した行の色が変化しました。
Interpreter でも、GDB のブレークポイントがセットされていることを確認できます。
プログラムは、Object ウインドウの上部にある[Quick Lounch]ボタンから再起動できます。
また、ここにあるボタンで、[Step into] などの操作を行うことができます。
Ghidra のデバッガで標準入力を送れるようにする
ブレークポイントを設定したのでプログラムを実行していきます。
普通にプログラムを実行していくと、scanf("%s", password);
の行で標準入力を待つため、処理が停止します。
しかし、Ghidra のデバッガの未解決の制限事項で、Interpreter からプログラムに標準入力を与えることができません。
参考:Unable to put input value into interpreter. · Issue #3174 · NationalSecurityAgency/ghidra
このような問題の回避のため、ターミナルの tty を確認し、Ghidra デバッガの Interpreter から接続するという方法を使用します。
まず、端末で起動しているターミナルでtty
コマンドを実行し、デバイスの pts を確認します。
次に、ターミナル側でおまじないとしてsleep 10000000
を実行したのち、Ghidra デバッガの Interpreter でset inferior-tty [TTY]
を入力し、プログラムを再起動します。
これでscanf("%s", password);
が呼ばれるコードまで処理を進めると、ターミナル側に入力プロンプトが表示されるため、ここから標準入力を与えてデバッガの処理を継続することができます。
中々面倒な手順ですが、Ghidra デバッガで標準入力を与える方法としては、他にも GDB スクリプトや Python を使用する方法があるようです。
また、Ghidra の Interpreter ウインドウから、run < input.txt
のように GDB コマンドを実行する方法でも標準入力を与えることができます。
レジストリとメモリの情報を確認する
無事に標準入力をプログラムに与えることができたので、先ほどブレークポイントを設定した以下の箇所で処理が停止しました。
printf("Input yout password: ");
scanf("%s", password);
int len = strlen(password);
if (len != 30) {
printf("Wrong!!\n");
return 0;
}
ここで、デフォルトで右ペインに存在している Register ウインドウからレジストリの値を確認してみます。
各レジストリの並び替えを好きなように変えられるのは結構便利ですね。
Ghidra デバッガを使用すると、レジスタ値の表示、検索、変更などができるようです。
この時点では、パスワードの 1 文字目が間違っていたため、super_secure_checker
関数の戻り値が格納される EAX の値は 0 になっています。
この値を 1 に書き換えてみます。
Register ウインドウは、デフォルトでは Read Only モードになっています。
Edit モードに切り替えるには、右上のペンのようなマークのボタンをクリックします。
これで、各レジストリの値をダブルクリックすることで任意の値を書き込むことができるようになります。
続いてメモリの情報を参照していきます。
メモリウインドウを起動するには、ツールバーの[Windows]>[Debugger]から[New Memory View]を選択します。
ここからメモリ内の情報を表示、編集することができます。
今回は特に使用しないので割愛しますが、メモリ内の検索もここから行えます。
また、右上のカメラのようなボタンからデバッグ時点でのメモリのスナップショットが取得できるようです。
これはなかなか使えそうな機能ですね。
フラグを取得する
さて、いよいよフラグを取得していきます。
本来であれば、 GhidraScript か、Ghidrathon から起動した Python インタプリタを使用してブルートフォースでフラグを取得する予定だったのですが、残念ながら断念しました。
Python から Ghidra のデバッガを操作する API 自体は、FlatDebuggerAPI
として用意されており、from ghidra.debug.flatapi import FlatDebuggerAPI
でインポートすることで使用できそうなのですが、残念ながらFlatDebuggerAPI
はまだghidra_docsでドキュメント化されていない API で、issue やサンプルコードもなく、実装が難しかったです。
一応writeMemory
とかbreakpointSetSoftwareExecute
といった名前のメソッドは実装されているようでしたので、ソースコードレベルで参照していけば使えるかもですが、Ghithub のソースコード検索でもghidra.debug.flatapi
を使っているコードはほとんど見つからなかったので、現状はまだ使われていないようですね。
一応唯一のサンプルコードを使ってプログラムを再実行したりメモリやレジスタの値を読み取ったりなどはできたので、頑張れば実装できなくはなさそうですね。
というわけで、 Ghidra のデバッガの GUI 操作を併用しつつ、フラグを取得していきたいと思います。
まずは、先ほどセットしたsuper_secure_checker
関数の戻り値を取得するためのブレークポイントは削除し、super_secure_checker
関数内で復号されたフラグ文字がレジスタに格納され、入力値との検証が行われるアドレスにブレークポイントを設定します。
これで処理を進めてみると、入力値として適当に与えた文字 A が DIL レジスタに格納され、復号されたフラグの 1 文字目である H と比較されていることがわかります。
ちなみに、Ghidra のデバッガの Register ウインドウでは Type の列は任意のデータ型を設定でき、上記のように Value が指定した型と一致する場合は、 Repr 列にその型の表現を出力してくれます。
このまま処理を続行してもらいたいので、 Register ウインドウの編集モードで、 AL レジスタの値を 0x41 に書き換えます。
そうするとパスワードの検証をパスすることができるので、処理を進めることで、2 文字目のフラグを取得できます。
この操作を 30 回繰り返すことで、HimitsukichiCTF{you_w0n_dra!!}
というフラグを取得することができました。
Windows で Ghidra のデバッガを使う
さて、ここまで来たらついでに Windows でも Ghidra のデバッガを使ってみたいですよね。
使用するファイルは、先ほどと同じtask.c
を Visual Studio 2022 でビルドした PE ファイルです。
サンプルプログラムからダウンロードできます。
デバッガを起動する
基本的な操作は前述の Linux 環境と同様ですが、Windows の場合は、 GDB ではなく WinDbg に依存しているので、WinDbg のセットアップをしていない環境の場合は事前にインストールしておく必要があります。
また、WinDbg と同様に、ユーザモードプログラムの解析であってもメモリアクセスのために High Integrity が必要になります。
そのため、Ghidra を管理者権限で起動しておきます。
Ghidra を起動したら、[Debugger] ツールバーから、[in dbgeng locally IN-VM] を選択してデバッガを起動します。
dbgeng は、WinDbg などの Windows デバッガで使用されるインターフェースです。
今回はローカルデバッグを行うので [Connect] の設定は既定値のまま進みます。
コマンドライン引数が必要な場合はここで指定できます。
デバッガが起動すると、GDB のときと同じく、Interpreter ウインドウに WinDbg のインタプリタと同様のコンソールが表示されます。
ブレークポイントを設定する
デバッガが起動したら、デコンパイラを使ってmain
関数のアドレスを特定して、ブレークポイントを設定していきます。
PE ファイルの静的解析については今回はスコープ外ですが、 entry 関数から追っていくとすぐにmain
関数が見つかると思います。
最適化の兼ね合いかよくわからないですが、デコンパイル結果ではsuper_secure_checker
関数の処理もmain
関数の処理の中に組み込まれてしまっているようでした。
そのため、Linux のときと同じく入力値と復号されたパスワードを比較している行にSW_EXECUTE
のブレークポイントを設定します。
ブレークポイントを設定した状態でプログラムの実行を開始すると、コンソールプログラムが起動します。
標準入力もここから与えることができるので Linux のようにハマることもありませんでした。
レジスタの情報を参照する
さて、復号されたフラグの文字列を確認するためにレジスタの情報を確認したいのですが、今回は Register ウインドウではなく Watches ウインドウを使ってみたいと思います。
デフォルトで右下に配置されている Watches ウインドウを開き、[+] ボタンから対象を追加します。
今回は、 [Expression] 列に RCX と RSP を設定し、それぞれのレジスタの値をウォッチすることにしました。
プログラムを実行すると、ブレークポイントを設定したタイミングで RCX に復号された 1 文字目のフラグ文字が格納されていることがわかります。
ユーザが入力した文字列についてはローカルスタックに格納されます。
しかし、どうやら現在の Ghidra のデバッガではメモリ内の値を参照することはできないようです。
ヘルプを見ると*:4 (RSP+8)
のような記法でメモリ内の情報が参照できるような記載があったのですが、僕の環境では例外を発生して値が取得できませんでした。
以下の issue を見る限り、現時点では Ghidra からメモリ内の情報を参照する機能は上手く動作しない場合があるようで、 Interpreter からデバッガコマンドを使う方法以外に回避策はなさそうでした。
参考:[Debugger]: Stack Frame Memory Viewer / Editor · Issue #2866 · NationalSecurityAgency/ghidra
というわけで、上記のスクショ上でも WinDbg の da コマンドを使って、ローカルスタック内の入力文字列を確認しています。
フラグを取得する
最後にフラグを取得していきます。
Linux の時は Register ウインドウからレジスタの情報を書き換えて 1 文字ずつフラグを取得していきましたが、 Watches ウインドウからも同様に編集モードでレジスタの値を変更することが可能です。
今回は RCX の値を入力値と同じ 0x41 に変更することでパスワード検証をパスし、2 文字目以降のフラグを取得できるようになりました。
まとめ
今回は Ghidra のデバッガ機能を使ってシンプルなリバーシング問題を解いてみました。
軽く使ってみた感想としては、「良さそう」と「使いづらい」が 4:6 くらいの印象でした。
使いづらいなと思った主な点は、致命的にナレッジが少ない点と、非常に動作が不安定な点です。
ナレッジについては、Ghidra のヘルプの情報自体が最小限である上に、英語でググってもブログ記事すらほとんどヒットしない状況でした。
一方で、 issue には結構いろんな質問や回答が上がっており、 issue がほぼ唯一の有用な情報ソースとして重宝していました。
また、動作については単純に僕の環境が悪い可能性もありますが、よくわからないタイミングで例外発生してクラッシュしたりデバッガがハングしたりすることがかなり頻繁に発生しました。
Ghidra のデバッガ側で未実装の機能はまだ多そうですが、Interpreter ウインドウから GDB と WinDbg のコマンドが実行できるので、デバッグ能力的な面での制限はあんまりないように思いますが、大した解析もしてないのに頻繁に動作が不安定になるのはちょっと辛いものがありました。
しかし、無料で使えるツールにも関わらず PE と ELF を同じ UI で操作できるのは普通に嬉しいですし、デコンパイル結果からそのままブレークポイントを設定したりできるのも便利だと感じました。(IDA はほぼ使ったことがないので、、、)
また、今回の記事では触れませんでしたが、 Ghidra デバッガの Time 機能という、実行時トレースのスナップショットを取得して、その時点に遡って解析を行える機能が非常によさそうでした。
スナップショットのタイミングしか追えないとはいえ、WinDbg Preview の Time Travel Debugging(TTD) に近い機能を ELF の解析でも使えるのがとても嬉しいですね。
もう少し使いこなせるようになると活用の幅が広がりそうに感じているので、今後もできるだけ Ghidra のデバッガを使っていきたいと思います。