9/13(金)に行われたAutomotive CTF Japanの決勝に「TeamONE」のメンバーとして参加してきた。結果は2位で、米国デトロイトで開催される「Automotive CTF 2024」の決勝に参加できることに。
参加レポート
Automotive CTF Japanはオンラインの予選があったんだけど、そちらが開催されていた期間はほとんど時間が取れず、気が付いたら全完されていて0点フィニッシュをキメた。この0点は「みんなのサポートに徹して0点」ではなく「何もしてなくて0点」なのである。
そんな予選を経て、メンバー5人が顔を合わせたのはこの日が初だった。とはいっても基本はDicsordでコミュニケーションを取って作戦会議とか勉強会とかオンラインでやってたので特にコミュニケーションに困るところはなかった。
Japanの決勝ではハードウェアを使った問題が出ると事前にアナウンスがあった。UARTやJTAG、OpenOCDなどがキーワードとして挙げられてたので、事前の準備として秋月でUARTとJTAGに使えるUSB-シリアル変換モジュールを買うなどした。この週やけに秋葉原に行っていたのはこの準備のためである。
ハードウェアが何台出てくるのかわからなかったため、各自この辺のデバイスを用意していったが、実際与えられたのは、RAMNというトヨタがオープンソースとして公開している自動車周りの学習用ハードウェアだった。ちなみにラーメンと読む模様。
ハードウェアは1台しかないので、車周りに一番詳しかったlaysakuraさんのPCにRAMNは繋いで、他のメンバーはデータをもらったりコマンド送信を依頼したりして解いていった。問題一覧を見るとわかるとおり、カテゴリRAMNの問題が大半を占めていた。このハードウェアの中にフラグに関わる諸々が詰め込まれていたのである。
スコアボードの緑の線が弊チーム。競技時間の半分かからずに1問以外全部解けて「これは優勝もらったなガハハ」と思ってたら最後の1問がずっと解けずに全完を成し遂げたieraeに抜かれて終了。終わり際は他のチームに抜かれるんじゃないかとずっとビクビクしてた。心臓に悪い。何とか逃げ切れてよかった。
車に関するCTFということで、普段やってるCTFとはかなり性質の違うもので新鮮味があり楽しかった。何であっても新しい知識を身につけて実践できると楽しいのである。問題の難易度が、自分が解法を聞いて理解できるレベルのものだったというのも満足度を上げていると思う。
予選ではいろいろよろしくない点が見受けられたが、Japan決勝の競技自体はそんなに粗も感じられず(運営に気になるところがいくつかありはした)、変な問題もなく良かったと思う。競技を楽しめたのが一番。
Automotive CTF Japan運営の皆様、そしてチームメンバーのbeaさん、laysakuraさん、kusano_kさん、hamayanhamayanさん、ありがとうございました!
Writeup
さて、以下は自分がSubmitした問題のwriteup。フラグはメモし忘れたので無し。他の問題についてはメンバーのブログを参照あれ。
[ECU A] Takeover (1000)
<日本語> 各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.
RAMNの各インタフェースをいじってCANメッセージの値を指定されたものにすればRAMNに搭載された画面にフラグが表示される。指定された値は特異なものではなく、微調整は必要だがレバーを動かしたりスイッチを入れたりするだけで設定可能な値。この問題によってどのインタフェースがどのCAN IDに対応するかを確認できるのでとても大事な入門問題。自分はSavvyCanの画面を見ながら挑戦したけど、後からRAMNの画面に値が表示されていることに気づいたのでRAMN単体でもなんとかできた。
[ECU D] UART (1000)
<日本語> フラグはECU DのLPUART1インターフェースに115200 bpsでブロードキャストされます。
<英語> This flag is broadcasted on ECU D’s LPUART1 interface @115200 bps.
ECU DがUART通信でフラグを垂れ流しているのでそれを受信する問題。ECU DのExpansion SocketのどこかにUARTがあるはず、ということでドキュメントやGitHubのレポジトリ内を探してもいい情報を見つけられず。
結局わからなかったので、まずGNDだけ接続し、USB-シリアルの繋がっているPC側でminicomを動かしたままにしながらRXのピンを各ソケットに順番に刺していった。ハードウェア的ブルートフォース。こちらからデータを送信するわけじゃないのでGNDとRXだけ接続されていればいいのだ。PA2かPA3あたりでminicom側にフラグが表示されたのでそれを入力して完了。
[ECU A] Override (1200)
<日本語> アクセルを0xFFF以上の有効なCANメッセージに強制できれば、画面の下部にフラグが表示されます。
注意:
- 正しいCRCタイプとエンディアンを特定する必要があります。
- 画面に表示されるフラグ内の空白は”_”に置き換えてください。
<英語> Flag will be displayed at the bottom of the screen if you can force the accelerator to a value higher than 0xFFF with a valid CAN message.
Note: You must identify the correct CRC type and endian. Note: Please replace blank as “_” in the displayed flag.
RAMNのインタフェースをいじっただけでは到達できない値にする問題。CANはブロードキャストで認証などない、ということでARP Spoofingのように横からCANメッセージを送りまくれば通るだろうと推測。
アクセルを変化させながら取ってもらったcandumpを見ると、アクセル(CAN ID 0x010)のデータは8バイトであることがわかり、データをざっと見ることで、最初2バイトがアクセルの値、次の2バイトがデータ送信1回ごとに1増える何らかのカウンターと推測できる。残り4バイトはランダムっぽく見えている。問題文にCRCとあったので、多分前4バイト分のCRC32だろうとあたりをつけて検証してみたところ、当たっていた。リトルエンディアンらしく、CRC32の出力と実際のデータ列は並びが逆になっている。
これでメッセージフォーマットはわかったので、アクセルの値が 0xfff
なCANメッセージを生成して投げまくればいいと以下のスクリプトを作成。バイト列からリストを生成するやり方がわからなくて愚直に書いてしまっている。スマートになりたい。もしかしてこういう時に pack
とか unpack
を使うのか。
#!/usr/bin/python3
import can
import binascii
import time
import sys
with can.Bus(interface = "socketcan", channel = "can0") as bus:
accel = [ 0x0f, 0xff, 0, 1, 2, 3, 4, 5 ]
i = 0
while True:
accel[2] = (i & 0xff00) >> 8
accel[3] = i & 0xff
crc = binascii.crc32(bytes(accel[0:4]))
accel[4] = crc & 0xff
accel[5] = (crc & 0xff00) >> 8
accel[6] = (crc & 0xff0000) >> 16
accel[7] = (crc & 0xff000000) >> 24
i = (i + 1) & 0xffff
msg = can.Message(arbitration_id = 0x010, data = accel, is_extended_id = False)
try:
bus.send(msg)
except can.CanError:
print("Send message failed")
time.sleep(0.001)
しかし、データは期待通り送信されているものの、フラグは一向に現れない。頭を抱えていたところ、ふと思い立って送信するアクセル値を 0xFFF
から 0x1000
に変えてみたところフラグが現れた。問題文には「0xFFF以上の~」と書かれていたのに、と思ってよくよく見ると、英語の方には「a value higher than 0xFFF」となっていた。以上じゃねーよ!誤訳だ!
[ECU C] Secret code (1200)
<日本語> ECU Cは秘密のCANメッセージを待っています。
注意: エンディアンに注意してください。
<英語> ECU C is waiting for a secret CAN message.
Note: Pay attention to endians.
この問題、私がフラグの表示を見たのでフラグをSubmitしたけど、解いたのは他の人だった。奪ってしまって申し訳ない。責任を取って解法を書く。
添付ファイルとして、ファームウェアの一部の関数のアセンブラとそのCコード(こちらは肝心のところはぼかされている)が与えられていた。アセンブラを読む力が衰えていたので、ChatGPTに与えたらしっかり読んでくれて特殊動作の条件を教えてくれた。
久しぶりにChatGPTを使ってみたけど賢くなってるし速くなってるしで隔世の感がある。生成AI、3日会わざれば刮目して見よ。表現変えた別のプロンプトでやってみたらデータの方の条件を出してくれなかったけど。
あとはエンディアンを考慮して、CAN IDが 0x5AA
でデータが PLS_MR_!
となっているCANメッセージを送ればフラグゲット。
[ECU B] RAM peak (2000)
<日本語> RAMにはReadMemoryByAddressサービスで読み取れるフラグがあります。フラグの長さは17文字です。
<英語> There is a flag in RAM that can be read with the ReadMemoryByAddress Service. Flag length is 17 characters.
解けなかった問題。UDSのReadMemoryByAddressメッセージでRAMを読む問題。ただし、フラグが格納されているアドレスをピンポイントで当てないと requestOutOfRange
と返ってくる。他にヒントらしいヒントがないので、いろいろなアドレスで試した。ECUに使われているチップのデータシートを読んでSRAMのアドレスをブルートフォースしたり、問題名の「peak」に惑わされて、いろいろなセグメントの終わり際のアドレスと試したり、「Flag」という文字列をアドレスにして試したり。
競技時間の半分以上をかけても結局解けずに終わってしまった。解けたieraeチームに聞いたところ、どうやらSRAMのアドレスをブルートフォースしてたら見つかったらしい。うちも同じところをブルートフォースしていたはずなんだけど、途中で根気が尽きてやめてしまったか何かしら送るデータにミスがあったか。無念。