🐧

SECCON CTF 14 Quals WriteUp

に公開

こちら

Reversingしか解けなかったので僕が解いた問題だけ書きます。

Ez Flag Checker

問題の概要
この問題では、フラグを要求するバイナリ chall が提供されました。フラグの形式は SECCON{...}。
解析

  1. 静的解析
    main関数とsigma_encrypt関数をデコンパイルしました。
    暗号化アルゴリズムを特定しました:16バイトの鍵とXORを使用するカスタムストリーム暗号(Sigma?)です。
    暗号化されたフラグのバイト列 (flag_enc) と初期鍵 (sigma_words = "expand 32-byte k") を抽出しました。
    暗号化ロジック: output[j] = input[j] ^ ((key[j%16] + j) & 0xFF)
  2. 最初の試み
    抽出した鍵とデータを使用して、Pythonで復号ツール(デクリプタ)を実装しました。
    結果: SECCON{fLagc<9yYW5k<b19!!}
    検証: ./chall はこのフラグを拒否しました。
  3. デバッグ
    実行時のメモリを調査するためにGDBを使用しました。
    sigma_words(鍵)が、ファイル上のデータと比較してメモリ上で変更されていることを発見しました。
    ファイル: expand 32-byte k
    メモリ: expanb 32-byte k (インデックス5の'd'が'b'に変わっていた)
  4. 解決
    変更後の鍵 expanb 32-byte k を反映して復号ツールを更新しました。
    新しい結果: SECCON{flagc29yYW5k<b19!!}
    検証: ./chall はこのフラグを受け入れました!

フラグ

SECCON{flagc29yYW5k<b19!!}

Crown Flash

問題
SECCON{...} 形式のフラグを見つけること。
ファイル: crown_flash (ELF 64-bit LSB executable)
解析

  1. 初期解析
    まずfileコマンドとstringsコマンドを使用してバイナリを解析しました。
    バイナリはstrippedされた64-bit ELFでした。
    strings の結果、"Correct!" と"Wrong"という文字列が見つかり、標準的なフラグチェッカーの構造であることがわかりました。
    バイナリを実行すると、"Flag: " という入力プロンプトが表示されます。
  2. リバースエンジニアリング
    objdump とgrepを使用して、"Correct!" と"Wrong"を参照しているコード箇所を特定しました。
    正解ルート: "Correct!" という文字列は0x251ef5で参照されています。
    Main関数: エントリポイントから0x251f10にあるmain関数へと繋がっています。
    間接呼び出し: 判定における重要なチェック処理は、0x251e44 にあるcall *0xc8(%rsp)という間接呼び出し(Indirect Call)経由で行われています。
  3. 動的解析
    チェック関数が間接的に呼び出されていたため、GDBを使用して実行時の飛び先アドレスを調査しました。
    JITコード: 呼び出し先は、0x7ffff7ff8000 にある動的に生成された(JIT)関数でした。
    ランダム性: この関数はr13dレジスタ内のハードコードされた定数(例: 0x96fd1598, 0xe9210552)を使用しますが、この値は実行ごとに変化します。
    ハッシュアルゴリズム: 関数は以下のような独自のハッシュ計算を実装していました:
     入力文字と4バイトの鍵(0x7ffff7ff80d0 にある 0x42, 0x19, 0x66, 0x99)とのXOR。
     定数(0x9e3779b9, 0x45d9f3b)との乗算。
     ビット回転(Rotation)とXOR演算。
     静的配列 A(バイナリから抽出可能)との比較。
    "ランダム"の罠
    JITコードには、ランダムなr13dの値に基づいた条件分岐が含まれていました:
test   $0x80000000,%r13d
je     ...
imul   ...
xor    %r13d,%r15d
add    %r15d,%r11d

もしr13dの最上位ビットがセットされている場合、計算されたハッシュ値がr13dを使って変更されてしまいます。r13d はランダムであるため、これではフラグ自体もランダムになってしまい、標準的なCTFの問題としてはあり得ません。
推論 (予想?): 正しいフラグは、このランダムな分岐が通らないか、あるいはその影響が無効化/無視されるような条件("good" path)である必要があります。ここでは、計算されたハッシュ値を静的配列Aから導出されるターゲット値と直接比較する、より単純なケースを想定しました。

  1. 解法
    以下の機能を持つPythonソルバを実装しました:
     検証用の静的配列Aをバイナリから抽出する(オフセット 126368)。
     JITハッシュアルゴリズムを再現する(ただし、ランダムなr13dの分岐は無視する)。
     1文字ずつ総当たり(ブルートフォース)を行う(char[i] のハッシュ計算はiと char[i]
     のみに依存し、前の文字には依存しないため)。
import struct

def solve():
    # 抽出した配列 A を読み込む
    try:
        with open('array.bin', 'rb') as f:
            data = f.read()
    except FileNotFoundError:
        print("array.bin not found. Run: dd if=crown_flash bs=1 skip=126368 count=148 status=none > array.bin")
        return
    
    A = struct.unpack('<37I', data)
    key = [0x42, 0x19, 0x66, 0x99]
    hash_val = 0x72616e64
    flag = bytearray()
    
    for i in range(37):
        # 配列 A からターゲット値を計算
        r14d = (i + 1) * 0x7ed55d16
        r14d = (r14d + 0xc761c23c) & 0xffffffff
        r9d = A[i]
        target = r9d ^ r14d
        
        # 文字を総当たり (Brute-force)
        for char in range(256):
            r8d = char
            k = key[i % 4]
            r8d ^= k
            temp = (i * 0x9e3779b9) & 0xffffffff
            r8d = (r8d + temp) & 0xffffffff
            
            new_hash = (hash_val + r8d) & 0xffffffff
            new_hash = (new_hash * 0x45d9f3b) & 0xffffffff
            
            rol7 = ((new_hash << 7) | (new_hash >> 25)) & 0xffffffff
            final_hash = (new_hash + rol7) & 0xffffffff
            
            check_val = (final_hash ^ (final_hash >> 16)) & 0xffffffff
            
            if check_val == target:
                flag.append(char)
                hash_val = final_hash
                break
            
    print(f"Flag: {flag.decode('utf-8', errors='ignore')}")

if __name__ == "__main__":
    solve()

フラグ

SECCON{good->sPLqsLsooJY,EFwBU8Std7Y}

Discussion