SECCON CTF 2021 Writeup

Sun Dec 12 2021

    12/11-12 で開催していた SECCON CTF 2021 にチーム WreckTheLine で参加しました。結果は 14th/506 (得点のあるチームのみカウント) でした。

    自分は Crypto に集中して取り組みました。どの Crypto 問もシンプルだけど非自明な感じがあって好きでした。 ところで終了後の solves 数を改めて見ると、自分の解いた感覚の難易度と solves 数が結構違くて🤔 なんかシンプルな解法の見落としがあるのかな… (個人の感想としての難易度は難しいほうから oOoOoO > CCC > cerberus > Sign Wars > XXX > pppp でした)

    以下、 Crypto の問題について writeup をまとめます。

    Crypto

    Sign Wars

    8 solves

    challenge.sage
    from Crypto.Util.number import bytes_to_long, long_to_bytes
    from Crypto.Util.Padding import pad
    import random
    from secret import msg1, msg2, flag
    
    flag = pad(flag, 96)
    flag1 = flag[:48]
    flag2 = flag[48:]
    
    # P-384 Curve
    p = 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319
    a = -3
    b = 27580193559959705877849011840389048093056905856361568521428707301988689241309860865136260764883745107765439761230575
    curve = EllipticCurve(GF(p), [a, b])
    order = 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643
    Z_n = GF(order)
    gx = 26247035095799689268623156744566981891852923491109213387815615900925518854738050089022388053975719786650872476732087
    gy = 8325710961489029985546751289520108179287853048861315594709205902480503199884419224438643760392947333078086511627871
    G = curve(gx, gy)
    
    for b in msg1:
        assert b >= 0x20 and b <= 0x7f
    z1 = bytes_to_long(msg1)
    assert z1 < 2^128
    
    for b in msg2:
        assert b >= 0x20 and b <= 0x7f
    z2 = bytes_to_long(msg2)
    assert z2 < 2^384
    
    # prequel trilogy
    def sign_prequel():
        d = bytes_to_long(flag1)
        sigs = []
        for _ in range(80):
            # normal ECDSA. all bits of k are unknown.
            k1 = random.getrandbits(128)
            k2 = z1
            k3 = random.getrandbits(128)
            k = (k3 << 256) + (k2 << 128) + k1
            kG = k*G
            r, _ = kG.xy()
            r = Z_n(r)
            k = Z_n(k)
            s = (z1 + r*d) / k
            sigs.append((r,s))
    
        return sigs
    
    # original trilogy
    def sign_original():
        d = bytes_to_long(flag2)
        sigs = []
        for _ in range(3):
            # normal ECDSA
            k = random.getrandbits(384)
            kG = k*G
            r, _ = kG.xy()
            r = Z_n(r)
            k = Z_n(k)
            s = (z2 + r*d) / k
            sigs.append((r,s))
    
        return sigs
    
    
    def sign():
        sigs1 = sign_prequel()
        print(sigs1)
        sigs2 = sign_original()
        print(sigs2)
    
    
    if __name__ == "__main__":
        sign()

    ECDSA の鍵としてフラグの前半部 (sign_prequel) と後半部 (sign_original) が使われています。前半部→後半部とフラグを求めていきます。

    sign_prequel ii 番目の署名について、k12256+k3=z1(si12128)+risi1dmodNk_1 2^{256} + k_3 = z_1 (s_i^{-1} - 2^{128}) + r_i s_i^{-1} d \mod N という式が成り立ちます。 k1,k3k_1, k_3 は128 bitの乱数です。署名は80個得られています。

    この式の両辺にある定数をかけることで左辺を小さい値にすることができれば、 Hidden Number Problem (HNP) として LLL で dd を求めることができます。まずはこのような定数を探しました。ソルバーとして rkm-san の実装 を使いました。

    # [x, k1, k2] mat
    mat = matrix(ZZ, 3, 3)
    mat[0, 0] = 2**256
    mat[1, 0] = -order
    mat[0, 1]
    mat[2, 1] = -order
    mat[0, 2] = 1
    lb = [0, 0, 0]
    ub = [2**190] * 3
    res = solve(mat, lb, ub)
    x = int(res[2][0])

    ↑で求めた xx を両辺にかけることで、左辺は 2190+1282^{190+128} 程度の大きさになります。あとは HNP を解きます。

    # [z1, d, l1, l2, ...] mat
    mat = matrix(ZZ, 82, 82)
    for i in range(80):
        r, s = sigs1[i]
        mat[0, i] = (pow(s, -1, order) - 2**128) * x
        mat[1, i] = r * pow(s, -1, order) * x
        mat[i+2, i] = -order
    mat[0, 80] = 1
    mat[1, 81] = 1
    lb = [0] * 82
    ub = [2**(190+128)] * 80 + [2**128, 2**(48*8)]
    res = solve(mat, lb, ub)
    z1 = int(res[2][0])
    d1 = int(res[2][1])

    これで前半部のフラグ (d1) が求まりました。

    sign_original ECDSA の部分を見ると署名は3個しかなく、kkの乱数も384 bitで生成しているので、前半の手法を使うことができません。普通だったらこの条件で dd を求めることはできません。

    というわけで前半→後半と別れている意味を考えます。乱数生成に注目すると、前半の時点で 128×2×80=20480128 \times 2 \times 80 = 20480bits 分が getrandbits で生成されています。これは32 bit整数640個分に相当します。 getrandbits は Mersenne Twister を使用していますが、この乱数生成方法は32 bit整数が624個がわかっているとき後続の乱数が再現されます。つまり今回の問題のケースでは、後半部分の kk の値を乱数生成の観点からリークさせることができます。 kk がわかれば当然 dd も求まります。

    rands = []
    for i in range(80):
        r, s = sigs1[i]
        k = int((z1 * pow(s, -1, order) + r * pow(s, -1, order) * d1) % order)
        rands.append(k % 2**128)
        k = k // 2**256
        assert k < 2**128
        rands.append(k)
    
    rands32 = []
    for rand in rands:
        for i in range(4):
            rands32.append(int(rand % 2**32))
            rand = rand // 2**32
    
    
    # https://inaz2.hatenablog.com/entry/2016/03/07/194147
    def untemper(x):
        x = unBitshiftRightXor(x, 18)
        x = unBitshiftLeftXor(x, 15, 0xefc60000)
        x = unBitshiftLeftXor(x, 7, 0x9d2c5680)
        x = unBitshiftRightXor(x, 11)
        return x
    
    def unBitshiftRightXor(x, shift):
        i = 1
        y = x
        while i * shift < 32:
            z = y >> shift
            y = x ^^ z
            i += 1
        return y
    
    def unBitshiftLeftXor(x, shift, mask):
        i = 1
        y = x
        while i * shift < 32:
            z = y << shift
            y = x ^^ (z & mask)
            i += 1
        return y
    
    mt_state = tuple([int(untemper(x)) for x in rands32[:624]] + [int(624)])
    random.setstate((int(3), mt_state, None))
    for _ in range(640 - 624):
        random.getrandbits(32)
    
    k1 = random.getrandbits(384)
    k2 = random.getrandbits(384)
    
    kG1 = k1*G
    kG2 = k2*G
    r1, s1 = sigs2[0]
    r2, s2 = sigs2[1]
    assert kG1.xy()[0] == r1
    assert kG2.xy()[0] == r2
    
    d2 = int((s1 * k1 - s2 * k2) * pow(r1 - r2, -1, order))
    long_to_bytes(d1) + long_to_bytes(d2)

    SECCON{New_STARWARS_Spin-Off_The_Book_Of_Boba_Fett_Will_Premiere_On_December_29-107c360aab}

    XXX

    14 solves

    task.sage
    import os
    
    flag = os.getenv("FLAG", "fake{fakeflag_blahblah}")
    x = int.from_bytes(flag.encode(), "big")
    
    p = random_prime(1 << int(x.bit_length() * 2.5))
    Fp = GF(p)
    
    params = []
    while len(params) != 6:
        try:
            y = randint(2, x)
            a = randint(2, p-1)
            b = (y^2 - (x^3 + a*x)) % p
    
            EC = EllipticCurve(Fp, [a, b])
            EC(x, y)
    
            params.append([a, b])
        except ValueError:
            pass
    
    print(p)
    print(params)

    フラグの値を xx をしたとき、ある yy をランダムに生成し、 (x,y)(x, y) が楕円曲線 y2=x3+ax+by^2 = x^3 + ax + b 上に乗るような (a,b)(a, b) がランダムに生成されます。 (a,b)(a, b) が6種類与えられています。

    ii 番目の楕円曲線を y2=x3+aix+biy^2 = x^3 + a_ix + b_i とおきます。 ii 番目と jj 番目の式の差を考えると、 yi2yj2=(aiaj)x+(bibj)y_i^2 - y_j^2 = (a_i - a_j)x + (b_i - b_j) となります。ここで各項のbit数を見てみると、左辺が右辺の各項に対して小さいことがわかります。そのため、 LLL を使って xx が求められそうです。

    solve.py
    from itertools import combinations
    from Crypto.Util.number import long_to_bytes
    
    
    mat = matrix(ZZ, 17, 17)
    for k, (i, j) in enumerate(combinations(range(6), 2)):
        ai, bi = params[i]
        aj, bj = params[j]
        mat[0, k] = ai - aj
        mat[1, k] = bi - bj
        mat[k+2, k] = -p
    mat[0, 15] = 1
    mat[1, 16] = p
    res = mat.LLL()
    C = mat.solve_left(res)
    
    for c in C:
        print(long_to_bytes(abs(round(c[0]))))

    SECCON{9dd4e461268c8034f5c8564e155c67a6}

    cerberus

    16 solves

    problem.py
    import base64
    from Crypto.Cipher import AES
    from Crypto.Random import get_random_bytes
    from Crypto.Util.Padding import pad, unpad
    from Crypto.Util.strxor import strxor
    from flag import flag
    import signal
    
    key = get_random_bytes(16)
    block_size = 16
    
    # encrypt by AES-PCBC
    # https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Propagating_cipher_block_chaining_(PCBC)
    def encrypt(m):
        cipher = AES.new(key, AES.MODE_ECB)  # MODE_PCBC is not FOUND :sob: :sob:
        m = pad(m, 16)
        m = [m[i : i + block_size] for i in range(0, len(m), block_size)]
    
        iv = get_random_bytes(16)
    
        c = []
        prev = iv
        for m_block in m:
            c.append(cipher.encrypt(strxor(m_block, prev)))
            prev = strxor(c[-1], m_block)
    
        return iv, b"".join(c)
    
    
    # decrypt by AES-PCBC
    # https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Propagating_cipher_block_chaining_(PCBC)
    def decrypt(iv, c):
        cipher = AES.new(key, AES.MODE_ECB)  # MODE_PCBC is not FOUND :sob: :sob:
        c = [c[i : i + block_size] for i in range(0, len(c), block_size)]
    
        m = []
        prev = iv
        for c_block in c:
            m.append(strxor(prev, cipher.decrypt(c_block)))
            prev = strxor(m[-1], c_block)
    
        return b"".join(m)
    
    
    # The flag is padded with 16 bytes prefix
    # flag = padding (16 bytes) + "SECCON{..."
    signal.alarm(3600)
    ref_iv, ref_c = encrypt(flag)
    print("I teach you a spell! repeat after me!")
    print(base64.b64encode(ref_iv + ref_c).decode("utf-8"))
    
    while True:
        c = base64.b64decode(input("spell:"))
        iv = c[:16]
        c = c[16:]
    
        if not c.startswith(ref_c):
            print("Grrrrrrr!!!!")
            continue
    
        m = decrypt(iv, c)
    
        try:
            unpad(m, block_size)
        except:
            print("little different :(")
            continue
    
        print("Great :)")

    AES の PCBC モード を使った Padding Oracle Attack 問題。 PCBC モード初めて知った… 少々困る制約として、復号してもらう暗号の前半部分は、フラグを暗号化したものである必要があります。 フラグの暗号文が64bytesなことから4ブロックあることがわかります。

    上記リンクの復号スキームの画像とにらめっこをしているとすぐわかることですが、 IV に対して何らかの値で xor をとると、復号結果の各ブロックそれぞれに対してその値の xor が加えられます。 この特徴を使うことで暗号文の最後のブロックに対して Padding Oracle Attack が適用でき、復号することができます。

    よくある Padding Oracle Attack の問題では、最後のブロックを一つ求めては削っていくことで全ブロックの復号ができます。しかしこの問題では送信する暗号の前半部がフラグの暗号と一致している必要があり、最後のブロックを削るということができません。ひと工夫必要です。

    今、 C0C1C2C3C0C_0 || C_1 || C_2 || C_3 || C_0ref_iv を使って復号してもらうことを考えます。最後のブロックの復号結果は P4=Deck(C0)(C3P3)P_4 = \mathrm{Dec}_k(C_0) \oplus (C_3 \oplus P_3) となります。 P0=IVDeck(C0)P_0 = \mathrm{IV} \oplus \mathrm{Dec}_k(C_0) であることを使うと P4=P0IVC3P3P_4 = P_0 \oplus \mathrm{IV} \oplus C_3 \oplus P_3 となります。ここから最後のブロックの復号結果が P0P_0 と同じになるための iv を求めることができ、その状態から Padding Oracle Attack を使って P0P_0 を求められます。 これを繰り返すことで全てのブロックの復号結果をリークできます。

    solve.py
    from pwn import remote
    
    io = remote("cerberus.quals.seccon.jp", 8080)
    _ = io.recvline()
    c = b64decode(io.recvline().strip())
    ref_iv = c[:16]
    ref_c = c[16:]
    
    
    def try_decrypt(iv, c):
        io.sendlineafter(b"spell:", b64encode(iv + c))
        ret = io.recvline().strip().decode()
        if "little" in ret:
            return False
        elif "Great" in ret:
            return True
        else:
            io.close()
            raise ValueError
    
    
    dec = b""
    iv = ref_iv
    for idx in range(16)[::-1]:
        print(idx)
        for i in range(256):
            tmp_iv = iv[:idx] + long_to_bytes(iv[idx] ^ i) + iv[idx+1:]
            if (idx == 15 and i == 0) or not try_decrypt(tmp_iv, ref_c):
                continue
            dec = long_to_bytes((16 - idx) ^ i) + dec
            print(dec)
            iv = iv[:idx] + long_to_bytes(iv[idx] ^ i) + iv[idx+1:]
            iv = iv[:idx] + xor(iv[idx:], long_to_bytes(16-idx), long_to_bytes(16-idx+1))
            break
    print(dec)
    dec3 = dec
    iv = xor(iv, b"\x11", ref_c[-16:], ref_iv)
    dec = b""
    for idx in range(16)[::-1]:
        print(idx)
        for i in range(256):
            tmp_iv = iv[:idx] + long_to_bytes(iv[idx] ^ i) + iv[idx+1:]
            if (idx == 15 and i == 0) or not try_decrypt(tmp_iv, ref_c+ref_c[:16]):
                continue
            dec = long_to_bytes((16 - idx) ^ i) + dec
            print(dec)
            iv = iv[:idx] + long_to_bytes(iv[idx] ^ i) + iv[idx+1:]
            iv = iv[:idx] + xor(iv[idx:], long_to_bytes(16-idx), long_to_bytes(16-idx+1))
            break
    print(dec)
    dec0 = dec
    iv = xor(iv, b"\x11", ref_c[:16], ref_c[:16], dec)
    dec = b""
    for idx in range(16)[::-1]:
        print(idx)
        for i in range(256):
            tmp_iv = iv[:idx] + long_to_bytes(iv[idx] ^ i) + iv[idx+1:]
            if (idx == 15 and i == 0) or not try_decrypt(tmp_iv, ref_c+ref_c[:32]):
                continue
            dec = long_to_bytes((16 - idx) ^ i) + dec
            print(dec)
            iv = iv[:idx] + long_to_bytes(iv[idx] ^ i) + iv[idx+1:]
            iv = iv[:idx] + xor(iv[idx:], long_to_bytes(16-idx), long_to_bytes(16-idx+1))
            break
    print(dec)
    dec1 = dec
    iv = xor(iv, b"\x11", ref_c[16:32], ref_c[16:32], dec)
    dec = b""
    for idx in range(16)[::-1]:
        print(idx)
        for i in range(256):
            tmp_iv = iv[:idx] + long_to_bytes(iv[idx] ^ i) + iv[idx+1:]
            if (idx == 15 and i == 0) or not try_decrypt(tmp_iv, ref_c+ref_c[:48]):
                continue
            dec = long_to_bytes((16 - idx) ^ i) + dec
            print(dec)
            iv = iv[:idx] + long_to_bytes(iv[idx] ^ i) + iv[idx+1:]
            iv = iv[:idx] + xor(iv[idx:], long_to_bytes(16-idx), long_to_bytes(16-idx+1))
            break
    print(dec)
    dec2 = dec
    flag = dec0 + dec1 + dec2 + dec3
    print(flag)

    SECCON{v._.^v-_-v^._.^_S0und_oF_0rpHeUs_Aha~~}

    日本のサーバーだからなのか、 Padding Oracle Attack 問なのに oracle が高速だったので待ち時間があまりなくストレスフリーでした。

    CCC

    17 solves

    challenge.py
    from Crypto.Util.number import bytes_to_long, getPrime, getRandomInteger, isPrime
    from secret import flag
    
    
    def create_prime(p_bit_len, add_bit_len, a):
        p = getPrime(p_bit_len)
        p_bit_len2 = 2*p_bit_len // 3 + add_bit_len
        while True:
            b = getRandomInteger(p_bit_len2)
            _p = a * p
            q = _p**2 + 3*_p*b + 3*b**2
            if isPrime(q):
                return p, q
    
    
    def encrypt(p_bit_len, add_bit_len, a, plain_text):
        p, q = create_prime(p_bit_len, add_bit_len, a)
        n = p*q
        e = 65537
    
        c = pow(plain_text, e, n)
        print(f"{n=}")
        print(f"{e=}")
        print(f"{c=}")
        print(f"{a=}")
    
    
    if __name__ == "__main__":
        encrypt(1024, 9, 23, bytes_to_long(flag))

    RSA の公開鍵n=pqn = pq について、 q=a2p2+3abp+3b2q = a^2p^2 + 3abp + 3b^2 が成り立ちます。a=23a = 23bb は乱数で生成されています。pp は1024bitで bb は691bitです。

    手計算をすると an=(ap)3+3b(ap)2+3b2(ap)=(ap+b)3b3an = (ap)^3 + 3b(ap)^2 + 3b^2(ap) = (ap + b)^3 - b^3 と変形できます。 b3b^3 は十分小さいため無視して考えると ap+b(an)1/3ap + b \simeq (an)^{1/3} と近似できます。 試しに自分で生成した p,bp, b について以上の近似がどれだけ成り立つか確認してみると、2122^{12}bit程度の誤差になりました。これは全探索するのに十分小さい値なので ap+bap + b の正確な値を探索させました。

    solve.py
    from gmpy2 import iroot
    
    
    ap_b_approx = int(iroot(a * n, 3)[0])
    for i in range(10000):
        tmp = ap_b_approx + i
        if tmp ** 3 - a * n < 0:
            continue
        root, exact = iroot(tmp ** 3 - a * n, 3)
        if exact:
            b = int(root)
            ap_b = tmp
            print(root)
    p = (ap_b - b) // a
    assert n % p == 0
    q = n // p
    phi = (p - 1) * (q - 1)
    d = int(pow(e, -1, phi))
    print(long_to_bytes(int(pow(c, d, n))))

    SECCON{CCC_means_Cubic_root_and_the_CTF_I_learnt_a_lot_about_fermat's_factorization_method}

    oOoOoO

    26 solves

    problem.py
    import signal
    from Crypto.Util.number import long_to_bytes, bytes_to_long, getPrime
    import random
    from flag import flag
    
    message = b""
    for _ in range(128):
        message += b"o" if random.getrandbits(1) == 1 else b"O"
    
    M = getPrime(len(message) * 5)
    S = bytes_to_long(message) % M
    
    print("M =", M)
    print('S =', S)
    print('MESSAGE =', message.upper().decode("utf-8"))
    
    signal.alarm(600)
    ans = input('message =').strip().encode()
    
    if ans == message:
        print(flag)
    else:
        print("🧙")

    oO で構成された128文字の文字列の数値に対して、ある素数 MM で mod を計算したもの SS が与えられます。元の文字列を当てられればフラグが手に入ります。

    o0x6fO0x4f です。この文字列を数値で表すと k=0127(79+32xk)256k\sum_{k=0}^{127}(79 + 32x_k)256^{k} となります。ここで xkx_k は末尾から数えてkk番目が O のとき0, o のとき1となります。 MMSS の関係性は k=0127(79+32xk)256k=SmodM\sum_{k=0}^{127}(79 + 32x_k)256^{k} = S \mod M と記せます。 問題の見方を変えると、集合 {32×256k0k<128}\{32 \times 256^k | 0 \le k < 128 \} のどの部分集合の和を取ると S79k=0127256kS - 79\sum_{k=0}^{127} 256^k になるかを求める問題となります。これは 部分和問題 です。

    自分はこの問題を k=012732×256kxk=S79k=0127256kmodM\sum_{k=0}^{127} 32\times 256^k x_k = S - 79\sum_{k=0}^{127}256^k \mod M となる xkx_k を LLL のような格子基底簡約アルゴリズムを使って解くことにしました。 mod のぶんは全探索しました (最大でも20-30種類程度しかない)。しかし LLL だと答えが出ず、 BKZ を使ってみると答えが得られました。 BKZ よく知らないんだよな…

    solve.py
    from pwn import remote
    
    io = remote("oooooo.quals.seccon.jp", 8000)
    io.recvuntil(b"M = ")
    M = int(io.recvline())
    io.recvuntil(b"S = ")
    S = int(io.recvline())
    
    res = 0
    for i in range(128):
        res += 0x4f * 256**i
    res -= S
    res %= M
    target = -res % M
    cands = []
    for i in range(128):
        cands.append(int(0x20 * 256**i % M))
    
    def check(res):
        for r in res[0]:
            if abs(r) != 0 and abs(r) != 1:
                return False
        return True
    
    for j in range(20):
        print(j)
        mat = matrix(ZZ, 129, 129)
        for i in range(128):
            mat[i, i] = 1
        for i in range(128):
            mat[i, 128] = cands[i]
        mat[128, 128] = -(j*M+target)
        weights = diagonal_matrix([1] * 128 + [12])
        mat *= weights
        res = mat.BKZ()
        res /= weights
        if check(res):
            break
    dec = b""
    for i in range(128):
        if res[0][i] == 0:
            dec = b"O" + dec
        elif abs(res[0][i]) == 1:
            dec = b"o" + dec
    print(dec)
    io.sendlineafter(b"message =", dec)
    print(io.recvline())

    SECCON{Here_is_Huge-Huge_Island_yahhoOoOoOoOoOoO}

    この問題がここまで解かれてるのすごい、 SECCON beginners でナップサック暗号が出たからなのだろうか。

    pppp

    70 solves

    problem.sage
    from Crypto.Util.number import *
    from flag import flag
    
    p = getStrongPrime(512)
    q = getStrongPrime(512)
    n = p*q
    
    mid = len(flag) // 2
    
    e = 65537
    
    m1 = int.from_bytes(flag[:mid], byteorder='big')
    m2 = int.from_bytes(flag[mid:], byteorder='big')
    
    assert m1 < 2**256
    assert m2 < 2**256
    
    m = [[p,p,p,p], [0,m1,m1,m1], [0,0,m2,m2],[0,0,0,1]]
    
    # add padding
    for i in range(4):
        for j in range(4):
            m[i][j] *= getPrime(768)
    
    m = matrix(Zmod(p*q), m)
    
    c = m^e
    
    print("n =", n)
    print("e =", e)
    print("c =", list(c))

    上三角行列を RSA で暗号化した問題。

    上三角行列なので対角項の暗号文 CiiC_{ii}Cii=MiiemodnC_{ii} = M_{ii}^e \mod n となります。 M00=k00pM_{00} = k_{00}p (kk は未知の素数) であることを考慮すると、 gcd(C00,n)\gcd(C_{00}, n)pp が求まります。 pp が求まったので RSA の復号ができるようになり、もともとの行列の対角にある M11=k11m1,M22=k22m2M_{11} = k_{11}m_1, M_{22} = k_{22}m_2 も求まります。 これらの M11,M22M_{11}, M_{22} が素因数分解できればフラグは求まるのですが、流石にできないようになっていました (M11M_{11} のほうはできたけど M22M_{22} はできなかった)。

    次に C12C_{12}C23C_{23} の値がMM の値を使ってどう表されるかを見てみると、 C12=M12i=0e1M22iM11e1imodnC_{12} = M_{12}\sum_{i=0}^{e-1} M_{22}^i M_{11}^{e-1-i} \mod n となります。 M11,M22M_{11}, M_{22} は上記の方法で求まるので M12M_{12} も求まります。 M12=k12m1M_{12} = k_{12} m_1 という形なので、 gcd(M11,M12)\gcd(M_{11}, M_{12})m1m_1 が求まります。 C23C_{23} に注目すると同様の方法で m2m_2 も求まります。

    solve.py
    p = gcd(c[0][0], n)
    q = n // p
    c1 = c[1][1]
    c2 = c[2][2]
    phi = (p - 1) * (q - 1)
    d = int(pow(e, -1, phi))
    
    m00 = int(pow(c[0][0], d, n))
    m11 = int(pow(c1, d, n))
    m22 = int(pow(c2, d, n))
    m33 = int(pow(c[3][3], d, n))
    
    tmp = 0
    for i in range(e):
        tmp += pow(m22, i, n) * pow(m11, e-1-i, n)
    m12 = int(c[1][2] / tmp)
    
    tmp = 0
    for i in range(e):
        tmp += pow(m33, i, n) * pow(m22, e-1-i, n)
    m23 = int(c[2][3] / tmp)
    
    print(long_to_bytes(gcd(m11, m12)) + long_to_bytes(gcd(m22, m23)))

    SECCON{C4n_y0u_prove_why_decryptable?}