# LITCTF 2021 ## 7 More Caesar Salads (397 solves, 106 pts) Here’s an easy Crypto problem, try to decipher mshn{Dlsjvtl_Av_Jyfwavnyhwof} 很明顯是 Caesar cipher,直接丟 dcode.fr :::success flag{Welcome_To_Cryptography} ::: ## Zzz (241 solves, 110 pts) Oh no, somebody shuffled the alphabet, can you help us restore the message? Note that the flag is not wrapped with flag{}, so you will have to do that yourself. 題目直接表明是 Substitution cipher,再次丟 dcode.fr XD :::success flag{HOW_DO_YOU_KNOW_YOU_ARE_NOT_SLEEPING } ::: ## RSA Improved (161 solves, 116 pts) The standard RSA only uses 2 prime factors, way too insecure. Let’s improve it by using more factors! More is better, ...right? ```python= from Crypto.Util.number import getPrime import math # string to decimal pt = int(open('flag.txt','rb').read().hex(),16); primes = []; n = 1 # euler totient phi = 1 # public key e = 65537 while math.log2(n) < 640: primes.append(getPrime(32)); n *= (primes[-1]); phi *= (primes[-1] - 1); # No duplicates assert(len(primes) == len(list(set(primes)))); # private key d = pow(e,-1,phi); # cipher text ct = pow(pt,e,n); def decrypt(ct): # decode ciphertext for plaintext pt = pow(ct,d,n); # convert decimal back to string return bytes.fromhex(hex()[2:]).decode("utf-8"); print("n = " + str(n)); print("e = 65537"); print("ct = " + str(ct)) ``` 可以看到它的 $n$ 是由一堆 32-bits 的質數組成,他們小到直接用 factordb 就會找到所有因數 ... ```python= from output import * from factordb.factordb import FactorDB f = FactorDB(n) f.connect() primes = f.get_factor_list() phi = 1 for p in primes: phi *= (p-1) pt = pow(ct, pow(e, -1, phi), n) from Crypto.Util.number import long_to_bytes flag = long_to_bytes(pt) print(flag) ``` ## Geometry is Fun! (104 solves, 126 pts) 通靈題 ... 聽說是將所有三角形面積算出來,拼起來就是 flag 了 ... ## Scottish Flag (87 solves, 131 pts) Was is it Scottish or British? Oh who remembers these days ```python= import random import binascii import math import sympy def d(x0,x1,y0,y1): return (x0-y0)**2 + (x1-y1)**2 class PRNG: def __init__(self,seed0,seed1): self.seed0 = seed0; self.seed1 = seed1; self.L = 1; # Returns a random number between [0,x) def rand(self,x0,x1): return d(self.seed0, self.seed1, x0, x1) def str2Dec(str): return int(binascii.hexlify(str.encode("utf-8")),16); flag = open('flag.txt','rb').read().decode("utf-8"); flag0 = flag[:len(flag) // 2]; flag1 = flag[len(flag) // 2:len(flag)]; ct0 = str2Dec(flag0); ct1 = str2Dec(flag1); g = PRNG(ct0,ct1); a0 = 1000000000000000000; a1 = random.randint(0,1000000000000000000); print(g.rand(0,a0)); print(g.rand(a1,0)); print(g.rand(a1,a0)); ``` 所以我們有以下三條等式: $$ \begin{align} g(0, a_0) &= x^2 +\left(y-a_0\right)^2 \\ g(a_1, 0) &= \left(x-a_1\right)^2 +y^2 \\ g(a_1, a_0) &= \left(x-a_1\right)^2 +\left(y-a_0\right)^2 \end{align} $$ 其中 $a_0$ 是已知的。後兩式相減得到 $y$ 的一次方程式 $$2a_0y-a_0^2 = g(a_1, 0) - g(a_1, a_0), \quad y = \frac{g(a_1, 0) - g(a_1, a_0) + a_0^2}{2a_0}$$ 代回第一條就可以得到 $x$,$$x = \sqrt{g(0, a_0)-\left(y-a_0\right)^2}$$ ```python= from output import * ### a = x^2+(y-a0)^2 ### b = (x-a1)^2+y^2 ### c = (x-a1)^2+(y-a0)^2 a0 = 1000000000000000000 y = (b - c + a0 ** 2) // (2 * a0) from decimal import * getcontext().prec = 3000 x = int(Decimal(a - (y-a0) ** 2) ** (Decimal(1)/Decimal(2))) import binascii flag1 = binascii.unhexlify(hex(x)[2:]) print(flag1) flag2 = binascii.unhexlify(hex(y)[2:]) print(flag2) ``` :::success flag{6r1t15h_cr0s5_mak35_g00d_pro6I3m} ::: ## Leftovers (53 solves, 152 pts) Leftovers are bad, but I guess at least they help the environment by not wasting food. ```python= import random import binascii import math import sympy random.seed(1337) class PRNG: def __init__(self,seed): self.seed = seed; self.L = 1; # Returns a random number between [0,x) def rand(self,x): return self.seed % sympy.prevprime(x); def str2Dec(str): return int(binascii.hexlify(str.encode("utf-8")),16); flag = open('flag.txt','rb').read().decode("utf-8"); ct = str2Dec(flag); NUMBER_OF_DIGITS = 128; assert(math.log10(ct) <= 128) g = PRNG(ct); res = []; for i in range(12): x = random.randint(1,4e10); res.append(g.rand(x)); print(res); ``` 這隻程式有先固定 random seed,因此迴圈中的 $x$ 是固定的,從而 PRNG 回傳的是 flag 除以一個已知的 $p$ 的餘數。用 CRT 就能找到 $$x \pmod{\prod_{i=1}^{12}{p_i}}$$ 已知 flag 有 128 位數,我們必須要有 $$10^{128} \le x + k\prod_{i=1}^{12}{p_i} < 10^{129}$$ ```python= import random import sympy random.seed(1337) primes = [] product = 1 for i in range(12): x = random.randint(1, 4e10) primes.append(sympy.prevprime(x)) product *= primes[-1] from output import * crt = 0 for p, r in zip(primes, res): crt += (r * pow(product // p, -1, p) * (product // p)) crt %= product for i in range(12): assert crt % primes[i] == res[i] from Crypto.Util.number import long_to_bytes for _ in range(50000): try: flag = long_to_bytes(crt + _ * product) if b"flag" in flag: print(flag) except: continue ``` :::success flag{ch1nese_f00d_l3ft0v3r_th30r3m_1s_v3ry_d3l1c10u5} ::: ## Merkle-Heavenwoman (19 solves, 227 pts) Do you know that you can make a cryptosystem from the knapsack problem? ```python= import random import math def makeKey(n): privKey = [random.randint(1,4 ** n)]; for i in range(1,n): privKey.append(random.randint(2 * privKey[-1],4 ** (n + i))); q = random.randint(privKey[-1] + 1,2 * privKey[-1]); r = random.randint(privKey[-1] + 1,2 * privKey[-1]); pubKey = []; for i in range(0,n): if(i < (n // 2)): pubKey.append(privKey[i] ^ q); else: pubKey.append(privKey[i] ^ r); return privKey,q,r,pubKey; def enc(msg,pubKey): n = len(pubKey); cipher = 0; i = 0; a = 0; b = 0; for bit in msg: cipher ^= (int(bit) * pubKey[i]); if(i < (n // 2)): a ^= int(bit); else: b ^= int(bit); i += 1; return a,b,bin(cipher)[2:]; def dec(msg,privKey,q,r,a,b): msg ^= (q * a) ^ (r * b); pt = ""; n = len(privKey) for i in range(n - 1,-1,-1): highestBit = 1 << (int)(math.log2(privKey[i])); pt = ('0','1')[(msg & highestBit) != 0] + pt; msg ^= ((msg & highestBit) != 0) * privKey[i]; return bytes.fromhex(hex(int(pt,2))[2:]).decode("utf-8"); flag = open('flag.txt','rb').read(); binary = bin(int(flag.hex(),16))[2:]; keyPair = makeKey(len(binary)); print(keyPair[3]); ct = enc(binary,keyPair[3]); print(int(ct[2],2)); ``` 這題是 Merkle-Hellman 的變形,唯一的區別是 pubKey[i] 都沒有 mod 一個大質數。又因為 2*privKey[i] < privKey[i+1],當我們將 pubKey 們用二進位表示時,能夠根據 pubKey[i] 與 pubKey[i+1] 第一個不一樣的 bit 決定 q 或 r 對應的 bit 值。如此一來,我們便能夠輕易地取得偽 privKey,從而解密。 ```python= from output import * import math highest_bit = 0 for key in public_key: highest_bit = max(highest_bit, int(math.log2(key)) + 1) ### Goal: find q, r such that 2 * (public_key[i] ^ q) < 2 * public_key[i+1] n = len(public_key) # n % 2 == 1 public_key = public_key[::-1] current_i = 0 current_bit = 1 << highest_bit q = 0 while current_i < n - n // 2: ### Find the first different bit if public_key[current_i] & current_bit != public_key[current_i + 1] & current_bit: if public_key[current_i] & current_bit == 0: q ^= current_bit current_i += 1 else: if public_key[current_i] & current_bit == current_bit: q ^= current_bit current_bit = current_bit // 2 r = 0 for i in range(highest_bit, int(math.log2(current_bit)), -1): test_bit = 1 << i if public_key[current_i] & test_bit == test_bit: r ^= test_bit while current_i < n - 1: if public_key[current_i] & current_bit != public_key[current_i + 1] & current_bit: if public_key[current_i] & current_bit == 0: r ^= current_bit current_i += 1 else: if public_key[current_i] & current_bit == current_bit: r ^= current_bit current_bit = current_bit // 2 for i in range(n): if i < n - n // 2: public_key[i] = public_key[i] ^ q else: public_key[i] = public_key[i] ^ r for i in range(n - 1): assert public_key[i] > 2 * public_key[i+1] def dec(msg, privKey, q, r, a, b): msg ^= (q * a) ^ (r * b); pt = ""; n = len(privKey) for i in range(n - 1,-1,-1): highestBit = 1 << (int)(math.log2(privKey[i])); pt = ('0','1')[(msg & highestBit) != 0] + pt; msg ^= ((msg & highestBit) != 0) * privKey[i]; return bytes.fromhex(hex(int(pt,2))[2:]).decode("utf-8"); public_key = public_key[::-1] for a in range(2): for b in range(2): try: flag = dec(ct, public_key, q, r, a, b) print(flag) except: pass ``` :::success flag{why_n0t_u53_nph4rd_f0r_3ncypt10n} ::: ## Filter Collector (9 solves, 303 pts) I look cute in Instagram Filters. Let’s try to collect some more nc filtercollector.litctf.live 1337 ```python= #!/usr/bin/python3 from Crypto.Util.number import getPrime import random import math import cmath Welcome = "Instagram filters are fun, aren't they?" print(Welcome); flag = int(open('flag.txt','rb').read().hex(),16); print(flag) k = 7 p = int(input("Input your favorite mod: ")); assert(p * p < flag); # Divides tot randomly into n parts def get_partition(tot,n): partitions = [tot]; for i in range(n - 1): partitions.append(random.randint(0,tot)); partitions.sort() for i in range(n - 1,0,-1): partitions[i] -= partitions[i - 1]; return partitions def gen_poly(partitions,n): poly = []; cnt = 0 for i in range(n): if(i % k == 0): poly.append(partitions[cnt]); cnt += 1; else: poly.append(random.randint(0,p - 1)); assert(cnt == len(partitions)); return poly def hash(poly,x): res = 0; for i,c in enumerate(poly): res += c * pow(x,i,p) % p; return res % p; partitions = get_partition(flag,(199 // k) + 1); poly = gen_poly(partitions,200); for i in range(k): x = int(input("Input the a number: ")); y = hash(poly,x); print("The hash of the number under your mod filter is " + str(y)); ``` 由代碼可知,server 會生成一個 $199$ 次的多項式 $$f(x) = \sum\limits_{i=0}^{199}a_{i}x^{i}$$ 滿足 $$a_0+a_7+\cdots+a_{196} = flag$$ 而我們能夠做的事情是給定 $p$ 跟 $x_1, x_2, \dots, x_7$,然後拿到 $f\left(x_1\right), f\left(x_2\right), \dots, f\left(x_7\right)$ 模 $p$ 的值。這有沒有模 $p$ 其實沒什麼差,我們就是要代所有的 $7$ 次方根進去,然後線性組合會得到 $flag$ 模 $p$。 假設 $g$ 是非 $1$ 的 $7$ 次方根,那麼 $$0 = g^7 - 1 = \left(g-1\right)\left(\sum\limits_{i=0}^{6}{g^{i}}\right) \rightarrow \sum\limits_{i=0}^{6}{g^{i}} = 0 $$ 因此 $$\sum\limits_{i=1}^{7}f(g^{i}) = \sum\limits_{i=0}^{199}a_{i}\left(\sum\limits_{j=1}^{7}{g^{ij}}\right) = 7*flag$$ 因為有規定 $p^2 < flag$,我們要連到 server 數次拿到 $flag$ 模 $p$ 的值。 ```python= from pwn import * from Crypto.Util.number import isPrime, long_to_bytes primes = [] generators = [] for p in range(7001, 14001, 7): if isPrime(p): for i in range(2, p): if pow(i, 7, p) == 1: primes.append(p) _ = [pow(i, j, p) for j in range(7)] generators.append(_) break continue res = [] for p, g in zip(primes, generators): r = process("filter_collector.py") r.recvuntil("favorite mod: ") r.sendline(str(p)) residues = [] for i in range(7): r.recvuntil("Input the a number: ") r.sendline(str(g[i])) r.recvuntil("filter is ") residues.append(int(r.recvline().strip())) r.close() _ = (sum(residues) * pow(7, -1, p)) % p res.append(_) product = 1 for p in primes: product *= p crt = 0 for p, r in zip(primes, res): crt += r * pow(product // p, -1, p) * (product // p) crt %= product flag = long_to_bytes(crt) print(flag) ``` :::success flag{w3_mu5t_un1t3_th3_r00t5_t0g3th3r} ::: ## Tree Hash (9 solves, 303 pts) ```python= #!/usr/bin/python3 from sympy import prevprime from random import randint import sys #init sys.setrecursionlimit(1000) #constants n = 100 m = prevprime(n ** (n - 2)) k = 65537 w = randint(1, n) mx = 80 #less than log256(m) flagkey = b"This piece of text is the flag key wow omg!" flag = open("./flag.txt", "r").read() #hash funcs def hexToStr(s): try: return bytes.fromhex(s).rstrip(b'\x00') except: return "" def strToInt(s): return int.from_bytes(s, "little") def intHash(x): return pow(x, k, m) def intToArray(x): a = [] for i in range(n - 2): a.append(x % n) x //= n return a def arrayToTree(a): d = [0] * n for i in a: d[i] += 1 a.append(a[-1]) j, l = 0, 0 e = [] for i in a: while d[j] != 0 or i == j: j += 1 if d[l] != 0 or i == l: l = j e.append((i, l)) d[i] -= 1 d[l] -= 1 if d[i] == 0 and i < j: l = i return e def dfs(g, c, p): ret = c for i in g[c]: if i != p: ret = max(ret, dfs(g, i, c)) return ret def treeHash(e): g = [[] for i in range(n)] for i in e: g[i[0]].append(i[1]) g[i[1]].append(i[0]) ret = 1 for i in e: ret *= (dfs(g, i[0], i[1]) + dfs(g, i[1], i[0]) + w) return ret def H(s): x = strToInt(s) y = intHash(x) a = intToArray(y) e = arrayToTree(a) return treeHash(e) #interaction print("Welcome to my TreeHash generator.") print(f"You can input any string of length up to {mx} chars, and I'll give you its hash!") print("Note: trailing null bytes are stripped.\n") print("I am testing its security, so if you can generate a collision with the flag key I'll give you the flag!") print(f"The flag key is: {str(flagkey)[1:]}\n") while(1): s = input("Input a hex encoded string: ") print('') s = hexToStr(s) if len(s) == 0: print("Invalid hex format.\n") continue if len(s) > mx: print("String length too large.\n") continue print(f"String {str(s)[1:]} has hash: \n{H(s)}\n") if s == flagkey: print("This is just the flag key smh.\n") elif H(s) != H(flagkey): print("The string is not a collision.\n") else: print("Wtmoo you found a collision!") print(f"The flag is: {flag}") break ``` 我們的目標是求出 $s$ 使得 $$H(s)=H(flag\_key)$$ 一個很直覺的想法是讓 $$dfs(g, i[0], i[1]) = dfs(g, i'[0], i'[1]), dfs(g, i[1], i[0]) = dfs(g, i'[1], i'[0])$$ 不難發現 $dfs$ 是去算將 $cp$ 這條邊砍掉後,$c$ 的連通塊中最大的下標。因此兩個 $dfs$ 代表的是將某條邊砍掉後,兩個連通塊中個別最大的下標。因此問題便轉換成,給定 $n-1$ 組 $\{(a_i, b_i)\}$,我們是否能找到一棵樹使得,依次砍掉 $n-1$ 條邊,蒐集兩個連通塊個別最大的下標組成的數對,它們形成的集合恰恰好是 $\{(a_i, b_i)\}$ 不妨假設 $a_i \le a_{i+1} < b_j = n-1$,現在考慮一棵像竹子的樹,且將 $n$ 放在最右邊,我們希望由左而右砍掉邊,左邊最大的數恰恰是 $a_i$。作法很簡單,如果 $a_{i-1} < a_i$,我們就把 $a_i$ 直接放上去;否則,用隨便一個沒用過的小數字替代 $a_{i-1}$ 這個點,然後把 $a_{i-1}$ 接回去。 ```python= ### Construct a path-like tree such that the corresponding ### treehash equals to that of flag key cnt, d, parent ,visited = [0] * n, [0] * n, [0] * n, [False] * n for i in a: cnt[i] += 1 j, prev, y = 0, -1, -1 for i in range(n-1): if cnt[i] > 0: if prev != -1: d[i] += 1 d[prev] += 1 parent[prev] = i else: y = i # record the leaf with least ind prev = i cnt[i] -= 1 visited[i] = True tmp = [] while cnt[i] > 0: while visited[j]: j += 1 tmp.append(j) cnt[i] -= 1 visited[j] = True shuffle(tmp) for v in tmp: d[v] += 1 d[prev] += 1 parent[prev] = v prev = v d[prev] += 1 d[n - 1] += 1 parent[prev] = n - 1 parent[n - 1] = -1 ``` 下一個挑戰是將樹轉回陣列,那這邊它在做的其實是 Prufer code, ```python= Prufer = [] j = y # the leaf with least ind for i in range(n - 2): x = parent[j] Prufer.append(x) d[x] -= 1 # if y becomes a leaf with ind_y < ind_x, then it must be the next leaf with least ind if d[x] == 1 and x < y: j = x else: y += 1 while d[y] != 1: y += 1 j = y ``` 最後將 Prufer 這個陣列轉回字串 ```python= res = 0 for i, j in enumerate(Prufer): res += j * (n ** i) num = pow(res, pow(k, -1, m - 1), m) s = num.to_bytes((7 + num.bit_length()) // 8, 'little') payload = s.hex() ``` 注意到題目有要求 $s$ 不能太長,所以上述的做法才會隨機 shuffle 準備加入的小數字,讓最終換回來的 $num$ 不會太大。 :::success flag{crypt0_1s_b4s1c4lly_cp_4nyw4y} ::: ## The Oracle's Prophecy (1 solves, 463 pts) 似乎是 AES-PCBC Adjacent Swap Attack,之後有空再研究。