LINE CTF 2022 Writeup

この大会は2022/3/19 19:30(JST)~2022/3/21 19:30(JST)に開催されました。
今回もチームで参戦。結果は484点で665チーム中55位でした。
自分で解けた問題をWriteupとして書いておきます。

Welcome (Misc)

問題にフラグが書いてあった。

LINECTF{welcome_to_LINECTF2022}

X Factor (Crypto, Warmup)

RSA署名でnとeはわかっている。平文とその対応するシグネチャのペアが7つ提示されている。0x686178656c696f6eのシグネチャを算出する問題。
素因数分解してみる。

>>> 0x686178656c696f6e
7521425229691318126L

G = 7521425229691318126 = 2 * 197 * 947 * 2098711 * 9605087

>>> 0x945d86b04b2e7c7
668178073886320583L
>>> 0x5de2
24034
>>> 0xa16b201cdd42ad70da249
12196433909207788967273033L
>>> 0x6d993121ed46b
1928075547694187L
>>> 0x726fa7a7
1919920039
>>> 0x31e828d97a0874cff
57538707471611677951L
>>> 0x904a515
151299349

E1 = 668178073886320583 = 811 * 947^3 * 970111
E2 = 24034 = 2 * 61 * 197
E3 = 12196433909207788967273033 = 970111 * 2098711^2 * 2854343
E4 = 1928075547694187 = 947 * 970111 * 2098711
E5 = 1919920039 = 61 * 197^2 * 811
E6 = 57538707471611677951 = 2098711 * 2854343 * 9605087
E7 = 151299349 = 197 * 811 * 947

    2 61 197 811 947 970111 2098711 2854343 9605087
G   1  0   1   0   1      0       1       0       1
E1  0  0   0   1   3      1       0       0       0
E2  1  1   1   0   0      0       0       0       0
E3  0  0   0   0   0      1       2       1       0
E4  0  0   0   0   1      1       1       0       0
E5  0  1   2   1   0      0       0       0       0
E6  0  0   0   0   0      0       1       1       1
E7  0  0   1   1   1      0       0       0       0

方程式にして各値の積の組み合わせの等式を作る。実行結果から以下が成り立つ。

G * E1 * E3 * E5 = E2 * E4 * E4 * E6 * E7 * E7

これは署名でも同様に成り立つため、目的のシグネチャを算出することができる。

#!/usr/bin/env python3
from sympy import *

n = 0xa9e7da28ebecf1f88efe012b8502122d70b167bdcfa11fd24429c23f27f55ee2cc3dcd7f337d0e630985152e114830423bfaf83f4f15d2d05826bf511c343c1b13bef744ff2232fb91416484be4e130a007a9b432225c5ead5a1faf02fa1b1b53d1adc6e62236c798f76695bb59f737d2701fe42f1fbf57385c29de12e79c5b3
signs = [
    0x17bb21949d5a0f590c6126e26dc830b51d52b8d0eb4f2b69494a9f9a637edb1061bec153f0c1d9dd55b1ad0fd4d58c46e2df51d293cdaaf1f74d5eb2f230568304eebb327e30879163790f3f860ca2da53ee0c60c5e1b2c3964dbcf194c27697a830a88d53b6e0ae29c616e4f9826ec91f7d390fb42409593e1815dbe48f7ed4,
    0x3ea73715787028b52796061fb887a7d36fb1ba1f9734e9fd6cb6188e087da5bfc26c4bfe1b4f0cbfa0d693d4ac0494efa58888e8415964c124f7ef293a8ee2bc403cad6e9a201cdd442c102b30009a3b63fa61cdd7b31ce9da03507901b49a654e4bb2b03979aea0fab3731d4e564c3c30c75aa1d079594723b60248d9bdde50,
    0x9444e3fc71056d25489e5ce78c6c986c029f12b61f4f4b5cbd4a0ce6b999919d12c8872b8f2a8a7e91bd0f263a4ead8f2aa4f7e9fdb9096c2ea11f693f6aa73d6b9d5e351617d6f95849f9c73edabd6a6fde6cc2e4559e67b0e4a2ea8d6897b32675be6fc72a6172fd42a8a8e96adfc2b899015b73ff80d09c35909be0a6e13a,
    0x2b7a1c4a1a9e9f9179ab7b05dd9e0089695f895864b52c73bfbc37af3008e5c187518b56b9e819cc2f9dfdffdfb86b7cc44222b66d3ea49db72c72eb50377c8e6eb6f6cbf62efab760e4a697cbfdcdc47d1adc183cc790d2e86490da0705717e5908ad1af85c58c9429e15ea7c83ccf7d86048571d50bd721e5b3a0912bed7c,
    0xa7d5548d5e4339176a54ae1b3832d328e7c512be5252dabd05afa28cd92c7932b7d1c582dc26a0ce4f06b1e96814ee362ed475ddaf30dd37af0022441b36f08ec8c7c4135d6174167a43fa34f587abf806a4820e4f74708624518044f272e3e1215404e65b0219d42a706e5c295b9bf0ee8b7b7f9b6a75d76be64cf7c27dfaeb,
    0x67832c41a913bcc79631780088784e46402a0a0820826e648d84f9cc14ac99f7d8c10cf48a6774388daabcc0546d4e1e8e345ee7fc60b249d95d953ad4d923ca3ac96492ba71c9085d40753cab256948d61aeee96e0fe6c9a0134b807734a32f26430b325df7b6c9f8ba445e7152c2bf86b4dfd4293a53a8d6f003bf8cf5dffd,
    0x927a6ecd74bb7c7829741d290bc4a1fd844fa384ae3503b487ed51dbf9f79308bb11238f2ac389f8290e5bcebb0a4b9e09eda084f27add7b1995eeda57eb043deee72bfef97c3f90171b7b91785c2629ac9c31cbdcb25d081b8a1abc4d98c4a1fd9f074b583b5298b2b6cc38ca0832c2174c96f2c629afe74949d97918cbee4a
]

