DEFCON CTF Qual 2016 / xkcd : writeup

 

そういえば、この間DEFCON予選で一つだけ僕らのチームが解けた問題があったので、そのwriteupを書けるうちに書いておきたいと思う。

 

[DEFCON CTF Qual 2016 / xkcd  : writeup]

問題の構成 ->>

問題名、IPアドレス、ポート番号、「read comic」という文章が書かれた問題文、

接続先にはflagが書かれたファイル(もちろんそう簡単に開けない)とflagファイルの中身を読むことのできる脆弱性を含んだ実行可能ファイル「xkcd」の二つ。

取り敢えず普通に実行してみると、「Could not open the flag.」と表示されるだけでプログラムが終了してしまう。

gdb-pedaを用いて処理の動きを見てみると、fopen64という関数に0x487de6(flag)と0x487de4(r)という二つの引数が渡されており、まず最初に読み取り専用でflagという名前のファイルを開いていることがわかる。なので、ここはxkcdのお望み通り、xkcdと同じディレクトリに「flag」という名前のファイルを作成してあげる。

    echo flag > flag

その後再度実行してみると、どうやら文字入力を受け付けているようだった。

適当にaaaなどと文字を入力してみると「MALFORMED REQUEST(それは違うよ!)」と言われ、プログラムが終了した。

再びgdb-pedaを用いて処理の動きを見て行く。

call命令によってxkcdに使用された関数の一覧 ->>

▶setvbuf:0x6b3f80,0,2,0
0x6b3f80をポインタとして、入力操作が行われる度にファイルから直接読み込み直ちにファイルに書き込まれるという設定をしている。

▶setvbuf:0x6b41c0,0,2,0

0x6b41c0をポインタとして、入力操作が行われる度にファイルから直接読み込み直ちにファイルに書き込まれるという設定をしている。

▶bzero:0x6b7540,0x100

0x6b7540から始まる0x100バイトを0で埋める。 -> 0x6b7540から0x6b7640までが全て0。

▶fopen64:0x487de6(flag),0x487de4(r)

冒頭で紹介した関数。読み取り専用でflagという名前のファイルを開く

▶fread:0x6b7540,0x1,0x100,0x6babd0

0x6babd0をポインタとするファイルから1バイトのデータを100個読み込み、0x6b7540に格納する。

▶fgetln:0x6b41c0(IO_stdin),0x7fffffffe400,0x7fffffffe400

0x6b41c0(IO_stdin)をポインタとする場所、つまり標準入力から一行読み込み、そのポインタを返す。文字数は0x7fffffffe400に格納。

▶strtok:0x6bae10(入力文字列),0x487e04(?)

入力した文字列を?で分割する -> 入力文字列に「?」は必須

▶call 0x4002d0:0x6bae10(入力文字列の?の前),0x487e06(SERVER, ARE YOU STILL THERE)

0x4002d0がどのような処理をしているかわからないのでブレークポイントを刺してステップイン実行して潜ってみる
strcmpと書いてある。文字列を比較しているのか?
pxor:xorして第一オペランドに格納
pcmpeqb:やっぱり比較だ。
0x408260(puts):"MALFORMED REQUEST"
つまり、入力文字列の「?」より前と「SERVER, ARE YOU STILL THERE」が一致しなければ「MALFORMED REQUEST」を表示させてからプログラムを終了する、という処理。 -> 入力文字列は「SERVER, ARE YOU STILL THERE?」まで確定

▶strtok:0x0,「"」

「"」で分割するのはわかるが、0x0ってなんだろう? -> ブレークポイントを刺しステップイン実行 -> 途中に「?」より後の入力文字列を一文字ずつずらして「"」と比較している処理がある。 -> 入力文字列に「"」が含まれることが確定

▶call 0x4002d0:(入力文字列)," IF SO, REPLY "

関数の名前がわからないのでどのような処理をしているか正確にはわからないが、引数を見るにまた文字列の比較をしているのだろう。
一応ブレークポイントを刺してステップイン実行で詳細を確認してみる -> strcmpと書いてあるのでやはり文字列比較だろう。 -> 入力文字列の中に「 IF SO, REPLY 」が含まれることが確定。

▶strtok:0x0,「"」

先ほどと全く同じ処理。これで何らかの文字列が「"」で囲まれることが確定した。

▶strlen:(入力文字列)

入力文字列の文字数を計測

▶memcpy:0x6b7340,0x6bae3b(入力文字列)

入力文字列を0x6b7340のメモリにコピー。

