st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏

Automotive CTF 2024 Japan決勝 writeup

9/13にオンサイトで6時間半ぐらい開催されていた。チーム藤原豆腐店で参加して3位。5名までが参加できるということだったけれども、うち3名はイタリアにいた。ルール上は原則*1オンサイトでの参加ということだったが、運営のご厚意で彼らもリモート参加できることになった*2。ただ、上位2チームのみがデトロイトへの切符をつかめるということで、今年度大会については我々はUS決勝へ進めなかった。

US決勝について、ieraeとTeamONEがそれぞれ1, 2位ということで進出権を手にしていた。おめでとうございます🎉🎉🎉 ieraeは予選でも1位、今回も早々に全完して1位ということでさすがの結果だった。TeamONEも、競技開始前のチーム紹介によると全員がオンサイトで顔を合わせるのは今回が初めてということであったが、素晴らしい連携で1問を除いてすべての問題を解き、単独での2位であった。

藤原豆腐店は2問を残しての3位(また、FCCPCおよびPwn4s0n1cも同点)と完敗だった。私は現地参加ではあったものの、ハードウェアを全然知らないためにjptomoyaさんに頼り切りであまりチームに貢献できなかったのが申し訳なく、この方面の技術を身に着けたいところ。

問題について、事前に参加者向けに「ハードウェアに関連する問題も出題される予定」というアナウンスがあった通り、また同アナウンスで言及されていたように、RAMNを題材としたものが多く出題されていた。ただ、ハードウェアに関連する問題「も」出題というレベルではなく、全15問のうちRAMNの問題が12問だった。もうちょっと問題の多様性があると嬉しかった*3*4かもしれない。RAMNの配布は1チーム1台で、問題数が多くまたある意味ボトルネックになってしまう問題*5が存在するといった課題はありつつも、オンサイトならではの問題ばかりで面白かったように思う*6。いや、ハードウェアに関しては私はあまり貢献できていないので知ったかぶりだが…

さて、予選の際に懸念していた通り、グローバル予選では海外チームを含めた総合順位で4位以内に入っており、日本に居住していなければオンライン予選から直接デトロイトへ行けたはずの、藤原豆腐店とFCCPCの2チームが敗退となった。国内決勝で脱落してしまったわけだから負け惜しみではあるけれども、やはりこの理不尽に思えるルールや、頑迷な日本・グローバルの両運営による裁定*7を遺憾に思う*8気持ちは変わらない。


競技時間中に解いた問題

[xNexus 1000] CAN Bus Anomaly #1 (5 solves)

<日本語> xNexusでCAN ID 0x3B9と0x3D1のCANバス異常を追跡して、その脆弱性IDを特定してください。ハッカーが通常参照するものを適切なフラグ形式で提出してください。

xNexusアカウント - (xNexusへの接続情報)

注意: アカウントのパスワードを変更したチームは即座に失格となります。絶対に変更しないでください。

<英語> Track the CAN Bus anomaly on xNexus for CAN ids 0x3B9 and 0x3D1. Determine its vulnerability ID. Yeah, what hackers usually refer to and submit it in a proper flag format.

xNexus account - (xNexusへの接続情報)

Note: Modificiation of the account password will result in immediate disqualification of your ENTIRE team from the CTF. DO NOT MODIFY.

予選でも似たような問題が出ていたが、VicOneが提供している、脅威を可視化できるxNexusというプラットフォームへの接続情報が与えられている。前回の手順を参考にしつつ、OAT Detectionsで叩かれているAPIを使って、今年に入ってからのデータをすべて保存する。CAN IDが0x3B9, 0x3D1であるデータについて、出現する全組み合わせは次の通り。

$ python3 s.py | sort -k2 | uniq | grep 3b9
0000000081949b 000003b9
010101010101 000003b9
0f550b26430785 000003b9
4359253c8d804803 000003b9
43a38d753a404327 000003b9
7dfc4c7f740caf2b 000003b9
7e56f254fa3f427d 000003b9
$ python3 s.py | sort -k2 | uniq | grep 3d1
007d 000003d1
0bc4a17587d59e1b 000003d1
16fa63 000003d1
1baa904fbc2c3926 000003d1
9f61db7e77 000003d1
afdccd2d080f 000003d1
c0854353fca5 000003d1
d75fa83168b0207e 000003d1

