fr33f0r4ll

自分用雑記

SECCON CTF 2017 Quals writeup

チームで参加できたのでたくさん解けた。 僕が解いたのはputchar_music, vigenere3d, ps_and_qsの3つ。 チームは200位くらい。

コードはここ

putchar music

映画のタイトル当てろという問題。 TLにこの時点で分かってる人いて笑った。

Cのワンライナーが降ってくるので取り敢えずコンパイルmath.hがいるっぽいのでリンクすれば警告は出るがちゃんと動くプログラムが出力される。 実行すると無限ループしてバイナリを吐き出す。

タイトルにmusicとあるので、おそらく生の音声データか何かだろうと当たりを付けた。 linuxで実行できるとあったので多分コマンドラインで標準入力から音声データ受け付けるようなプレイヤーがあるんだろうなと決めつけて調べた。 すると、どうもplayaplayがあるらしいことが分かった。 とりあえずaplayの方にパイプしてみると、8bitっぽい音楽が流れはじめた。

これで解ける!と思いきや、ここで問題発生。 映画のタイトルがまったく分からない! 音楽のタイトルを調べてくれるサービスなんかも当たってみたが音が劣化してるのでヒットしない。 これはもうどうしようもないと思ってしばらくBGMにして流してた。 知ってそうなメンバーとありそうな候補話してるうちに正解を思い出した。 というわけで解けました。

こういう問題どうかと思います!(スターウォーズ見てない人)

vigenere3d

ウ゛ィジュネル暗号の置換表をさらにもう1次元拡張して用いるようになってる暗号化スクリプトが降ってくる。 スクリプト中に置換表生成部分が残っている。 また、暗号化するのに使う鍵が2つあるが、どうも1つの鍵を逆順にして用いてるらしい。 1つの鍵を使い回すのは大抵の場合よろしくない。

こういう換字式暗号は平文の一部が分かっていると対応する鍵が特定できたりする。 この暗号もそんな感じになっていて、平文か鍵のうちどちらかが特定できると、対応する暗号文の文字になるような平文の文字と鍵の文字の組み合せが特定できる(はず)。 正確に元の鍵とは一致しないけど違う鍵でも同じように復号されるようになるはずなので問題ないんじゃあないかな。

そして、平文の先頭は"SECCON{"になっていて、2つ目の鍵には1つ目の鍵が逆順になっているものが使われるので、鍵の先頭と末尾の7文字が特定可能になっていることになる。 そして伏せ字の数からして鍵の長さは14、つまり全部分かる。

解読用スクリプトはこれ。

import sys


def _l(idx, s):
    return s[idx:] + s[:idx]


s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
t = [ [_l((i+j) % len(s), s) for j in range(len(s))] for i in range(len(s))]

cipher = "POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9"
k1_len = 14
k2_len = 14


# first seven chars is SECCON{, so I can decrypto key's first seven and last seven.

s.find('S')


def specify_key(plain_char, cipher_char):
    table = t[s.find(plain_char)]

    for i in range(len(s)):
        for j in range(len(s)):
            if table[i][j] == cipher_char:
                return (i, j)


def get_key():
    p = 'SECCON{'
    c = cipher[:7]
    former = ''
    latter = ''

    for p_ch, c_ch in zip(p, c):
        f_i, l_i = specify_key(p_ch, c_ch)
        former += s[f_i]
        latter += s[l_i]

    return (former, latter)

def decrypto(cipher, key):
    k1 = key
    k2 = key[::-1]
    i1 = 0
    i2 = 0
    plain = ''

    for a in cipher:
        for index, table in enumerate(t):
            if table[s.find(k1[i1])][s.find(k2[i2])] == a:
                plain += s[index]
                break
        i1 = (i1 + 1) % len(k1)
        i2 = (i2 + 1) % len(k2)

    return plain

key = 'AAAAAAA_aZ2PK_'
# gocha! 'SECCON{Welc0me_to_SECCON_CTF_2017}'

PS and QS

RSA問題。

降ってくるのはpemファイルだったのでopenssl使って表示させた。 使い方憶えないとなぁ。

この時点で異なる公開鍵2つと暗号文1つが手に入る。 nは4096bitで単純に素因数分解はできそうになく、eは共通でよくある値(0x10001)だった。

この時点で出来そうな攻撃というとgcdかな?と思い試してみることにした。 2つのnが同じ素数を使っていた場合はこれで特定できる。 gcdはかなり効率が良いアルゴリズムがあって、大抵の実装はこのアルゴリズムなのでお手持ちの言語でgcdしてみる。 するとどうやら素数を使い回してるらしかった。

あとは単純な整数の割り算で他の素数も算出でき、2つの素数とeがあれば秘密鍵dを計算できる。 この計算はmodの逆元計算になるので、拡張gcdかなにかのアルゴリズムで解ける。 pythonならgmpyか何かに実装されている。

これで秘密鍵が分かったので、復号するだけである。 復号してみると、片方では最後の方にフラグが表示されていた。 解けた。

