😎

ソースコードを使ってバイナリにデバッグシンボルを付ける

2022/12/03に公開

これは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的には、RELROFORTIFYを見れば、大体のオプションが推測できます。
結果とオプションの対応は、

  • RELRO
    • Full RELROの場合: コンパイルオプションに -Wl,-z,relro -Wl,-z,now を追加
    • Partial RELROの場合: 2022年現在これがデフォルトなので何もなし
    • No RELROの場合: あんまり無いが -Wl,-z,norelro を付ける
  • 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 linkbuild 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