雑に "00 00 00 00 81 94 9b" で検索*9したところ、CVE-2022-26269の解説記事がヒットした。「脆弱性IDを特定」するのはCVE番号がわかったのでよいとして、「ハッカーが通常参照するもの」とはなんだろうか。悩んでもういいやとCVE番号で試そうとしたところ、先にSatokiさんが試し通ったようだった。素直にCVE番号と言えばよいものを、わざわざ「ハッカーが通常参照するもの」と詩的な表現をする意図がわからない。

bh{CVE-2022-26269}

予選で出題されていた問題の中でも同名のシリーズが、フラグ形式の曖昧さでも純粋なエスパー度でも特に凶悪で、悪夢の再来かと思われた。が、言うほどでもなかった。予選の問題のおかげである程度の耐性を得ることができたのかもしれない。

このようなフラグ形式等でのどうでもいい「意地悪」によって、単にプレイヤーを困らせるだけの問題ばかりを出題するのはやめていただきたい。この一連の問題では何が問われていたのだろうか。問題文から作問者の気持ちを読み取って、求められているらしい情報を抽出する能力だろうか。CTFdへ機械的にフラグを提出するスクリプトを書く能力だろうか。

[xNexus 1000] CAN Bus Anomaly #2 (5 solves)

<日本語> おっと、誰かが車両の全シリンダーへの燃料供給を停止するCANフレームを送信しています。完全なCANフレームを提供してください。 フラグ形式の例:bh{1337#0201}

xNexusアカウント - (xNexusへの接続情報)

注意: アカウントのパスワードを変更したチームは即座に失格となります。絶対に変更しないでください。

<英語> Oh no someone is sending a CAN Frame that kills fuel to all the cylinders of the vehicle. Provide the complete CAN Frame. Flag Format: bh{1337#0201}

xNexus account - (xNexusへの接続情報)

Note: Modificiation of the account password will result in immediate disqualification of your ENTIRE team from the CTF. DO NOT MODIFY.

「誰かが車両の全シリンダーへの燃料供給を停止するCANフレームを送信しています」と言われてもなあ。またxNexusで途方もない旅をする羽目になるのだろうか、OSINTをする必要があるのだろうか、あるいはユニークなCAN IDとCANフレームの組み合わせを全部提出すべきなのだろうかと思ってしまったけれども、すでに解いたチームの情報を見てみると、開始から30分で解いていたり、あるいは#1から5分程度で解いていたりする。意外と簡単らしい。

特に期待もせず、もし情報が出てきたらいいなぐらいの気持ちで "can messages" "kill fuel" と検索してみたところ、出てきた。しかもこの 06 30 1C 00 0F A5 01 00 というパケットは今回も出現していることがわかった。これだ。

ということで提出したいが、また困ったことにフラグ形式が曖昧だ。bh{1337#0201} という例が挙げられているけれども、CANフレームの方はhexでよいとして、CAN IDの方はどういうフォーマットにすればよいのだろうか。数字しかないから10進数か16進数かわからない*10。16進数ならばアルファベットは小文字だろうか、大文字だろうか。1337 と4桁だから、答えが1-3桁ならばゼロ埋めをすべきなのかもわからない。結局のところ、以下のフラグで通った。

bh{7e0#06301c000fa50100}

こちらも、意図したものとは言えないと思うがフラグ形式での「意地悪」で困ってしまった。フラグの例示は結構だけれども、極端な例だが「CAN IDとCANフレームを # でつなげ、bh{} で囲んだものをフラグとして提出せよ。なお、CAN IDはゼロ埋めしない16進数表記で、CAN IDとCANフレームのいずれもアルファベットが出現した際は小文字で回答せよ。例: bh{12a#0102abcd}」と指示する、もしくはもう少しゆるい指示をしつつも、正規表現等である程度柔軟にフラグを受け取るようにしていただきたかった。

[RAMN, ECU A 1000] Takeover (5 solves)

<日本語> 各CANメッセージが、ブレーキ 0xF0x、アクセル 0xDDx、ステアリングホイール 0xF1x、エンジンキー 0x02、ライトスイッチ 0x01、サイドブレーキ 0x00の場合、画面の下部にフラグが表示されます。

注意:

  • 末尾のxはCANメッセージの末尾4bitは無視することを意味します。
  • このチャレンジではCRCとカウンターは無視されます。
  • 画面に表示されるフラグ内の空白は"_"に置き換えてください。

<英語> Flag will be displayed at the bottom of the screen if brake CAN sensor data is 0xF0x (x meaning last 4 bits are ignored), accelerator data is 0xDDx, steering wheel data is 0xF1x, engine key data is 0x02, lighting switch data is 0x01, and side brake data is 0x00. Note: CRCs and counters are ignored for this challenge. Note: Please replace blank as "_" in the displayed flag.

問題文の意図がやや汲み取りづらいけれども、ブレーキやアクセル、エンジンキー等のECU内部での状態(正確にはCANメッセージで表現されている状態)が、それぞれ指定されているものにできれば勝ちらしい。

ではそれらの値はどうやって確認し、また変更できるか。以下の写真はRAMNを写したもので、これを見つつ説明する。普通の状態で撮った写真がなかったのでフラグがすでに出ているが、それは御愛嬌。

まず値の変更について。スライドスイッチやつまみが基板に取り付けられているが、取り付けられている箇所に ACCEL, BRAKE, Steering といったことが書かれており、これを参照しつつ対応するつまみ等をいじることで値が変更できる。緑と白と2色で文字が表示されているけれども、白は直前のメッセージから変化した箇所を表す。

値の確認だけれども、画面上部のディスプレイを参照されたい。ディスプレイ中央部に直近のCANメッセージが表示されているほか、ディスプレイの上下にもう少しわかりやすい形で、たとえばアクセルやブレーキが何%ぐらい入力されているか等が表示されている。

ということで、指定された各要素に対応するつまみやスイッチをいじって、diffが発生した箇所を探す。その値と現在の状態が対応しているはずなので、頑張って求められている値に近づける。これをやっていくとフラグが表示された。チュートリアル的で素直な問題で面白かった。

bh{EXC3LLENT_BOOTH}

競技時間中に解けなかった問題

[RAMN, ECU C 2000] Where? (4 solves)

<日本語> CAN ID 0x0ABのメッセージのタイミングにフラグが隠されています。

注意:

  • フラグは "bh{" で始まるASCII文字列です。
  • 1分間のCANメッセージログにフラグを取得するために必要なすべてが含まれています。

<英語> There is a flag hidden in the timing of the message with CAN ID 0x0AB.

Note: the flag is an ASCII string that starts with "bh{". Note: A 1-minute CAN log has all you need to retrieve the flag.

jptomoyaさんによるダンプから、次のようなデータが得られていた。はあ、という感じ。

…
[2024-09-13 13:56:17.157] t0AB4000175A1
[2024-09-13 13:56:17.257] t0AB4000175A2
[2024-09-13 13:56:17.357] t0AB4000175A3
[2024-09-13 13:56:17.457] t0AB4000175A4
[2024-09-13 13:56:17.657] t0AB4000175A6
[2024-09-13 13:56:17.757] t0AB4000175A7
[2024-09-13 13:56:17.957] t0AB4000175A9
[2024-09-13 13:56:18.357] t0AB4000175AD
[2024-09-13 13:56:18.457] t0AB4000175AE
…

「メッセージのタイミングにフラグが隠されてい」るということだけれども、CANメッセージの方も怪しい。一見カウンターに見えるけれども、2, 3, 4 と来て 6 に飛んだり、7 から 9 に飛んだりしている。どういうことだろうか。

とりあえず、ptr-yudaiさんによって上記のログを 2024-09-13 11:57:31.067 0ab#00012a59 のような形式に変換し扱いやすくされた dump.txt を対象に、各メッセージ間のカウンターとタイムスタンプのそれぞれ差分を確認してみる。

import dateutil.parser

with open('dump.txt') as f:
    s = f.read().splitlines()

aa = []
ss = [int(x.split('#')[-1], 16) for x in s]
for a, b in zip(ss[:-1], ss[1:]):
    aa.append(b - a)
print(aa)

aa = []
ss = [dateutil.parser.parse(x.split(' ')[1]).timestamp() for x in s]
for a, b in zip(ss[:-1], ss[1:]):
    aa.append(round((b - a) * 10))
print(aa)

3, 1, 2, 4, 2, 2, 4, … と出てきたし、カウンターとタイムスタンプのいずれも結果はほとんど変わらなかった。ならばカウンターの方が扱いやすいのでそちらを使っていく。

さて、フラグの隠されたメッセージは繰り返し送信されているわけだが、これらのメッセージ間のカウンターの差分について周期はあるだろうか。適当に 3, 1, 2, 4, … で検索してみると複数見つかった。ここから、以下の77個が1周期であるとわかった。

3, 1, 2, 4, 2, 2, 4, 4, 2, 2, 1, 1, 1, 1, 2, 2, 1, 4, 3, 1, 2, 5, 1, 1, 1, 2, 1, 2, 4, 1, 3, 3, 1, 5, 1, 3, 1, 2, 4, 2, 2, 2, 1, 1, 1, 1, 2, 4, 1, 3, 1, 2, 1, 4, 1, 3, 2, 2, 1, 3, 2, 2, 1, 1, 3, 1, 2, 2, 1, 1, 1, 1, 2, 1, 3, 1, 1

では、これらの数値はどのような意味を持つのだろうか。5進数, ハフマン符号, …色々メンバーで考えたりローカルで色々な組み合わせをブルートフォースしたりしたがわからない。問題文等にはまったくもって有用な情報はない。こうして何時間も悩んでいるうちに、競技が終了してしまった。


懇親会で他チームの方に聞いたところ、これらの数値は次に 1 が現れるまでのビット数 + 1を表しているということだった。フラグは bh{ から始まるということだけれども、これは2進数で 01100010 01101000 01111011 だ。1 が登場する間隔は0ビット, 3ビット, 2ビット, 0ビット, 1ビット, 4ビット, 0ビット, 0ビット, …と続いていく。それぞれ1を足した 1, 4, 3, 1, 2, 5, 1, 1 で検索すると、先程の周期のひとかたまりの中に見つかった。なるほどなあ。

これをもとに周期の始点を修正しつつ、以下のスクリプトを書く。

let s = `1, 4, 3, 1, 2, 5, 1, 1, 1, 2, 1, 2, 4, 1, 3, 3, 1, 5, 1, 3, 1, 2, 4, 2, 2, 2, 1, 1, 1, 1, 2, 4, 1, 3, 1, 2, 1, 4, 1, 3, 2, 2, 1, 3, 2, 2, 1, 1, 3, 1, 2, 2, 1, 1, 1, 1, 2, 1, 3, 1, 1, 3, 1, 2, 4, 2, 2, 4, 4, 2, 2, 1, 1, 1, 1, 2, 2`;
s = '01' + s.split(', ').map(x => '0'.repeat(parseInt(x, 10) - 1)).join('1');
console.log(String.fromCharCode(...s.match(/.{8}/g).map(x => parseInt(x, 2))));

実行するとフラグが得られた。はあ。

bh{FL3E_Flees_g4TE}

Steganography というタグが付いていることから、まず最初にこの問題の質を推し量れたけれども、それはそれとしてほかのすべてのチームが解いている中でこれを解けなかったのは痛恨の極み。色々アイデアは出ていたし答えにある程度迫れていたようには思うけれども、最後のアイデアが出てこなかったのは痛い。帰りたいとポストしていたのはこれに苦しんでのことだった。

新規性があり絶対に面白い問題だと言える自信がある*11とか、解く流れや使えるツールを書いたガイドを付けて、初心者向けにCTFではこういった問題も出うると示す目的があるとか、ジョークで開催するCTFだとかいった事情がない限り、ステガノグラフィー問題は出すべきではないと思う。

*1:わざわざ原則と書くぐらいなら例外があるのだろうと思ってしまうが、ルール上は特にリモート参加が認められる条件を示しているような但し書きはなかった

*2:向こうの時間では深夜2時開始ということで大変そうだったけれども、それは仕方のないこと

*3:RAMNカテゴリ内の問題は多様だったけれども

*4:予選のように「多様」な問題は嫌だが

*5:RAMのどこかにあるフラグの読み取りやUDSのサービスの発見のためにブルートフォースが必要とされる等、取り組み始めると同時に色々と試せない問題がいくつもあった

*6:このwriteupで書いた問題はTakeover以外がアレなので信用できないかもしれない

*7:なお、懇親会の際に日本運営の方等とお話しし、次回以降もし開催される場合はルールについては検討するという回答をいただき、ルールに存在する問題は伝わっていると理解した。今回でなく次回以降か、と思うものの

*8:このルールはイカンなあとも思うし、このような裁定を下した経緯如何? と運営に聞きたい

*9:これ以外のデータも検索したし、スペースを入れずに詰めたり、\x00\x00…のようにエスケープしたりも試した

*10:悪いことに、予選では10進数でCAN IDを答える問題があった

*11:たとえば、TSG CTFのHarekazeは面白かった