x1 = symbols('x1')
x2 = symbols('x2')
x3 = symbols('x3')
x4 = symbols('x4')
x5 = symbols('x5')
x6 = symbols('x6')
x7 = symbols('x7')

eq1 = Eq(x2, 1)
eq2 = Eq(x2 + x5, 0)
eq3 = Eq(x2 + x5 * 2 + x7, 1)
eq4 = Eq(x1 + x5 + x7, 0)
eq5 = Eq(x1 * 3 + x4 + x7, 1)
eq6 = Eq(x1 + x3 + x4, 0)
eq7 = Eq(x3 * 2 + x4 + x6, 1)
eq8 = Eq(x3 + x6, 0)
eq9 = Eq(x6, 1)
sol = solve([eq1, eq2, eq3, eq4, eq5, eq6, eq7, eq8, eq9],
    [x1, x2, x3, x4, x5, x6, x7])

xs = []
for xi in sol:
    xs.append(int(sol[xi]))

sign = 1
for i in range(len(xs)):
    sign *= pow(signs[i], xs[i], n)
    sign %= n

flag = 'LINECTF{%s}' % hex(sign)[-32:]
print(flag)
LINECTF{a049347a7db8226d496eb55c15b1d840}

ss-puzzle (Crypto)

以下のようにフラグを分割して、xorをしてデータを作成している。

S[0] = FLAG[0:8]
S[1] = FLAG[8:16]
S[2] = FLAG[16:24]
S[3] = FLAG[24:32]
R[0] = FLAG[32:40]
R[1] = FLAG[40:48]
R[2] = FLAG[48:56]
R[3] = FLAG[56:64]

Share[0] = R[0]            + xor(R[1], S[3]) + xor(R[2], S[2]) + xor(R[3],S[1])
Share[1] = xor(R[0], S[0]) + R[1]            + xor(R[2], S[3]) + xor(R[3],S[2])
Share[2] = xor(R[0], S[1]) + xor(R[1], S[0]) + R[2]            + xor(R[3],S[3])
Share[3] = xor(R[0], S[2]) + xor(R[1], S[1]) + xor(R[2], S[0]) + R[3]
Share[4] = xor(R[0], S[3]) + xor(R[1], S[2]) + xor(R[2], S[1]) + xor(R[3],S[0])

また以下のようにして一部データ(9~16バイト目)が壊れた状態になっている。

Share[1] = Share[1][0:8]   + b'\x00'*8       + Share[1][16:24] + Share[1][24:32]

フラグ形式から以下がわかる。

S[0] = b'LINECTF{'

順に割り出していく。

