# TMUCTF 2021 Rank: 58th Points: 2254 ## Common Factor (52 solves, 383 pts) :::spoiler **Source Code** ```python= from Crypto.Util.number import * from functools import reduce def encrypt(msg, n): enc = pow(bytes_to_long(msg), e, n) return enc e = 65537 primes = [getPrime(2048) for i in range(5)] n = reduce(lambda a, x: a * x, primes, 1) print(n) x1 = primes[1] ** 2 x2 = primes[2] ** 2 x3 = primes[1] * primes[2] y1 = x1 * primes[2] + x2 * primes[1] y2 = x2 * (primes[3] + 1) - 1 y3 = x3 * (primes[3] + 1) - 1 print(x2 + x3 + y1) print(y2 + y3) with open('flag', 'rb') as f: flag = f.read() print(encrypt(flag, n)) ``` ::: 從題目名稱也也能看出這題要幹嘛 :D。首先,根據定義化簡可得$$x_2+x_3+y_1 = p_2(p_1+p_2)(p_1+1)$$ 以及 $$y_2+y_3 = p_2(p_1+p_2)(p_3+1)-2$$ 因為質數都是隨機取的,所以基本上可以確定 $$p_2 = (x_2+x_3+y_1, n)$$ 另一方面,我們知道 $$p_2(p_1+p_2) \mid (x_2+x_3+y_1, y_2+y_3+2) $$ 並且他們的商是 $(p_1+1, p_3+1)$,可以預期它不會太大。直接窮舉就能得到 $p_1$,從而也知道 $p_3$。 一般來說,RSA 要完全分解才能解密,但是我們並沒有 $p_0, p_4$ 的資訊,因此就想說把它當作 $N = p_1p_2p_3$ 的 RSA (參考 CryptoCTF 2020 Three Raven) :::spoiler **Script** ```python= from Crypto.Util.number import * from functools import reduce from output import * from math import gcd ### x2 + x3 + y1 = p2(p1 + p2)(p1 + 1) ### y2 + y3 + 2 = p2(p1 + p2)(p3 + 1) p2 = gcd(n, a) assert isPrime(p2) _ = gcd(a, b+2) // p2 for i in range(10, 30): if _ % i == 0: p1 = _ // i - p2 if isPrime(p1) and n % p1 == 0: break assert a == p2 * (p1 + p2) * (p1 + 1) p3 = (b+2) // (p2 * (p1 + p2)) - 1 assert (b+2) == p2 * (p1 + p2) * (p3 + 1) and isPrime(p3) fake_phi = (p1 - 1) * (p2 - 1) * (p3 - 1) d = pow(65537, -1, fake_phi) pt = pow(c, d, p1 * p2 * p3) flag = long_to_bytes(pt) print(flag) ``` ::: :::success TMUCTF{Y35!!!__M4Y_N0t_4lW4y5_N33d_4ll_p21M3_f4c70R5} ::: ## 435! (45 solves, 413 pts) :::spoiler **Source Code** ```python= import binascii import hashlib import sys from Crypto.Cipher import AES key = b'*XhN2*8d%8Slp3*v' key_len = len(key) def pad(message): padding = bytes((key_len - len(message) % key_len) * chr(key_len - len(message) % key_len), encoding='utf-8') return message + padding def encrypt(message, key, iv): aes = AES.new(key, AES.MODE_CBC, iv) return aes.encrypt(message) h = hashlib.sha256(key).hexdigest() hidden = binascii.unhexlify(h)[:10] message = b'CBC (Cipher Blocker Chaining) is an advanced form of block cipher encryption' + hidden with open('flag', 'rb') as f: IV = f.read().strip(b'TMUCTF{').strip(b'}') print(binascii.hexlify(encrypt(pad(message), key, IV))) ``` ::: 題目給了 AES-CBC 的部分 key,然後用 flag 當作 IV,加密一個跟 key 有關的訊息,然後密文也只有給一部分。 注意到 key 只有被遮掉三個 character,並且密文後一個半個 block 都沒被碼掉,所以能去暴搜所有可能的 key,然後看下面的等式有沒有可能成立:$$ AES_{ECB}(msg[i] \oplus block[i-1]) = block[i] $$ 具體的方式是去看 $$ AES_{ECB}^{-1}(block[i]) \oplus msg[i] $$ 跟 $block[i-1]$ 已知的 bytes 有沒有一致。若有的話,代表是可能的 key。 :::spoiler **Find key** ```python= import binascii import hashlib from Crypto.Cipher import AES from Crypto.Util.number import long_to_bytes key_len = 16 def pad(message): padding = bytes((key_len - len(message) % key_len) * chr(key_len - len(message) % key_len), encoding='utf-8') return message + padding def Decrypt(c, k): aes = AES.new(k, AES.MODE_ECB) return aes.decrypt(c) # block1 = binascii.unhexlify("************1b3*******9f43fd6634") ind = [6, 11, 12, 13, 14, 15] block1 = { 6: b'\x1b', 11: b'\x9f', 12: b'\x43', 13: b'\xfd', 14: b'\x66', 15: b'\x34' } block2 = binascii.unhexlify("1f3ef3fab2bbfc838b9ef71867c3bcbb") chars = [chr(_) for _ in range(128)] for char1 in chars: for char2 in chars: for char3 in chars: key = char1 + "XhN2" + char2 + "8d%8Slp3" + char3 + "v" key = key.encode() h = hashlib.sha256(key).hexdigest() hidden = binascii.unhexlify(h)[:10] message = b'CBC (Cipher Blocker Chaining) is an advanced form of block cipher encryption' + hidden message = pad(message) message = message[-16:] decrypted_block2 = Decrypt(block2, key) check = True for i in ind: _ = decrypted_block2[i] ^ block1[i][0] if message[i:i+1] != long_to_bytes(_): check = False if check: print("Found!") print(key) ``` ::: 倒著解密回去就可以知道 $flag\oplus msg[0]$ 的值,也就拿到 flag 啦! :::spoiler **Get flag** ```python= import binascii import hashlib from Crypto.Cipher import AES from Crypto.Util.number import long_to_bytes key = b'0XhN2!8d%8Slp3Ov' key_len = len(key) def pad(message): padding = bytes((key_len - len(message) % key_len) * chr(key_len - len(message) % key_len), encoding='utf-8') return message + padding h = hashlib.sha256(key).hexdigest() hidden = binascii.unhexlify(h)[:10] message = b'CBC (Cipher Blocker Chaining) is an advanced form of block cipher encryption' + hidden message = pad(message) def decrypt(c, k): aes = AES.new(k, AES.MODE_ECB) return aes.decrypt(c) def xor_blocks(b1, b2): res = b'' for x, y in zip(b1, b2): res += long_to_bytes(x^y) return res current_block = binascii.unhexlify("1f3ef3fab2bbfc838b9ef71867c3bcbb") for i in range(6): current_block = decrypt(current_block, key) j, k = -16 * (i+1), -16 * i if k: c = message[j: k] else: c = message[j:] current_block = xor_blocks(current_block, c) print(current_block) ``` ::: :::success TMUCTF{Y0U_D3CrYP73D_17} ::: ## Broken RSA (21 solves, 482 pts) :::spoiler **Source Code** ```python= from Crypto.Util.number import * e = 65537 with open('n', 'rb') as f: n = int(f.read()) with open('secret', 'rb') as f: secret_msg = f.read() pads = [b'\x04', b'\x02', b'\x00', b'\x01', b'\x03'] with open('out.txt', 'w') as f: for i in range(len(pads)): for j in range(len(pads)): msg = pads[j] * (i + 1) + b'TMUCTF' + pads[len(pads) - j - 1] * (i + 1) enc = pow(bytes_to_long(msg), e, n) f.write(str(enc) + '\n') enc = pow(bytes_to_long(secret_msg), e, n) f.write(str(enc)) ``` ::: 我們拿到了很多個已知明文的加密結果以及加密後的 flag,算 gcd 就能把 $n$ 給找回來,但也僅限於此。關鍵點是要去看 $n$ 的二進制,然後會發現中間有很多 $0$,頭尾看起來是形如 $$2^{k}\triangle + (2^{k}-1-\triangle) = (2^{k}-1)(\triangle+1)$$ 的東西,於是 $n$ 會被 $2^{k-1}$ 整除,剩下就輕鬆ㄌ! :::spoiler **Script** ```python= from Crypto.Util.number import * with open('output.txt', 'r') as f: for _ in range(25): f.readline() enc = int(f.readline().strip()) from tmp import n p = (1 << 1279) - 1 q = n // p assert isPrime(p) and isPrime(q) flag = pow(enc, pow(65537, -1, (p-1)*(q-1)), n) print(long_to_bytes(flag)) ``` ::: :::success TMUCTF{Ju57_4n07h3r_R54_ch4ll3n63!_C0u1d_y0u_50lv3_17?!?} ::: ## Baby Encoder (17 solves, 489 pts) :::spoiler **Source Code** ```python= from Crypto.Util.number import bytes_to_long def displace(a, base): res = [] for i in range(base): if base + i >= len(a): for j in range(base - 1, i - 1, -1): res.append(a[j]) return res res.append(a[base + i]) res.append(a[i]) for j in range(len(a) - 1, 2 * base - 1, -1): res.append(a[j]) return res def flag_encoder(flag): encoded_flag = [] n = len(flag) for i in range(n): encoded_flag.append(ord(flag[i]) ^ ord(flag[i - 1])) for i in range(n): encoded_flag[i] ^= encoded_flag[n - i - 1] a = [] for i in range(0, n, 3): a.append(encoded_flag[i] + encoded_flag[i + 1]) a.append(encoded_flag[i + 1] + encoded_flag[i + 2]) a.append(encoded_flag[i + 2] + encoded_flag[i]) encoded_flag = a for i in range(1, n): if i % 6 == 0: encoded_flag = displace(encoded_flag, i) encoded_flag = ''.join(chr(encoded_flag[i]) for i in range(n)) return encoded_flag with open('flag', 'rb') as f: flag = f.read().decode('UTF-8') print(str(bytes_to_long(flag_encoder(flag).encode()))) ``` ::: 不難注意到 displace 是將陣列重排,我們也不用管它具體怎麼操作,把 identity 丟進去給它排,就能輕鬆的逆向回來。 :::spoiler **Reverse displace** ```python= def rev_displace(a, base): identity = [i for i in range(n)] inv = displace(identity, base) res = [0 for i in range(n)] for _ in range(n): res[inv[_]] = a[_] return res for i in range(n-1, 0, -1): if i % 6 == 0: encoded_flag = rev_displace(encoded_flag, i) ``` ::: 接著是將陣列三個三個一組,然後換成兩兩相加的結果,也就是 $$ (a,b,c) \rightarrow (a+b, b+c, c+a) $$ 很明顯地,$$ (d,e,f) \rightarrow \left(\frac{d+f-e}{2},\frac{e+d-f}{2},\frac{f+e-d}{2}\right) $$ 是它的逆操作。在前一步是將陣列頭尾配對,依序給它們做 XOR,那因為 XOR 是 involution,所以直接反著 XOR 回去就能得到原本的陣列。 :::spoiler ```python= for i in range(0, n, 3): encoded_flag[i] = (a[i] + a[i+2] - a[i+1]) // 2 encoded_flag[i+1] = (a[i+1] + a[i] - a[i+2]) // 2 encoded_flag[i+2] = (a[i+2] + a[i+1] - a[i]) // 2 for i in range(n-1, -1, -1): encoded_flag[i] ^= encoded_flag[n - i - 1] ``` ::: 最後由於陣列是透過 flag 相鄰的字元 XOR 得來的,我們枚舉所有可能的開頭,然後看解出來的 flag 有沒有 TMUCTF 即可。 :::spoiler **Get Flag** ```python= for c in range(128): possible_flag = str(chr(c)) for i in range(1, n): next_char = chr(ord(possible_flag[-1]) ^ encoded_flag[i]) possible_flag += next_char if "TMUCTF" in possible_flag: print(possible_flag) ``` ::: :::success TMUCTF{0h!_3nc0d1n6_15_n07_4lw4y5_4_600d_1d34_70_h1d3_7h3_fl46__d0_y0u_46r33?} ::: ## Signed Flag (15 solves, 492 pts) :::spoiler **Source Code** ```python= from string import ascii_uppercase, ascii_lowercase, digits from random import randrange, choice from Crypto.PublicKey import DSA from hashlib import sha1 from gmpy2 import xmpz, to_binary, invert, powmod, is_prime def gen_rand_str(size=40, chars=ascii_uppercase + ascii_lowercase + digits): return ''.join(choice(chars) for _ in range(size)) def gen_g(p, q): while True: h = randrange(2, p - 1) exp = xmpz((p - 1) // q) g = powmod(h, exp, p) if g > 1: break return g def keys(g, p, q): d = randrange(2, q) e = powmod(g, d, p) return e, d def sign(msg, k, p, q, g, d): while True: r = powmod(g, k, p) % q h = int(sha1(msg).hexdigest(), 16) try: s = (invert(k, q) * (h + d * r)) % q return r, s except ZeroDivisionError: pass if __name__ == "__main__": print("\n") print(".___________..___ ___. __ __ ______ .___________. _______ ___ ___ ___ __ ") print("| || \/ | | | | | / || || ____| |__ \ / _ \ |__ \ /_ | ") print('`---| |----`| \ / | | | | | | ,----"`---| |----`| |__ ) | | | | | ) | | | ') print(" | | | |\/| | | | | | | | | | | __| / / | | | | / / | | ") print(" | | | | | | | `--' | | `----. | | | | / /_ | |_| | / /_ | | ") print(" |__| |__| |__| \______/ \______| |__| |__| |____| \___/ |____| |_| ") steps = 10 for i in range(steps): key = DSA.generate(2048) p, q = key.p, key.q print("\n\nq =", q) g = gen_g(p, q) e, d = keys(g, p, q) k = randrange(2, q) msg1 = gen_rand_str() msg2 = gen_rand_str() msg1 = str.encode(msg1, "ascii") msg2 = str.encode(msg2, "ascii") r1, s1 = sign(msg1, k, p, q, g, d) r2, s2 = sign(msg2, k, p, q, g, d) print("\nsign('" + msg1.decode() + "') =", s1) print("\nsign('" + msg2.decode() + "') =", s2) if i == (steps - 1): with open('flag', 'rb') as f: flag = f.read() secret = flag else: secret = gen_rand_str() secret = str.encode(secret, "ascii") r3, s3 = sign(secret, k, p, q, g, d) print("\nsign(secret) =", s3, r3) h = input("\nGive me SHA1(secret) : ") if h == str(int(sha1(secret).hexdigest(), 16)): print("\nThat's right, the secret is", secret.decode()) else: print("\nSorry, I cannot give you the secret. Bye!") break ``` ::: 簡單來說,題目每次會給兩個訊息跟它們的 DSA 簽章,接著在給個簽章,問你說知不知道它簽了什麼訊息。那本身 DSA 沒有問題,有問題的是它重複使用了 nonce,也就是說我們有兩條式子:$$ \begin{align} s_1 & = k^{-1}(h_1+dr) \pmod{q} \\ s_2 & = k^{-1}(h_2+dr) \pmod{q} \end{align} $$ 於是我們有 $$ k = \frac{h_1-h_2}{s_1-s_2} \pmod{q} $$ 解得 $k$ 後,對於新的簽章 $s_3$,我們便可以很輕易地算出 $h_3$,$$ h_3 = k(s_3-s_1)+h_1 \pmod{q} $$ :::spoiler **Script** ```python= from pwn import * from hashlib import sha1 bb = b"\') = " print(bb) def connect(): r = remote('185.235.41.166', 5000) return r def sign(r): r.recvuntil("q = ") q = int(r.recvline().strip()) r.recvuntil("sign") _ = r.recvline()[2:].strip().split(bb) h1 = int(sha1(_[0]).hexdigest(), 16) s1 = int(_[1]) r.recvuntil("sign") _ = r.recvline()[2:].strip().split(bb) h2 = int(sha1(_[0]).hexdigest(), 16) s2 = int(_[1]) r.recvuntil("sign(secret) = ") _ = r.recvline().strip().split(b" ") s3 = int(_[0]) r3 = int(_[1]) ### s1 - s2 = k^-1 (h1 - h2) ### s3 - s1 = k^-1 (h3 - h1) inv_k = pow(h1 - h2, -1, q) * (s1 - s2) % q h3 = (pow(inv_k, -1, q) * (s3 - s1) + h1) % q c.sendline(str(h3)) c = connect() step = 10 for i in range(step): sign(c) c.interactive() ``` ::: :::success TMUCTF{7h15_w45_my_m1574k3__1_f0r607_7h47_1_5h0uld_n3v3r_516n_mul71pl3_m3554635_w17h_4_dupl1c473_k3y!!!} :::