# CSAW CTF Qual 2021 ## Forgery (127 solves, 405pts) Felicity and Cisco would like to hire you as an intern for a new security company that they are forming. They have given you a black box signature verification system to test out and see if you can forge a signature. Forge it and you will get a passphrase to be hired! :::spoiler **Source Code** ```python= from Crypto.Util.number import getPrime from random import randint from math import gcd with open("flag.txt",'r') as f: flag = f.read() p = getPrime(1024) g = 3 MASK = 2**1024 - 1 def gen_keys(): x = randint(1, p-2) y = pow(g, x, p) return (x, y) def sign(answer: str, x: int): while True: m = int(asnwer, 16) & MASK k = randint(2, p-2) if gcd(k, p - 1) != 1: continue r = pow(g, k, p) s = (m - x*r) * pow(k,-1,p-1) % (p - 1) if s == 0: continue return (r,s) def verify(answer: str, r: int, s: int, y: int): m = int(answer, 16) & MASK if any([x <= 0 or x >= p-1 for x in [m,r,s]]): return False return pow(g, m, p) == (pow(y, r, p) * pow(r, s, p)) % p def main(): x, y = gen_keys() print(f"Server's public key (p,g,y): {p} {g} {y}") print("Who do you think is the tech wizard: Felicity or Cisco or both? Please answer it with your signnature (r,s)") print('Answer: ') answer = input() print('r: ') r = int(input()) print('s: ') s = int(input()) answer_bytes = bytes.fromhex(answer) if b'Felicity' not in answer_bytes and b'Cisco' not in answer_bytes and b'both' not in answer_bytes: print("Error: the answer is not valid!") elif verify(answer, r, s, y): if b'Felicity' in answer_bytes: print("I see you are a fan of Arrow!") elif b'Cisco' in answer_bytes: print("I see you are a fan of Flash!") else: print("Brown noser!") print(flag) else: print("Error: message does not match given signature") if __name__ == "__main__": main() ``` ::: ### Overview The server asks you to provide a tuple $(m, r, s)$ such that the corresponding bytes of $m$ contains a keyword and $$ g^{m \pmod{2^{1024}}} \equiv y^r \times r^s \pmod{p} $$ ### Valid Signature Forgery First of all, we need to find, at least, a valid tuple $(m, r, s)$. However, computing the discrete log of $y$ seems impossible. Notice that the constructable power of $y$ is $1$, a reasonable choice of $(r,s)$ is $$ (y, \quad -y \pmod{p-1}) $$ Now, for LHS, we need $0 < m < p-1$ with $g^{m} \equiv 1 \pmod{p}$. One may take $m = \frac{p-1}{2}$ and hope it works :D ### Sign Message Containing Given Keyword This is quite straightforward. Because the server only cares the residue of $m$ divided by $2^{1024}$, we simply change our $m$ to $$ Cisco \times 2^{1064} + \frac{p-1}{2} $$ ### Capture the Flag ```python= if pow(g, (p-1) // 2, p) != 1: r.close() print("3 is primitive root of p G_G") exit() answer = (p-1) // 2 + bytes_to_long(b"Ciscoo" + b"\x00" * 133) r.sendline(hex(answer)[2:]) r.sendline(str(y)) r.sendline(str(p-1-y)) r.recvuntil("I see you are a fan of Flash!") r.recvline() flag = r.recvline() print(flag) ``` :::success flag{7h3_4rr0wv3r53_15_4w350M3!} ::: ## ECC Pop Quiz (63 solves, 478 pts) Your crypto professor has taught y'all about ECC and now, here's a pop quiz regarding some of the attacks on it. See if you can pass the quiz! ### Overview 1. Smart Attack 2. Moving Attack 3. Singular Curve ### Capture the Flag :::success flag{4Ll_0f_tH353_4tT4cK5_R3lY_0N_51mPl1FY1n9_th3_D15cr3t3_l09_pr08l3m} ::: ## Bits (24 solves, 497 pts) I wrote this oracle in rust so that it can't sue companies over java stuff. :::spoiler **Source Code** ```rust= use std::io::BufRead; use getrandom::getrandom; use rug::{ rand::{RandGen,RandState}, Integer }; use sha2::{Sha256,Digest}; use aes::{Aes256,Aes256Ctr,NewBlockCipher,cipher::{FromBlockCipher,StreamCipher}}; use generic_array::GenericArray; // Secret sauce // N = p*q; p ≡ q ≡ 3 (mod 4); p, q prime use hardcore::{dlog, N, G, ORDER, FLAG}; struct SystemRandom; impl RandGen for SystemRandom { fn gen(&mut self) -> u32 { let mut buf: [u8; 4] = [0; 4]; let _ = getrandom(&mut buf).unwrap(); ((buf[0] as u32) << 24) | ((buf[1] as u32) << 16) | ((buf[2] as u32) << 8) | (buf[3] as u32) } } fn encrypt_flag(shared: Integer) { let mut hasher = Sha256::new(); hasher.update(shared.to_string()); let key = hasher.finalize(); let mut cipher = Aes256Ctr::from_block_cipher( Aes256::new_from_slice(&key.as_slice()).unwrap(), &GenericArray::clone_from_slice(&[0; 16]) ); let mut flag = FLAG.clone(); cipher.apply_keystream(&mut flag); println!("FLAG = {}", flag.iter().map(|c| format!("{:02x}", c)).collect::<String>()); } fn main() { println!("+++++++++++++++++++++++++++++++++++++++++++++++\n\ + I hear there's a mythical oracle at Delphi. +\n\ +++++++++++++++++++++++++++++++++++++++++++++++\n"); let mut sysrng = SystemRandom; let mut rnd = RandState::new_custom(&mut sysrng); let d = Integer::from(&*ORDER).random_below(&mut rnd); let publ = Integer::from(&*G).pow_mod(&d, &*N).unwrap(); let nbits = ORDER.significant_bits(); let alice = Integer::from(&*G).pow_mod(&Integer::from(&*ORDER).random_below(&mut rnd), &*N).unwrap(); println!("N = {}\nG = {}\npubl = {}\nalice = {}\nnbits = {}", *N, *G, publ, alice, nbits); encrypt_flag(alice.pow_mod(&d, &N).unwrap()); for line in std::io::stdin().lock().lines() { let input = line.unwrap().parse::<Integer>().unwrap(); match dlog(input.clone()) { None => println!("-1"), Some(x) => { assert!(G.clone().pow_mod(&x, &*N).unwrap() == input % &*N); assert!(x < *ORDER); assert!(x >= 0); println!("{}", x.get_bit(nbits - 123) as i32) } } } } ``` ::: ### Overview The server uses static generator and modulus $g, N$ and generates two public keys $A, B$ as follows: $$ A = g^{r_1}, \quad B = g^{r_2} \pmod{N} $$ where $r_1, r_2$ are two random numbers. Then the server calculates the shared secret $$A^{r_2} = g^{r_1r_2} = B^{r_1} \pmod{N} $$ and use it to encrypt the flag by AES256 in CTR mode. The server also provides a discrete-log oracle. For a given input $y$, it finds $x$ so that $$ g^{x} \equiv y \pmod{N} $$ If no such $x$, then it returns $-1$. Otherwise, it return $$ \begin{cases} 1 \quad & \textit{if} \quad x \& 2^{883} > 0 \\ 0 \quad & \textit{otherwise} \end{cases} $$ ### To decrypt the flag, it suffices find the secret exponent $r_1$. Notice that one may get a bit of $$r_1 + k \pmod{Ord_{N}(g)} $$ by sending $A \times g^{k} \pmod{N}$ to the oracle. ### Retrieve LSBs of $r_1$ Suppose the binary representation of $r_1$ is $b_{n}b_{n-1}\dots b_{0}$ or equivalently, $$ r_1 = \sum\limits_{i=0}^{n}{b_{i}\cdot 2^{i}} $$ Further, let $m$ be the smallest index such that $$ b_m \neq 0, \quad m > 883 $$ The key observation is $$ r_1 - \left(\sum\limits_{i=a+1}^{883}{b_i\cdot 2^{i}} + 2^{a}\right) = \begin{cases} \\ \\ \sum\limits_{i=m+1}^{1006}{b_i\cdot 2^{i}} + \sum\limits_{i=a}^{m-1}{\cdot 2^{i}} + \sum\limits_{i=0}^{a-1}{b_i\cdot 2^{i}} & \quad \textit{if} \quad b_a = 0 \\ \\ \sum\limits_{i=884}^{1006}{b_i\cdot 2^{i}} + \sum\limits_{i=0}^{a-1}{b_i\cdot 2^{i}} & \quad \textit{if} \quad b_a = 1 \end{cases} $$ So the oracle returns $1$ iff $b_{a} = 0$ and we can sequentially determine the lower bits of $r_1$ ```python= d_lsb = 0 for i in range(nbits - 123, -1, -1): k = d_lsb | (1 << i) payload = (A * pow(g, -k, N)) % N r.sendline(str(payload)) _ = r.recvline() if b"0" in _: d_lsb |= (1 << i) ``` ### Binary Search MSBs of $r_1$ Note that $$ Ord_{N}(g) \ \mid \ \phi(N) < N $$ We know that the bit-lengths of $Ord_{N}(g), N$ are $1006, 1007$, respectively. One may expect that $Ord_{N}(g)$ is exactly $\phi(N) = N - p - q + 1$ and it follows that the MSBs of $Ord_{N}(g)$ and that of $N$ are the same. For convenience, we define two functions $MSBs$ and $LSBs$ by the following rules: $$ MSBs(x) = \left\lfloor\frac{x}{2^{884}}\right\rfloor, \quad LSBs(x) = x \pmod{2^{884}} $$ To apply binary search, we need to distinguish whether $MSBs(r_1)+MSBs(k)$ is larger than $MSBs(Ord_{N}(g))$. The following process will only work for the case that $883$-th bit of $Ord_{N}(g)$ equals to $0$. We're going to find $MSBs(Ord_{N}(g))-MSBs(r_1)$ by binary search. Begin with bounds $L, H = 0, MSBs(Ord_{N}(g))$ * Each time, we send $$A \times g^{-LSBs(r_1)} \times g^{m \cdot 2^{884}} \pmod{N}$$ where $m=\lfloor\frac{L+H}{2}\rfloor$ * If $MSBs(r_1) + m \le MSB(Ord_{N}(g))$, then it's clear that the oracle returns $0$. * Otherwise, the oracle returns the $883$-th bit of the following number $$ M = (MSBs(r_1)+m) \times 2^{884}-Ord_{N}(g) $$ which equals to $$ (MSBs(r_1)+m-MSBs(Ord_{N}(g))-1) \times 2^{884} + (2^{884}-LSBs(Ord_{N}(g))) $$ Assume the binary representation of $LSBs(Ord_{N}(g))$ is $c_{883}c_{882}\dots c_{0}$ Then $LSBs(M)$ is $$2^{884}-LSBs(Ord_{N}(g)) = \sum\limits_{i=0}^{883}(1-c_{i})\cdot 2^{i} + 1$$ Because $c_{883} = 0$, the oracle returns $1$ * Update $L, H$ according to the output ```python= T = (A * pow(g, -d_lsb, N)) % N L, H = 0, ord_msb while L + 1 < H: mid = (L + H) // 2 k = mid * (1 << (nbits - 123 + 1)) payload = T * pow(g, k, N) r.sendline(str(payload)) _ = r.recvline().strip() if b"1" in _: H = mid else: L = mid d_msb = ord_msb - L ``` ### Capture the Flag So we have the secret exponent $$ r_1 = MSBs(r_1) \times 2^{884} + LSBs(r_1) $$ and the shared secret $$B^{r_1} \pmod{N} $$ Let's decrypt the ciphertext! ```python= d = d_msb * (1 << (nbits - 123 + 1)) + d_lsb shared_secret = pow(B, d, N) Sha = hashlib.sha256() Sha.update(str(shared_secret).encode('ascii')) key = Sha.digest() cipher = AES.new(key, AES.MODE_CTR, nonce = b"\x00" * 8) flag = cipher.decrypt(encrypted_flag) print(pt) ``` :::success flag{https://www.youtube.com/watch?v=uhTCeZasCmc} :::