S[0] -> R[0] (Share1)
S[0] -> R[3] (Share4)
R[0] -> S[3] (Share4)
R[3] -> S[2] (Share1)
S[3] -> R[2] (Share1)
S[2] -> R[1] (Share4)
R[2] -> S[1] (Share4)

あとは連結すればフラグになる。

#!/usr/bin/env python3
def xor(a:bytes, b:bytes) -> bytes:
    return bytes(i^j for i, j in zip(a, b))

with open('Share1', 'rb') as f:
    Share1 = f.read()

with open('Share4', 'rb') as f:
    Share4 = f.read()

S = [None]*4
R = [None]*4

S[0] = b'LINECTF{'
R[0] = xor(Share1[:8], S[0])
R[3] = xor(Share4[24:], S[0])
S[3] = xor(Share4[:8], R[0])
S[2] = xor(Share1[24:], R[3])
R[2] = xor(Share1[16:24], S[3])
R[1] = xor(Share4[8:16], S[2])
S[1] = xor(Share4[16:24], R[2])

flag = (b''.join(S) + b''.join(R)).decode()
print(flag)
LINECTF{Yeah_known_plaintext_is_important_in_xor_based_puzzle!!}

Baby crypto revisited (Crypto)

ECDSAの問題。r, s, kの上位64bit、hashの組が100組あり、dを求める必要がある。https://blog.y011d4.com/20210321-line-ctf-writeup/を参考にLLLを使って解く。

#!/usr/bin/env sage
# secp160r1 parameters
p = 0xffffffffffffffffffffffffffffffff7fffffff
K = GF(p)
a = K(0xffffffffffffffffffffffffffffffff7ffffffc)
b = K(0x1c97befc54bd7a8b65acf89f81d4d4adc565fa45)
E = EllipticCurve(K, (a, b))
G = E(0x4a96b5688ef573284664698968c38bb913cbfc82, 0x23a628553168947d59dcc912042351377ac5fb32)
E.set_order(0x0100000000000000000001f4c8f927aed3ca752257 * 0x01)

n = int(E.order())

with open('Babycrypto_revisited_b1f108dea290b83253b80443260b12c3cadc0ed7.txt', 'r') as f:
    lines = f.read().splitlines()

N = len(lines)

r = [0] * N
s = [0] * N
k = [0] * N
h = [0] * N
for i in range(N):
    params = lines[i].split()
    r[i] = int(params[0], 16)
    s[i] = int(params[1], 16)
    k[i] = int(params[2], 16)
    h[i] = int(params[3], 16)

t = [r[i] * inverse_mod(s[i], n) % n for i in range(N)]
u = [(h[i] * inverse_mod(-s[i], n) % n) % n for i in range(N)]

nonce_max = 2**128

B = matrix(QQ, N + 2, N + 2)
B.set_block(0, 0, matrix.identity(N) * n)
B.set_block(N, 0, matrix(QQ, 1, N, t))
B.set_block(N+1, 0, matrix(QQ, 1, N, u))
B[N,N] = nonce_max / n
B[N+1,N+1] = nonce_max

L = B.LLL()
for row in list(L):
    k1 = int(abs(row[0]))
    if k1 != 0 and k1 != nonce_max and k1 < nonce_max:
        x = (k1*s[0] - h[0]) * inverse_mod(r[0], n) % n
        kk = (k1 >> 64) << 64
        assert kk in k
        flag = 'LINECTF{0x%x}' % x
        print(flag)
        break
LINECTF{0xd77d10fec685cbe16f64cba090db24d23b92f824}

Forward-or (Crypto)

暗号処理の概要は以下の通り。

