C言語でリターンアドレスを書き換える
C言語プログラムで関数のリターンアドレスを書き換え、意図的に他の関数に処理を移すサンプルコードを紹介します。
概要
C言語のプログラムは、スタックにリターンアドレスが積まれています。
関数を呼び出すときに、スタックに関数の引数、リターンアドレス、ベースポインタなどが積まれます。 関数の処理を終えたとき、その関数の呼び出し元に制御を移します。呼び出し元のプログラムを再開するアドレスは、リターンアドレスとしてスタックに保存されています。
今回、C言語プログラムで意図的に、リターンアドレスを書き換え、任意のコードを実行するサンプルプログラムを紹介します。 このドキュメントは、バッファオーバーランの理解するための1つの参考情報です。
やること
下記のコードを用意します。
void foo() { ...; } void bar() { ...; } int main () { bar(); }
main()がbar()を呼び出すだけのプログラムです。 bar()でリターンアドレスを書き換え、foo()に制御を移す、というのを実験的にやってみます。
ソースコード
rewrite_return_address1.c
/* * stack.c * Copyright (C) 2014 kaoru <kaoru@bsd> */ #include <stdio.h> void foo() { (void) puts("I'm foo"); } void bar() { void *p; *((&p) + 2)= &foo; } int main(int argc, char const* argv[]) { bar(); return 0; }
コンパイル
cc -ggdb rewrite_return_address1.c
実行例
意図通り、foo()が実行され、Segmentation faultが引き起こされました。 main()に戻らずに、強引に foo()を実行したからです。
% ./a.out I'm foo Segmentation fault
解説
リターンアドレスの書き換えは、bar()のコードで行っています。 自動変数 ポインタp から2つ上がリターンアドレスです。
void bar() { void *p; *((&p) + 2)= &foo; }
&p でpのアドレスを表し、pのアドレスを+2にしたアドレスにfooのアドレスを書き込むため、 *((&p) +2) という書式になります。
リターンアドレスは、スタック上の自動変数 p のアドレスから2つ先にあります。
スタック |
---|
自動変数 p |
ベースポインタ |
リターンアドレス |
gdbで確認するとfoo()のアドレスは、 0x80484a0 です。
(gdb) p foo $3 = {void ()} 0x80484a0 <foo>
(gdb) disassemble bar Dump of assembler code for function bar: 0x080484c0 <bar+0>: push %ebp 0x080484c1 <bar+1>: mov %esp,%ebp 0x080484c3 <bar+3>: push %eax 0x080484c4 <bar+4>: lea 0x80484a0,%eax 0x080484ca <bar+10>: mov %eax,0x4(%ebp) 0x080484cd <bar+13>: add $0x4,%esp 0x080484d0 <bar+16>: pop %ebp 0x080484d1 <bar+17>: ret End of assembler dump.
ここで fooのアドレスをレジスタ eax に代入し、eaxの内容をベースポインタ(ebp)の4byteずらしたリターンアドレスの領域に書き込みます(mov)。
0x080484c4 <bar+4>: lea 0x80484a0,%eax 0x080484ca <bar+10>: mov %eax,0x4(%ebp)
自動変数を利用して、リターンアドレスの書き換えができました。
関連項目
ツイート