Dice CTF 2023

Provably Secure 1/2

Chúng ta có 3 lựa chọn (cho đường đời) như sau:

  • Solve: chỉ ra \(m_{bit}\)\(0\) hay \(1\), nếu đúng thì vượt \(1\) ải, vượt thành công \(128\) ải thì qua môn!!!;

  • Query Encryption: ở mỗi round server tạo hai key RSA để mình dùng. Mình cần nhập hai message có độ dài \(16\) byte và gửi lên server. Dựa vào bit random \(m_{bit}\) mà server sẽ encrypt \(m_0\) hay \(m_1\). Server trả về ciphertext tương ứng (hai ciphertext với tổng độ dài \(512\) byte);

  • Query Decryption: để decrypt mình cần gửi lên server ciphertext \(512\) byte và chỉ được decrypt mỗi ciphertext một lần.

NOTE: mình chỉ được encrypt và decrypt \(8\) lần mỗi loại.

Hàm encrypt làm việc như sau:

  • lấy tham số là hai public key \(pk_0\)\(pk_1\), và plaintext \(msg\)\(16\) byte;

  • random một chuỗi \(r\)\(16\) byte;

  • tính \(r\_ prime = r \oplus msg\);

  • tính \(ct_0 = \text{ENC}(r, pk_0)\)\(ct_1 = \text{ENC}(r\_prime, pk_1)\);

  • cả hai ciphertext đều dùng padding là OAEP và hash là SHA256 nên độ dài là \(256\) byte. Hàm trả về ghép hai chuỗi ciphertext lại (\(512\) byte).

Hàm decrypt làm ngược lại và ở kết quả cuối thì xor hai plaintext lại (\(msg = r\_prime \oplus r\)).

Đề chỉ cho hai public key, mình thì thích private key hơn. :))

Mình nhận ra một điều, giả sử ciphertext của mình là \(\text{ENC}(r, pk_0)\)\(\text{ENC}(r\_prime = r \oplus msg, pk_1)\) thì nếu mình gửi hai lần decrypt với các ciphertext:

  • \(\text{ENC}(r, pk_0)\)\(\text{ENC}(00^{16}, pk_1)\) thì kết quả trả về là \(r \oplus 00^{16} = r\);

  • \(\text{ENC}(00^{16}, pk_0)\)\(\text{ENC}(r\_prime = r \oplus msg, pk_1)\) thì kết quả trả về là \(00^{16} \oplus r \oplus msg = r \oplus msg\).

Khi đó mình xor hai plaintext này lại là được \(msg\) và so sánh xem nó trùng với \(m_0\) hay \(m_1\) mình gửi lên server ban đầu.

NOTE: thật ra ở bài 1 không có công đoạn kiểm tra ciphertext không nằm trong ciphertext có sẵn nên mình chỉ cần gửi lên ciphertext vừa được encrypt là ra. Code trên được dùng để giải cả hai bài Provably Secure 1/2.

Flag Provably Secure: dice{yeah_I_lost_like_10_points_on_that_proof_lmao}.

Flag Provably Secure 2: dice{my_professor_would_not_be_proud_of_me}.

BBBB

Bài này khoai thật sự. :))))

Bài này giống một phần bài BBB của giải SECCON nhưng có chút khác bọt.

Đề cho mình một số nguyên tố \(p\) \(512\) bit và số \(b\) nhỏ hơn \(p\). Mình cần nhập số \(a\) và từ đó các số mũ \(e\) dùng trong RSA sẽ được tạo bởi hàm rng.

Dựa trên bài của giải SECCON, chiến thuật làm bài này là cố gắng khiến hàm rng tạo nhiều \(e=11\) nhất có thể (ở bài này sẽ là \(3\) vì rất khó lấy đủ \(5\)).

Điểm khó của bài này là hàm rng tuyến tính, nghĩa là với mỗi output chỉ tìm được đúng một input. Tuy nhiên chúng ta có thể tìm rng sao cho các input tạo thành vòng, nói cách khác là:

\begin{align*} a X_{i} + b & \equiv X_{i+1} & \pmod p \\ a X_{i+1} + b & \equiv X_{i+2} & \pmod p \\ a X_{i+2} + b & \equiv X_{i+3} & \pmod p \\ a X_{i+3} + b & \equiv X_{i+4} & \pmod p \\ a X_{i+4} + b & \equiv X_{i} & \pmod p \end{align*}

Ở đây \(X_{i} = X_{i+5}\), tức là sau \(5\) lần thì các giá trị \(X_i\) lặp lại và mình sẽ tìm \(a\) thỏa mãn đống này.

Trừ phương trình dưới cho phương trình trên vế theo vế, mình có:

\begin{align*} a (X_{i+1} - X_i) & \equiv X_{i+2} - X_{i+1} & \pmod p \\ a (X_{i+2} - X_{i+1}) & \equiv X_{i+3} - X_{i+2} & \pmod p \\ a (X_{i+3} - X_{i+2}) & \equiv X_{i+4} - X_{i+3} & \pmod p \\ a (X_{i+4} - X_{i+3}) & \equiv X_{i} - X_{i+4} & \pmod p \\ a (X_{i} - X_{i+4}) & \equiv X_{i+1} - X_i & \pmod p \end{align*}

Như vậy \(a^5 \equiv 1 \pmod p\). Nếu phương trình này có nghiệm khác \(1\) thì ta chọn làm tham số \(a\).

Tiếp theo, mình cần \(e=11\) nằm trong vòng lặp này nên mình cứ chọn \(X_0 = e = 11\) rồi theo trình tự \(X_{i+1} = a X_i + b \pmod p\) thôi.

Câu hỏi là tại sao lại cần tới \(3\) bộ có \(e=11\) mà không phải \(2\)?

Vì với mỗi public key \(2048\) bit, phương pháp Coppersmith sẽ hoạt động hiệu quả khi \((2048 * T) \times (1/11 - \varepsilon) \approx 8 \times (L + 4)\) với \(L\) là độ dài flag (tối đa là \(49\)) và \(4\) byte random. Như vậy khi \(T = 3\) thì \(\varepsilon > 0\) là điều mình cần nhắm tới. Mình tham khảo ở hastads.

Cuối cùng, sử dụng CRT và hastad attack (ví dụ như ở github) để giải.

Mình cần lưu ý rằng mình đã biết \(5\) byte đầu của flag là dice{ nên mình có thể giảm độ dài flag cần tìm xuống, từ đó Coppersmith sẽ hiệu quả hơn.

Mỗi phương trình của mình có dạng \((\text{FLAG} \cdot 256^{16} + r_i)^e = c_i \pmod{n_i}\) mà FLAG có \(5\) byte đầu là dice{ nên mình có thể gọi C = bytes_to_long(b"dice{") << (8 * (L + 4 + 16)). Phương trình trở thành

\[(\text{FLAG}' \cdot 256^{16} + C + r_i)^e = c_i \pmod{n_i}.\]

NOTE: việc chọn beta và epsilon khá khó nhằn, công thức trong code là từ writeup người giải ra, và không phải lúc nào cũng có \(a\) thỏa cũng như đủ \(3\) bộ có \(e = 11\).