・key: '0'~'3'で構成された20バイト文字列
・cipher = CTFMode(key)
 ・self.cipher = DoubleRoundReducedPresent(key)
  ・self.block_size = 8
  ・self.key_length = 160
  ・self.round = 16
  ・self.cipher0 = Present(key[0:10], self.round)
   ・self.rounds = 16
   ・self.roundkeys = generateRoundkeys80(byte2number(key[0:10]),self.rounds)
  ・self.cipher1 = Present(key[10:20], self.round)
   ・self.rounds = 16
   ・self.roundkeys = generateRoundkeys80(byte2number(key[10:20],self.rounds)
 ・self.nonce = os.urandom(4)
・ciphertext = cipher.encrypt(plain)
 ・self.XorStream(plain)
  ・plain、8バイトごとに以下を処理する。
   ・keystream = self.cipher.encrypt(self.nonce+counter.to_bytes(self.cipher.block_size//2, 'big'))
    nonce+counterの暗号化
    ・ciphertext = self.cipher0.encrypt(nonce+counter)
    ・ciphertext = self.cipher1.encrypt(ciphertext)
   ・keystreamとplainの8バイトのXOR
   ・counter: +1
・暗号化結果、nonceを表示

Presentの内容はあまり気にする必要はない。フラグの先頭はLINECTF{であることから、最初のブロックの平文と暗号の組み合わせがわかる。異なる10バイトのキーで2回暗号化しているので、平文のkey[0:10]で暗号化したものと暗号文のkey[10:20]で復号したものが一致するものを探す。

#!/usr/bin/env python3
from present import Present
from Crypto.Util.strxor import strxor
import itertools

class CTRMode():
    def __init__(self, key, nonce=None):
        self.key = key # 20bytes
        self.cipher = DoubleRoundReducedPresent(key)
        if None==nonce:
            nonce = os.urandom(self.cipher.block_size//2)
        self.nonce = nonce # 4bytes
    
    def XorStream(self, data):
        output = b""
        counter = 0
        for i in range(0, len(data), self.cipher.block_size):
            keystream = self.cipher.encrypt(self.nonce+counter.to_bytes(self.cipher.block_size//2, 'big'))
            if b""==keystream:
                exit(1)

            if len(data)<i+self.cipher.block_size:
                block = data[i:len(data)]
            block = data[i:i+self.cipher.block_size]
            block = strxor(keystream[:len(block)], block)
            
            output+=block
            counter+=1
        return output

    def encrypt(self, plaintext):
        return self.XorStream(plaintext)

    def decrypt(self, ciphertext):
        return self.XorStream(ciphertext)

class DoubleRoundReducedPresent():

    def __init__(self, key):
        self.block_size = 8
        self.key_length = 160 # bits
        self.round = 16
        self.cipher0 = Present(key[0:10], self.round)
        self.cipher1 = Present(key[10:20], self.round)
    
    def encrypt(self, plaintext):
        if len(plaintext)>self.block_size:
            print("Error: Plaintext must be less than %d bytes per block" % self.block_size)
            return b""
        return self.cipher1.encrypt(self.cipher0.encrypt(plaintext))
    
    def decrypt(self, ciphertext):
        if len(ciphertext)>self.block_size:
            print("Error: Ciphertext must be less than %d bytes per block" % self.block_size)
            return b""
        return self.cipher0.decrypt(self.cipher1.decrypt(ciphertext))

def base10_to_4(X):
    if (int(X // 4)):
        return base10_to_4(int(X // 4)) + str(X % 4)
    return str(X % 4)

ciphertext_hex = '3201339d0fcffbd152f169ddcb8349647d8bc36a73abc4d981d3206f4b1d98468995b9b1c15dc0f0'
nonce_hex = '32e10325'

ciphertext = bytes.fromhex(ciphertext_hex)
nonce = bytes.fromhex(nonce_hex)

flag0 = 'LINECTF{'.encode('ascii')
counter = 0
nonce_ctr = nonce + counter.to_bytes(4, 'big')
xor0 = strxor(flag0, ciphertext[:8])

cts = []
pts = []
for c in itertools.product('0123', repeat=10):
    key0 = ''.join(c).encode('ascii')
    key1 = ''.join(c).encode('ascii')

    cipher0 = Present(key0, 16)
    cipher1 = Present(key1, 16)

    ct = cipher0.encrypt(nonce_ctr)
    pt = cipher1.decrypt(xor0)

    cts.append(ct)
    pts.append(pt)

ct_set = set(cts)
pt_set = set(pts)

ct = list(ct_set & pt_set)[0]
index1 = cts.index(ct)
index2 = pts.index(ct)
key0 = base10_to_4(index1).encode('ascii')
key1 = base10_to_4(index2).encode('ascii')
key = key0 + key1
print('[+] key:', key.decode())

cipher = CTRMode(key, nonce)
flag = cipher.decrypt(ciphertext).decode()
print('[*] flag:', flag)

実行結果は以下の通り。

[+] key: 32013230202123003302
[*] flag: LINECTF{|->TH3Y_m3t_UP_1n_th3_m1ddl3<-|}
LINECTF{|->TH3Y_m3t_UP_1n_th3_m1ddl3<-|}