
gdb で実行ファイル解析、バイナリパッチ作成、デバッグ作業などを行う時に使う gdb コマンドや手順のまとめです。CTF なんかでも使いますので忘れっぽい自分の備忘として。
ここでは gdb の基本操作、バイナリ実行で cmp 命令の比較結果(レジスタ)を書き換えて、通常入らない分岐内容を実行する流れを説明してます。
gdb 9.2 で Ubuntu20.04 LTS 上で確認してます。 といいつつ、Windows でも MacOS でも同じような手順となるはずです。
とりあえず gdb [実行ファイル]
でデバッグ開始!
バージョン情報とかがつらつら表示されたのち、
(gdb)
と出力され、入力できるようになったら作業できます。
下は実行可能ファイルの解析で良く使う gdb コマンド一覧です。
コマンド | 省略した書き方 | できること |
---|---|---|
help [コマンド名] | h [コマンド名] | 使い方の詳細を表示 |
run | r | 起動時に指定したファイルを実行 |
break [ラベル名 or 行番号] | b [ラベル名 or 行番号] | 対象のラベル位置や行番号にブレークポイントを設定 |
info break | i b | 設定してるブレークポイント一覧を表示 |
delete [番号] | d [番号] | 指定したブレークポイントの番号を削除 |
continue | c | break 終了。プログラム再開 |
next | n | 次の行までステップ実行(関数呼び出しならまるっと実行) |
step | s | 次の行までステップ実行(関数呼び出しは中に入っていく) |
nexti | ni | 次の機械語一つを実行(関数呼び出しならまるっと実行) |
stepi | si | 次の機械語一つを実行(関数呼び出しは中に入っていく) |
disassemble | disas | 現在のラベル内をアセンブラ表示する |
info register | i r | レジスタを表示する |
set $[レジスタ名]=値 | レジスタの中身を値に書き換える | |
layout src | lay src | 実行ファイルのソースコード情報(あれば)を表示 |
layout asm | lay asm | 実行コードを逆アセンブルする。デフォルトだとAT&T記法 |
layout regs | lay regs | 現在のレジスタの中身を表示する |
set disassembly-flavor intel | layout asm の逆アセンブルを Intel記法へ | |
+ → + | layout src、asm、regs で表示してる部分(TUIレイアウト)を消したり表示したり切替できる | |
print [式] | p [式] | 式の結果を表示する |
x [アドレス] | x [アドレス] | 指定したアドレスの中身を表示する |
display | 指定したレジスタ等の中身を追跡表示する | |
undisplay | display の指定を解除 |
たとえば下のような C言語(C++、GOでもOKです)のソースコードがコンパイル されていたとします。
#include <stdio.h>
int main(int argc, char *argv[]) {
int lv = 3; // 変数 lv に 3 を代入
if ( lv == 100 ) {
// 到達しないコード
printf("レベル100とは、すごい力じゃ!!まいった!\n");
} else {
// 通常実行されるコード
printf("そなたは貧弱じゃのぅ。\n~You are Dead.~\n");
}
return 0;
}
レベル(変数 lv )は 3 なので、 「そなたは貧弱じゃのぅ。<改行>~You are Dead.~」 と表示されるはずです。
ここで、無理やり lv == 100 の条件分岐を実行して確認・テストしたいとき、
gdb でレジスタの内容を書き換えたり、実行しているアドレスを 書き換えることで 7~8行目を実行することができます。
以下実際に実行の流れです。
以下、Linux 上で gcc を使って C 言語のソースコード(gamesample.c)をコンパイルして、 生成した実行可能ファイル(a.out) を実行した結果。
$ gcc gamesample.c -o a.out
$ ./a.out
そなたは貧弱じゃのぅ。
~You are Dead.~
gdb でデバッグ開始。(gdb)
と最後にでたら入力できるようになる
$ gdb a.out
GNU gdb (Ubuntu 9.1-0ubuntu1) 9.1
: 起動ログ省略
Reading symbols from a.out...
(No debugging symbols found in a.out)
(gdb)
main 関数内の処理を見たいので、ブレークポイントを main 関数の頭に貼り付ける
(gdb) break main
Breakpoint 1 at 0x1149
これで main の場所にブレークポイントが貼られました。 gdb 上でプログラムを実行するとき、このブレークポイントを貼った場所で 止まります。
では、ブレークポイントを貼った場所(main関数入口)までプログラムを実行します。
(gdb) run
Starting program: /mnt/c/Users/amaji/sample/a.out
Breakpoint 1, 0x0000555555555149 in main ()
main 関数の中身をアセンブラで確認します。 確認する前に、個人的に Intel 構文で表示したいので
(gdb) set disassembly-flavor intel
で、アセンブラ表記をデフォルトのAT&T構文からIntel構文に変更しておきます。
(gdb) disassem
Dump of assembler code for function main:
=> 0x0000555555555149 <+0>: endbr64
0x000055555555514d <+4>: push rbp
0x000055555555514e <+5>: mov rbp,rsp
0x0000555555555151 <+8>: sub rsp,0x20
0x0000555555555155 <+12>: mov DWORD PTR [rbp-0x14],edi
0x0000555555555158 <+15>: mov QWORD PTR [rbp-0x20],rsi
0x000055555555515c <+19>: mov DWORD PTR [rbp-0x4],0x3
0x0000555555555163 <+26>: cmp DWORD PTR [rbp-0x4],0x64
0x0000555555555167 <+30>: jne 0x555555555177 <main+46>
0x0000555555555169 <+32>: lea rdi,[rip+0xe98] # 0x555555556008
0x0000555555555170 <+39>: call 0x555555555050 <puts@plt>
0x0000555555555175 <+44>: jmp 0x555555555183 <main+58>
0x0000555555555177 <+46>: lea rdi,[rip+0xeca] # 0x555555556048
0x000055555555517e <+53>: call 0x555555555050 <puts@plt>
0x0000555555555183 <+58>: mov eax,0x0
0x0000555555555188 <+63>: leave
0x0000555555555189 <+64>: ret
End of assembler dump.
3行目の「=>」が表示されてる行が現在プログラムを実行中の位置です。
ではでは、実際にプログラムの実行を進めていきますが、その前に。
実際に現在どこを実行しているのか確認しながら、コマンドを打ちたいので、
(gdb) layout asm
で以下のように、gdb コマンド入力欄とアセンブラの表示を上下に表示するように しておきます。
以降、nexti
コマンドで次の行に実行をすすめてみます。
一回 nexti するごとに、実行行が次に移動していくのが分かります。
nexti nexti や stepi の "i" は instruction (意味:命令)のこと。
では、逆アセンブルした結果を眺めてコメント付きにしてみます。 Linux Intel x86-64bit です。
アセンブリコードの右側に簡単な解説を追記しておきます。
<+0> : endbr64
<+4> : push rbp
<+5> : mov rbp,rsp
<+8> : sub rsp,0x20
<+12>: mov DWORD PTR [rbp-0x14],edi
<+15>: mov QWORD PTR [rbp-0x20],rsi
<+19>: mov DWORD PTR [rbp-0x4],0x3 # int lv=3 を実行
<+26>: cmp DWORD PTR [rbp-0x4],0x64 # if ( lv == 100 ) 比較
<+30>: jne 0x555555555177 <main+46> # 比較した結果が false なら <+46> 行へジャンプ
<+32>: lea rdi,[rip+0xe98] # 文字列 "レベル100とは.." を読込
<+39>: call 0x555555555050 <puts@plt> # putsで文字列を表示
<+44>: jmp 0x555555555183 <main+58> # <+58>の行へジャンプ
<+46>: lea rdi,[rip+0xeca] # 文字列 "そなたは貧弱.." を読込
<+53>: call 0x555555555050 <puts@plt> # putsで文字列を表示
<+58>: mov eax,0x0
<+63>: leave
<+64>: ret
上記を見てみると、10~11行目(<+32>~<+39>)が実行したい場所と分かります。
nexti
で <+26> まで進みます。
ここまでで、アドレス [rbp-0x4] には lv=3 の式の結果で値「3」が入っています。
一応、x
コマンドを使ってアドレスの中身を確認してみます。
(gdb) x $rbp-0x4
0x7fffffffde0c: 0x00000003
ちゃんと値「3」が入ってます。
cmp は比較する命令で、結果をフラグレジスタ(EFLAGS)というレジスタに格納します。 <+44>の jne ではフラグレジスタ(EFLAGS)の中身を参照して cmp の計算結果をチェックすることでジャンプするかを判断しているので、 フラグレジスタの表示、操作を出来るようにします。
事前に eflags の中身を追跡表示できるようにしておきましょう。
(gdb) display $eflags
これで、eflags の中身が毎回表示されるようになります。 eflags は一つのレジスタで、レジスタの中の各ビットがいろんな意味を持ちます。
以下、良く見るフラグレジスタだけ記載。
ビットn番目 | 呼び方 |
---|---|
0ビット | Carry Flag CF と呼ばれる |
6ビット | Zero Flag ZF と呼ばれる |
7ビット | Sign Flag SF と呼ばれる |
gdb 上で print $eflags
や display $eflags
で以下のような
結果が表示されるかと思います。
(gdb) print $eflags
$5 = [ PF ZF IF ]
これは eflags の中身で PF、ZF、IF のフラグが立っている状態です。
参考として、ZF フラグを消す操作と、再度立てる操作を書いておきます。 ZF フラグは 6 ビット目にあるので、ビット演算でフラグを立てたり消したりしてます。
もっと良いやり方があるかもです。
(gdb) print $eflags
$5 = [ PF ZF IF ] ← ZFフラグが立っている状態
(gdb) set $eflags &= ~(1 << 6) ← 6ビット目(ZFフラグの場所)を 0 に変更
(gdb) print $eflags
$6 = [ PF IF ] ← ZFフラグが消えてる!
(gdb) set $eflags |= (1 << 6) ← 6ビット目(ZFフラグの場所)を 1 に変更
(gdb) print $eflags
$7 = [ PF ZF IF ] ← ZFフラグが立った!
jne は jump not equals の略で、「直前のcmp命令の結果、 a ≠ b ならジャンプする」ことになります。
直前の cmp が a=b という結果だった、という内容にフラグレジスタを書き換えます。
<+0> : endbr64
<+4> : push rbp
<+5> : mov rbp,rsp
<+8> : sub rsp,0x20
<+12>: mov DWORD PTR [rbp-0x14],edi
<+15>: mov QWORD PTR [rbp-0x20],rsi
<+19>: mov DWORD PTR [rbp-0x4],0x3 # int lv=3 を実行
<+26>: cmp DWORD PTR [rbp-0x4],0x64 # if ( lv == 100 ) 比較
<+30>: jne 0x555555555177 <main+46> # 比較した結果が false なら <+46> 行へジャンプ
<+32>: lea rdi,[rip+0xe98] # 文字列 "レベル100とは.." を読込
<+39>: call 0x555555555050 <puts@plt> # putsで文字列を表示
<+44>: jmp 0x555555555183 <main+58> # <+58>の行へジャンプ
<+46>: lea rdi,[rip+0xeca] # 文字列 "そなたは貧弱.." を読込
<+53>: call 0x555555555050 <puts@plt> # putsで文字列を表示
<+58>: mov eax,0x0
<+63>: leave
<+64>: ret
<+30>まで nexti などで進みます。 <+28>の cmp 命令の結果、 eflags は以下の結果となってます。
(gdb) print $eflags
$eflags = [ CF PF AF SF IF ]
<+28>の cmp 命令が a==b だった場合、ZF フラグが立った状態となってますので、ZFフラグを立てます。(ほんとはそれ以外のフラグレジスタもキレイにしてあげるのが良さそうですが、動きはするので省略)
(gdb) print $eflags
$eflags = [ CF PF AF SF IF ] ← フラグ変更前
(gdb) set $eflags |= (1 << 6)
(gdb) print $eflags
$14 = [ CF PF AF ZF SF IF ] ← ZF フラグが立った
これで nexti を実行すると、ジャンプせずに if ( lv == 100 ) {...} の 内部へ入りました。 下の通り、「レベル100とは、すごい力じゃ!!まいった!」が出力されました。
(gdb) nexti
レベル100とは、すごい力じゃ!!まいった!
0x0000555555555175 in main ()
無事にレベル100のケースが実行できました。
おしまい。
コメント、ありがとうございます。
ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。
Twitter:@NodachiSoft_jpお名前:以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください
お名前: