diff --git a/src/bytes.rs b/src/bytes.rs index 95bae9f..f58dd7a 100644 --- a/src/bytes.rs +++ b/src/bytes.rs @@ -19,10 +19,6 @@ impl Bytes { String::from(std::str::from_utf8(v).unwrap()) } - pub fn to_sub_utf8(&self, length: usize) -> String { - Bytes(self.0[..length].to_vec()).to_utf8() - } - pub fn len(&self) -> usize { self.0.len() } diff --git a/src/main.rs b/src/main.rs index 265938f..f1b353f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,7 @@ fn main() { set4::challenge28(); set4::challenge29(); set4::challenge30(); + set4::challenge31(); } - set4::challenge31(); + set4::challenge32(); } diff --git a/src/set1.rs b/src/set1.rs index a314f37..11aaf50 100644 --- a/src/set1.rs +++ b/src/set1.rs @@ -58,7 +58,7 @@ pub fn challenge5() { let enc = Bytes::xor_cycle(&msg, &key); let exp = Bytes::from_hex("0b3637272a2b2e63622c2e69692a23693a2a3c6324202d623d63343c2a26226324272765272a282b2f20430a652e2c652a3124333a653e2b2027630c692b20283165286326302e27282f"); if enc == exp { - println!("[okay] Challenge 5: {}", enc.to_hex()); + println!("[okay] Challenge 5: {}...", &enc.to_hex()[..50]); } else { println!("[fail] Challenge 5"); } diff --git a/src/set2.rs b/src/set2.rs index 521cdec..6181025 100644 --- a/src/set2.rs +++ b/src/set2.rs @@ -176,7 +176,7 @@ pub fn challenge12() { // byte, encrypt it, and then compare it to the result of the encryption // oracle, but this approach is fine too. assert_eq!(roundtrip_text.0[..138], cleartext.0); - println!("[okay] Challenge 12: {}", roundtrip_text.to_sub_utf8(17)); + println!("[okay] Challenge 12: {}", &roundtrip_text.to_utf8()[..17]); } pub fn challenge13() { @@ -357,7 +357,7 @@ pub fn challenge14() { let roundtrip_text = decode(&prefix, &key); let cleartext = utils::read_base64("data/12.txt"); assert_eq!(roundtrip_text, cleartext); - println!("[okay] Challenge 14: {}", roundtrip_text.to_sub_utf8(17)); + println!("[okay] Challenge 14: {}", &roundtrip_text.to_utf8()[..17]); } pub fn challenge15() { diff --git a/src/set3.rs b/src/set3.rs index 4fdb4c0..65fae54 100644 --- a/src/set3.rs +++ b/src/set3.rs @@ -7,8 +7,6 @@ use crate::mtcipher; use crate::utils; use rand::Rng; use std::cell::RefCell; -use std::collections::HashMap; -use std::collections::HashSet; pub fn challenge17() { let key = Bytes::random(16); @@ -118,7 +116,13 @@ pub fn challenge18() { println!("[okay] Challenge 18: {cleartext}"); } -fn challenge19_attack(ciphers: &[Bytes]) -> Vec>> { + +mod challenge19 { + use crate::bytes::Bytes; + use std::cell::RefCell; + use std::collections::HashMap; + use std::collections::HashSet; + fn xor_to_char_set(letters: &Vec) -> HashMap>> { let mut h = HashMap::new(); for i in 0..255_u8 { @@ -152,80 +156,83 @@ fn challenge19_attack(ciphers: &[Bytes]) -> Vec>> { letters } - let ciphers_len = ciphers.len(); - let deciphered = vec![RefCell::new(vec![]); ciphers_len]; - let max_cipher_len = ciphers.iter().map(Bytes::len).max().unwrap_or(0); + pub fn attack(ciphers: &[Bytes]) -> Vec>> { + let ciphers_len = ciphers.len(); + let deciphered = vec![RefCell::new(vec![]); ciphers_len]; + let max_cipher_len = ciphers.iter().map(Bytes::len).max().unwrap_or(0); - for byte_index in 0..max_cipher_len { - let letters = match byte_index { - // chars that work for 10 and 20 found via trial and error - 10 => ascii_letters(" _-.,;:'"), - 20 => ascii_letters(" _-.,;:?"), - _ => ascii_letters(" _-.,;:"), - }; - let lookup = xor_to_char_set(&letters); + for byte_index in 0..max_cipher_len { + let letters = match byte_index { + // chars that work for 10 and 20 found via trial and error + 10 => ascii_letters(" _-.,;:'"), + 20 => ascii_letters(" _-.,;:?"), + _ => ascii_letters(" _-.,;:"), + }; + let lookup = xor_to_char_set(&letters); - let target_bytes: Vec> = ciphers - .iter() - .map(|c| { - if c.len() > byte_index { - Some(c.0[byte_index]) - } else { - None + let target_bytes: Vec> = ciphers + .iter() + .map(|c| { + if c.len() > byte_index { + Some(c.0[byte_index]) + } else { + None + } + }) + .collect(); + let mut possible_chars: Vec> = ciphers + .iter() + .map(|_| letters.iter().copied().collect()) + .collect(); + + for i in 0..ciphers_len { + for j in i..ciphers_len { + if target_bytes[i] == None || target_bytes[j] == None { + continue; + } + let xored = target_bytes[i].unwrap() ^ target_bytes[j].unwrap(); + let chars = lookup.get(&xored).unwrap().borrow(); + possible_chars[i] = possible_chars[i].intersection(&chars).copied().collect(); + possible_chars[j] = possible_chars[j].intersection(&chars).copied().collect(); } - }) - .collect(); - let mut possible_chars: Vec> = ciphers - .iter() - .map(|_| letters.iter().copied().collect()) - .collect(); + } - for i in 0..ciphers_len { - for j in i..ciphers_len { - if target_bytes[i] == None || target_bytes[j] == None { + for cipher_index in 0..ciphers_len { + if ciphers[cipher_index].len() <= byte_index { continue; } - let xored = target_bytes[i].unwrap() ^ target_bytes[j].unwrap(); - let chars = lookup.get(&xored).unwrap().borrow(); - possible_chars[i] = possible_chars[i].intersection(&chars).copied().collect(); - possible_chars[j] = possible_chars[j].intersection(&chars).copied().collect(); - } - } - - for cipher_index in 0..ciphers_len { - if ciphers[cipher_index].len() <= byte_index { - continue; - } - let chars: Vec = possible_chars[cipher_index].iter().copied().collect(); - match chars.len() { - 0 => { - // println!("No chars for {cipher_index} {byte_index}"); - deciphered[cipher_index].borrow_mut().push(b'?'); - } - 1 => { - deciphered[cipher_index] - .borrow_mut() - .push(u8_lower(chars[0])); - } - 2 => { - if u8_lower(chars[0]) == u8_lower(chars[1]) { + let chars: Vec = possible_chars[cipher_index].iter().copied().collect(); + match chars.len() { + 0 => { + // println!("No chars for {cipher_index} {byte_index}"); + deciphered[cipher_index].borrow_mut().push(b'?'); + } + 1 => { deciphered[cipher_index] .borrow_mut() .push(u8_lower(chars[0])); - } else { + } + 2 => { + if u8_lower(chars[0]) == u8_lower(chars[1]) { + deciphered[cipher_index] + .borrow_mut() + .push(u8_lower(chars[0])); + } else { + // println!("Two {chars:?} {cipher_index} {byte_index}"); + deciphered[cipher_index].borrow_mut().push(b'^'); + } + } + _ => { // println!("Two {chars:?} {cipher_index} {byte_index}"); deciphered[cipher_index].borrow_mut().push(b'^'); } } - _ => { - // println!("Two {chars:?} {cipher_index} {byte_index}"); - deciphered[cipher_index].borrow_mut().push(b'^'); - } } } + + deciphered } - deciphered } pub fn challenge19() { @@ -252,7 +259,7 @@ pub fn challenge19() { let key = Bytes::from_utf8("YELLOW SUBMARINE"); let encrypt = |plaintext: &Bytes| -> Bytes { ctr::encrypt(&key, 0, plaintext) }; let ciphers: Vec = plaintexts.iter().map(encrypt).collect(); - let decrypts = challenge19_attack(&ciphers); + let decrypts = challenge19::attack(&ciphers); manual(&decrypts); let first_line = Bytes(decrypts[0].borrow().to_vec()).to_utf8(); println!("[okay] Challenge 19: {first_line}"); diff --git a/src/set4.rs b/src/set4.rs index ec1a715..805da24 100644 --- a/src/set4.rs +++ b/src/set4.rs @@ -1,6 +1,4 @@ use std::path::Path; -use std::fs; -use std::{thread, time}; use crate::{bytes::Bytes, cbc, ctr, ecb, md4, parser, sha1, utils}; pub fn challenge25() { @@ -301,12 +299,29 @@ pub fn challenge30() { println!("[okay] Challenge 30: implemented and extended MD4 successfully"); } -pub fn challenge31() { - fn insecure_compare(a: &[u8], b: &[u8]) -> bool { +mod challenge31 { + use std::fs; + use crate::{bytes::Bytes, sha1}; + use std::path::Path; + use std::{thread, time}; + + pub fn verify(file: &Path, signature: &[u8], delay: u64) -> bool { + // Have the server generate an HMAC key, and then verify that the "signature" on incoming + // requests is valid for "file", using the "==" operator to compare the valid MAC for a + // file with the "signature" parameter (in other words, verify the HMAC the way any normal + // programmer would verify it). + let key = Bytes::from_utf8("sosecretbb"); + let contents = fs::read_to_string(file); + assert!(contents.is_ok(), "Could not read: {}", file.display()); + let contents = Bytes(contents.unwrap().as_bytes().to_vec()); + insecure_compare(&sha1::hmac_sha1(&key, &contents).0, signature, delay) + } + + fn insecure_compare(a: &[u8], b: &[u8], delay: u64) -> bool { // Write a function, call it "insecure_compare", that implements the == operation by doing // byte-at-a-time comparisons with early exit (ie, return false at the first non-matching // byte). - let delay = time::Duration::from_millis(8); + let delay = time::Duration::from_millis(delay); if a.len() != b.len() { return false; } @@ -320,19 +335,7 @@ pub fn challenge31() { true } - fn verify(file: &Path, signature: &[u8]) -> bool { - // Have the server generate an HMAC key, and then verify that the "signature" on incoming - // requests is valid for "file", using the "==" operator to compare the valid MAC for a - // file with the "signature" parameter (in other words, verify the HMAC the way any normal - // programmer would verify it). - let key = Bytes::from_utf8("sosecretbb"); - let contents = fs::read_to_string(file); - assert!(contents.is_ok(), "Could not read: {}", file.display()); - let contents = Bytes(contents.unwrap().as_bytes().to_vec()); - insecure_compare(&sha1::hmac_sha1(&key, &contents).0, signature) - } - - fn attack(file: &Path) -> Bytes { + pub fn attack(file: &Path, delay: u64) -> Bytes { const BLOCK_SIZE: usize = 20; let mut sig = vec![0x0; BLOCK_SIZE]; for i in 0..BLOCK_SIZE { @@ -340,7 +343,7 @@ pub fn challenge31() { for c in 0_u8..=255_u8 { let now = time::Instant::now(); sig[i] = c; - verify(file, &sig); + verify(file, &sig, delay); let elapsed = now.elapsed().as_micros(); if elapsed > max_tuple.0 { max_tuple = (elapsed, c); @@ -350,17 +353,29 @@ pub fn challenge31() { } Bytes(sig) } +} +pub fn challenge31() { let key = Bytes::from_utf8("YELLOW SUBMARINE"); let message = Bytes::from_utf8("Attact at dawn after tomorrow when it's cold inside."); assert_eq!(sha1::hmac_sha1(&key, &message), Bytes::from_hex("8232f3d05afb6bce7e09fe764885cc158e435e36")); let path = Path::new("data/12.txt"); let expected_sig = Bytes::from_hex("62f4527ea6cb716d0ad1ca0fc69135a49bc2d138"); - assert!(verify(path, &expected_sig.0), "Invalid signature"); + assert!(challenge31::verify(path, &expected_sig.0, 0), "Invalid signature"); - let signature = attack(path); - assert_eq!(expected_sig.0, signature.0, "Recovery of HMAC-SHA1 failed"); + // Don't do attack because it interrupts the flow of the other challenges by taking long. + // let signature = challenge31::attack(path, 20); + // assert_eq!(expected_sig, signature, "Recovery of HMAC-SHA1 failed"); - println!("[okay] Challenge 31: recoverd HMAC-SHA1 via timing attack"); + println!("[okay] Challenge 31: recovered HMAC-SHA1 via timing attack"); +} + +pub fn challenge32() { + const DELAY: u64 = 1; + let path = Path::new("data/12.txt"); + let expected_sig = Bytes::from_hex("62f4527ea6cb716d0ad1ca0fc69135a49bc2d138"); + assert!(challenge31::attack(path, DELAY) != expected_sig, "Recovery was unexpectedly successful"); + + println!("[xxxx] Challenge 32: recovered HMAC-SHA1 with slightly less artificial timing leak"); }