From d81a8c3c30ba4e8a6ec62b0d0c4ba5668b3b7662 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Sun, 5 Feb 2023 17:42:58 -0500 Subject: [PATCH] Finish challenge set 6, original challenges completed --- README.md | 55 ++++++++++++ data/set6c47.py | 18 ++-- src/main.rs | 19 ++-- src/set6.rs | 227 +++++++++++++++++++++++++++++++++++++++--------- src/utils.rs | 35 +++++++- 5 files changed, 293 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index aab6502..7da49c7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,58 @@ # cryptopals My solutions to the [cryptopals](https://cryptopals.com/) challenges. + +As of February 2023, after 123 FocusMate sessions, I have completed the +(original) six challenge sets. I consider this project to be complete. + +``` +[okay] Challenge 1: SSdtIGtpbGxpbmcgeW91ciBicmFpbiBsaWtlIGEgcG9pc29ub3VzIG11c2hyb29t +[okay] Challenge 2: 746865206b696420646f6e277420706c6179 +[okay] Challenge 3: Cooking MC's like a pound of bacon +[okay] Challenge 4: Now that the party is jumping +[okay] Challenge 5: 0b3637272a2b2e63622c2e69692a23693a2a3c6324202d623d... +[okay] Challenge 6: I'm back and I'm rin... +[okay] Challenge 7: I'm back and I'm rin... +[okay] Challenge 8: Cipher 132 [d880619740...] with rating 2398 (average = 2873.8186) is the solution. +[okay] Challenge 9: "YELLOW SUBMARINE\u{4}\u{4}\u{4}\u{4}" +[okay] Challenge 10: I'm back and I'm +[okay] Challenge 11: [10 / 10] +[okay] Challenge 12: Rollin' in my 5.0 +[okay] Challenge 13: role=admin +[okay] Challenge 14: Rollin' in my 5.0 +[okay] Challenge 15: PKCS7 works +[okay] Challenge 16: admin=true +[okay] Challenge 17: 000003Cooking MC's like a pound of bacon +[okay] Challenge 18: Yo, VIP Let's kick it Ice, Ice, baby Ice, Ice, baby +[okay] Challenge 19: i have met them at close of day +[okay] Challenge 20: I'm rated "R"...this is a warning, ya better void / P +[okay] Challenge 21: implemented MT19937 +[okay] Challenge 22: cracked MT19937 seed +[okay] Challenge 23: MT19937 RNG successfully cloned from output +[okay] Challenge 24: MT19937 stream cipher implemented and cracked +[okay] Challenge 25: recovered AES CTR plaintext via edit +[okay] Challenge 26: admin=true +[okay] Challenge 27: recovered key successfully +[okay] Challenge 28: implemented SHA-1 +[okay] Challenge 29: extended SHA-1 keyed message successfully +[okay] Challenge 30: implemented and extended MD4 successfully +[okay] Challenge 31: recovered HMAC-SHA1 via timing attack +[okay] Challenge 32: recovered HMAC-SHA1 with slightly less artificial timing leak +[okay] Challenge 33: implemented Diffie-Hellman +[okay] Challenge 34: implement MITM key-fixing attack on DH +[okay] Challenge 35: implement MITM with malicious g attack on DH +[okay] Challenge 36: implement secure remote password +[okay] Challenge 37: break SRP with zero key +[okay] Challenge 38: offline dictionary attack on SRP +[okay] Challenge 39: implement RSA +[okay] Challenge 40: implement an e=3 RSA Broadcast attack +[okay] Challenge 41: implement unpadded message recovery oracle +[okay] Challenge 42: Bleichenbacher's e=3 RSA Attack +[okay] Challenge 43: DSA key recovery from nonce +[okay] Challenge 44: DSA nonce recovery from repeated nonce +[okay] Challenge 45: DSA parameter tampering +[okay] Challenge 46: RSA parity oracle +[okay] Challenge 47: Bleichenbacher's PKCS 1.5 Padding Oracle (Simple Case) +[okay] Challenge 48: Bleichenbacher's PKCS 1.5 Padding Oracle (Complex Case) +``` + diff --git a/data/set6c47.py b/data/set6c47.py index 884b1ca..865fef1 100644 --- a/data/set6c47.py +++ b/data/set6c47.py @@ -2,11 +2,16 @@ import sys from math import log, ceil, floor from fractions import Fraction - +# Case for which 2.b does not execute (Simple Case) e = 3 d = 49245850836238243386848117224834103046337172957950760944544575720018018155267 n = 73868776254357365080272175837251154570062235041494422684546086388903725268033 +# Case for which 2.b does execute (Complex Case) +e = 3 +d = 7436226431632429054084263734954342197144411188982219770498201348078527629483 +n = 11154339647448643581126395602431513296069677965376957820612905826953621832839 + def bytes_needed(n: int) -> int: if n == 0: @@ -63,10 +68,6 @@ def test(): padded_m = 5300541194335152988749892502228755547482451611528547105226896651010982723 assert(padded_m == add_padding(m, k)) - c_new = encrypt(padded_m) - c = 71554147358804792877798821486588152314859921438911236615156507964101619628630 - assert(c == c_new) - assert(m == remove_padding(add_padding(m, k), k)) print('[tests passed]') @@ -98,7 +99,9 @@ def main(): s += 1 elif len(m) > 1: # Step 2.b: Searching with more than one interval left. - raise Exception("Not implemented") + s += 1 + while not oracle(c * pow(s, e, n) % n): + s += 1 elif len(m) == 1: # Step 2.c: Searching with one interval left. a, b = m[0] @@ -145,8 +148,7 @@ def main(): assert(m == 5300541194335152988749892502228755547482451611528547105226896651010982723) m = remove_padding(m, k) assert(m == m_orig) - print("[okay] Challenge 47: Bleichenbacher's PKCS 1.5 Padding Oracle (Simple Case)") - + print("[okay] Challenge 48: Bleichenbacher's PKCS 1.5 Padding Oracle (Complex Case)") main() diff --git a/src/main.rs b/src/main.rs index 6ae0005..f4f7fd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ #![warn(clippy::pedantic)] +#![warn(clippy::nursery)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_precision_loss)] #![allow(clippy::items_after_statements)] #![allow(clippy::many_single_char_names)] #![allow(clippy::module_name_repetitions)] -#![feature(string_remove_matches)] mod bytes; mod bytes_base64; mod cbc; @@ -28,7 +28,7 @@ mod srp; mod utils; fn main() { - const RUN_ALL: bool = false; + const RUN_ALL: bool = true; if RUN_ALL { set1::challenge1(); set1::challenge2(); @@ -70,12 +70,13 @@ fn main() { set5::challenge38().unwrap_or_else(|| println!("[fail] challenge 38")); set5::challenge39().unwrap_or_else(|| println!("[fail] challenge 39")); set5::challenge40().unwrap_or_else(|| println!("[fail] challenge 40")); + set6::challenge41().unwrap_or_else(|_| println!("[fail] challenge 41")); + set6::challenge42().unwrap_or_else(|_| println!("[fail] challenge 42")); + set6::challenge43().unwrap_or_else(|| println!("[fail] challenge 43")); + set6::challenge44().unwrap_or_else(|| println!("[fail] challenge 44")); + set6::challenge45().unwrap_or_else(|| println!("[fail] challenge 45")); + set6::challenge46().unwrap_or_else(|_| println!("[fail] challenge 46")); + set6::challenge47().unwrap_or_else(|_| println!("[fail] challenge 47")); + set6::challenge48().unwrap_or_else(|_| println!("[fail] challenge 48")); } - set6::challenge41().unwrap_or_else(|_| println!("[fail] challenge 41")); - set6::challenge42().unwrap_or_else(|_| println!("[fail] challenge 42")); - set6::challenge43().unwrap_or_else(|| println!("[fail] challenge 43")); - set6::challenge44().unwrap_or_else(|| println!("[fail] challenge 44")); - set6::challenge45().unwrap_or_else(|| println!("[fail] challenge 45")); - set6::challenge46().unwrap_or_else(|_| println!("[fail] challenge 46")); - set6::challenge47().unwrap_or_else(|_| println!("[fail] challenge 47")); } diff --git a/src/set6.rs b/src/set6.rs index 0aa1390..390ed40 100644 --- a/src/set6.rs +++ b/src/set6.rs @@ -2,10 +2,8 @@ use crate::bytes::Bytes; use crate::bytes_base64::BytesBase64; use crate::dsa; use crate::rsa; -use crate::utils::bnclone; -use num_bigint::BigUint; -use openssl::bn::BigNum; -use openssl::bn::BigNumContext; +use crate::utils::{bn, bnclone, cube_root, div_ceil, div_floor}; +use openssl::bn::{BigNum, BigNumContext}; use openssl::error::ErrorStack; use openssl::sha::sha256; @@ -61,12 +59,6 @@ pub fn challenge42() -> Result<(), ErrorStack> { "RSA verify does not work" ); - fn cube_root(n: &BigNum) -> Result { - let b = BigUint::from_bytes_be(&n.to_vec()); - let b = b.nth_root(3); - BigNum::from_slice(&b.to_bytes_be()) - } - fn _cube(n: &BigNum) -> BigNum { n * &(n * n) } @@ -82,7 +74,7 @@ pub fn challenge42() -> Result<(), ErrorStack> { // Add one to the cube root to ensure that when the number is // cubed again it contains the desired signature. let sig_cubed = BigNum::from_slice(&v)?; - let mut sig = cube_root(&sig_cubed)?; + let mut sig = cube_root(&sig_cubed); sig.add_word(1)?; Ok(sig) @@ -192,12 +184,12 @@ pub mod challenge44 { let file = std::fs::File::open("data/44.txt").unwrap(); let mut result = Vec::new(); let mut lines: Vec = BufReader::new(file).lines().map(|l| l.unwrap()).collect(); - // each message cosists of four lines: msg, s, r, m (sha1 hash of msg) for line in lines.chunks_mut(4) { - line[0].remove_matches("msg: "); - line[1].remove_matches("s: "); - line[2].remove_matches("r: "); - line[3].remove_matches("m: "); + // each message cosists of four lines: msg, s, r, m (sha1 hash of msg) + line[0] = line[0][5..].to_string(); + line[1] = line[1][3..].to_string(); + line[2] = line[2][3..].to_string(); + line[3] = line[3][3..].to_string(); let msg = Bytes::from_utf8(&line[0]); let s = BigNum::from_dec_str(&line[1]).unwrap(); let r = BigNum::from_dec_str(&line[2]).unwrap(); @@ -205,7 +197,7 @@ pub mod challenge44 { assert_eq!( dsa::h(&msg).unwrap(), m, - "Message hash from data/44.txt does not match" + "hashes in data/44.txt do not match" ); let d = DsaSignedMsg { msg, r, s, m }; result.push(d); @@ -407,20 +399,136 @@ pub fn challenge46() -> Result<(), ErrorStack> { Ok(()) } +mod challenge47 { + use crate::rsa; + use crate::utils::{bn, bnclone, div_ceil, div_floor}; + use openssl::bn::{BigNum, BigNumContext}; + use std::cmp; + + pub fn bleichenbachers_pkcs15_padding_oracle_attack( + c: &BigNum, + e: &BigNum, + d: &BigNum, + n: &BigNum, + ) -> BigNum { + #![allow(non_snake_case)] + let mut ctx = BigNumContext::new().unwrap(); + + let public_key = rsa::RsaPublicKey { + e: bnclone(e), + n: bnclone(n), + }; + + let private_key = rsa::RsaPrivateKey { + d: bnclone(d), + n: bnclone(n), + }; + + // k is number of bytes of n as specified in Bleichenbacher's paper + let n_bytes = n.num_bytes(); + let k: u32 = n_bytes.try_into().unwrap(); + + let oracle = |cipher: &BigNum| -> bool { + let cleartext: BigNum = rsa::rsa_decrypt_unpadded(cipher, &private_key).unwrap(); + let v = cleartext.to_vec_padded(n_bytes).unwrap(); + return v[0] == 0x0 && v[1] == 0x2; + }; + + let mul = |s: &BigNum| -> BigNum { + let mut ctx = BigNumContext::new().unwrap(); + let mut f = BigNum::new().unwrap(); + f.mod_exp(s, &public_key.e, &n, &mut ctx).unwrap(); + &(c * &f) % n + }; + + let mut B = BigNum::new().unwrap(); + // B = 2^(8(k−2)) + B.exp(&bn(2), &(&bn(8) * &(&bn(k) - &bn(2))), &mut ctx) + .unwrap(); + + // Step 1: Blinding (not necessary because already PKCS conforming). + let mut i: u32 = 1; + let mut s: BigNum = bn(1); + let mut m: Vec<(BigNum, BigNum)> = vec![(&bn(2) * &B, &(&bn(3) * &B) - &bn(1))]; + let solution: BigNum; + + // Step 2: Searching for PKCS conforming messages. + loop { + if i == 1 { + // Step 2.a: Starting the search. + s = div_ceil(&n, &(&bn(3) * &B)); + while !oracle(&mul(&s)) { + s.add_word(1).unwrap(); + } + } else if m.len() > 1 { + // Step 2.b: Searching with more than one interval left. + s.add_word(1).unwrap(); + while !oracle(&mul(&s)) { + s.add_word(1).unwrap(); + } + } else if m.len() == 1 { + // Step 2.c: Searching with one interval left. + let (a, b) = (bnclone(&m[0].0), bnclone(&m[0].1)); + let mut found = false; + let mut r = div_ceil(&(&bn(2) * &(&(&b * &s) - &(&bn(2) * &(B)))), &n); + while !found { + let mut s_lower = div_ceil(&(&(&bn(2) * &B) + &(&r * n)), &b); + let s_upper = div_ceil(&(&(&bn(3) * &B) + &(&r * n)), &a); + while s_lower < s_upper { + if oracle(&mul(&s_lower)) { + found = true; + s = s_lower; + break; + } + s_lower.add_word(1).unwrap(); + } + r.add_word(1).unwrap(); + } + } else { + panic!("step 2 -- no more intervals?"); + } + + // Step 3: Narrowing the set of solutions. + let mut m_new: Vec<(BigNum, BigNum)> = vec![]; + for (a, b) in m { + let lower_ceil = div_ceil(&(&(&a * &s) - &(&(&bn(3) * &B) + &bn(1))), &n); + let mut upper_ceil = div_ceil(&(&(&b * &s) - &(&bn(2) * &B)), &n); + let upper_floor = div_floor(&(&(&b * &s) - &(&bn(2) * &B)), &n); + if upper_floor == upper_ceil { + upper_ceil.add_word(1).unwrap(); + } + + let mut r = lower_ceil; + while r < upper_ceil { + let a_new = cmp::max(bnclone(&a), div_ceil(&(&(&bn(2) * &B) + &(&r * n)), &s)); + let b_new = cmp::min( + bnclone(&b), + div_floor(&(&(&(&bn(3) * &B) - &bn(1)) + &(&r * n)), &s), + ); + m_new.push((a_new, b_new)); + r.add_word(1).unwrap(); + } + } + m = m_new; + + // Step 4: Computing the solutions. + if m.len() == 1 && m[0].0 == m[0].1 { + solution = rsa::rsa_padding_remove_pkcs1(&m[0].0, n_bytes).unwrap(); + return solution; + } + i += 1; + } + } +} + pub fn challenge47() -> Result<(), ErrorStack> { // Generate a 256 bit keypair (that is, p and q will each be 128 bit primes), [n, e, d]. let (public_key, private_key) = rsa::rsa_gen_keys_with_size(128, 128)?; - println!("e={:?}", public_key.e); - println!("d={:?}", private_key.d); - println!("n={:?}", private_key.n); // PKCS1.5-pad a short message, like "kick it, CC", and call it "m". Encrypt to to get "c". - let m = Bytes::from_utf8("kick it, CC"); - let m = BigNum::from_slice(&m.0)?; - let n = bnclone(&public_key.n); - let n_bytes = n.num_bytes(); - println!("m={:?}", m); - println!("n_bytes={}", n_bytes); + let m = BigNum::from_slice(&"kick it, CC".as_bytes())?; + let c = rsa::rsa_encrypt(&m, &public_key)?; + let n_bytes = private_key.n.num_bytes(); // Build an oracle function, just like you did in the last exercise, but have it check for // plaintext[0] == 0 and plaintext[1] == 2. @@ -430,24 +538,57 @@ pub fn challenge47() -> Result<(), ErrorStack> { return v[0] == 0x0 && v[1] == 0x2; }; - // Decrypt "c" using your padding oracle. - let c_unpadded = rsa::rsa_encrypt_unpadded(&m, &public_key)?; - let c = rsa::rsa_encrypt(&m, &public_key)?; - println!("c={:?}", c); + assert!(oracle(&c), "c is padded"); + assert!(div_floor(&bn(3), &bn(2)) == bn(1)); + assert!(div_ceil(&bn(3), &bn(2)) == bn(2)); + assert!(div_floor(&bn(4), &bn(2)) == bn(2)); + assert!(div_ceil(&bn(4), &bn(2)) == bn(2)); - assert!(!oracle(&c_unpadded), "oracle wrongly thinks unpadded message is padded"); - assert!(oracle(&c), "oracle wrongly thinks padded message is not padded"); + let solution = challenge47::bleichenbachers_pkcs15_padding_oracle_attack( + &c, + &public_key.e, + &private_key.d, + &private_key.n, + ); - // B = 2^(8(k−2)); k is the length of n in bytes; - // let mut ctx = BigNumContext::new()?; - // let mut p = BigNum::new()?; - // let k = BigNum::from_u32(n_bytes.try_into().unwrap())?; - // p.checked_sub(&k, &BigNum::from_u32(2)?); - // p = &p * BigNum::from_u32(8); - - // b.exp(&BigNum::from_u32(2)?, &BigNum::from_u32(8)? * &(&n_bytes - &BigNum::from_u32(2)), &mut ctx); - - - println!("[xxxx] Challenge 47: Bleichenbacher's PKCS 1.5 Padding Oracle (Simple Case)"); + assert_eq!(m, solution, "Bleichenbacher's attack failed"); + println!("[okay] Challenge 47: Bleichenbacher's PKCS 1.5 Padding Oracle (Simple Case)"); + Ok(()) +} + +pub fn challenge48() -> Result<(), ErrorStack> { + // Set yourself up the way you did in #47, but this time generate a 768 bit modulus. + + // Use e, d, and n that execute step 2.b. (not all 768 bit modulus do) + let public_key = rsa::RsaPublicKey { + e: BigNum::from_dec_str("3").unwrap(), + n: BigNum::from_dec_str( + "11154339647448643581126395602431513296069677965376957820612905826953621832839", + ) + .unwrap(), + }; + + let private_key = rsa::RsaPrivateKey { + d: BigNum::from_dec_str( + "7436226431632429054084263734954342197144411188982219770498201348078527629483", + ) + .unwrap(), + n: BigNum::from_dec_str( + "11154339647448643581126395602431513296069677965376957820612905826953621832839", + ) + .unwrap(), + }; + + let m = BigNum::from_slice(&"kick it, CC".as_bytes())?; + let c = rsa::rsa_encrypt(&m, &public_key)?; + let solution = challenge47::bleichenbachers_pkcs15_padding_oracle_attack( + &c, + &public_key.e, + &private_key.d, + &private_key.n, + ); + assert_eq!(m, solution, "Bleichenbacher's attack failed"); + + println!("[okay] Challenge 48: Bleichenbacher's PKCS 1.5 Padding Oracle (Complex Case)"); Ok(()) } diff --git a/src/utils.rs b/src/utils.rs index c40b928..0f99de2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ use crate::{bytes::Bytes, bytes_base64::BytesBase64}; -use openssl::bn::BigNum; +use num_bigint::BigUint; +use openssl::bn::{BigNum, BigNumContext}; use std::{ io::{BufRead, BufReader}, time::{SystemTime, UNIX_EPOCH}, @@ -55,3 +56,35 @@ pub fn xor(a: &[u8], b: &[u8]) -> Vec { pub fn bnclone(b: &BigNum) -> BigNum { BigNum::from_slice(&b.to_vec()).unwrap() } + +pub fn cube_root(n: &BigNum) -> BigNum { + let b = BigUint::from_bytes_be(&n.to_vec()); + let b = b.nth_root(3); + BigNum::from_slice(&b.to_bytes_be()).unwrap() +} + +pub fn bn(n: u32) -> BigNum { + BigNum::from_u32(n).unwrap() +} + +pub fn div_rem(a: &BigNum, b: &BigNum) -> (BigNum, BigNum) { + // kReturns the divider and remainder of a / b. + let mut ctx = BigNumContext::new().unwrap(); + let mut div = BigNum::new().unwrap(); + let mut rem = BigNum::new().unwrap(); + div.div_rem(&mut rem, a, b, &mut ctx).unwrap(); + (div, rem) +} + +pub fn div_floor(a: &BigNum, b: &BigNum) -> BigNum { + let (div, _) = div_rem(a, b); + div +} + +pub fn div_ceil(a: &BigNum, b: &BigNum) -> BigNum { + let (mut div, rem) = div_rem(a, b); + if rem.num_bits() != 0 { + div.add_word(1).unwrap(); + } + div +}