「gdbでC言語プログラムのリターンアドレスを書き換える」の版間の差分
(ページの作成:「ここでは、C言語プログラムのリターンアドレスをgdbで書き換える例を示します。 __TOC__ == 概要 == C言語のプログラムは、...」) |
(相違点なし)
|
2014年2月17日 (月) 00:07時点における版
ここでは、C言語プログラムのリターンアドレスをgdbで書き換える例を示します。
概要
C言語のプログラムは、スタックを利用して動作しています。 スタックには、関数の引数が積まれたり、ローカル変数の記憶領域として利用されます。 プログラムは、関数を呼び出します。呼び出された関数の処理を終えたとき、呼び出し元のプログラムに制御を戻します。 制御を戻すときに、呼び出し元プログラムの関数呼び出しを行った次のプログラムから再開します。
以下の例であれば、bar()を呼び出し、bar()の処理が終わったら、bar()の次の行からプログラムを再開します。
int main() { bar(); ... }
関数呼び出しから復帰するときに、制御を戻すポイントは、「リターンアドレス」(return address)としてスタックに積まれています。 bar()の関数の処理が終わったとき、スタックのリターンアドレスを利用して、元の関数に制御を戻します。
スタックには、関数の引数やローカル変数、リターンアドレスが積まれます。以下の表に例を示します。
スタックフレーム | スタック |
---|---|
barのスタックフレーム | barのローカル領域 |
リターンアドレス | |
barの引数領域 | |
mainのスタックフレーム | mainのローカル領域 |
リターンアドレス | |
mainの引数領域 | |
_startのスタックフレーム | _start ... |
プログラムの制御を乗っ取るために、シェルコードでリターンアドレスを書き換えるといったことが行われることがあります。 ここでは、gdbを用いて、意図的にリターンアドレスを書き換えて、プログラム内の別の関数を呼び出してみます。
サンプルプログラム
今回、利用するプログラムです。
return_address1.c
/* * stack.c * Copyright (C) 2014 kaoru <kaoru@bsd> */ #include <stdio.h> void foo() { (void) puts("I'm foo"); } void bar() { char s[4] = "abc"; puts(s); } int main(int argc, char const* argv[]) { bar(); return 0; }
コンパイル
cc -ggdb return_address1.c
今回何をするのか
今回、bar()がmain()に戻るときに、main()には、戻らずに、foo()に戻るようにgdbでリターンアドレスを書き換えます。
実行例
以下の手順で、リターンアドレスを書き換えました。
% gdb -q ./a.out (gdb) b bar Breakpoint 1 at 0x80484c9: file return_address1.c, line 16. (gdb) run Starting program: /home/kaoru/a.out Breakpoint 1, bar () at return_address1.c:16 16 char s[4] = "abc"; Current language: auto; currently minimal (gdb) x s 0xbfbfd740: 0x00000000 (gdb) disassemble main Dump of assembler code for function main: 0x080484f0 <main+0>: push %ebp 0x080484f1 <main+1>: mov %esp,%ebp 0x080484f3 <main+3>: sub $0xc,%esp 0x080484f6 <main+6>: mov 0xc(%ebp),%eax 0x080484f9 <main+9>: mov 0x8(%ebp),%ecx 0x080484fc <main+12>: movl $0x0,-0x4(%ebp) 0x08048503 <main+19>: mov %ecx,-0x8(%ebp) 0x08048506 <main+22>: mov %eax,-0xc(%ebp) 0x08048509 <main+25>: call 0x80484c0 <bar> 0x0804850e <main+30>: mov $0x0,%eax 0x08048513 <main+35>: add $0xc,%esp 0x08048516 <main+38>: pop %ebp 0x08048517 <main+39>: ret End of assembler dump. (gdb) x/w s+8 0xbfbfd748: 0x0804850e (gdb) x foo 0x80484a0 <foo>: 0x83e58955 (gdb) set {int}0xbfbfd748=0x80484a0 (gdb) n 17 puts(s); (gdb) n abc 18 } (gdb) n I'm foo Program received signal SIGSEGV, Segmentation fault. 0xbfbfd7ab in ?? ()
bar()からfoo()が呼び出されていることが確認できました。
解説
ここからは、少しだけ詳しく解説します。
gdbを起動します。
% gdb -q ./a.out
bar()関数にブレイクポイントを設定します。
(gdb) b bar Breakpoint 1 at 0x80484c9: file return_address1.c, line 16.
プログラムを実行します。
(gdb) run Starting program: /home/kaoru/a.out Breakpoint 1, bar () at return_address1.c:16 16 char s[4] = "abc"; Current language: auto; currently minimal
barのブレイクポイントで停止しました。
これは、特に意味が無いですが、bar()のローカル変数 s のアドレスを確認します。 アドレスは、0xbfbfd740です。
(gdb) x s 0xbfbfd740: 0x00000000
main()関数を逆アセンブルします。
(gdb) disassemble main Dump of assembler code for function main: 0x080484f0 <main+0>: push %ebp 0x080484f1 <main+1>: mov %esp,%ebp 0x080484f3 <main+3>: sub $0xc,%esp 0x080484f6 <main+6>: mov 0xc(%ebp),%eax 0x080484f9 <main+9>: mov 0x8(%ebp),%ecx 0x080484fc <main+12>: movl $0x0,-0x4(%ebp) 0x08048503 <main+19>: mov %ecx,-0x8(%ebp) 0x08048506 <main+22>: mov %eax,-0xc(%ebp) 0x08048509 <main+25>: call 0x80484c0 <bar> 0x0804850e <main+30>: mov $0x0,%eax 0x08048513 <main+35>: add $0xc,%esp 0x08048516 <main+38>: pop %ebp 0x08048517 <main+39>: ret End of assembler dump.
bar()の呼び出しは、 0x08048509 <main+25> で行われています。 bar()が終わると 0x0804850e <main+30> に返ります。
0x08048509 <main+25>: call 0x80484c0 <bar> 0x0804850e <main+30>: mov $0x0,%eax
bar()のローカル変数 s の8byte上のアドレスの1ワード(4byte)を確認します。
(gdb) x/w s+8 0xbfbfd748: 0x0804850e
0xbfbfd748 のアドレスは、barのスタックフレームのリターンアドレスが格納されています。 main()のbar()を呼び出した次の行のアドレスは、0x0804850e と先ほど説明しました。 s+8 のアドレスに含まれる値は、0x0804850eです。つまり、bar()の処理が終わったあとに、戻るべきmain()の制御のアドレスです。
bar()の処理からmain()に戻らずに、fooを呼び出させるため、barのリターンアドレスをfooのアドレスに書き換えます。 fooのアドレスを確認します。
(gdb) x foo 0x80484a0 <foo>: 0x83e58955
fooのアドレスが 0x80484a0 とわかったので、リターンアドレスのメモリに代入します。
(gdb) set {int}0xbfbfd748=0x80484a0
これで、リターンアドレスの書き換えが完了しました。
あとは、処理を続行させましょう。 bar()のputs()が実行されました。
(gdb) n 17 puts(s); (gdb) n abc 18 }
これで、bar()の処理が終わりました。
次に実行させると、foo()が呼び出され、foo()内のputs()が呼び出されました。
(gdb) n I'm foo Program received signal SIGSEGV, Segmentation fault. 0xbfbfd7ab in ?? ()
main()に制御が戻らなかったため、プログラムが SIGSEGV を引き起こしました。