# ImaginaryCTF2025 - Crypto > Đây là các chall mình giải được trong quá trình thi giải này cùng team aespaFanClub. > Shout out to others member for trying their best and reached the top 15 of the world, ranked 1st University of Technology. ## leaky-rsa :::success ![image](https://hackmd.io/_uploads/HJRRJ-hqxl.png) ::: :::spoiler `chall.py` ```python= #!/usr/local/bin/python3 import json from Crypto.Util.number import getPrime from Crypto.Cipher import AES from Crypto.Util.Padding import pad from secrets import randbelow, token_bytes from hashlib import sha256 with open('flag.txt') as f: flag = f.read() p = getPrime(512) q = getPrime(512) n = p * q e = 65537 d = pow(e, -1, (p-1)*(q-1)) key_m = randbelow(n) key_c = pow(key_m, e, n) key = sha256(str(key_m).encode()).digest()[:16] iv = token_bytes(16) ct = AES.new(key, AES.MODE_CBC, IV=iv).encrypt(pad(flag.encode(), 16)) print(json.dumps({'n': n, 'c': key_c, 'iv': iv.hex(), 'ct': ct.hex()})) def get_bit(n, k): return (n >> k) % 2 for _ in range(1024): idx = randbelow(4) print(json.dumps({'idx': idx})) try: response = json.loads(input()) c = response['c'] % n assert c != key_c m = pow(c, d, n) b = get_bit(m, idx) except (json.JSONDecodeError, TypeError, KeyError, ValueError, AssertionError): b = 2 print(json.dumps({'b': b})) print(key_m) ``` ::: ### Phân tích và lời giải Nhìn sơ qua thì đây là một bài ứng dụng thuật AES-RSA Sơ đồ chính của thử thách là: Random `key_m` bé hơn n -> dùng `key_m` mã hóa flag thành `ct` bằng AES mode CBC. Mặt khác: Dùng RSA mã hóa `key_m` thành `key_c` Cuối cùng tiết lộ `n, key_c, iv, ct` và cho ta gửi 1024 bản mã. Với mỗi bản mã, tiết lộ 1 bit nằm trong khoảng vị trí thứ 0 đến 3 từ phải qua sau khi giải mã với thuật RSA. Tuy nhiên trong bài này, `key_m` đã bị leak khi hoàn thành 1024 bản mã và thế là ta có thể giải mã được AES mode CBC ngay ```python= # solution.py from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from hashlib import sha256 from pwn import * import json context.log_level = 'debug' # io = process(['python3', 'chall.py']) io = remote("leaky-rsa.chal.imaginaryctf.org", 1337) io.recvline() data = json.loads(io.recvline()) n, iv, ct, c = data['n'], bytes.fromhex(data['iv']), bytes.fromhex(data['ct']), data['c'] count = 0 for _ in range(1024): idx = json.loads(io.recvline())['idx'] send = json.dumps({'c': 0}).encode() io.sendline(send) b = json.loads(io.recvline())['b'] count += 1 print(count) key_m = int(io.recvline().decode()) key = sha256(str(key_m).encode()).digest()[:16] flag = unpad(AES.new(key, AES.MODE_CBC, IV=iv).decrypt(ct), 16).decode() print(flag) ``` Flag: ictf{p13cin9_7h3_b1t5_t0g37her_3f0068c1b9be2547ada52a8020420fb0} ## zkpow :::success ![image](https://hackmd.io/_uploads/rkNys-hcgx.png) ::: :::spoiler `zkpow.py` ```python= #!/usr/bin/env python3 import hashlib, secrets, json, time # --- Utility functions --- def sha256(b: bytes) -> bytes: return hashlib.sha256(b).digest() def hexf(b: bytes) -> str: return b.hex() def commit_vertex(v: int, color_label: int, nonce: bytes) -> bytes: return sha256(b"vertex:" + str(v).encode() + b":" + str(color_label).encode() + b":" + nonce) # --- Merkle tree helpers --- def build_merkle_tree(leaves_hex): leaves = [bytes.fromhex(h) for h in leaves_hex] if len(leaves) == 0: return hexf(sha256(b"")), [[sha256(b"")]] levels = [leaves] cur = leaves while len(cur) > 1: nxt = [] for i in range(0, len(cur), 2): left = cur[i] right = cur[i+1] if i+1 < len(cur) else left nxt.append(sha256(left + right)) levels.append(nxt) cur = nxt return hexf(levels[-1][0]), levels def merkle_proof_for_index(levels, index): proof = [] idx = index for level in levels[:-1]: if idx % 2 == 0: sib_index = idx + 1 if idx + 1 < len(level) else idx sibling = level[sib_index] proof.append((hexf(sibling), False)) else: sib_index = idx - 1 sibling = level[sib_index] proof.append((hexf(sibling), True)) idx //= 2 return proof def verify_merkle_proof(root_hex, leaf_hex, proof): cur = bytes.fromhex(leaf_hex) for sibling_hex, sibling_is_left in proof: sibling = bytes.fromhex(sibling_hex) if sibling_is_left: cur = sha256(sibling + cur) else: cur = sha256(cur + sibling) return hexf(cur) == root_hex # --- Fiat-Shamir edge selection --- def fiat_shamir_select_index(root_hex, m): return int.from_bytes(hashlib.sha256(root_hex.encode()).digest(), "big") % m # --- Configurable graph generator --- def make_graph(n_vertices=1000, p_good=0.75, p_bad=0.003): coloring = [secrets.randbelow(3) for _ in range(n_vertices)] parts = {0: [], 1: [], 2: []} for v, c in enumerate(coloring): parts[c].append(v) edges = [] for c1 in range(3): for c2 in range(c1+1, 3): A, B = parts[c1], parts[c2] for u in A: for v in B: if secrets.randbelow(1_000_000) / 1_000_000 < p_good: edges.append((u, v)) # spice things up :) for c in range(3): part = parts[c] for i in range(len(part)): for j in range(i+1, len(part)): if secrets.randbelow(1_000_000) / 1_000_000 < p_bad: edges.append((part[i], part[j])) return edges, n_vertices # --- zkPoW prover --- def zkpow_prove(edges, coloring, n_vertices=1000): verts = list(range(n_vertices)) # permutation + colors perm = [0,1,2] secrets.SystemRandom().shuffle(perm) permuted = {v: perm[coloring[v]] for v in verts} nonces = {v: secrets.token_bytes(16) for v in verts} leaves_hex = [hexf(commit_vertex(v, permuted[v], nonces[v])) for v in verts] merkle_root, levels = build_merkle_tree(leaves_hex) # pick single edge idx = fiat_shamir_select_index(merkle_root, len(edges)) u,v = edges[idx] # prepare openings openings = {} for w in (u,v): openings[w] = { "color": permuted[w], "nonce": hexf(nonces[w]), "merkle_proof": merkle_proof_for_index(levels, w) } proof = { "merkle_root": merkle_root, "openings": openings, } return json.dumps(proof) # --- zkPoW verifier --- def zkpow_verify(proof, edges): merkle_root = proof["merkle_root"] openings = proof["openings"] # verify Merkle proofs for v_s, opened in openings.items(): v = int(v_s) leaf_hex = hexf(commit_vertex(v, opened["color"], bytes.fromhex(opened["nonce"]))) if not verify_merkle_proof(merkle_root, leaf_hex, opened["merkle_proof"]): print(f"Merkle proof failed for vertex {v}") return False # recompute chosen edge idx = fiat_shamir_select_index(merkle_root, len(edges)) u,v = map(str, edges[idx]) if u not in openings or v not in openings: print(f"Missing opening for endpoints of edge {idx}") return False if openings[u]["color"] == openings[v]["color"]: print(f"Edge {idx} endpoints same color -> invalid") return False return True def main(): print("==zk-proof-of-work: enabled==") for i in range(50): print(f"==round {i}==") edges, n_vertices = make_graph(i * 33 + 10, 0.8) print(json.dumps({"n": n_vertices, "edges": edges})) start = time.time() proof = json.loads(input("proof: ")) end = time.time() if end - start > 5: print("too slow!") exit(-1) ok = zkpow_verify(proof, edges) if ok: print("verified!") else: print("failed!") exit(-1) flag = open("flag.txt").read() print("flag:", flag) if __name__ == "__main__": main() ``` ::: ### Phân tích và lời giải Ở thử thách này, ta không cần phải tô 3 màu cho đồ thị. Hàm `verify` chỉ kiểm tra hai điểm cuối của một cạnh, và chỉ số cạnh đó là H(merkle_root) % m. Vì ta kiểm soát tất cả các nonce (vì là gốc Merkle) nên có thể thử nonce của một lá cho đến khi thử thách trỏ đến một cạnh có các điểm cuối có màu khác nhau theo bất kỳ màu ngẫu nhiên nào ta chọn. Điều này làm cho mỗi vòng trở nên đơn giản, không cần `backtracking` mà chỉ cần một vài lần băm. Sau đây là các bước giải: 1. Chọn một màu 3 ngẫu nhiên (bất kỳ màu nào cũng được). 2. Xây dựng cây Merkle một lần. 3. Liên tục điều chỉnh nonce của một lá và tính toán lại gốc Merkle theo từng bước (O(log n) mỗi lần thử). 4. Dừng ngay khi H(root) % m chọn một cạnh có màu khác nhau. 5. Gán hai đỉnh đó cho `openings` ```python= # solution.py from pwn import * import json, hashlib, secrets # ===== Challenge-compatible helpers ===== def sha256(b: bytes) -> bytes: return hashlib.sha256(b).digest() def commit_vertex_bytes(v: int, color_label: int, nonce: bytes) -> bytes: # EXACT match to the server's commit function (but returns raw bytes) return sha256(b"vertex:" + str(v).encode() + b":" + str(color_label).encode() + b":" + nonce) def fiat_shamir_select_index(root_hex: str, m: int) -> int: return int.from_bytes(hashlib.sha256(root_hex.encode()).digest(), "big") % m # ===== Merkle (bytes throughout, then hex only when serializing the proof) ===== def build_merkle_tree_bytes(leaves): """ leaves: list[bytes] returns: (root_bytes, levels) levels[0] = leaves (bytes) levels[h] = list of bytes at height h """ if not leaves: z = sha256(b"") return z, [[z]] levels = [list(leaves)] cur = levels[0] while len(cur) > 1: nxt = [] for i in range(0, len(cur), 2): left = cur[i] right = cur[i+1] if i+1 < len(cur) else left nxt.append(sha256(left + right)) levels.append(nxt) cur = nxt return levels[-1][0], levels def update_leaf_inplace(levels, index, new_leaf): """ Update levels in place after changing one leaf. O(log n) hashes up to the root, honoring the 'duplicate last' rule. """ levels[0][index] = new_leaf idx = index for h in range(0, len(levels) - 1): level = levels[h] # figure siblings and parent if idx % 2 == 0: right_idx = idx + 1 if idx + 1 < len(level) else idx left = level[idx] right = level[right_idx] parent = sha256(left + right) else: left_idx = idx - 1 left = level[left_idx] right = level[idx] parent = sha256(left + right) # write parent levels[h + 1][idx // 2] = parent idx //= 2 def merkle_proof_for_index(levels, index): """ Returns proof as list of (sibling_hex, sibling_is_left_bool), exactly like the challenge expects. """ proof = [] idx = index for level in levels[:-1]: if idx % 2 == 0: sib_index = idx + 1 if idx + 1 < len(level) else idx sibling = level[sib_index] proof.append((sibling.hex(), False)) # sibling on the right else: sib_index = idx - 1 sibling = level[sib_index] proof.append((sibling.hex(), True)) # sibling on the left idx //= 2 return proof # ===== Core: nonce grinding (no coloring solve needed) ===== def build_and_print_proof(graph_line: bytes) -> str: obj = json.loads(graph_line.decode()) n = obj["n"] edges = obj["edges"] # list of [u, v] m = len(edges) # 1) Any random 3-coloring works (we only need one good edge per round) rng = secrets.SystemRandom() colors = [rng.randrange(3) for _ in range(n)] # 2) Random nonces for all vertices; build initial tree once nonces = [secrets.token_bytes(16) for _ in range(n)] leaves = [commit_vertex_bytes(v, colors[v], nonces[v]) for v in range(n)] root_bytes, levels = build_merkle_tree_bytes(leaves) # 3) Grind a SINGLE leaf's nonce until H(root) % m hits a "good" edge pivot = 0 # any index works # Keep a small safety cap; expected tries ~ 1.5 because ~2/3 edges are "good" max_tries = 4096 for _ in range(max_tries): # new nonce for pivot leaf nonces[pivot] = secrets.token_bytes(16) new_leaf = commit_vertex_bytes(pivot, colors[pivot], nonces[pivot]) update_leaf_inplace(levels, pivot, new_leaf) root_hex = levels[-1][0].hex() idx = fiat_shamir_select_index(root_hex, m) u, v = edges[idx] if colors[u] != colors[v]: # 4) Prepare openings for the challenged edge only openings = {} for w in (u, v): openings[str(w)] = { "color": colors[w], "nonce": nonces[w].hex(), "merkle_proof": merkle_proof_for_index(levels, w) } proof = { "merkle_root": root_hex, "openings": openings } return json.dumps(proof) # In practice we should never get here; fallback (recolor & retry once) colors = [rng.randrange(3) for _ in range(n)] nonces = [secrets.token_bytes(16) for _ in range(n)] leaves = [commit_vertex_bytes(v, colors[v], nonces[v]) for v in range(n)] root_bytes, levels = build_merkle_tree_bytes(leaves) for _ in range(max_tries): nonces[pivot] = secrets.token_bytes(16) new_leaf = commit_vertex_bytes(pivot, colors[pivot], nonces[pivot]) update_leaf_inplace(levels, pivot, new_leaf) root_hex = levels[-1][0].hex() idx = fiat_shamir_select_index(root_hex, m) u, v = edges[idx] if colors[u] != colors[v]: openings = {} for w in (u, v): openings[str(w)] = { "color": colors[w], "nonce": nonces[w].hex(), "merkle_proof": merkle_proof_for_index(levels, w) } proof = { "merkle_root": root_hex, "openings": openings } return json.dumps(proof) # If still unlucky, bail so you can inspect raise RuntimeError("Grinding failed unexpectedly") # ===== Runner ===== def main(): context.log_level = "error" # keep pwntools quiet & fast # io = process(['python3', 'zkpow.py']) io = remote("zkpow.chal.imaginaryctf.org", 1337) # banner io.recvline() io.recvline() for i in range(50): io.recvline() # "==round i==" graph_line = io.recvline() # JSON line with n & edges proof = build_and_print_proof(graph_line) io.sendline(proof.encode()) res = io.recvline().decode().strip() # print progress without slowing down print(f"Round {i+1}: {res}") # flag print(io.recvline().decode().strip()) if __name__ == "__main__": main() ``` Flag: ictf{zero_knowledge_proof_more_like_i_have_zero_knowledge_of_how_to_prove_this} ## scalar-division :::success ![image](https://hackmd.io/_uploads/SkB5UpJigx.png) ::: ```python= # chall.sage assert ((E:=EllipticCurve(GF(0xbde3c425157a83cbe69cee172d27e2ef9c1bd754ff052d4e7e6a26074efcea673eab9438dc45e0786c4ea54a89f9079ddb21),[5,7])).order().factor(limit=2**10)[3][0]*E.lift_x(ZZ(int.from_bytes((flag:=input('ictf{')).encode())))).x() == 0x686be42f9c3f431296a928c288145a847364bb259c9f5738270d48a7fba035377cc23b27f69d6ae0fad76d745fab25d504d5 and not print('\033[53C\033[1A}') ``` ### Phân tích và lời giải `assert` kiểm tra: nếu điểm có hoành độ là `Q.xy()[0]` lift và nhân với k thì nó phải bằng target_x. Tức là `assert(k * lift_x(x_Q)).x() == target_x`. Ý tưởng chính: Nguyên lý dùng ở đây là: nếu thứ tự nhóm điểm của elliptic curve là n và n có một nhân tử là k, thì phép nhân $[k]:P\rightarrow kP$ không phải là đơn ánh mà nó tồn tại một nhân tử (kernel) thứ bậc k. :::spoiler Source If n is a positive integer, we denote by $E(\mathbb{Q})[n]$ the subgroup of rational points of order dividing n, which is the kernel of the multiplication map from E to itself. https://johncremona.github.io/book/fulltext/chapter3.pdf ::: Bằng cách dựng $Q = k^{-1} * R$ và sau đó cộng mọi phần tử thuộc kernel (jS), ta thu được nhiều $x_Q$ sao cho khi nhân k vẫn trả về R (tức là target_x). ```python= # solution.py from sage.all import * from Crypto.Util.number import long_to_bytes import string p = 0xbde3c425157a83cbe69cee172d27e2ef9c1bd754ff052d4e7e6a26074efcea673eab9438dc45e0786c4ea54a89f9079ddb21 E = EllipticCurve(GF(p), [5,7]) n = E.order() fac = n.factor(limit=2**10) k = int(fac[3][0]) target_x = 0x686be42f9c3f431296a928c288145a847364bb259c9f5738270d48a7fba035377cc23b27f69d6ae0fad76d745fab25d504d5 m = n // k R = E.lift_x(ZZ(target_x)) k_inv_mod_m = inverse_mod(k, m) Q = k_inv_mod_m * R assert ((E:=EllipticCurve(GF(0xbde3c425157a83cbe69cee172d27e2ef9c1bd754ff052d4e7e6a26074efcea673eab9438dc45e0786c4ea54a89f9079ddb21),[5,7])).order().factor(limit=2**10)[3][0]*E.lift_x(ZZ(Q.xy()[0]))).x() == 0x686be42f9c3f431296a928c288145a847364bb259c9f5738270d48a7fba035377cc23b27f69d6ae0fad76d745fab25d504d5 and not print('\033[53C\033[1A}') # S được chọn sao cho nhóm con sinh bởi S có kích thước k (tức là kS = O) S = E.lift_x(ZZ(1908615609373310359393680708495309867245478461545179513106385994207950225114719305735749421285909081171302218073610177595)) lst = [] for j in range(k): Pj = Q + j * S x = int(Pj.xy()[0]) b = long_to_bytes(x).decode('utf-8', errors='ignore') if all(ch in string.printable for ch in b): print(b) ``` Flag: ictf{mayb3_d0nt_m4ke_th3_sca1ar_a_f4ctor_0f_the_ord3r} ## redacted :::warning ![image](https://hackmd.io/_uploads/SkhUIKHolx.png) ![image](https://hackmd.io/_uploads/Syuj8tHixg.png) ::: ### Phân tích và lời giải Theo lẽ thường, qua phép XOR thì 2 giá trị như nhau sẽ cho ra giá trị 0. Tuy nhiên, ở đây cùng 1 giá trị nhưng lại cho ra 1 output có giá trị khác 0... Là bởi phần key của phép XOR không phải dạng ASCII mà là dạng hex nên nó sẽ chỉ lấy những giá trị trong hệ thập lục phân. Ở đây, phần key của CyberChef dạng hex được lấy theo kiểu sau: ```python= def cyberchef_hexparse(a): h = '' for ch in a: if ch in string.hexdigits: h += ch if ' ' not in h[-2:]: h += ' ' else: h += ' ' return bytes([int(x, 16) for x in h.split()]) ``` Với bài này thì mình thử brute-force và mong rằng độ dài key là nhỏ :3 ```python= # solution.py #!/bin/python import string def cyberchef_hexparse(a): h = '' for ch in a: if ch in string.hexdigits: h += ch if ' ' not in h[-2:]: h += ' ' else: h += ' ' return bytes([int(x, 16) for x in h.split()]) def cyberchef_xor(a, key): return bytes(a[i] ^ key[i % len(key)] for i in range(len(a))) OUTPUT = bytes.fromhex('656cce6bc175617e5366c952d86c6a536e6ede52df636d7e757fce64d56373') FLAG_PREFIX = b'ictf{' key_prefix = cyberchef_xor(FLAG_PREFIX, OUTPUT[:len(FLAG_PREFIX)]) for attempt_length in range(len(key_prefix), len(OUTPUT) // 2): print(f'{attempt_length = }') for attempt in range(256 ** (attempt_length - len(key_prefix))): key = key_prefix + attempt.to_bytes(attempt_length - len(key_prefix)) flag = cyberchef_xor(OUTPUT, key) if flag.isascii() and cyberchef_hexparse(flag.decode()) == key: print(f'Found: {flag}') exit(0) ``` Flag: ictf{xor_is_bad_bad_encryption} ## leaky-rsa-revenge :::warning ![image](https://hackmd.io/_uploads/HJ8iRtLjge.png) ::: :::spoiler `chall.py` ```python= #!/usr/local/bin/python3 import json from Crypto.Util.number import getPrime from Crypto.Cipher import AES from Crypto.Util.Padding import pad from secrets import randbelow, token_bytes from hashlib import sha256 with open('flag.txt') as f: flag = f.read() p = getPrime(512) q = getPrime(512) n = p * q e = 65537 d = pow(e, -1, (p-1)*(q-1)) key_m = randbelow(n) key_c = pow(key_m, e, n) key = sha256(str(key_m).encode()).digest()[:16] iv = token_bytes(16) ct = AES.new(key, AES.MODE_CBC, IV=iv).encrypt(pad(flag.encode(), 16)) print(json.dumps({'n': n, 'c': key_c, 'iv': iv.hex(), 'ct': ct.hex()})) def get_bit(n, k): return (n >> k) % 2 for _ in range(1024): idx = randbelow(4) print(json.dumps({'idx': idx})) try: response = json.loads(input()) c = response['c'] % n assert c != key_c m = pow(c, d, n) b = get_bit(m, idx) except (json.JSONDecodeError, TypeError, KeyError, ValueError, AssertionError): b = 2 print(json.dumps({'b': b})) ``` ::: ### Phân tích Thử thách này đã sửa lỗi của bài trước bằng cách bỏ phần leak `key_m` ở cuối. Ta được cung cấp các tham số như sau: - `n`: tích của hai số nguyên tố 512 bit `p` và `q` - `e`: Khóa công khai, có giá trị bằng `65537` - `key_c`: là khóa `key_m` sau khi được mã hóa RSA với `n` và `e` - `iv`: Là init vector khi mã hóa AES `flag` bằng khóa là `sha256` của `key_m` - `ct`: Là mã hóa AES của `flag`. Đề bài cho phép ta gửi lần lượt 1024 số và nhận một trong các bit thứ 0-3 của giải mã RSA của các số đó: ```python! for _ in range(1024): idx = randbelow(4) print(json.dumps({'idx': idx})) try: response = json.loads(input()) c = response['c'] % n assert c != key_c m = pow(c, d, n) b = get_bit(m, idx) except (json.JSONDecodeError, TypeError, KeyError, ValueError, AssertionError): b = 2 print(json.dumps({'b': b})) ``` => Ta phải khôi phục lại khóa `key_m` (từ đây gọi tắt là `m`) để lấy flag. ### Hướng giải Bài này tương tự như tấn công LSB oracle. :::info https://crypto.stackexchange.com/questions/11053/rsa-least-significant-bit-oracle-attack ::: Để tấn công, ta gửi $(2^{e \times i} \mod n)*c$ để giải mã và nhận $(((2^i\mod n) \times m) \mod n) \mod 2$. Để ý rằng $((2^{i+1}\mod n) \times m) \mod n$ bằng $((2^i\mod n) \times m) \mod n$ và trừ cho $0$ hoặc $n$, tùy thuộc vào việc $((2^i\mod n) \times m) \mod n$ lớn hơn hay nhỏ hơn $\dfrac{n}{2}$. Vì vậy, việc so sánh $(((2^i\mod n) \times m) \mod n) \mod 2$ và $(((2^{i+1}\mod n) \times m) \mod n) \mod 2$ cho ta MSB của $(2^i\mod n) \times m$. Ở đây, ta nhận được ngẫu nhiên một trong bốn bit thấp. Đầu tiên, ta thử các trường hợp cho đến khi ta nhận được $n\equiv -1 \mod 16$. Bây giờ, giả sử ta nhận được LSB thứ ba (chỉ số 2, vị trí thứ tư). Ta có thể gửi $(2^{e \times (i+3)} \mod n)*c$ và nhận lại $((8\times (2^i\mod n) \times m) \mod n)\ \& \ 4$. Như vậy, $$((8\times (2^i\mod n) \times m) \mod n)\ \& \ 4 =int((((8\times (2^i\mod n) \times m) \mod n)\mod 8) \ge 4)$$ và giá trị $((8\times (2^i\mod n) \times m) \mod n)\mod 8$ là bit đúng sai của đẳng thức $$0-n\times \lfloor\dfrac{(8\times (2^i\mod n) \times m)}{n}\rfloor\mod 8 =\lfloor\dfrac{(8\times (2^i\mod n) \times m)}{n}\rfloor\mod 8$$ Giá trị này cho chúng ta biết $(2^i \mod n) \times m$ nằm trong khoảng nào trong số các khoảng $[0, n/8), ..., [7*n/8, n)$, nhưng chúng ta chỉ nhận được MSB của nó (từ $\ge 4$ hoặc $\& \ 4$), vì vậy về cơ bản chúng ta chỉ nhận được một MSG của $(2^i \mod n) \times m$. Lặp lại điều này sẽ cho phép chúng ta thu được toàn bộ `m`. Về bản chất, phương pháp này là binary-search trên phần fractional m/n: mỗi bit thu được sẽ cho ta cắt đôi khoảng hiện tại (giống LSB-oracle). Dùng việc hỏi “bit của octant” là một biến thể nhưng về lượng thông tin mỗi truy vấn vẫn ~1 bit, do đó độ phức tạp tương đương LSB-oracle cơ bản. Sau đây là phần code giải: ```python= # get_key_m.py from fractions import Fraction from pwn import * import json # context.log_level = 'debug' def get_data(): # target = process(["python3", "chall.py"]) target = remote('leaky-rsa-revenge.chal.imaginaryctf.org', 1337) target.recvline() data = target.recvline() params = json.loads(data) c = int(params['c']) n = int(params['n']) iv = params['iv'] ct = params['ct'] e = 65537 return target, n, e, c, iv, ct target, n, e, c, iv, ct = get_data() while n & 0xf != 0xf: target.close() target, n, e, c, iv, ct = get_data() open("ct.hex", "w").write(json.dumps({'iv': iv, 'ct': ct})) print("n:", n) print("c:", c) low, high = Fraction(0), Fraction(n) for _ in range(n.bit_length()): x = json.loads(target.recvline())['idx'] print("----------------------") print(f"Round {_}, idx: {x}") c_i = c * pow(2**(_+1+x), e, n)% n target.sendline(json.dumps({'c': c_i})) parity = json.loads(target.recvline())['b'] print("parity:", parity) mid = (low + high) / 2 if parity == 0: high = mid else: low = mid if high - low <= Fraction(1, n): break print(f"low: {int(low)}") print(f"high: {int(high)}") print(f"high - low: {int(high - low)}") print(int(high)) ``` ```python= # get_flag.py from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from hashlib import sha256 import json with open("ct.hex", "r") as f: data = json.loads(f.read()) iv, ct = bytes.fromhex(data["iv"]), bytes.fromhex(data["ct"]) # copy key_m from the result key_m = 66913879129427921532141095805213088350786068884711371183610467808888726432628555235077071240182816868700010352314530781554928879532699339797452869208244793928847297090122325104212375055788476001149144065203543329623887717342649312948304461426083404294042817825376050063313290113639594521177002093126711190702 key = sha256(str(key_m).encode()).digest()[:16] flag = unpad(AES.new(key, AES.MODE_CBC, IV=iv).decrypt(ct), 16).decode() print(flag) ``` Flag: ictf{p13cin9_7h3_b1t5_t0g37her_7d092f5d43ebbf6fa60fba8c9e9ac4466daba9a71d04def7e5bf09bcce5649c8} ## clcg :::warning ![image](https://hackmd.io/_uploads/HkHj9cLsgl.png) ::: :::spoiler `chall.py` ```python= from Crypto.Util.number import getPrime from Crypto.Cipher import AES from Crypto.Util.Padding import pad from secrets import randbelow, token_bytes import json with open('flag.txt') as f: flag = f.read().strip() class CLCG: def __init__(self, length): self.p = getPrime(256) self.A = [randbelow(self.p) for _ in range(length)] self.C = [randbelow(self.p) for _ in range(length)] self.X = [randbelow(self.p) for _ in range(length)] def rand(self): self.X = [(a * x + c) % self.p for a, x, c in zip(self.A, self.X, self.C)] return int.to_bytes((sum(self.X) % self.p) >> 192, 8) NUM_HINTS = 36 clcg = CLCG(8) data = dict() data['p'] = clcg.p data['A'] = clcg.A data['hints'] = [clcg.rand().hex() for _ in range(NUM_HINTS)] key = clcg.rand() + clcg.rand() iv = token_bytes(16) cipher = AES.new(key, AES.MODE_CBC, iv=iv) data['iv'] = iv.hex() data['ct'] = cipher.encrypt(pad(flag.encode(), 16)).hex() print(json.dumps(data)) ``` ::: ### Phân tích đề bài - p, A, C, X lần luợt là số nguyên tố 256 bit và các list gồm 1 độ dài nhất định (theo đề là 8) các giá trị ngẫu nhiên bé hơn p. - Hàm `rand()` cho đầu ra là top 64 bit đầu của tổng S là 8 giá trị LCG không phụ thuộc modulo p của các cặp (A, X, C) tương ứng và gán X mới lần lượt là 8 giá trị LCG này. $$X^{(i)}_{t+1}\equiv a_i\times X^{(i)}_t+c_i\mod p,i=[1, 8]$$ $$S =\displaystyle \sum_{i=1}^8 X_t^{(i)}\mod p$$ - Lấy hàm `rand()` 36 lần rồi lấy 2 lần tiếp theo làm key cho chế độ mã hóa AES mode CBC với iv là ngẫu nhiên và plaintext là flag. ```python= data['hints'] = [clcg.rand().hex() for _ in range(36)] key = clcg.rand() + clcg.rand() iv = token_bytes(16) cipher = AES.new(key, AES.MODE_CBC, iv=iv) ``` ### Hướng giải :::spoiler Hint from admin Note that if `x1 = a*x0 + c` then `x1 + c/(a-1) = a*x0 + ac/(a-1) = a(x0 + c/(a-1))` so `xi = C*a**i - c/(a-1)`. If we add up 8 LCGs, it will be of the form `sum(Cj*aj**i for j) - D`, and taking the partial differences gets rid of `D`. We now note that this satisfies the linear recurrence whose characteristic polynomial is `prod(x - aj for j)`. We use this recurrence to build a lattice that will be `0` mod `p` when multiplied by the vector of true differences between state sums. ::: #### Khai thác từ LCG cho trước Từ phép biến đổi affine LCG ta sẽ thử tạo chuỗi cấp số và trình bày công thức. Giả sử, tồn tại $k$ sao cho: $$X^{(i)}_{t+1} + k\equiv a_i\times (X^{(i)}_t+k)\mod p,i=[1, 8]$$ $$\Leftrightarrow (a_i-1)\times k\equiv c\mod p,i=[1, 8]\Leftrightarrow k\equiv \dfrac{c}{a_i-1}\mod p,i=[1, 8]$$ Như vậy, ta hoàn toàn có thể biến đổi dãy trên thành dãy số hệ số nhân $$C^{(i)}_{t+1}\equiv a_i\times C^{(i)}_t\mod p,i=[1, 8]$$, với $C_i = X_i+\dfrac{c}{a_i-1}\mod p,i=[1, 8]$ Do đó, tổng S biến đổi thành:$$S\equiv \displaystyle \sum_{j=1}^8 K_ja^i_j-D\mod p$$ Có nghĩa là nếu ta lấy từng cặp hiệu của S đôi một liên tiếp thì nó sẽ khử D ($\Delta S_i=S_{i+1}-S_i$), để lại tổng của các thừa số thỏa mãn hồi quy tuyến tính bậc 8 với đa thức đặc trưng là: $$m(x)=\displaystyle \prod_{j=1}^8(x-a_j)\in \mathbb{F}_p[x]$$ #### Khôi phục lại $\Delta S_t$ Theo đề bài, ta sẽ tách $\Delta S_t$ thành $2^{192}\Delta b_t+d_t$ với $d_t\in (-2^{192}, 2^{192})$ Từ phép hồi quy trên, suy ra được: $$\displaystyle \sum_{k=0}^8c_k\Delta S_{t+k}\equiv 0 \mod p\ \ (1)$$ Vì vậy, khi ta sử dụng phép tách trên, $(1)$ sẽ trở thành phương trình đồng dư tuyến tính modulo p với ẩn $d_t$. ```python= from Crypto.Cipher import AES from sage.all import * import json with open("out.txt") as f: out = json.load(f) p = out["p"] A = out["A"] hints = [int(hint, 16) for hint in out["hints"]] last = hints[-1] << 192 hints = [hints[i + 1] - hints[i] for i in range(len(hints) - 1)] ct = bytes.fromhex(out["ct"]) iv = bytes.fromhex(out["iv"]) def decrypt(rand1, rand2): cipher = AES.new( int.to_bytes(rand1, 8) + int.to_bytes(rand2, 8), AES.MODE_CBC, iv=iv ) return cipher.decrypt(ct) x = PolynomialRing(GF(p), "x").gen() rec = list(map(int, prod(x - a for a in A))) L = [ [0 for _ in range(i)] + rec + [0 for _ in range(len(hints) - len(rec) - i)] for i in range(len(hints) - len(rec) + 1) ] + [[p * int(i == j) for j in range(len(hints))] for i in range(len(rec) - 1)] L = Matrix(ZZ, L) B = L.LLL() yprime = (2**192) * vector(ZZ, hints) Byprime = B * yprime v = vector(ZZ, [round(i / p) for i in Byprime]) Bzprime = v * p - Byprime zprime = (B ** (-1)) * Bzprime # print([len(bin(i)) for i in zprime]) - debug - should be around 192 xprime = list(yprime + zprime) for _ in range(2): xprime.append((-vector(rec[:-1]) * vector(xprime[-len(rec) + 1 :])) % p) rand1 = ((last + xprime[-2]) % p) >> 192 rand2 = ((last + xprime[-2] + xprime[-1]) % p) >> 192 for r1 in range(rand1 - 4, rand1 + 4): for r2 in range(rand2 - 4, rand2 + 4): flag = decrypt(r1, r2) if b"ictf" in flag: print(flag) ``` Flag: ictf{y3t_an07h3r_lcg_ch411_7b24ac314588057bfd4b70b10585a277} ## Bigger-RSA :::warning ![image](https://hackmd.io/_uploads/HkRU3tD3eg.png) ::: :::spoiler `bigger_rsa.sage` ```python= from Crypto.Util.number import getPrime, bytes_to_long import secrets n = 32 e = 0x10001 N = 64 flag = b'ictf{REDACTED}' flag = secrets.token_bytes((n * 63) - len(flag)) + flag ps = [getPrime(512) for _ in range(n)] m = 1 for i in ps: m *= i nums = [CRT([1 + secrets.randbits(260) for _ in range(n)],ps) for __ in range(N)] ct = pow(bytes_to_long(flag),e,m) print(f"ct={ct}") print(f"m={m}") print(f"nums={nums}") ``` ::: ## Tài liệu 1. (scalar-division): https://johncremona.github.io/book/fulltext/chapter3.pdf 2. (zkpow): - https://en.wikipedia.org/wiki/Merkle_tree - https://en.wikipedia.org/wiki/Zero-knowledge_proof - https://blog.codeminer42.com/zero-knowledge-proofs-and-merkle-trees-an-overview-before-diving-into-it/ 3. (leaky-rsa-revenge): https://crypto.stackexchange.com/questions/11053/rsa-least-significant-bit-oracle-attack 4. (clcg): - https://en.wikipedia.org/wiki/Linear_congruential_generator - https://crypto.stackexchange.com/questions/2086/predicting-values-from-a-linear-congruential-generator