Nodachisoft Nodachi Sword Icon
  
@あまじ✎ 2020年7月31日に更新

gdb でコード解析、デバッグするときによく使うコマンドまとめ

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記法へ
Ctrl+xCtrl+a layout src、asm、regs で表示してる部分(TUIレイアウト)を消したり表示したり切替できる
print [式] p [式] 式の結果を表示する
x [アドレス] x [アドレス] 指定したアドレスの中身を表示する
display 指定したレジスタ等の中身を追跡表示する
undisplay display の指定を解除

実行コードの制御(プログラム例)

たとえば下のような C言語(C++、GOでもOKです)のソースコードがコンパイル されていたとします。

 
gamesample.c
#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) を実行した結果。

 
Linux上でコンパイル&実行結果
$ gcc gamesample.c -o a.out
$ ./a.out
そなたは貧弱じゃのぅ。
~You are Dead.~

gdb でデバッグ開始。(gdb)と最後にでたら入力できるようになる

gdb実行
$ gdb a.outGNU gdb (Ubuntu 9.1-0ubuntu1) 9.1
   : 起動ログ省略
Reading symbols from a.out...
(No debugging symbols found in a.out)
(gdb)

main 関数内の処理を見たいので、ブレークポイントを main 関数の頭に貼り付ける

gdbでbreak
(gdb) break main
Breakpoint 1 at 0x1149

これで main の場所にブレークポイントが貼られました。 gdb 上でプログラムを実行するとき、このブレークポイントを貼った場所で 止まります。

では、ブレークポイントを貼った場所(main関数入口)までプログラムを実行します。

gdbでbreak
(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行目の「=>」が表示されてる行が現在プログラムを実行中の位置です。

ではでは、実際にプログラムの実行を進めていきますが、その前に。

実際に現在どこを実行しているのか確認しながら、コマンドを打ちたいので、

layout
(gdb) layout asm

で以下のように、gdb コマンド入力欄とアセンブラの表示を上下に表示するように しておきます。

gdb_layout_asm

以降、nexti コマンドで次の行に実行をすすめてみます。 一回 nexti するごとに、実行行が次に移動していくのが分かります。

nexti

nexti や stepi の "i" は instruction (意味:命令)のこと。

では、逆アセンブルした結果を眺めてコメント付きにしてみます。 Linux Intel x86-64bit です。

アセンブリコードの右側に簡単な解説を追記しておきます。

main関数の逆アセンブル結果確認
<+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でアドレスの中身確認
(gdb) x $rbp-0x4
0x7fffffffde0c: 0x00000003

ちゃんと値「3」が入ってます。

cmp は比較する命令で、結果をフラグレジスタ(EFLAGS)というレジスタに格納します。 <+44>の jne ではフラグレジスタ(EFLAGS)の中身を参照して cmp の計算結果をチェックすることでジャンプするかを判断しているので、 フラグレジスタの表示、操作を出来るようにします。

フラグレジスタの表示、見方

事前に eflags の中身を追跡表示できるようにしておきましょう。

eflags
(gdb) display $eflags

これで、eflags の中身が毎回表示されるようになります。 eflags は一つのレジスタで、レジスタの中の各ビットがいろんな意味を持ちます。

以下、良く見るフラグレジスタだけ記載。

ビットn番目 呼び方
0ビット Carry Flag
CF と呼ばれる
6ビット Zero Flag
ZF と呼ばれる
7ビット Sign Flag
SF と呼ばれる

gdb 上で print $eflagsdisplay $eflags で以下のような 結果が表示されるかと思います。

(gdb) print $eflags
$5 = [ PF ZF IF ]

これは eflags の中身で PF、ZF、IF のフラグが立っている状態です。

フラグレジスタの消し方、立て方

参考として、ZF フラグを消す操作と、再度立てる操作を書いておきます。 ZF フラグは 6 ビット目にあるので、ビット演算でフラグを立てたり消したりしてます。

もっと良いやり方があるかもです。

ZFフラグを消したり立てたり
(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 という結果だった、という内容にフラグレジスタを書き換えます。

main関数の逆アセンブル結果確認
<+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
(gdb) print $eflags
$eflags = [ CF PF AF SF IF ]

<+28>の cmp 命令が a==b だった場合、ZF フラグが立った状態となってますので、ZFフラグを立てます。(ほんとはそれ以外のフラグレジスタもキレイにしてあげるのが良さそうですが、動きはするので省略)

gdb
(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
(gdb) nexti
レベル100とは、すごい力じゃ!!まいった!
0x0000555555555175 in main ()

無事にレベル100のケースが実行できました。

おしまい。

参考

変更履歴

  • 2020/07/31 初版公開
 
 
送信しました!

コメント、ありがとうございます。

なんかエラーでした

ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。

Twitter:@NodachiSoft_jp
お名前:
 
連絡先:
 
メッセージ:
 
戻る
内容の確認!

以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください

お名前:
 
連絡先:
 
メッセージ:
 
Roboto からの操作ではないという確認のため確認キーを入れてください。
確認キー=95
戻る
 / 
送信確認へ
コメント欄
コメント送信確認へ

関連ありそうな記事(1件)です!

python で素数を計算する

#Python#コードスニペット#CTF✎ 2020-08-04
python で素数を計算する
広告領域
追従 広告領域
目次
gdb でコード解析、デバッグするときによく使うコマンドまとめ
gdb でコード解析、デバッグするときによく使うコマンドまとめ
実行可能ファイルの解析
実行可能ファイルの解析
実行コードの制御(プログラム例)
実行コードの制御(プログラム例)
レジスタ書き換えでの制御
レジスタ書き換えでの制御
フラグレジスタの表示、見方
フラグレジスタの表示、見方
フラグレジスタの消し方、立て方
フラグレジスタの消し方、立て方
ジャンプ条件判断
ジャンプ条件判断
参考
参考
変更履歴
変更履歴
Nodachisoft © 2020