SECCON CTF 2022 Finals writeup

Domesticの方にいた。
いろいろあって書けてなくて今更感あるけどけじめとして。

チーム内で分担してて、私はJeopardy担当だったのでKing of the Hillの方は見ておらず何もわからない。
Jeopardyでは2問解いた。

[reversing] whisky (100pt)

Do you like whisky(※本番ではURLがリンクされてた)?
Read /flag.txt to get the flag!

backdoor_plugin.so 77a3acac658f6f5bba266bee6f7707d80959fab2

ファイルは以下から。

問題文のリンク先に行くとWikipediaのウイスキーの画像が貼られているだけ。

配布されたファイルをGhidraで読み込んでみると、Exportsにbackdoorという関数シンボルがある。

backdoor関数の中身を見てみると、uwsgi_response_add_header関数を呼んでいることがわかる。

どうやらこのファイルはuWSGIのプラグインモジュールらしいことがわかった。

さて、backdoor関数ではBackdoorヘッダを追加しているが、そのようなヘッダは先のウイスキー画像が返ってきたときにはついていない。
ということは何らかの条件でこのbackdoor関数が呼ばれると推測できるため、その条件を解明して満たしてやればよさそうである。
backdoor関数の呼び出し元を探ると、uwsgi_backdoor_request関数から呼ばれている。
Wikipediaのウイスキー画像への参照も出力していることもあり、この関数がリクエスト処理を行うメインの関数と思われる。

backdoor関数が呼ばれるには3つの条件を満たす必要がある。

  1. HTTP_BACKDOOR変数が”enabled”であること
  2. *(short *)(param_1 + 0x1d8)0x10であること
  3. *(long *)(param_1 + 0xc0)0でないこと

2.と3.はわからないのでひとまず置いておいて、1.について考える。
HTTP_BACKDOOR変数がどこから来ているか。
この変数の値はuwsgi_get_var関数で取得されているが、この関数に限らずuWSGIの関数はリファレンスが無い。
仕方がないのでぐぐったりuWSGIのGitHubレポジトリを検索したりしていると、HTTP_BACKDOORに相当する部分にREMOTE_ADDRとかHTTP_COOKIEとかSERVER_PORTとかが見えてくる。
ああ、これはCGIなどでの環境変数と一緒だなとわかるので、1.の条件はリクエストにBACKDOORヘッダを"enabled"としてセットすれば満たせるとわかった。

続いて2.と3.を考える。
まずparam_1の型は、uwsgi_get_var関数の宣言などからstruct wsgi_request *であるとわかる。
wsgi_requestのメンバーはソースコードを見ればわかる
オフセットを手計算で求めていくこともできるが数が多くて絶対ミスする自信があるのでプログラムでやった。

#define PRINTMEMBER(PARAM) printf("%04lx: %s\n", (long)&r.PARAM - (long)&r, #PARAM)

