🐧
SECCON CTF 14 Quals WriteUp
こちら

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

Ez Flag Checker
問題の概要
この問題では、フラグを要求するバイナリ chall が提供されました。フラグの形式は SECCON{...}。
解析
- 静的解析
main関数とsigma_encrypt関数をデコンパイルしました。
暗号化アルゴリズムを特定しました:16バイトの鍵とXORを使用するカスタムストリーム暗号(Sigma?)です。
暗号化されたフラグのバイト列(flag_enc)と初期鍵(sigma_words = "expand 32-byte k")を抽出しました。
暗号化ロジック:output[j] = input[j] ^ ((key[j%16] + j) & 0xFF) - 最初の試み
抽出した鍵とデータを使用して、Pythonで復号ツール(デクリプタ)を実装しました。
結果:SECCON{fLagc<9yYW5k<b19!!}
検証:./challはこのフラグを拒否しました。 - デバッグ
実行時のメモリを調査するためにGDBを使用しました。
sigma_words(鍵)が、ファイル上のデータと比較してメモリ上で変更されていることを発見しました。
ファイル:expand 32-byte k
メモリ:expanb 32-byte k(インデックス5の'd'が'b'に変わっていた) - 解決
変更後の鍵expanb 32-byte kを反映して復号ツールを更新しました。
新しい結果:SECCON{flagc29yYW5k<b19!!}
検証:./challはこのフラグを受け入れました!
フラグ
SECCON{flagc29yYW5k<b19!!}
Crown Flash
問題
SECCON{...} 形式のフラグを見つけること。
ファイル: crown_flash (ELF 64-bit LSB executable)
解析
- 初期解析
まずfileコマンドとstringsコマンドを使用してバイナリを解析しました。
バイナリはstrippedされた64-bit ELFでした。
strings の結果、"Correct!"と"Wrong"という文字列が見つかり、標準的なフラグチェッカーの構造であることがわかりました。
バイナリを実行すると、"Flag: "という入力プロンプトが表示されます。 - リバースエンジニアリング
objdumpとgrepを使用して、"Correct!"と"Wrong"を参照しているコード箇所を特定しました。
正解ルート:"Correct!"という文字列は0x251ef5で参照されています。
Main関数: エントリポイントから0x251f10にあるmain関数へと繋がっています。
間接呼び出し: 判定における重要なチェック処理は、0x251e44にあるcall *0xc8(%rsp)という間接呼び出し(Indirect Call)経由で行われています。 - 動的解析
チェック関数が間接的に呼び出されていたため、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から導出されるターゲット値と直接比較する、より単純なケースを想定しました。
- 解法
以下の機能を持つ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