▶strtok:0x0,「(」

先ほどのstrtok:0x0,「"」の比較対象が「"」から「(」に変わっただけ。

▶strtok:0x0,「)」

何らかの文字列が()で囲まれることが確定。

▶sscanf:(入力文字列),「%d LETTERS」

scanfの入力がキーボード入力からではなく文字列からになったバージョン。入力の際に書式指定もされる。
%dの部分になんらかの数値が入った、「%d LETTERS」を入力することが確定。

▶strlen:(入力文字列)

再び入力文字列の文字数を計測

▶puts:(入力文字列)

入力文字列を表示

▶exit:0xffffffff

プログラム終了

 

以上の結果から、入力が確定したものをまとめてみると

 

「SERVER, ARE YOU STILL THERE?」

「 IF SO, REPLY 」

「"○○○"」
「(□□□)」
「%d LETTERS」

 

処理の順番や英語の文脈的に考えると妥当なのは

「SERVER, ARE YOU STILL THERE? IF SO, REPLY "○○○" (%d LETTERS)」

だろうか。

"○○○"に入るのは「REPLY」つまり「返事」である。それはつまり、あちら(サーバサイド)からの出力をこちらで指定することができるということを表している。

そこで僕は「どうにかしてflagをREPLYとして出力させることはできないか」と考えた。そこで、ダブルクォーテーションの中や%dの値を色々と変えて送信していると、%dに入っている数値だけ、ダブルクォーテーションで囲まれた文字が表示されていることに気付いた。そこで僕は、文字数を本来よりも多く指定することで、本来の領域を超えたものもまとめて巻き込んで指定することができるのではないか、と考えた。

この脆弱性のことを、「HEARTBLEED脆弱性」という。

ちなみに、

1.問題名「xkcd」

2.ポート番号「1354」

3.「read comic」という問題文
これら3つの情報を元に「xkcd 1354 comic」などとggってみると、この脆弱性を漫画で説明したサイト(xkcd: Heartbleed Explanation)が出てくる。

つまり、「%d LETTERS」の%dには、ダブルクォーテーションで囲んだ文字列の文字数よりも大きな数値を指定すればよいことがわかる。

次に、ダブルクォーテーションで囲まれた○○○には何を入れればよいのかを考えた。%dのところで過大な数値を指定してしまえばここは関係ないのではないか、と思い、最初はaaaなどとしていたのだが、どうやら、例の漫画とは違いaaaで区切られてしまい、%dで過大な数値を指定しても意味がなかった。

おいおい、漫画と違うじゃないか。

まずは、どうして区切られてしまうのかを考えよう。

考えられるものとしては、文字列の終端に必ず付加される"NULL文字"「\0」の存在。「\0」を認識してそこで区切るようになっているならば、漫画と同じ手法は使えない。

...と思ったのだが、この「\0」を消す方法が一つあった。それは、入力可能な領域を限界まで入力してしまえば、終端に「\0」が入る余地がなくなる、というものだ。
例えば5文字入力可能な領域に3文字のaを入力した場合、入力結果は自動的に「aaa\0」となる。この「\0」が「文字列の終了ですよ」ということを表しており、今回の問題ではこれがとても邪魔なのである。
そこで、この「\0」を消すために、5文字入力可能な領域にMAXである5文字のaを入力してみよう。すると、

 

【「aaaaa」\0 】-> 「aaaaa」

 

となり、終端の「\0」が入る余地がなくなってしまうのである。

ここで、memcpyで入力文字列が読み込まれたアドレスとfreadでflagファイルの中身が読み込まれたアドレスを見比べてみよう。

▶memcpy:0x6b7340,0x6bae3b(入力文字列)

入力文字列を0x6b7340のメモリにコピー。

▶fread:0x6b7540,0x1,0x100,0x6babd0

0x6babd0をポインタとするファイルから1バイトのデータを100個読み込み、0x6b7540に格納する。

そう、今回は偶然なことに、

入力文字列が格納されるアドレスは
「0x6b7340」
flagファイルの中身が読み込まれるアドレスは
「0x6b7540」
と、とても近いのである。

それでは、ここでの「入力可能な限界の文字数」はここではいくつになるのだろうか

【 0x6b7540 - 0x6b7340 == 0x200 】

なので、16進数で200、つまり、10進数で512バイト、すなわち512文字となる。

この問題のメモリの状況をわかりやすく書くとこうなる。

「(memcpy領域)(flag領域)」

ここに、入力文字列が格納される領域、つまりmemcpy領域に3文字のaを入力すると

「(aaa\0)(flag領域)」

となり、「\0」の存在のせいで区切られてしまいaaaまでしか出力されないが、MAXである512文字のaを入力すると、

「(aaa...aaa)(flag領域)」 -> 「(aaa...aaaflag領域)」

となり、\0が消えたことによって「文字列の終端ですよ」ということがわからなくなり、flag領域にある文字列までひとくくりにされてしまうのである。

こうすることで、「REPLY」としてflagファイルの中身も出力させることができるようになった。

まとめると、最終的に入力すれば良い文字列は以下のようになる。

 

SERVER, ARE YOU STILL THERE? IF SO, REPLY "aaaaaaaaaaaaaaa(中略)aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" (547 LETTERS)

 

あまりにも長すぎるため中略してるが、aを512文字入力してください。

aを512文字入力するにはpythonを開きprint("a"*512)などと入力して出てきたものをコピーするとよいでしょう。

 

次のように、pythonとパイプを用いて一気に送信する方法もある。

 

python -c 'print("SERVER, ARE YOU STILL THERE? IF SO, REPLY " + "\"" + "a"*512 + "\"" + " (547 LETTERS)")' | ./xkcd

 

これを入力することで、

 

aaaaaaaaaaa(中略)aaaaaaaaaaaaThe flag is: bl33ding h34rt5 l1b3ral

MALFORMED REQUEST

 

というように、flagファイルの中身が読み込まれた領域まで巻き込んで出力させることができた。

 

以上。