gdbでタイムトラベルをしよう(rrの紹介)
この記事はCTF tools/tips Advent Calendar 2022の12日目の記事です。
遅れてしまって申し訳ない...
この記事の対象
- 確率的に発生するバグがある
- この変数の以前の値を知りたい
TL;DR
rrを使う。
タイムトラベル機能について
gdbを使っていて「この値が書き換わったタイミングまで戻りたいな」とか、「たまにエラーで落ちる」といったシーンがあると思います。
そういった、gdb上でタイムトラベルを行い過去の状態へ戻れると嬉しいです。実はそういった機能は標準のgdbでサポートされており、record
コマンドを使うことでタイムトラベルするために一つ一つの挙動を記録しながら実行してくれます。
しかし、少し試したら分かりますが、この機能はAVX命令をサポートしていません。
libcでは至るところで高速化のため使われていますし、コンパイラも最適化のためガンガンSIMD命令に置き換えてくるこの時代において、この制約では実用は難しそうです。
そこで、AVX命令をサポートし、更にちょっとした便利機能をつけてくれるrrを使います。
このツールはもともとMozillaが開発し、Firefoxのデバッグ等に使われているらしいです。
使い方
基本的な使い方は、recordしたトレース情報をreplayするという繰り返しになります。
例として、次のソースコードを解析してみます。
#include<stdio.h>
void blackbox1(int* ptr) {
// i didn't touch :)
}
void blackbox2(int* ptr) {
// i didn't touch :)
}
void blackbox3(int* ptr) {
*ptr = 2;
// HAHA i changed value!
}
int main() {
int x = 0;
blackbox1(&x);
blackbox2(&x);
blackbox3(&x);
if(x != 0){
puts("Did anyone change the value?");
}
asm("int3");
}
これをデバッグシンボル付きでコンパイルします。
デバッグする前に、トレース情報記録のためには以下のコマンドを使います。
$ rr record ./main
rr: Saving execution to trace directory `/home/username/.local/share/rr/main-1'.
Someone changed value...?
recordが終わったらreplayをします。
$ rr replay
gdbが立ち上がり、ローダー内でbreakした状態になると思います。c
コマンドでint3まで進めます。
ここから一行ずつ過去へタイムトラベルしてみましょう。
reverse-next
とコマンドを入力すると、一行ずつ実行が巻き戻っているのが分かります。
if文の中を通過していたかどうかも分かるため、プログラムの挙動が非常にわかりやすいですね。
このツールの真価を発揮するのは、確率的にバグが発生するようなケースです。このツールがない場合、「これからバグが発生するかもしれない!」という時、バグ発生するまで何回もgdbを起動し、実際に変な挙動が発生したセッションを我が子のように大切にするみたいなことをしていました。
しかし、このツールはその苦しみを消し去ることができます。バグが発生したのを確認してから最初の実行に戻ればいいからです。
もちろんpwnの問題でも非常に有用に使うことができます。
更に便利に使うため
コマンドがいちいち長いのでaliasを張っておくと良いです。
# tt = time travel
alias ttc = reverse-continue
alias ttfin = reverse-finish
alias ttn = reverse-next
alias ttni = reverse-nexti
alias ttrs = reverse-search
alias tts = reverse-step
alias ttsi = reverse-stepi
また、gdb以外を使いたい場合(lldbみたいなgdbの代替や、ソースコード表示機能を強くしたcgdbなど)、replay時にデバッガを指定することができます。
$ rr replay -d debugger
終わりに
誤字・脱字・間違い等は、ご指摘いただければ修正します。
コメントでもTwitter(@iwancof_ptr)でも
Discussion