## Crypto ### Linear 101 Source code: ```python= import random import os n = 128 random.seed("Wanna Win?") def encrypt(A, x): b = [0] * n for i in range(n): for j in range(n): b[i] = max(b[i], A[i][j] + x[j]) return b def game(): for round in range(64): try: print(f"Round {round+1}/64") A = [random.randbytes(n) for _ in range(n)] x = os.urandom(128) b = encrypt(A, x) print(f"{b = }") sol = bytes.fromhex(input("x = ")) if len(sol) != n: return False if encrypt(A, sol) != b: print("Wrong!") return False except: return False return True if game(): print(open("flag.txt", "r").read()) else: print("You lose...") ``` Bug lớn nhất ở trong bài này đó chính là việc tiết lộ cho ta biết `seed` của hàm `random`. Từ đó ta có thể sinh ra lại mảng `A`. Hàm `encrypt(A, x)` sẽ trả về một mảng `b` có `n` phần tử với `b[i] = max(b[i], A[i][j] + x[j])`. Tức là: \begin{gather} b_i = \text{max}_j(A_{ij}+x_j) \\ \Rightarrow A_{ij} + x_j \le b_i \\ \Rightarrow x_j \le b_i - A_{ij} \\ \Rightarrow x_j \le min_i(b_i - A_{ij}) \\ \Rightarrow x_j = min_i(b_i - A_{ij}) \end{gather} Vậy, ta đã có thể tạo được một mảng `x` thỏa mãn điều kiện `encrypt(A, sol) == b`. Solve script: ```python= from pwn import * import random, json io = remote("challenge.cnsc.com.vn", 30429, level='debug') N = 128 rand = random.Random() rand.seed("Wanna Win?") As = [[rand.randbytes(N) for _ in range(N)] for _ in range(64)] def calc(A, b): x = [0] * N for j in range(N): m = 10**9 for i in range(N): m = min(m, b[i] - A[i][j]) x[j] = m % 256 return bytes(x) for r in range(64): io.recvuntil(b"b = ") b = json.loads(io.recvline().decode().strip()) io.recvuntil(b"x = ") x = calc(As[r], b) io.sendline(x.hex()) print(io.recvall()) # W1{W3I1-l_THiNK_lts_3ASler_Than_nOrmAL-Lln3Ar_41gebra_problem0} ``` ### Boring Signing Source code: ```C= // gcc base85.c chall.c prime.c rsa.c -o chall -lcrypto #include <stdio.h> #include <string.h> #include <stdint.h> #include <ctype.h> #include "prime.h" #include "rsa.h" #include "base85.h" struct __attribute__((packed)) variable { uint8_t msg[64]; uint8_t N[384]; uint8_t prime[3][128]; uint8_t sig[384]; } v; int main() { setvbuf(stdout, NULL, _IONBF, 0); int choice; keygen(v.N, v.prime, 1024 * 3); printf("N = "); print_b85(v.N, sizeof(v.N)); printf("\n"); uint8_t target[64] = "1_d4r3_y0u_70_519n_7h15_3x4c7_51x7y_f0ur_by73_57r1n9_w17h_my_k3y"; for (int i = 0; i <= 20; i++) { printf("Sign(0) or Verify(1): "); scanf("%d", &choice); int c; while ((c = getchar()) != '\n' && c != EOF); switch (choice) { case 0: printf("Input your message in base85:\n"); input_b85(v.msg, 64); while ((c = getchar()) != '\n' && c != EOF); if (!memcmp(v.msg, target, 64)) { printf("Nuh uh\n"); return 0; } sign(v.msg, v.N, v.prime, v.sig); printf("sig = "); print_b85(v.sig, sizeof(v.sig)); printf("\n"); break; case 1: printf("Provide your signature in base85:\n"); uint8_t check[384]; input_b85(check, 384); while ((c = getchar()) != '\n' && c != EOF); if (verify(target, check, v.N)) { uint8_t flag[64]; FILE *f_flag = fopen("flag", "r"); flag[fread(flag, 1, sizeof flag - 1, f_flag)] = 0; printf("%s\n", flag); fclose(f_flag); return 0; } else { printf("Wrong !\n"); return 0; } break; default: printf("Huh ?\n"); return 0; } } } ``` Sau khi phân tích các file code của challenge thì mình nhận thấy rằng có một bug ở trong `base85.c` là hàm mã hóa và giải mã base85. ![image](https://hackmd.io/_uploads/Byx3uxVfbl.png) Hàm `input_b85` sẽ nhận con trỏ `buf` làm nơi chứa dữ liệu sau khi giải mã base85. Trong giải mã base85 thì cứ 5 bytes input sẽ tạo thành 4 bytes output. Và trong code này author đã cố tình sử dụng trực tiếp `buf` để chứa dữ liệu từ `stdin` thông qua lệnh `fread(buf, 1, 5, stdin);`. Thì khi ta gọi `input_b85(v.msg, 64)`, vòng lặp `while` sẽ xử lý mỗi lần 5 bytes input. Đến vòng lặp cuối cùng (`n = 4`): con trỏ `buf` đang trỏ đến `v.msg[60]` (là bytes thứ 61 của msg). Thì hàm `fread(buf, 1, 5, stdin)` sẽ đọc 5 bytes từ bàn phím và ghi vào bộ nhớ bắt đầu từ `v.msg[60]`. Khi đó: ``` Byte input 1 -> buf[0] -> v.msg[60] Byte input 2 -> buf[1] -> v.msg[61] Byte input 3 -> buf[2] -> v.msg[62] Byte input 4 -> buf[3] -> v.msg[63] Byte input 5 -> buf[4] -> v.msg[64] (buffer overflow) ``` Và nhìn vào `struct` được tạo trong file `chall.c` ta thấy: ![image](https://hackmd.io/_uploads/HJhgsxNGZg.png) Vì struct `variable` được định nghĩa với `packed` nên compiler sẽ không padding byte nào giữa các biến. Khi đó `v.N` sẽ nằm ngay sau `v.msg`, suy ra byte bị tràn **sẽ là byte đầu tiên của N**. Tận dụng bug này, mình đã thử từng giá trị byte có thể (256 giá trị) để thay thế byte đầu tiên của `N` để tìm một giá trị `N'` sao cho `N'` là một số nguyên tố. Khi đó `phi(N') = N' - 1` từ đó tính được `d` và ký cho `target`. Lí do làm như vậy là bởi vì hàm `verify()` đó sử dụng luôn giá trị `N` sau khi bị ghi đè. Solve script: ```python= from pwn import * from Crypto.Util.number import bytes_to_long, long_to_bytes, isPrime, inverse from hashlib import sha256 import base64 def solve(): io = remote('challenge.cnsc.com.vn', 32730, level='debug') io.recvuntil(b"N = ") N_b85 = io.recvline().strip() N_bytes = base64.a85decode(N_b85) found_byte = None prime_N = 0 for b in range(256): candidate_bytes = bytearray(N_bytes) candidate_bytes[0] = b candidate_N = bytes_to_long(candidate_bytes) if isPrime(candidate_N): found_byte = b prime_N = candidate_N break if found_byte is None: io.close() return None d = inverse(0x10001, prime_N - 1) target = b"1_d4r3_y0u_70_519n_7h15_3x4c7_51x7y_f0ur_by73_57r1n9_w17h_my_k3y" target = bytes_to_long(sha256(target).digest()) sig_forged_int = pow(target, d, prime_N) io.sendlineafter(b"Sign(0) or Verify(1): ", b"0") payload = b'!!!!!' * 15 + b'!!!!' + bytes([found_byte]) io.sendlineafter(b"base85: \n", payload) io.sendlineafter(b"Sign(0) or Verify(1): ", b"1") sig_bytes = long_to_bytes(sig_forged_int) if len(sig_bytes) < 384: sig_bytes = b'\x00' * (384 - len(sig_bytes)) + sig_bytes sig_payload = base64.a85encode(sig_bytes) io.sendlineafter(b"base85\n", sig_payload) result = io.recvline() if b"Wrong" in result or b"Huh" in result: return None return result while True: flag = solve() if flag is not None: print(flag) break # W1{I_ShOUlD-u53-PYTHon-t0-lmPI3M3n7-CRYp7O9rApHIC_SCH3M3S..8bf} ``` ### Interesting Signing Source code: ```C= // gcc base85.c chall.c prime.c rsa.c -o chall -lcrypto #include <stdio.h> #include <string.h> #include <stdint.h> #include <ctype.h> #include "prime.h" #include "rsa.h" #include "base85.h" struct __attribute__((packed)) variable { uint8_t msg[64]; uint8_t N[384]; uint8_t prime[3][128]; uint8_t sig[384]; } v; int main() { setvbuf(stdout, NULL, _IONBF, 0); int choice; keygen(v.N, v.prime, 1024 * 3); printf("N = "); print_b85(v.N, sizeof(v.N)); printf("\n"); uint8_t target[64] = "1_d4r3_y0u_70_519n_7h15_3x4c7_51x7y_f0ur_by73_57r1n9_w17h_my_k3y"; for (int i = 0; i <= 20; i++) { printf("Sign(0) or Verify(1): "); scanf("%d", &choice); int c; while ((c = getchar()) != '\n' && c != EOF); switch (choice) { case 0: printf("Input your message in base85:\n"); input_b85(v.msg, 64); while ((c = getchar()) != '\n' && c != EOF); if (!memcmp(v.msg, target, 64)) { printf("Nuh uh\n"); return 0; } sign(v.msg, v.N, v.prime, v.sig); printf("sig = "); print_b85(v.sig, sizeof(v.sig)); printf("\n"); break; case 1: printf("Provide your signature in base85:\n"); uint8_t check[384]; input_b85(check, 384); while ((c = getchar()) != '\n' && c != EOF); if (verify(target, check, v.prime)) { uint8_t flag[64]; FILE *f_flag = fopen("flag", "r"); flag[fread(flag, 1, sizeof flag - 1, f_flag)] = 0; printf("%s\n", flag); fclose(f_flag); return 0; } else { printf("Wrong !\n"); return 0; } break; default: printf("Huh ?\n"); return 0; } } } ``` Bài này là một phiên bản khó hơn rất nhiều so với bài trước khi đã cách check của hàm `verify()`, thay vì sử dụng giá trị `N'` đã bị ghi đè thì nó sẽ tính lại `N` ban đầu từ các giá trị `p, q, r`. Nhưng vì lí do như vậy, hướng đi duy nhất của ta sẽ là tìm cách recover `p, q, r` từ việc sử dụng `sign()` với `N_fault`. **Bug base85** của bài `Boring Signing` vẫn được sử dụng trong bài này => Ta có thể thay đổi byte đầu tiên của `N_origin` theo ý của mình. Nhưng làm sao để recover `p, q, r`? Trong lúc giải đang diễn ra, mình đã osint được một cái [paper](https://eprint.iacr.org/2011/388.pdf) rất giống bài này chỉ khác ở chỗ trong paper nó sử dụng `N = pq` (còn bài này thì `N = pqr`). Paper này nói về cách attack chống lại **RSA-CRT Signature**. Cụ thể hơn: Ta có: $\sigma_p = H(m)^d \pmod p, \ \ \ \ \sigma_q = H(m)^d \pmod q, \ \ \ \ \sigma_r = H(m)^d \pmod r$ Hàm `sign()` sẽ trả về chữ ký: \begin{gather} \sigma = \sigma_p \cdot \alpha + \sigma_q \cdot \beta + \sigma_r \cdot \theta \pmod N \end{gather} Với: \begin{gather} \alpha = qr \cdot ((qr)^{-1} \mod p) \\ \beta = pr \cdot ((pr)^{-1} \mod q) \\ \theta = pq \cdot ((pq)^{-1} \mod r) \end{gather} Và một chữ ký $\sigma'$ của cùng $H(m)$ được tính bởi modulo $N'$ (chỉ khác duy nhất byte MSB so với $N$). \begin{gather} \sigma' = \sigma_p \cdot \alpha + \sigma_q \cdot \beta + \sigma_r \cdot \theta \pmod {N'} \end{gather} Thì khi ta CRT lên $\sigma, \sigma'$ sẽ được: \begin{gather} v = \sigma_p \cdot \alpha + \sigma_q \cdot \beta + \sigma_r \cdot \theta \pmod {N \cdot N'} \end{gather} Vì $\sigma_p \cdot \alpha + \sigma_q \cdot \beta + \sigma_r \cdot \theta < 3N$ (mỗi số hạng đều **bé hơn N**), và $N \cdot N' \approx N^2$ rất lớn, nên khi CRT thì giá trị $v$ chính xác là $\sigma_p \cdot \alpha + \sigma_q \cdot \beta + \sigma_r \cdot \theta$ (không bị modulo). Khi đó $v$ chính là một tổ hợp tuyến tính của $\alpha, \beta, \theta$ trong $\mathbb{Z}$. Thuật toán của ta sẽ như sau: Ban đầu lấy $l$ cặp $\sigma_i, \sigma'_i$ tương ứng là các chữ ký của message $m$ với modulo $N, N'$ ($m, N'$ thay đổi tùy ý với từng cặp). Sau đó **CRT** từng cặp để tính ra giá trị $v_i = \text{CRT}_{N, N'}(\sigma_i, \sigma'_i)$ tương ứng. Giả sử ta xét trên modulo $p$, ta có: \begin{gather} v \equiv \sigma_p \pmod p \\ \Rightarrow v = k \cdot p + \sigma_p \end{gather} Với $\sigma_p$ (khoảng 1024 bit), $v$ (khoảng 6000 bit), $k$ (khoảng 5000 bit). Ta đã lấy được $l$ giá trị $v_i$: \begin{cases} v_1 = k_1p + \sigma_{p, 1} \\ v_2 = k_2p + \sigma_{p, 2} \\ ... \\ v_l = k_lp + \sigma_{p, l} \\ \end{cases} Gọi vector $v = (v_1, v_2, ..., v_l) \in \mathbb{Z}^l$. Bây giờ ta muốn tìm lại các giá trị $\sigma_{p,1},\sigma_{p,2},...,\sigma_{p,l}$. Mục đích là để ta có thể recover $p = \text{GCD}(\frac{v_i}{\sigma_{p,i}}, N)$. **Note:** Nếu lattice $L$ và $L^{\perp}$ nằm trong cùng không gian $\mathbb{R}^n$ thì $\text{rank}(L) + \text{rank}(L^{\perp}) = n$. Dùng thuật toán **LLL** để tìm một cơ sở $\{ b_1, b_2, ..., b_{l-1} \}$ của lattice $v^{\perp} \subset \mathbb{Z}^{\ell}$ (lattice này có rank $l-1$) là những vector trực giao với $v$. Ta xây dựng ma trận như sau: $$ M_1 = \begin{pmatrix} Kv_1 & 1 & 0 & \cdots & 0 \\ Kv_2 & 0 & 1 & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ Kv_l & 0 & 0 & \cdots & 1 \end{pmatrix} $$ Nếu ta chọn $K$ rất lớn, **LLL** sẽ ưu tiên tìm các vector có cột đầu tiên rất nhỏ (xấp xỉ 0). Khi đó, vector chứa các hệ số bên phải sẽ là một vector trong lattice $v^{\perp}$. Giả sử sau khi **LLL** ta tìm được một vector $c = (c_1, c_2, ..., c_l) \in v^{\perp}$, khi đó: \begin{gather} \sum_{i=1}^{l} c_i \cdot v_i = 0 \\ \Rightarrow \sum_{i=1}^{l} c_i \cdot (k_ip + \sigma_{p,i}) = 0 \\ \Rightarrow \sum_{i=1}^{l} c_i \cdot k_ip + \sum_{i=1}^{l} c_i \cdot \sigma_{p,i} = 0 \\ \end{gather} Vì $k_ip$ rất lớn nên **LLL** sẽ cố gắng chọn những $c_i$ sao cho $\sum_{i=1}^{l} c_i \cdot k_ip$ nhỏ nhất có thể (tốt nhất là bằng 0). Khi đó ta sẽ có một ràng buộc cho lattice $v^{\perp}$ là $\sum_{i=1}^{l} c_i \cdot \sigma_{p,i} = 0$. Đây cũng chính là lí do tại sao $v^{\perp}$ có rank là $l-1$. Sau đó, ta lấy $l-3$ vector $\{ b_1, b_2, ..., b_{l-3} \}$ làm cơ sở cho lattice $L' \subset \mathbb{Z}^l$ có rank là $l-3$ Ta xây dựng ma trận: $$ M_2 = \begin{pmatrix} K' b_{1,1} & \cdots & K' b_{l-2,1} & 1 & \cdots & 0 \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ K' b_{1,l} & \cdots & K' b_{l-2,l} & 0 & \cdots & 1 \end{pmatrix} $$ Khi đó, sử dụng **LLL** sẽ tìm được vector ngắn nhất $\sigma_p = (\sigma_{p,1}, \sigma_{p,2},..., \sigma_{p,l})$ (ở đây mình đang giả sử tính trên modulo $p$ thôi chứ thực ra có thể là $q, r$ bởi vì 3 giá trị đó độ lớn ngang nhau nên **LLL** sẽ không phân biệt được) của lattice $L'^{\perp}$ sao cho $\sigma_p \cdot b_i = 0$. Đây sẽ là các giá trị $\sigma_p$ mà ta cần tìm. Giải thích tại sao lattice $L'$ lại có rank là $l-3$: Ban đầu ta có: \begin{gather} \langle c, v \rangle = 0 \Rightarrow \langle c, \sigma_p \cdot \alpha + \sigma_q \cdot \beta + \sigma_r \cdot \theta \rangle =0 \\ \Rightarrow \alpha \langle c, \sigma_p \rangle + \beta \langle c, \sigma_q \rangle + \theta \langle c, \sigma_r \rangle = 0 \end{gather} Vì $\alpha, \beta, \theta$ là các số rất lớn và độc lập tuyến tính trên $\mathbb{Z}$ nên phương trình này chỉ đúng khi và chỉ khi: \begin{cases} \langle c, \sigma_p \rangle = 0 \\ \langle c, \sigma_q \rangle = 0 \\ \langle c, \sigma_r \rangle = 0 \\ \end{cases} Giả sử $W = \text{span}(\sigma_p, \sigma_q, \sigma_r)$, vì 3 vector đó độc lập tuyến tính nên $\text{dim}(W) = 3$. Khi đó, $\text{dim}(W^{\perp}) = \text{dim}(\mathbb{Z}^l) - \text{dim}(W) = l - 3$. Đây chính là lattice $L'$ mà ta đã tìm được sau lần **LLL** đầu tiên. Khi đã có được $\sigma_p$ rồi thì ta có thể recover $p = \text{GCD}(\frac{v_i}{\sigma_{p,i}}, N)$, tương tự với $q, r$. Sau khi đã recover được $p, q, r$ thì việc còn lại rất đơn giản là tạo $\phi(N), d$ sau đó ký **target** rồi gửi chữ ký đó cho server và lấy flag. Solve script: ```python= from pwn import * from hashlib import sha256 import base64 from Crypto.Util.number import * from sage.all import * import os import itertools while True: io = remote('challenge.cnsc.com.vn', 30289, level='debug') io.recvuntil(b"N = ") N_bytes = base64.a85decode(io.recvline().strip()) N = bytes_to_long(N_bytes) def sign(payload): io.sendlineafter(b"Sign(0) or Verify(1): ", b"0") io.sendlineafter(b"base85:\n", payload) sig = io.recvline().strip().decode().split(' = ')[1] return bytes_to_long(base64.a85decode(sig)) sig_1, sig_2, N_fault = [], [], [] NUM_SAMPLES = 9 MSG = [b'!', b'@', b'#', b'$', b'%', b'^', b'&', b'*', b'(', b')'] for b in range(NUM_SAMPLES): payload = MSG[b] * 79 + bytes([N_bytes[0]]) sig_1.append(sign(payload)) for b in range(NUM_SAMPLES): payload = MSG[b] * 79 + bytes([b + 1]) sig_2.append(sign(payload)) N_fault.append(bytes([b + 1]) + N_bytes[1:]) N_fault = [bytes_to_long(nf) for nf in N_fault] v = [] for i in range(NUM_SAMPLES): val = crt([sig_1[i], sig_2[i]], [N, N_fault[i]]) v.append(val) num_primes = 3 num_ortho = NUM_SAMPLES - num_primes K1 = 2 * N dim1 = NUM_SAMPLES + 1 base1 = [] for i in range(NUM_SAMPLES): vec = [0] * dim1 vec[0] = K1 * v[i] vec[i + 1] = 1 base1.append(vec) M1 = Matrix(ZZ, base1) print("Running LLL 1...") reduced1 = M1.LLL() ortho_vecs = [] for i in range(num_ortho): row = list(reduced1[i]) ortho_vecs.append(row[1:]) K2 = 2**(1024 * 2) base2 = [] for i in range(NUM_SAMPLES): vec = [] for j in range(num_ortho): vec.append(K2 * ortho_vecs[j][i]) for j in range(NUM_SAMPLES): if i == j: vec.append(1) else: vec.append(0) base2.append(vec) M2 = Matrix(ZZ, base2) print("Running LLL 2...") reduced2 = M2.LLL() print(reduced2[0]) exit() found_factors = set() w_candidates = [] rows_to_check = min(reduced2.nrows(), 10) for r_idx in range(rows_to_check): val = reduced2[r_idx][num_ortho] w_candidates.append(val) import itertools coeffs = [-1, 0, 1] combinations = list(itertools.product(coeffs, repeat=min(len(w_candidates), 3))) for combo in combinations: if all(c==0 for c in combo): continue w_guess = sum(c*w for c, w in zip(combo, w_candidates[:3])) vals_to_check = [v[0] - w_guess, v[0] + w_guess] for val in vals_to_check: factor = gcd(val, N) if factor > 1 and factor < N: found_factors.add(factor) final_primes = set() candidates = list(found_factors) for f in found_factors: candidates.append(N // f) for i in range(len(candidates)): for j in range(i + 1, len(candidates)): g = gcd(candidates[i], candidates[j]) if g > 1: if is_prime(g): final_primes.add(g) if is_prime(candidates[i] // g): final_primes.add(candidates[i] // g) if is_prime(candidates[j] // g): final_primes.add(candidates[j] // g) for f in candidates: if is_prime(f): final_primes.add(f) sorted_primes = sorted(list(final_primes)) if len(sorted_primes) == 3: p = sorted_primes[0] q = sorted_primes[1] r = sorted_primes[2] print(f"p = {p}") print(f"q = {q}") print(f"r = {r}") if p * q * r == N: print("Recover Success !!!") phi = (p - 1) * (q - 1) * (r - 1) e = 0x10001 d = inverse(e, phi) target = b"1_d4r3_y0u_70_519n_7h15_3x4c7_51x7y_f0ur_by73_57r1n9_w17h_my_k3y" target = bytes_to_long(sha256(target).digest()) sig = pow(target, d, N) sig_bytes = long_to_bytes(sig) if len(sig_bytes) < 384: sig_bytes = b'\x00' * (384 - len(sig_bytes)) + sig_bytes sig_payload = base64.a85encode(sig_bytes) io.sendlineafter(b"Sign(0) or Verify(1): ", b"1") io.sendlineafter(b"base85:\n", sig_payload) io.recvline() exit() io.close() # W1{M@Y63_1-AM_NOt-@_G0oD-D3veIopeR...cf716} ```