この記事はKogakuin Univ Advent Calendar 2022 - Adventar 16日目です。
こんにちは。
12月にしか更新されないことで定評のあるブログです。
1年くらい前に学生団体(KogCoder)でGhidraのデバッガ関係入門講座みたいなものをやったのですが、未だに日本語情報があんまりなさそうなのでアドベントカレンダーのネタに流用共有したいと思います。
そもそもGhidraってなにさ?
A software reverse engineering (SRE) suite of tools developed by NSA's Research Directorate in support of the Cybersecurity mission
NSA(アメリカ国家安全保障局)が作ったソフトウェアのリバースエンジニアリング用のソフトです。
OSSかつ無料で使うことができますが、有料ソフト並みの機能を備えています。
IDA Pro(クソ高リバースエンジニアリング用ソフト、機能はとても優秀)をお持ちでない貧民の心強い味方です。
Ghidra昔話(Ghidra 9.x系)
Ghidraは元々NSAがマルウェア解析用に内部で使っていたものを一般公開したものなので静的解析に特化した機能開発がされていました。
その関係上、一般公開された当初のGhidraにはデバッガがなかったのす。
ですが多くの希望があったのかGhidra 10.0の公開に合わせてデバッガ機能が実装されました。やったね。
これによって解析結果と今動いているコードを突き合わせる作業が楽になりました。
しかし、Ghidraリリース当初になかった機能であるため日本語の情報が大変少なくなっております。
今回の記事では実際に実行ファイルをデバッガで実行しながら最低限の操作説明をしたいと思います。
なお、今回はWindowsでやりますがLinuxでも大きく変わることはないと思います。
使う実行ファイル
なお、解析作業は今回の本旨から逸れるためpdbファイルをセットで付けています。
exeファイルとpdbファイルを同じフォルダに置いた状態でGhidraに投げると関数名や変数名等がいい感じになります。
中身
だいたいこんなのです
const bool show_flag = false;
char flag[] = {172, 192, 161, 198, 189, 217, 234, 136, 253, 154, 253, 206, 188, 227, 141, 185, 215, 186, 138, 213, 162, 150, 253, 201, 187, 143, 225, 156};
void decrypt() {
int key = 0xca;
for (int i = 0; i < sizeof(flag); i++) {
flag[i] ^= key;
key ^= flag[i];
}
return;
}
int main() {
if (show_flag) {
puts("Congrats!");
decrypt();
puts(flag);
}
else {
puts("You can't reach to flag. Hahaha");
}
_getch();
return 0;
}
さてコードを読むとだいたい以下のことが分かると思います。
- flagは暗号化されてる
- ただのxor暗号ですが見なかったことにして下さい…
show_flag
がtrueのときのみflagを表示する- 普通に実行する限り
true
にはならない
- 普通に実行する限り
以降は、チェックをすっ飛ばしてflagを表示する処理に行くことを目標として進めていきます。
手順
新入生でも分かるを目標に作ってたのでかなりくどめの説明になってます
また、Ghidraに解析対象を登録して解析する作業は既に終わっているものとします。
1. デバッガを開く
まず、最初の画面から虫のマークを押してデバッガー画面を開きます。
開けたらこんな画面のはずです。
2. ファイルを開く
メニューバーの[File]→[Open...]を選択します。
インポートしたファイルを選択し[OK]を押します。
3. デバッガと接続する
この状態ではデバッガでどのファイルを開くかを指定しただけなので特に操作ができません。
なので、操作するデバッガと接続する必要があります。
まず、メニューバーの[Debugger]→[Debug [インポートした名前]]→[in dbgeng locally IN-VM]を選択します。
IN-VMとGADPの違いはデバッガを立ち上げるプロセスがGhidra本体か仲介用のソフトウェアを挟むかどうか(のはず)です。
所感としてはGADPだとたまに謎の不具合が出ますが、その代わりにデバッグ先が強制終了したときに巻き込まれてGhidra側に影響が出るようなことが少ないと感じました。
その後は、2回くらいなんかでてくるのでそのままOKを押します。
実行時引数を渡したい場合はここでどうにかして下さい。
4. ブレークポイントを設定する
さてこれでデバッグできるようになった訳ですが、このまま実行しても問答無用で終了するだけです。
なのでとりあえずmain
開始時にブレークポイントを入れて処理を止めましょう。
Symbol Tree から[Functions]→[main
]を選択します。
すると、Listing と Dynamic が表示するアドレスがmain
のアドレスになります。
流石に機械語のまま読むのはつらいので、Dynamicのウィンドウの55 8b ec ..
部分の55
で右クリックし[Disassemble]を押します。
55 ..
はpush rbp ; ..
といった関数の開始地点によくある処理の機械語表現なので覚えておくと何かの役に立つかもしれません。
またASLRの関係でアドレスは毎回変化します。
次に、Dynamic のディスアセンブルした部分の最初(55 PUSH RBP
)を右クリックし、[Set Breakpoint]→[SW_EXECUTE]を選択します。
SW_EXECUTEとHW_EXECUTEの違いはソフトウェアブレークポイントを使うかハードウェアブレークポイントを使うかどうかです。
ハードウェアブレークポイントのほうが何かと優秀なイメージですが設定可能な個数に上限があります。
ここはそのままOKで大丈夫です。
5. main関数まで実行
Objects ウィンドウのResumeボタンを押す、以上。
6. アセンブリ確認
さて、処理を飛ばすには「どこから」「どこに」飛ばすかを理解している必要があります。
それを調べるためにアセンブリを読みましょう。
それぞれのアセンブリはだいたいこんな意味です。
PUSH EBP
: EBPの内容をスタックに入れる。アセンブリにおける関数の呼び出し規則のようなものなのであまり気にしなくてOKMOV EBP,ESP
: ESPの内容をEBPにコピーする。アセンブリにおける関数の呼び出し規則のようなものなのであまり気にしなくてOKXOR EAX,EAX
: EAXとEAXの排他的論理和をEAXに入れる。つまりEAXを0にするJZ LAB_0040108a
: 直前に行われた計算の結果が0ならば0040108aにジャンプ- 00401067 - 00401088: flag表示処理
- 0040108a - 00401095: flag表示しない場合の処理
- 00401098l: 一時停止
- 0040109e - 004010a1: 関数終了時の規則
flagを表示させるのが目標なので、00401067に飛べばよさそうですね。
7. 処理を飛ばす
ということで実際に処理を飛ばしていきます。
まず、Interpreter ウィンドウの下にあるRegistersを押し、レジスタ一覧の表示に切り換えます。
次に、Enable editting of recorded register values ボタンを押し、レジスタの変更を許可します。
そして、RIP のValueをダブルクリックし、現在の値+7の値
に書き換えます。
RIPは現在実行中のアドレスを示すレジスタです。
また、+7
なのはmain関数の開始位置とflag表示処理の開始位置の差分が7であるためです。
書き変える際に値が16進数表記されていることに気を付けましょう。
8. いざ実行
Objects ウィンドウのResumeボタンを押します。
うまくいっていればflagが表示されるはずです。
flagはこれを書いてたときの僕の心の声です。
まとめ
いかがでしたか(定型文)
今回はGhidraのデバッガの使い方を紹介しました。
やったこと自体は他のデバッガでもできることですが、ディスアセンブラの優秀さの格が他のデバッガの比にならない(それはそう)なのでもっと大きなプログラムだと力を発揮しそうです。
また、日本語の情報がないと言いましたが英語の情報は公式ドキュメント含めけっこうあるので困ったらそっちを見るといいと思います。
では、明日のアドベントカレンダーの枠を誰かが埋めてくれることを信じつつ筆を置かせてもらいます。