著者:Arata, iwancof, iwashiira, satoki
アメリカのラスベガスで世界最大級のハッキングイベント DEF CON 32 が開催されました。このイベントでは、毎年 Capture The Flag (CTF) と呼ばれるハッキング大会の決勝戦 (Finals) が行われます。本記事では、弊社メンバーが Finals で担当した問題、各ブースの様子、そして現地での生活をお伝えします。昨年の様子は こちらの記事 からご覧いただけます。
2024年度は8月8日から11日の期間で、例年と異なる会場 Las Vegas Convention Center West Hall で開催されました。弊社社員・アルバイトは国際チーム「Blue Water」のメンバーとして Finals に出場しました。
DEF CON 32 会場 |
リチェルカセキュリティには、現役で CTF に取り組んでいる社員・アルバイトが多数在籍しています。CTF は多種多様な前提、制約、技術領域に触れる機会になり、業務の実行能力向上にも繋がります。弊社メンバーの自己研鑽の一環として、希望者全員の渡航費や宿泊費などを全額サポートしました。
DEF CON では、Village と呼ばれるブース単位でイベントが開催されています。各 Village ではそれぞれのテーマに沿ったコンテストや大小様々な CTF を開催しています。それら CTF の中で、最も古く難しいと言われるものが DEF CON 本体のイベント DEF CON CTF です。今年は、予選大会を突破した12のチームが世界各地から集まりました。
DEF CON CTF 32 |
本年度の DEF CON CTF は例年と変わらず、Nautilus Institute が運営しています。Finals では Attack&Defense (A&D) と、King of the Hill (KotH) と呼ばれる2種類の競技形式でそれぞれ問題が出題され、合計のスコアを競いました。
競技に取り組む弊社社員たち |
A&D では、各チームにセキュリティ上の問題があるシステムが配布され、それを稼働させながらお互いに攻撃と防御を行います。攻撃は、セキュリティ上の弱点を利用して他チームのシステムに侵入し、隠された情報 (フラグ) を手に入れることが目標です。防御は、自チームのシステムを調査し、システムの脆弱な箇所を特定して修正作業を行います。もちろん修正中にシステムを停止させることは許されません。
KotH では、決められた条件の中でサーバーを占有し続けたり、最も高得点のスコアを取り続け、”King”となることでポイントを獲得できます。効率的にスコアを稼ぐ方法を探さなければなりません。
今年の決勝戦では、 WASM のリバースエンジニアリングを主題とした「codewords」という KotH 形式の問題が出題されました。この問題では、まず各チームが次の2つの関数をもつプログラムを作成します。
uint64_t generate(uint32_t round_num)
codeword
はチームが任意に決めてよいuint64_t verify(uint32_t round_num, uint64_t codeword)
codeword
を引数に受け取り、正しい codeword
であれば1を、そうでなければ0を返すそして作成したプログラムをサーバーに提出すると、他のチームがverify
を呼び出せるようになります。例として、デフォルトで使用されていたプログラムを以下に示します。
#include <stdint.h>
uint64_t generate(uint32_t round_num) {
uint64_t a = 0x4142434445464748 + round_num;
return a;
}
uint64_t verify(uint32_t round_num, uint64_t codeword) {
uint64_t a = 0x4142434445464748 + round_num;
if (a != codeword) {
return 0;
}
return 1;
}
競技においては、各チームがこのプログラムをベースに攻撃と防御を行っていきます。
攻撃側は他チームのcodeword
を特定してverify
に1を返させることができれば得点を獲得できます。しかし、何も手がかりがなければcodeword
を特定することは困難です。そこで、他チームにはverify
を WASM にコンパイルした verify.wasm が公開されます。この公開された verify.wasm をリバースエンジニアリングすることで、他チームのcodeword
を特定することが目的です。したがって、攻撃側にはWASMを迅速にリバースエンジニアリングし、他チームのcodeword
を特定することが求められます。
防御側は、自分のcodeword
を特定されないようにすることが目的です。verify
は他チームに公開されますが、一方でgenerate
は公開されません。そのため、codeword
を生成するgenerate
のアルゴリズムが推測されにくくなるように verify
を作成する必要があります。ただし、generate
およびverify
が実行できる命令数には制限があります。つまり、計算量が一定以下のアルゴリズムを用いつつ、他チームに codeword を特定されないようにすることが求められます。
問題が公開された2日目以降、次のような時系列で競技が進みました。
全体として、2日目にハッシュ関数を使ったチームが登場して以降多くのチームが同様の方針をとっており、アルゴリズム面で大きな変化はなかったように感じています。以下が他チームの verify.wasm を wasm-decompile でデコンパイルしたコードです。
export function verify(round_num:int, codeword:long):long {
var h:int;
var c:int = i32_wrap_i64(codeword >> 32L);
var d:int = i32_wrap_i64(codeword);
var e:int = -559038737;
var f:int = -889275714;
var g:int = 26;
loop L_a {
f = ((e ^ (h = f) << 2) ^ (h << 1 & h << 8)) ^ d;
d = d * c ^ -1163005939;
e = h;
g = g + -1;
if (g) continue L_a;
}
return i64_extend_i32_u(
(i64_extend_i32_u(f) << 32L | i64_extend_i32_u(h)) ==
-6656796192791513755L);
}
そのほか、我々のチームでは angr を使用した自動ソルバーなどが作成されており、攻守ともにチームの個性が垣間見える、興味深い問題でした。
次に、A&D 形式の「helium」という問題について解説します。この問題の問題ファイルは競技1日目終了時に配布され、実際のサービスは競技2日目の全3時間のみ公開されていました。
配布されたバイナリは、libhydrogen というクリプトのライブラリを使って通信するプログラムでした。シンボル情報が stripされたバイナリなので、各アドレスの関数のシンボルを特定する必要があります。
関数のシンボルの特定に使用する情報は大まかに以下の通りです。
Noise_KK_hydro1
という文字列への参照があるので、 hydro_kx_kk_1()
hydro_kx_kk_1()
の中で1番目に呼ばれているので、 hydro_kx_init_state()
シンボル情報の特定は、以下のような Google スプレッドシート上で共有しながら並行して行いました。
このプログラムのコアの部分は FUN_001156ae()
に存在しました。この関数の中で呼ばれる様々な関数がインライン展開されている影響で、Ghidra でのデコンパイル結果はC言語のコードにして約56000行と膨れ上がっています。
発見した脆弱性は、FUN_001156ae()
のオフセット122069にある/proc/%s/stat
という文字列への処理にありました。この処理は、通信の暗号化に関連する処理を終えた後のコード部分に存在しています。以下の画像が、該当部分のコードです。 strstr()
で ..
という文字列が存在しているかどうかをチェックし、存在していなければ、文字列を snprintf()
で %s
部分に代入して path となる文字列を作成し、open しています。open したファイルの中身は後の処理で出力されます。
ここでの処理では..
という文字列をチェックしています。これはディレクトリトラバーサルを考慮したものと思われますが、実際には不十分です。
例えば、 /proc/self/cwd
のような文字列を path に渡せば、そのプロセスのカレントワーキングディレクトリにアクセスできます。文字列の末尾には /stat
が付加されてしまいますが、 snprintf()
はバッファから溢れる文字列を書き込まないので、うまく /stat
を溢れさせれば flag を open して中身を受け取ることができます。具体的には、/proc/self/cwd////////////flag
のように丁度溢れるくらいの /
を間に挟むことで、 flag の path を指定できます。
この問題の注目ポイントは、攻撃ペイロードの解析が不可能である点です。A&D 形式の問題では防御側の情報として、自チームのサービスに送られてきたパケットキャプチャの pcap ファイルと、各チームのホストしているサービスの Docker イメージを得ることができるようになっていました。
しかし、helium のパケットキャプチャ時に得られる通信は、鍵交換に必要な部分を除いて libhydrogen の正規の方法を用いて暗号化されています。上で解説した脆弱性は復号後のペイロードによって発火するので、通信内容を見て攻撃のメカニズムを把握したり、リプレイ攻撃を行ったりすることができないのです。
つまり、脆弱性を見つけることができていないチームは、自力で解析して見つける他には、攻撃を防御できているチームの Docker イメージのパッチ内容からしか脆弱性を特定できません。また仮に脆弱性を特定できたとしても、攻撃に繋げるまでには、前段の通信内容を暗号化するコード部分と整合する処理を行うクライアントを適切に実装する必要があります。この実装自体も、大変骨の折れる作業で、3時間で完了するのは不可能と言ってよいです。
防御に関しては、 strstr
のパース部分をどのようにパッチするか、という一点にかかっています。例えば、 /
が含まれないようにチェックするようにすれば防御することが可能です。しかし、他のチームが我々のパッチした Docker イメージをコピーして流用するだけで、我々の Exploit コード自体も防御されてしまいます。そのため、脆弱性をパッチしながらも、Docker イメージに上手いことバックドアを仕込んでおく必要があります。実際、攻撃に対する一時的な緩和策として、他チームの Docker イメージをコピーするという運用を行なっていたチームは複数ありました。バックドアが仮に仕込まれていたとしてもそのチームからの攻撃以外は防御できるはずなので、パッチを当てずに各チームから無防備に集中砲火を受けるよりはマシ、ということなのでしょう。
2日目の開始時点では、我々は strstr
にパッチを当てませんでした。また、この時1チームを除いた残りのチームに対して攻撃が刺さっていました。つまり、他のチームは脆弱性を見つけることができておらず、よく分からない通信が行われていることのみ分かっているという状況であると推定できます。仮に脆弱性を見つけてパッチされた場合でも、攻撃に転じるまでにはラグがあるでしょう。つまり、攻撃が刺さらなくなって来たタイミングで strstr
にバックドアを仕込んだパッチを適用すれば、他チームからの攻撃に対する防御が間に合うという戦略でした。また、パッチを当てて Docker イメージを更新したタイミングで、残りのチームが我々の Docker イメージをコピーするという緩和策を取るかもしれず、そのチームに対しては他のチームの攻撃が刺さらない一方で我々の攻撃は依然として成功するという状況を作り出せる、という狙いもありました。
結果的には、競技終了1時間前のタイミングでstrstr
に対するパッチを適用していました。サービスが公開されていた3時間のうち、2時間はほとんどのチームに攻撃が刺さり続けていたということです。
総括すると、攻撃ペイロードの解析が不可能であり、脆弱性を発火させるような攻撃コードの実装も大変な点から、攻撃と防御ともに難易度の高い問題でした。だからこそ、攻撃に成功していた我々のチームは点数を稼ぐことができました。しかしながら競技後に分かったことですが、 strstr
以外のコード部分へ行っていたパッチにミスがあり、幾分かの Defense ポイントを取りこぼしてしまっていたようです。攻撃ペイロードの解析が不可能であるが故に、攻撃されていることに気づかなかったこともミスが発覚しなかった原因の一つでしょう。
通信内容を暗号化するような A&D 形式の問題は、攻撃に成功した場合のリターンが大きく、取り組む価値が高いかもしれません。
heliumと同じく、A&D 形式の「cloud-cache」という問題について解説します。
この問題では、次の3つのファイルが攻撃対象となります。
jscache.ko は dmesg の取得やカーネル空間に存在するバッファの表示などの機能を提供し、entrypoint.bin は それらの関数を builtin function として QuickJS に登録しています。 攻撃者は telnet を使って QuickJS の REPL に接続することができ、そこからフラグを取得することが目的です。
この問題には数多くの脆弱性が含まれているため、防御側が忙しい問題でした。
先程も述べた通り、バイナリ中に埋め込まれた脆弱性の数が多く、中には2行でフラグを得ることができるほど自明なものもあったため、ここでは非自明であったものの内の一つを解説します。
entrypoint.bin 内で QuickJS に登録された関数のうち、jsExpand
という名前で次の関数が登録されていました。
qjs_add_global_func(ctx,"jsExpand",js_expand,2);
int js_expand(undefined8 param_1,undefined8 param_2,undefined8 param_3,int param_4,long *argv)
{
long lVar1;
int iVar2;
long in_FS_OFFSET;
int length;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
if (param_4 < 2) {
fwrite("Error: js_expand requires 2 arguments: <array> <length>\\n",1,0x38,_stderr);
length = 0;
}
else {
iVar2 = JS_ToInt32(param_1,&length,argv[2],argv[3]);
if (iVar2 == 0) {
lVar1 = *argv;
*(long *)(lVar1 + 0x40) = (long)length;
**(int **)(lVar1 + 0x20) = length;
}
else {
length = 0;
}
}
if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
return length;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
この関数の目的は、第一引数に渡された JavaScript の配列オブジェクトの長さを、第二引数に渡された整数に設定することです。しかし、バッファの長さなどのチェックを一切行わず長さを設定しているため、例えば次のようなコードを実行することでバッファオーバーフローが発生します。
let foo = [1.1, 1.1]
let bar = [2.2, 2.2, 2.2, 2.2, 2.2, 2.2]
jsExpand(foo, 20)
console.log(foo)
1.1,1.1,[unsupported type],[unsupported type],[unsupported type],[unsupported type],[unsupported type],[unsupported type],1,join,[unsupported type],1.19251584217e-313,2.2,2.2,2.2,2.2,2.2,[unsupported type],[unsupported type],\\nundefined\\n
この脆弱性の他に、フラグをファイルから読み込みメモリ上に配置する関数が用意されているため、これら2つを組み合わせることでフラグを取得することができます。
以上の脆弱性による攻撃を防ぐため、長さを代入する前に既存のバッファの長さをチェックすることでバッファオーバーフローを防ぐことができます。
ここまでは、Jeopardy 形式の問題と同じような解説でしたが、ここからは A&D 固有の戦略について解説します。
A&D では、相手が脆弱性に気が付きパッチを当ててしまうより先にその脆弱性を利用してフラグを取得することが重要です。その一方で、すべての脆弱性を相手より先に見つけるのは困難なため、自分が受けた攻撃を解析しこれ以上の失点が発生しないようにするという戦略が考えられます。
そこで、実際の競技では攻撃コードを作成したりパッチを当てたりする人以外に、ネットワークに流れるペイロードを監視し、自チームのフラグが流出していないか確認するツールが常に稼働していました。
通常の問題であれば、フラグを取得するペイロードを送信した場合、そのレスポンスとして平文のフラグがネットワークを流れるため、これを検知できました。一方、この問題では自由な JavaScript のコードを実行できるため、取得したフラグを暗号化して送信することで、フラグの流出を検知されることを防ぐことができます。 我々は、送信するフラグおよびスクリプトを難読化して攻撃することで、他チームからフラグを抜き取っていることを隠していました。
一つの問題にしては脆弱性の数が多くまたそれら脆弱性の質的な部分で疑問があったものの、Jeoparty にはない A&D 固有の戦略を取ったり、リアルタイムでの攻防があったりと、非常に新鮮で面白い問題でした。
DEF CON 32 CTF FInals は、世界2位という素晴らしい成績でした。 チームは途中まで1位を守っていましたが、惜しくも最終日に逆転されてしまいました。
投影されたスコアボード |
今年度は国際チームということもあり、各国のノウハウを結集して臨んだ Finals となりました。日本からは Reversing や Pwn ジャンルの人材が多く出場し、A&D システムの解析と攻撃 Exploit の作成に大きく貢献していました。他にも LLM を扱った問題では、不可視のユニコード文字でバックドアを作成する、ユニークな攻撃を行っていました。別の CTF では敵チームであることも多い他国のメンバーと、仲間として CTF に参加できる貴重な機会となりました。
DEF CON 32 では CTF 以外にも様々なイベントが開催されており、自由に出入りできます。興味深かった Village やイベントをいくつか紹介します。
毎日変わる会場のサイネージ |
DEF CON 初日に開催される、Registration とグッズ購入のために列に並ぶイベントのことを Linecon と呼びます。会場の端から端までを長蛇の列が埋め尽くします。今年は開場時間からすこし遅れたためか、4時間半並び続けました。
参加チケットである HUMAN BADGE |
毎年恒例の車をハックする Village で、今年は Rivian の車が鎮座しています。ECU ハックを試みることができるテーブルも設置されおり、ハッカーたちが各々侵入を試していました。同じ Village で Tesla 車が貰える CTF も開催されていました。
Car Hacking Village のハッカーたち |
物理的なセキュリティを扱う Village では、ブランクキーを加工することでオリジナルのキーを複製するコーナーが設置されていました。実際にテスト用のキーを複製し、開錠までを体験しました。
ブランクキーを加工する工具 |
DEF CON 公式のイベントではありませんが、毎年 CTF の終わりの夜に有志によるアフターパーティが開催されます。50階スイートルームを貸し切り行うパーティには、Finals に出たチームが一堂に会します。他チームのメンバーと解法を共有しながら議論できる貴重なイベントです。
窓の外の Sphere と冷やされるフリードリンク |
DEF CON CTF では、実際の業務で用いるアプリケーションの解析や Exploit の技術から、LLM のような最新のセキュリティテーマまでを広く学ぶ機会を与えてくれます。また、世界中のトップハッカーと競り合う経験は、エンジニアとして大きな成長の糧となります。これからもリチェルカセキュリティでは、社員・アルバイトの方の自己研鑽のための渡航費・宿泊費を支援する予定です。
本記事の執筆に携わったArata、iwancof、iwashiiraは、リチェルカセキュリティでパートタイマーとして働いています。当社には他にもCTFの技術を生かせる領域で若い才能にお任せしたい業務がたくさんあります。この記事を読んで当社の取り組みにご興味を持った方は、ぜひ カジュアル面談 にお申し込みください。
社員・アルバイトの昼食チャレンジ |
ラスベガスの夜景 |
DEF CON CTF に参加してくださった社員・アルバイトの方々、そして協力してくださった国内外チームのメンバーの方々、ありがとうございました!2025年の DEF CON 33 でお会いしましょう👋