int main(int argc, char **argv) {
    struct wsgi_request r;

    PRINTMEMBER(fd);
    PRINTMEMBER(uh);
    PRINTMEMBER(app_id);
    PRINTMEMBER(dynamic);
    PRINTMEMBER(parsed);

... snip ...

条件分岐とその後に使われているオフセットに対応するメンバーを抜き出す。

00c0: uri
00c8: uri_len
01d0: authorization
01d8: authorization_len

以上を踏まえて条件を書き直したものがこちら。

  1. Backdoorヘッダが"enabled"
  2. Authorizationヘッダが16バイトの文字列
  3. リクエストのパス部分が空文字列ではない

backdoor関数に戻って中身を見てみる。
第2引数にはリクエストのパス部分、第3引数にはAuthorizationヘッダの値が渡されることを踏まえて読み解いていくと、「リクエストのパス部分で指定されたファイルを読み込み、Authorizationヘッダので指定された値を鍵としてAES-128-ECBで暗号化した結果をレスポンスのBackdoorヘッダにHexでセットする」とわかる。

問題は/flag.txtを読むことなので、以下の画像のようなリクエストを送りレスポンスのBackdoorヘッダの値を自分で指定した鍵で復号してやればよい。
ヘッダをいじって送信するのは特別なツールを使わなくてもFirefoxの開発ツールでできる。

Flag: SECCON{Which_do_you_prefer:Whisky_Beer_Wine_Sake}

私は日本酒が好きです。
ちなみにこの問題はFirst blood取った。うれしい。他のチームはKing of the Hillとか点数高い問題を取りに行ってただけだろ。

[reversing] Paper House (250pt)

The Professor has successfully leaked the schematic and firmware of the safebox in the Paper House.
Can you crack the password to open the vault door?

The flag is SECCON{}.
i.e. If “1->2->A->B” is the key, the flag is “SECCON{12AB}”.

paper_house.tar.gz 41aa3ec1be4eb7bcf338cdd7ed83e56c559827ab

与えられたのは回路図とuf2ファイル。

回路図の右側にあるのがRaspberry Pi Pico Hで、uf2ファイルはそのファームウェア。
PicoのGPIOにキーマトリクス方式のキーパッドとスピーカーとドア制御システムが接続されている。
問題文から推察するに、キーパッドでパスワードを入力し、正解だとドア制御システムが稼働してドアが開く仕組み。

なんでも知ってるGoogleさんにより、同梱のファイルsafe.uf2はRaspberry Pi Picoのファームウェアファイルと判明。
uf2ファイルの仕様などはMicrosoftがGitHubのレポジトリに公開している。
仕様を読むと、uf2ファイルの中にはファームウェアが細切れになって入っているとのこと。
レポジトリ内のuf2conv.pyで中のファームウェアを取り出すことができるので、これを使ってsafe.uf2からsafe.binを取り出す。
また、uf2conv.pyでuf2ファイルの情報を確認することができる。

$ python3 uf2conf.py -i safe.uf2
--- UF2 File Header Info ---
Family ID is RP2040, hex value is 0xe48bff56
Target Address is 0x10000000
All block flag values consistent, 0x2000
----------------------------

RP2040はRaspberry Pi PicoのCPUで、ARM-Cortex/32bit/Little Endian。これとオフセットが0x10000000であることを使ってsafe.binをGhidraで読み込む。

ここまではよかったのだが、出てきたコードを見ても何が何やらわからない。どこがメインロジックかもわからない。
これを頑張って読み解こうとしたり、何かもっと解読しやすい形式に変換できたりしないか調べているうちにわからないまま1日目が終了。

今年のSECCONの会場は浅草橋である。
浅草橋は秋葉原から一駅である。
行きは秋葉原から歩いてきて、帰りは秋葉原まで歩いて電車に乗って帰るのである。
秋葉原といえば秋月があるのである。
というわけで。

帰りに秋月によって買ってきましたRaspberry Pi Pico。
Getting started with Raspberry Pi Picoを見つつサンプルをビルドしてblinkを動かしてLチカしてみたあたりで、「サンプルのコードとバイナリを見比べればバイナリ読み解けるかも」と気づく。

サンプルをビルドしたことにより、Cのソースコードとそれをコンパイル・リンクしたELFファイル、ファームウェアのバイナリファイルが手元にあることになり、それぞれを見比べることでバイナリのどの部分が何をしているかを読み解けるようになった。
これを足掛かりに調べていくとsafe.binの中身が大体読めるようになった。

使われているキーパッドはこれだが、秋月には在庫がなかった。
秋月のサイトに回路図があるので見てみると、キーマトリクススイッチになっている。

COLを1本0にしてROWを読む、で0にした列のどのスイッチがOnになってるかがわかる仕組み。
これを全部のCOLで順番にやることで全部のスイッチの状態を取得する。

[振り返り時の気づき]
…はずなんだけど、そのようなコードが見当たらない。
ちゃんと動くんだろうかこれ。
当日はそこまで読み込まず、「この関数でスイッチの状態取ってるんだろう」という推測で動いてたのでまったく気づいてなかった。
[/振り返り時の気づき]

入力キーが内部の値(0x00xF)と一致してなくて、シャッフルされていること、入力列のチェックが単純な比較ではなくちょっと処理が加わっていることに気をつけて正解となる入力列を求めると86EDAB934986A125となる。

Flag: SECCON{86EDAB934986A125}

[misc] Sniffer 1 (200pt), Sniffer 2 (100pt)

PC 2台が通信しており、間のケーブルにだけアクセスできる状態で通信内容を読み取る実機チャレンジ。
提供される機材はスイッチとかしめ器とRJ45コネクタ。
ということで、ケーブルをぶった切ってRJ45コネクタ取り付けてBobになりすませばOK!
なんだけど、当日はコネクタは問題なく取り付けてpingは通ったのに、なんかVMから通信ができなくて突破できず…
ブリッジさせるNICの設定間違えていた説があり、とても悔しい。
しかし、このような実機チャレンジはオンラインのCTFではできないものであり、新鮮でとても良かった。
願わくば今後も同様の取り組みが続きますよう。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください