gdbでC言語プログラムのリターンアドレスを書き換える

提供: セキュリティ
移動: 案内検索
スポンサーリンク

ここでは、C言語プログラムのリターンアドレスgdbで書き換える例を示します。

概要

C言語のプログラムは、スタックを利用して動作しています。 スタックには、関数の引数が積まれたり、ローカル変数の記憶領域として利用されます。 プログラムは、関数を呼び出します。呼び出された関数の処理を終えたとき、呼び出し元のプログラムに制御を戻します。 制御を戻すときに、呼び出し元プログラムの関数呼び出しを行った次のプログラムから再開します。

以下の例であれば、bar()を呼び出し、bar()の処理が終わったら、bar()の次の行からプログラムを再開します。

int
main()
{
	bar();
	...
}

関数呼び出しから復帰するときに、制御を戻すポイントは、「リターンアドレス」(return address)としてスタックに積まれています。 bar()の関数の処理が終わったとき、スタックリターンアドレスを利用して、元の関数に制御を戻します。

スタックには、関数の引数やローカル変数、リターンアドレスベースポインタ(bp)が積まれます。関数ごとのデータのまとまりをスタックフレームと呼びます。 以下の表に例を示します。

スタックとリターンアドレス
スタックフレーム スタック
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 を引き起こしました。

以上がリターンアドレスgdbで書き換える例です。

関連項目




スポンサーリンク