しかしこれ見落とすだろう、もう少し解けたことが分かりやすくならんかね。 フラグも適当に入れたら取れちゃいました!ってどこかのチームがやってそうなフラグだったし。

使ったスクリプトは以下。

(defparameter n1 #x00cfcfbbeea7df143a8ac208b1aa1d2f86545ac4cb588c94a3fb1c14ad91a4f0b936157c5a4b869c18a8b864f4726bf8fcdc020cb41042bac96784ab7d03f9374947efb0bc3d665831974340159ffc3db7c8e74b6390fda6eec30b81c6ff624e8d3f5b17bfb7a5c7ffd8ecf4e6518b393abefddd0faeba4308746ba63f8106b59d7e058943a00131a7d4e538c464b270577647edbc478cc1ce9585efe877305b3a7c2e7c44db5475eddadc345a2c90a946771cac0a454cdbcb461f2840e7613c83e9cecc94037fa09bb9daa3f180562c01df0be6c51f0c06e8f0e2d6e1a5e50d0a28c3881140770a9f45934146b7f359b939ce23f0fa507a6f4e454571430952003c20f1d97a67140b6e5fcbfb3b376e4e24969aeb1d489cfc72af4f15a4788a1aa97c89756d1d4d94aa47e7cd3a81aecb92448cc92c77d2ef576aa0dbc1350862accddaddbce80357f0cd5b854dd0f8c4627fe4b718b24ecfe11ed24c3be22f00643bbed4ee5e345af176e5b76d23a2f80e0ec6f34e5718c62a70fe5570c28b807b44f22eadebd9b5ff906f6a85be88c0c8f6e5f880a51f17f84db1c2eefea8af34040444ced1a37df0e4f5f72cc3f50b7e427c8c2d8b6186ead762f0c444b3ca3a0103ed12a93bce9cae7479a229ebbc0a648eaa6f97e5051a66eb09ebd7348e92f75f125ebdc367e2a7d1da7759d41fae2e2635bf4b7a7f91becab3ac7d05bd)

(defparameter n2 #xBB33CC7FCC8ECAF3BF9ED95C583792E1EC6B80EE875EC2064DBCF07595C8344923BF536524D4E0A75574C7798C73B197DD2B1B42054B1E49CB45FBF04E6F114CF8A365C3DF3645524F778268038A3FA26802E9D1EDBFBB5EDFB5A0C375370D7F10F57DABBD4F771DAD3632F01B9BCE10489966EE882DAB17A33B786AA5F73165A54051300B1DF9280392A3EDE9D3FC9C4D8A6A06351F6EF3598E8DE2B39D3B19AF64A1716CD15826C3F24CB13DEB722C3A03EF1D2BE2D0A5A6E210FF5D018367BE3BF99EA26BA006E5164A4DD55AABCD449DE5CE1864825DC160E50D509EB0E6FE723EF182681EDDB94084B83EC9E2E943E87CB87509AB0FD9B1CA22C1CEAFF39FCACF6729FC0E0578670D87D7F0F9CCBE09CB3E12CEB895572A9979D10BFDBFAFA260568D8DB184BE12B3E3193E07729CE3C1D9CD8283ED6983A06388036A0A70294F23392944778280E7DE9F60163A8150E30FF4A4EA02792CBE8305BAA2E99AFE51E17DAFC56BE0D384147BCD38E9D12934EC712622217773A4B3851A9B0C6C7C3E01F6111A1E1A557F4E2AE4A247CE9B75CCCCB1819825F3054AA1C055BD3E2340093AE2EF1D0FA5A176825EFDF79507027F5104080009142F0D43E2F10CFAD220813BBB9014D4F4325EDAC538FB5E82B753E2AD3B24607D7380AA64FCB98B59EA8B5A736B809383248CECE0B17255EA559E90127F778AF6D7E8A66DAD91)

(defparameter e #x10001)

(require 'sb-mpfr) ;; FIX:
(ql:quickload :hackrsa)

(defparameter p (hackrsa:gcd-attack n1 n2))

(defparameter q1 (/ n1 p))
(defparameter q2 (/ n2 p))

(defparameter d1 (hackrsa:private-key e p q1))
(defparameter d2 (hackrsa:private-key e p q2))

(defparameter cipher (with-open-file (in "cipher"
                     :element-type '(unsigned-byte 8))
               (let ((dec 0))
             (loop for b = (read-byte in nil nil)
                   if b
                 do (setf dec (+ (* dec (expt 2 8)) b))
                   else
                 do (return dec)))))

(defparameter p1 (hackrsa:decrypto cipher d1 n1))
(defparameter p2 (hackrsa:decrypto cipher d2 n2))

(print (hackrsa:decode-string p1))
(print (hackrsa:decode-string p2))

n1の方がフラグのある平文になる。

感想

pwn厳しい。

チームでやると盛り上るし、余裕もできるから楽しい。 ただ、カバーするやや範囲が被ってるために解けないジャンルが...