明けましておめでとうございます。さっそく、2021年12月に開催されたSECCON CTF2021のwriteupです。CTF初心者の方にも参考にしてもらえるよう、試行錯誤した過程も含めて記事にしてみました。別記事でもアップしましたが、今年の我々のチーム(ids-TeamCC)は、総合順位で17位、国内チームでは4番目でした。我々より1つ上の16位は、国内外のCTFで常に上位成績を残しているあのレジェンドチームTokyoWesternsさんです。すごいチームと競っていますよね!と、言っても仲間がすごいだけで、私が自慢できることは1mmもありませんが。。。
さて、今回は私が担当した「case-insensitive」というタイトルの問題を自身の備忘録という意味で記録しておこうと思います。カテゴリはmiscmisc(用語集①)です。SECCONは各カテゴリで問題数がほぼ均等に出題されるので、pwn(用語集②)ばかりが目立つDEFCONと比べて、とても好感が持てます。運営さん、今後もこの方針でお願いします。
過去のwriteupからも分かりますが、大会が始まると、私は最初に閃き系の匂いがする問題を探します。CTF経験が浅くても解ける可能性がありますし、仲間も含めて一般的には避ける傾向があるので、相対的に解くチーム数が少なく正解時のポイントが高くなるメリットもあります。悩みぬいた挙句、お風呂やトイレでピコーンと閃く快感が格別で、ワンチャンですがチーム内でヒーローになれるかもしれません。ただ、問題を初見で見極めることが難しいんですよね。
そんなことばかり考えているからか分かりませんが、今回の「case-insensitive」は、初見で閃き系の問題だと直感が働きました。極めてシンプルなソースコード。問題のために作った問題だと分かる潔さ。問題作者から「あなたにはわかりますか?」と挑戦されているように感じたので、この大会の24時間を、この問題1本に捧げようと心に決めました。
どんな問題?
問題文は意訳すると「bcryptベースのsignシステム作ったよ。お前にキーを暴露できる?」みたいな感じだったと思います。よく覚えていないので違っていたらすみません。加えて、サーバーアドレスとソースコードが添付されています。サーバーアドレスにnc(用語集③)すると、標準出力に「1.sign」「2.verify」とメニューのようなものが表示されます。1を入力すると
message: AAAA (ユーザー入力) mac: $2b$05$h1NBYudNoeAMubbFr8z20xwd0vT4...略(出力)
mac(用語集④)が返ってきてメニューに戻ります。2を入力すると
mac: $2b$05$h1NBYudNoeAMubbFr8z20xwd0vT4...略(ユーザー入力) message: AAAA (ユーザー入力) True(出力)
ユーザ入力をVerify(検証)した結果をTrueかFalseで返しているっぽい。ソースコードは、このサーバープログラムで、50行足らずというシンプルなもの。bcryptが何者かを調べたら、Blowfish暗号を基盤としたパスワードハッシュ化関数らしい。レインボーテーブル攻撃に対抗してソルトを組み込んでいて、わざと計算を重くしてブルートフォース攻撃にも対抗できるのが特徴とのこと。タイトルのcase-insensitiveは、「大文字小文字を区別しない」みたいな意味らしく、問題を解くヒントになっていると思われる。
与えられたソースコードをもう少し詳しく見ると、メニュー1では、ユーザー入力(message)を促し、messageとflag文字列を結合して、ソルトとともにhashpw関数を呼んで、結果macを出力。メニュー2では、入力されたmessageとmacからcheckpw関数を呼んで、結果(True,False)を出力。ポイントは、ユーザー入力のmessageは、長さが24文字以内である必要があり、かつupper関数で必ず大文字に変換され、変換後の文字は「ord('A') > c or c >ord('Z')」という条件があること。条件に違反すると、処理が終了する。
どこから手をつけるか
まずは、出力されるmacを調べてみると、下記構造だとわかる。
平文(message + flag)のうちmessageは自明、ソルト値とハッシュ値も出力結果から自明。不明な値はflag文字列だけ。ただし、ハッシュ値から平文には戻せないから、ローカル環境でhashpw関数を試行して、ncした時に出力されたハッシュ値と一致させることができれば、flag文字列を特定できるということになる。ここから残り時間の間にflagを特定できるような切り口を見つけられるかが勝負の問題。タイトル名が何かヒントを示しているとすればupper関数の入力値であるmessageが鍵になることは間違いないのだが、ぱっと見では、切り口は見えてこない。
試行錯誤
結論から書くと、大会開始からほぼ20時間(寝る時間や私用時間含む)は袋小路を彷徨っていました。備忘録としては、この試行錯誤の過程を詳しく記しておきたいのだけれど、文章量が爆発するのでダイジェストだけ書きます。
・blowfishの有名な脆弱性を突く ⇒[結論]これといった手がかりつかめず。というか、開発から遠ざかっていることもあり、私は勝手にblowfishは昔のアルゴリズムと思い込んでいましたが、今も現役なんですね。結構ファンもいるようで驚きました。
・バージョン2aのバグを利用 bcryptには2a,2b,2x,2yといった複数のバージョンがあるらしい。過去いくつかのバグ修正を経て、最新のバージョンは2b。現在は2aや2bが主に使われているが、2aには脆弱性があるらしい。ローカル試行する際、このバージョンをいじると何か起きるかも。 ⇒[結論]サーバーで実行されるバージョンは2bなんで、ローカルでバージョン変えても意味なし
・hashpw関数は実装上、平文を72byteより後ろを切り捨てている ⇒[結論]だとしてもmessageは最大24byteなので、48byteを総当たりなんて無理でしょ
・実はフラグ文字列は短くて総当たりできる ⇒ [結論]そんな訳ない(一応やってみた)
他にも、閃きと称してクダラナイ試行をいろいろとやった気もしますが割愛。さすがに、20時間かけて進展なしで、気が滅入ります。大会終了まで残り4時間の所で、最後のチームミーティングが開催されたので参加します。ミーティングの中で、仲間の暗号専門家から「case-insensitiveどう?」と聞かれて、状況を説明します。さすがに、暗号専門家からは「その問題は、現時点で解けたチーム数も少ないので、他の問題に移った方がいいんじゃ...」とアドバイスを受けるも、謎にこの問題の魅力を力説することで、継続することになりました。ただし、貴重な仲間の戦力を、この沼で浪費したくなかったので、最後まで自分一人でいくことを宣言します。(途中、ともにリーダを分担してくれているU氏に「一緒にこの問題やろうよ~」と誘ったことは内緒)
援軍
大会終了まで残り3時間、暗号専門家の仲間からチャットが入ります。どうやらチームミーティング後、この問題を気にかけてくれていたようです。まあ、私への援軍というよりは、「bcrypt」というキーワードに関心を示しただけだと思いますが。彼は、bcryptのソースコードや、ストレッチング回数が通常12回のところが5回しか行われていない点が気になっているようです。そして、次に届いたチャットが、
def hashpw(password, salt): ~ password = password[:72] ~ あ~!これじゃない?
でした。そこは私も気になったけど、passwordの文字列は、message + flagで構成されていて、messageは最大英字24文字(24byte)のユーザー入力値で、flagはbyte数も値も不明。残り48byteの総当たりは無理。仮にユーザー入力できるmessageの長さ24文字という制限がなく、messageを71byte分入力できれば、残り1byte(flagの先頭文字)を総当たりで見つけられるので、後はmessageを1byteずつ減らしながらflagを特定できるとは思っていました。
そう思っても、ソースコードのmessageの文字列処理は、ユーザー入力値を「len(message) >24」の判定後に「message.upper()」して「ord('A') > c or c >ord('Z')」判定しているだけ。どうやったら、このコードを書き替えられるのか?と、その方法ばかり考えていましたが、暗号専門家から次に届いたチャットに衝撃を受けます。。。。
エスツェット(ß)という文字をupper()メソッドで大文字にすると、2文字のSSになります。
「え!何それ?そんなのアリ?」ここまで22時間を費やしてきた身としては、いろいろと複雑な感情が込み上げてきましたが、それは一旦飲み込んで、"ß"を使えばmessageは48byteに拡張?できる。多分、世界には似たような文字があって、きっとupper()すると3文字になる文字も存在するはず。それを見つけられれば、messageだけで最大72byteまで拡張できるので、flagを1文字ずつ総当たりすることができる!この問題がmiscカテゴリーであることや、タイトル「case-insensitive」の意味からして、この切り口で間違いないと確信。残り時間は2時間でしたが、暗号専門家とともに、最後のピースとなる文字をネット上で探し始めました。
しかし、1時間かけて探しても見つからない。。。確かにupper()で3文字になる、よく分からない言語の文字は見つけられても、upper()後の文字が「ord('A') > c or c >ord('Z')」である条件が地味にキツイ。大会終了まで残り30分を切り、すでに仲間の暗号専門家は諦めて、他の問題に取り組んでいるようでした。しかし、奇跡が起きます。諦めかけた残り18分で別の仲間からチャットが入りました。
うぉぉぉー、これですべてのパーツがそろった!まさか、他の仲間も気にかけてくれていたとは、感謝!全部、仲間がお膳立てをしてくれたおかげで、サッカーに例えるなら、無人のゴール前で私の足元に絶妙アシストが来たから、あとは右足で合わせて流し込むだけという状態!こんな歳でも脳汁が出るんですね、エンドルフィン?か何か分かりませんが。脳が高速回転しはじめ、残り15分でflagをGETするため最適解をシミュレートします。試しに、手作業でncして得たハッシュ値をもとに、ローカル環境で総当たりするとflagの最初の1文字は「S」であることが分かりました。これはフラグフォーマットである「SECCON{*****}」の最初の1文字でしょう。当たりです。後は、同様の方法でflagを1文字ずつ総当たりしていけばOK。スーパーハカー(用語集⑤)なら、Pythonで自動プログラムを組むのでしょうが、私の力量では15分では無理。手作業でncしながら都度ハッシュ値を得て、ローカル環境で総当たりする。1文字を1分で特定できれば、flag文字数次第ではサブミットまで持っていけるかも?もう無我夢中でトランス状態になって打鍵し続けます。
時計を見る余裕もなかったけれど、残り時間を確認するためパソコン上の時間に目をやると。。。。残念ながら既に14時を数分回っていました。トランス状態が解除されて、いっきに脱力感が襲ってきます。1文字を1分という見通しがそもそも甘かったか!悔しすぎる。。。。
欧文合字処理(リガチャー)って何?
upperすると「FFI」3文字になるこの文字は、リガチャーと呼ばれる欧文の合字というものらしいです。日本では殆ど知られていないけれど、海外では(欧文の世界では?デザインの世界では?)まあまあ常識らしく、複数の文字が存在するようです。なぜ、合字というものが存在するかと言うと、単に見た目がカッコイイ(元が見づらい)かららしい。最終的に「case-insensitive」を解いたチームは、世界で7チームだけだったんですが、「この問題、ヨーロッパのチームが圧倒的有利じゃね?」とか思っちゃいました。
ちなみに、これがセキュリティ技術にどう関係しているかと言うと、セキュリティ機能をバイパスする攻撃技術の一つですね。こういった技術を組み合わせて、攻撃者はWAFやIPSを潜り抜けているのでしょう。勉強になりました。
用語集
- misc: Miscellaneousの略で、CTF問題のジャンルの中で、特定のカテゴリではないその他の問題
- pwn: CTF問題のジャンルのひとつで、主にメモリー領域にアクセスをしてサーバーを攻略する問題
- nc: クライアントプロセスやサーバープロセスを起動することができるコマンド
- mac:Message Authentication Code
- SH: ハッキング能力がとても高い人


記事の著者
セキュリティ製品「秘文」Web機能のプログラム開発リーダ及び、セキュリティコンサルタント チームのリーダを経て、現在は同社ホワイトハッカーチームのマネージャ業に従事。 主な社外活動として、IPA 情報処理技術者試験・情報処理安全確保支援士試験 委員、 経済産業省 情報セキュリティ人材の育成指標等の策定事業 委員、産業構造審議会 オブザーバ、 早稲田大学 非常勤講師、情報セキュリティ大学院大学 アドバイザリーボードなどを歴任する。
関連記事
RELATED ARTICLE