ソースコードを使ってバイナリにデバッグシンボルを付ける
これはCTF tools/tips Advent Calendar 2022の三日目の記事です。
モチベーション
- CTFでpwnの問題を解いている時、バイナリとソースコードの両方が渡された。
- しかし、バイナリがデバッグビルドじゃないためgdbデバッグ時にソースコードがつかない。
- デバッグするときにソースコードが見れたらわかりやすいし、やりやすい。
という時に、どうにかして渡されたバイナリにデバッグ情報をくっつけたい。
注意
結構な力技なので、CTFみたいな小さいバイナリじゃ無いと厳しいし、精度も完璧じゃ無いので、最初にちょっと試してみるぐらいが良いと思います。
小さいバイナリならソースコードを付ける意義も薄いけど....まぁ....
方針
バイナリがビルドされた環境を詳細に調べて再現したあと、デバッグビルドし、そこからdwarfだけ抽出して対象バイナリからdebugリンクを張る。
手順
例として、手元のgccでコンパイルしたバイナリを使います。
コンパイル環境を調べる
コンパイラのバージョンなどを objdump
コマンドを使って調べます。
それらの情報は .comment
領域にあることが多いので、
objdump -s --section .comment /path/to/binary
とすると、
/path/to/binary: file format elf64-x86-64
Contents of section .comment:
0000 4743433a 20285562 756e7475 2031312e GCC: (Ubuntu 11.
0010 332e302d 31756275 6e747531 7e32322e 3.0-1ubuntu1~22.
0020 30342920 31312e33 2e3000 04) 11.3.0.
というような情報が出力され、コンパイラのバージョンなどがわかります。
CTFで出題される問題は、体感的にはUbuntuビルドな事が多いので、これだけで色々な情報が手に入ります。
もし出てこない場合は strings
で見てそれらしい情報があるかgrepすると良いです。
それでも出てこない場合は、諦めます。
コンパイルオプションを調べる
checksec
を使い、バイナリのセキュリティ機能を調べ、どんなコンパイルオプションをつけてビルドしたのか推測します。
checksec --file=/path/to/binary
RELRO | STACK CANARY | NX | PIE | RPATH | RUNPATH | Symbols | FORTIFY | Fortified | Fortifiable | FILE |
---|---|---|---|---|---|---|---|---|---|---|
Partial RELRO | No canary found | NX enabled | PIE enabled | No RPATH | No RUNPATH | 38 Symbols | No | 0 | 0 | /path/to/binary |
CTF的には、RELRO
とFORTIFY
を見れば、大体のオプションが推測できます。
結果とオプションの対応は、
- RELRO
- Full RELROの場合: コンパイルオプションに
-Wl,-z,relro -Wl,-z,now
を追加 - Partial RELROの場合: 2022年現在これがデフォルトなので何もなし
- No RELROの場合: あんまり無いが
-Wl,-z,norelro
を付ける
- Full RELROの場合: コンパイルオプションに
- FORTIFY
- Noの場合: 2022年現在これがデフォルトなので何もなし
- Yesの場合:
-O1 -D_FORTIFY_SOURCE=2
を付ける。最適化レベルは調整が必要
ビルドする
ディストリビューションや、コンパイラのバージョンなどがわかったら、dockerを使って環境を用意します。
有名どころは大体 Docker Hub にあるので、推測したコンパイルオプションを元にデバッグビルドします。
$ docker run -it --rm --name ubuntu -v $(pwd):/project ubuntu:22.04
# apt update && apt install -y build-essential
# gcc /project/main.c COMPILE_OPTIONS -g -o /project/debug_binary
ビルドが終わったら、 nm
を使って渡されたバイナリとシンボルのオフセットが一致するか調べます。
ここで一致しなかったら諦めます(どうしてもという場合にはリンカースクリプトを使うことで解決できそうですが、現実的じゃないです)。
dwarf情報を抽出する
Dockerで生成したバイナリから、dwarfデータを抽出します。
デバッグ情報はすべてdwarfデータとしてバイナリ中に存在しているので、これを objcopy
で取り出します。
objcopy --only-keep-debug /path/to/debug_binary dwarf_info
これでデバッグ情報だけの dwarf_info が抽出できました。
デバッグ情報をリンクする
GDBには、デバッグ情報をバイナリの外部から得る手段として、debug link
とbuild ID
の2つの方法が用意されています。
debug link
は、バイナリ中にdwarfデータへのパスを記述しておき、デバッグ時に自動的に参照する方法です。
build ID
は、デバッグ情報をまとめたディレクトリを別に用意し、デバッグ時にパスを指定することで参照する方法です。最新のlibc6-dbg
などはこの方法を採用しています。
今回は debug link
を使って、配布バイナリからリンクを張ります。
また、リンクはバイナリ中に書き込まれるため、予めコピーしておくことをおすすめします。
objcopy --add-gnu-debuglink=dwarf_info /path/to/cloned_chall
パスを張った cloned_chall
を gdb で開くと、ソースコードの情報が表示されていると思います。
行数だけ表示されてコードが表示されない場合は、
set debug-file-directory /path/to/source_dir
を .gdbinit
に記述すると、ソースコードが認識されるようになります。
終わりに
誤字・脱字・間違い等があればご指摘いただければ修正します。
コメントでもTwitter(@iwancof_ptr)でも
参考文献
18.3 Debugging Information in Separate Files
[gcc][gdb]デバッグシンボルを別ファイルに分離してgdbで読み込ませる方法
Discussion