Docker Quizを力技で解く

こういうのがあった:

自社のインターンの課題だけど, 僕は今年のインターンには全く関わっていないので, 新鮮な気持ちで問題を楽しめた. みんな話題にしているのはQ7でなかなか骨がある. 全体的にCTFっぽさがある(参加したことないのでしらんけど)中で, Q7はとくにそっち方向の面白さがある.

Q7 このイメージのentrypointとして指定されているコマンドのソースコードを復元して、この問題の答えを取得せよ

ソースコードを復元して」と言われて「うーん, それホントに復元しないとダメかな?」と考えていたら「ワシも若い頃はバイナリ畑を守るバイナリアンとして村のために戦ったもんじゃ」みたいな気持ちが急激に湧いてきて, 復元せずに解いてやろう, となった.

ちなみに, 実際は途中で諦めて普通にソースコードを復元して解いてしまったので, ここに書くのは「後にして思えばこうやってたら解けてたな」というやつです. 後出しジャンケンです. あと想定された解き方と違うとはいえ, 読めばQ7の答えがわかってしまうので自分で挑戦したい人は挑戦した後で読むことをオススメします.

アセンブル

バイナリアンだとか威勢のいいことを言ったけど, 10年以上前に勉強のために実行ファイルをクラックする練習をちょっとやったことがある程度で, そのときもOllyDbgとか使っていた. アセンブリコードくらいにはなっていてもらわないと手も足も出ない.

今回の実行ファイルdocker_quizはどうやらGoで書かれていそうで, そうなるとgo tool objdumpで逆アセンブルできる. -Sというオプションもあったけど, そこまで役に立つ情報がたくさん出る感じではなかった. Goのツールでなくてもそこら辺の逆アセンブラを使ってもそこまで大差なさそう.

答え合わせしている箇所を特定

まず最初の方を見ると/usr/local/go/src/internal/cpu/cpu.goとか言っていて, これはきっとGoのランタイムの内部コードだろう. 静的リンクなのでそういうのもついてくる. 逆に一番下を見ると/app/docker_quiz.gomain.mainと言っていて, これがアプリケーションコードっぽい.

アセンブリコードの読み方もだいぶ忘れているし, 以前読んだことがあるのはCかC++からコンパイルされたもので, Goでコンパイルしたときの様子がどういう感じか全く知らないので, ガチで一字一句読むのはなるべく避けたい.

main.mainをざっくり眺めるとCALL main.printQuestions(SB)というのが目につく. きっとこれが問題を出力しているところだろう. さらに見ていくとCALL main.verify(SB)というのがある. 見るからに答え合わせしているところっぽい! この関数の中身に絞って見ていけばよさそうだ.

main.verifyはそこそこの長さだけど, 特徴的なところとしてcrypto/sha512を使っている. どうやら答え合わせはSHA-512のハッシュ値で照合してそうだ.

docker_quiz.go:96   0x4b0b0f    e82c91fdff    CALL crypto/sha512.(*digest).Sum(SB)

確かに, (問題文に書いてあるQ1やコマンドで出力されるQ2は別として)Q3~Q6の答えはバイナリファイルをstringsしても登場しないのだ. しかし, Q7の答えもSHA-512で照合してるのだとすると, たとえソースコードを復元しても答えがわからないのではないか? それともイマドキは短めの文字列ならブルートフォースでも割と現実的なのだろうか?

main.verifyをもう少し観察してみると, SHA-512を使っている辺りは後ろの方で, ここにやってくるのは条件分岐で飛んできたときだけのようだ.

docker_quiz.go:92   0x4b0975    4883fe03      CMPQ $0x3, SI
docker_quiz.go:92   0x4b0979    0f85ea000000  JNE 0x4b0a69

SI3じゃなかったときだけSHA-512で照合していそう. では逆に3だったときはどうなるのか. メモリ操作をしたりスライスを作ったりしているのを無視していくと, あった, これや!!

docker_quiz.go:106  0x4b09f2    83f3ff        XORL $-0x1, BX

-1とXORをとっている. -1というより0xFFとXORを取ると言った方がわかりやすいか. つまりQ7だけSHA-512ではなく0xFFとXORした値(ビット反転した値)で照合しているのではないか?

答えを特定

Q6までの答えはFLAG_xxxxxxxxというものが多く, Q7の答えもおそらくこの形式だろう. そうするとFLAG_の文字列をビット反転したb9b3beb8a0(16進)で始まるバイト列が実行ファイルの中に書かれているはずで, ここからさらに8バイト読めば照合先の値がわかるはず. バイナリエディタで検索すると, それっぽい箇所がある. ビンゴ!

00188510: 0100 0000 0000 0000 b9b3 beb8 a0ce c98a  ................
00188520: ca95 889d 8d00 0000 2f64 6576 2f75 7261  ......../dev/ura

あとはこれをビット反転して文字列化すればQ7の答えが得られる.

実際にはどうだったか

SHA-512で照合してそうだよね, までは上に書いたのと全く同じ流れ.

ただ「それともイマドキは短めの文字列ならブルートフォースでも割と現実的なのだろうか?」と思ったので, 時間かかるだろうからひとまずブルートフォースするコードは書いて走らせておいた. まずQ6までのSHA-512値が書いてある場所をバイナリエディタで探して, Q7のSHA-512値が書いてあるとしたらここ, というバイト列に対してブルートフォースする. FLAG_xxxxxxxxxには半角小文字英数字が入りそうで, 36^8なので3兆通りくらいある. ちなみにシングルスレッドで手元のマシンで走らせた感じだと意外とどんどん進んで, 1ヶ月くらいあれば普通に終わってそうだった. 実際にはSHA-512ではないのでぜんぶ外れる運命だけどね......

走らせてる間にSHA-512じゃない分岐の辺りを読んでみたけど, よくわからなかった. SHA-512じゃないとしたらXORとか足したり引いたりとかだろうなぁ, と思ってXORを見てもなんか関係なさそうなXORがちょいちょい出てきてよくわからん. そもそもここは3で分岐しててこれはQ3だけチェック方法が違うのでは? (とそのときは思ったけどもしそういう感じならQ4だろう.) うーん, わからん. かといってアセンブリコードをつぶさに読んでいくの, やりたくない. もういいや正攻法で解くか. という感じで諦めてしまっていた.

-1とXORしてるのは目に入っていたけど-1マジックナンバー感なくて目が滑ってしまっていた. 悔しい!