526 lines
19 KiB
Rust
526 lines
19 KiB
Rust
use crate::bytes::Bytes;
|
|
use crate::bytes_base64::BytesBase64;
|
|
use crate::cbc;
|
|
use crate::ctr;
|
|
use crate::mt19937;
|
|
use crate::mtcipher;
|
|
use crate::utils;
|
|
use rand::Rng;
|
|
use std::cell::RefCell;
|
|
|
|
pub fn challenge17() {
|
|
let key = Bytes::random(16);
|
|
let encrypt = || -> (Bytes, Bytes, usize) {
|
|
// The first function should select at random one of the ten strings
|
|
let cleartexts = utils::read_base64_lines("data/17.txt");
|
|
let index: usize = rand::thread_rng().gen_range(0..cleartexts.len());
|
|
let mut cleartext = Bytes(cleartexts[index].0.clone());
|
|
|
|
// pad the string out to the 16-byte AES block size and
|
|
cleartext.pad_pkcs7(16);
|
|
|
|
// CBC-encrypt it under that key, providing the caller the ciphertext
|
|
// and IV and cleartext index for check.
|
|
let iv = Bytes::random(16);
|
|
(cbc::encrypt(&key, &iv, &cleartext), iv, index)
|
|
};
|
|
|
|
// generate a random AES key (which it should save for all future encryptions)
|
|
let (cipher, iv, cleartext_index) = encrypt();
|
|
let decryption_oracle = |iv: &Bytes, cipher: &Bytes| -> bool {
|
|
// The second function should consume the ciphertext produced by the
|
|
// first function, decrypt it, check its padding, and return true or
|
|
// false depending on whether the padding is valid.
|
|
let cleartext = cbc::decrypt(&key, iv, cipher);
|
|
cleartext.has_valid_pkcs7(16)
|
|
};
|
|
|
|
let attack_block = |previous_block: &Bytes, cipher_block: &Bytes| -> Bytes {
|
|
// Good explanation: https://robertheaton.com/2013/07/29/padding-oracle-attack/
|
|
let block_size: u8 = cipher_block
|
|
.len()
|
|
.try_into()
|
|
.expect("block size should be less than 255");
|
|
let mut attack_vector = Bytes::random(block_size.into());
|
|
let mut intermittent_result = vec![];
|
|
|
|
for pad_byte in 1_u8..=block_size {
|
|
// preset attack vector so that paddinig is [1], [2, 2], [3, 3, 3], and so on.
|
|
let pad_byte_index: usize = (block_size - pad_byte).into();
|
|
attack_vector.0[pad_byte_index] = pad_byte;
|
|
for (i, intermittent_byte) in intermittent_result
|
|
.iter()
|
|
.enumerate()
|
|
.take(pad_byte as usize - 1)
|
|
{
|
|
attack_vector.0[block_size as usize - 1 - i] = (pad_byte as u8) ^ intermittent_byte;
|
|
}
|
|
|
|
// guess attack vector so that padding is valid
|
|
let guess_index = block_size - pad_byte;
|
|
for guess in 0..=255 {
|
|
attack_vector.0[guess_index as usize] = guess;
|
|
if decryption_oracle(&attack_vector, cipher_block) {
|
|
// println!("{guess:#016b}");
|
|
let c = (guess as u8) ^ (pad_byte as u8);
|
|
intermittent_result.push(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
// transform intermittent result by xoring it with previous block
|
|
intermittent_result.reverse();
|
|
let xored: Vec<u8> = Iterator::zip(previous_block.0.iter(), intermittent_result)
|
|
.map(|z| z.0 ^ z.1)
|
|
.collect();
|
|
assert_eq!(xored.len(), block_size.into());
|
|
Bytes(xored)
|
|
};
|
|
|
|
// Attack block by block.
|
|
let mut roundtrip = Bytes(vec![]);
|
|
let block_count = cipher.len() / 16;
|
|
for block in 0..block_count {
|
|
let mut clear_block = if block == 0 {
|
|
attack_block(&iv, &cipher.get_block(0, 16))
|
|
} else {
|
|
attack_block(
|
|
&cipher.get_block(block - 1, 16),
|
|
&cipher.get_block(block, 16),
|
|
)
|
|
};
|
|
roundtrip.0.append(&mut clear_block.0);
|
|
}
|
|
roundtrip.remove_pkcs7(16);
|
|
let cleartexts = utils::read_base64_lines("data/17.txt");
|
|
let cleartext = Bytes(cleartexts[cleartext_index].0.clone());
|
|
assert_eq!(roundtrip, cleartext);
|
|
println!("[okay] Challenge 17: {}", roundtrip.to_utf8());
|
|
}
|
|
|
|
pub fn challenge18() {
|
|
let key = Bytes::from_utf8("YELLOW SUBMARINE");
|
|
|
|
let nonce = 1337;
|
|
let cleartext = Bytes::from_utf8("Let's see if we can get the party started hard my friends.");
|
|
let cipher = ctr::encrypt(&key, nonce, &cleartext);
|
|
let roundtrip = ctr::decrypt(&key, nonce, &cipher);
|
|
assert_eq!(cleartext, roundtrip);
|
|
|
|
let cipher = BytesBase64::from_base64(
|
|
"L77na/nrFsKvynd6HzOoG7GHTLXsTVu9qvY/2syLXzhPweyyMTJULu/6/kXX0KSvoOLSFQ==",
|
|
)
|
|
.unwrap()
|
|
.to_bytes();
|
|
let cleartext = ctr::decrypt(&key, 0, &cipher).to_utf8();
|
|
println!("[okay] Challenge 18: {cleartext}");
|
|
}
|
|
|
|
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<u8>) -> HashMap<u8, RefCell<HashSet<u8>>> {
|
|
let mut h = HashMap::new();
|
|
for i in 0..255_u8 {
|
|
h.insert(i, RefCell::new(HashSet::new()));
|
|
}
|
|
for c1 in letters {
|
|
for c2 in letters {
|
|
let xored = c1 ^ c2;
|
|
if let Some(h) = h.get(&xored) {
|
|
let mut h_mut = h.borrow_mut();
|
|
h_mut.insert(*c1);
|
|
h_mut.insert(*c2);
|
|
};
|
|
}
|
|
}
|
|
h
|
|
}
|
|
|
|
fn u8_lower(s: u8) -> u8 {
|
|
if (b'A'..=b'Z').contains(&s) {
|
|
return s + 32;
|
|
}
|
|
s
|
|
}
|
|
|
|
fn ascii_letters(additional: &str) -> Vec<u8> {
|
|
let mut letters: Vec<u8> = (0..255_u8).filter(u8::is_ascii_alphabetic).collect();
|
|
for b in additional.as_bytes() {
|
|
letters.push(*b);
|
|
}
|
|
letters
|
|
}
|
|
|
|
pub fn attack(ciphers: &[Bytes]) -> Vec<RefCell<Vec<u8>>> {
|
|
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);
|
|
|
|
let target_bytes: Vec<Option<u8>> = ciphers
|
|
.iter()
|
|
.map(|c| {
|
|
if c.len() > byte_index {
|
|
Some(c.0[byte_index])
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
let mut possible_chars: Vec<HashSet<u8>> = ciphers
|
|
.iter()
|
|
.map(|_| letters.iter().copied().collect())
|
|
.collect();
|
|
|
|
for i in 0..ciphers_len {
|
|
for j in i..ciphers_len {
|
|
if target_bytes[i].is_none() || target_bytes[j].is_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();
|
|
}
|
|
}
|
|
|
|
for cipher_index in 0..ciphers_len {
|
|
if ciphers[cipher_index].len() <= byte_index {
|
|
continue;
|
|
}
|
|
let chars: Vec<u8> = 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]) {
|
|
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'^');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
deciphered
|
|
}
|
|
}
|
|
|
|
pub fn challenge19() {
|
|
fn manual(decrypts: &[RefCell<Vec<u8>>]) {
|
|
// Add manually guessed letters
|
|
decrypts[0].borrow_mut()[30] = b'y';
|
|
decrypts[2].borrow_mut()[30] = b'y';
|
|
let mut d4 = decrypts[4].borrow_mut();
|
|
d4[30] = b'e';
|
|
d4[32] = b'h';
|
|
d4[33] = b'e';
|
|
d4[34] = b'a';
|
|
d4[35] = b'd';
|
|
decrypts[6].borrow_mut()[30] = b'i';
|
|
decrypts[13].borrow_mut()[30] = b' ';
|
|
decrypts[20].borrow_mut()[30] = b' ';
|
|
decrypts[25].borrow_mut()[30] = b'n';
|
|
decrypts[28].borrow_mut()[30] = b' ';
|
|
decrypts[29].borrow_mut()[30] = b't';
|
|
decrypts[37].borrow_mut()[30] = b'i';
|
|
}
|
|
|
|
let plaintexts = utils::read_base64_lines("data/19.txt");
|
|
let key = Bytes::from_utf8("YELLOW SUBMARINE");
|
|
let encrypt = |plaintext: &Bytes| -> Bytes { ctr::encrypt(&key, 0, plaintext) };
|
|
let ciphers: Vec<Bytes> = plaintexts.iter().map(encrypt).collect();
|
|
let decrypts = challenge19::attack(&ciphers);
|
|
manual(&decrypts);
|
|
let first_line = Bytes(decrypts[0].borrow().to_vec()).to_utf8();
|
|
println!("[okay] Challenge 19: {first_line}");
|
|
}
|
|
|
|
pub fn challenge20() {
|
|
fn attack(ciphers: &[Bytes]) -> Vec<Bytes> {
|
|
let min_cipher_len = ciphers.iter().map(Bytes::len).min().unwrap_or(0);
|
|
let mut key: Vec<u8> = vec![];
|
|
for byte_index in 0..min_cipher_len {
|
|
let bytes = Bytes(ciphers.iter().map(|c| c.0[byte_index]).collect());
|
|
let key_char = Bytes::guess_key(&bytes);
|
|
key.push(key_char);
|
|
}
|
|
let key = Bytes(key);
|
|
ciphers
|
|
.iter()
|
|
.map(|cipher| Bytes::xor(&key, cipher))
|
|
.collect()
|
|
}
|
|
|
|
let plaintexts = utils::read_base64_lines("data/20.txt");
|
|
let key = Bytes::from_utf8("YELLOW SUBMARINE");
|
|
let encrypt = |plaintext: &Bytes| -> Bytes { ctr::encrypt(&key, 0, plaintext) };
|
|
let ciphers: Vec<Bytes> = plaintexts.iter().map(encrypt).collect();
|
|
let plaintexts = attack(&ciphers);
|
|
|
|
println!("[okay] Challenge 20: {}", plaintexts[0].to_utf8());
|
|
}
|
|
|
|
pub fn challenge21() {
|
|
// Implement the MT19937 Mersenne Twister RNG
|
|
let expected: Vec<u32> = vec![
|
|
0xD091_BB5C,
|
|
0x22AE_9EF6,
|
|
0xE7E1_FAEE,
|
|
0xD5C3_1F79,
|
|
0x2082_352C,
|
|
0xF807_B7DF,
|
|
0xE9D3_0005,
|
|
0x3895_AFE1,
|
|
0xA1E2_4BBA,
|
|
0x4EE4_092B,
|
|
];
|
|
let mut mt = mt19937::MT19937::new();
|
|
mt.seed(5489);
|
|
for e in expected {
|
|
assert_eq!(mt.extract_number(), e);
|
|
}
|
|
println!("[okay] Challenge 21: implemented MT19937");
|
|
}
|
|
|
|
pub fn challenge22() {
|
|
fn find_seed(rngout: u32) -> Option<u32> {
|
|
let mut mt = mt19937::MT19937::new();
|
|
let start = utils::unix_timestamp() - 2000;
|
|
for seed in start..(start + 4000) {
|
|
mt.seed(seed);
|
|
if rngout == mt.extract_number() {
|
|
return Some(seed);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
// Wait a random number of seconds between, I don't know, 40 and 1000.
|
|
let now = utils::unix_timestamp();
|
|
let wait_time: u32 = rand::thread_rng().gen_range(40..1000);
|
|
let seed = now + wait_time;
|
|
|
|
// Seeds the RNG with the current Unix timestamp.
|
|
let mut mt = mt19937::MT19937::new();
|
|
mt.seed(seed);
|
|
|
|
// Returns the first 32 bit output of the RNG.
|
|
let rngout = mt.extract_number();
|
|
|
|
// From the 32 bit RNG output, discover the seed.
|
|
let found_seed = find_seed(rngout);
|
|
assert_eq!(seed, found_seed.unwrap());
|
|
|
|
println!("[okay] Challenge 22: cracked MT19937 seed");
|
|
}
|
|
|
|
pub fn challenge23() {
|
|
const fn _temper(x: u32) -> u32 {
|
|
const S: u32 = 7;
|
|
const T: u32 = 15;
|
|
const U: u32 = 11;
|
|
const B: u32 = 0x9D2C_5680;
|
|
const C: u32 = 0xEFC6_0000;
|
|
const L: u32 = 18;
|
|
|
|
let mut y = x;
|
|
y = y ^ (y >> U);
|
|
y = y ^ ((y << S) & B);
|
|
y = y ^ ((y << T) & C);
|
|
y = y ^ (y >> L);
|
|
y
|
|
}
|
|
|
|
fn untemper(x: u32) -> u32 {
|
|
const B: u32 = 0x9D2C_5680;
|
|
const C: u32 = 0xEFC6_0000;
|
|
let mut y = x;
|
|
|
|
// reverse y = y ^ (y >> L); L = 18;
|
|
const UPPER_18_BITS: u32 = u32::MAX << 14;
|
|
const LOWER_14_BITS: u32 = u32::MAX >> 18;
|
|
let mut o = y & UPPER_18_BITS; // upper 18 bits are correct
|
|
o |= ((o & UPPER_18_BITS) >> 18) ^ (y & LOWER_14_BITS); // all 32 bits are correct
|
|
y = o;
|
|
|
|
// reverse y = y ^ ((y << T) & C); T = 15;
|
|
const LOWER_15_BITS: u32 = u32::MAX >> 17;
|
|
const MID_15_BITS: u32 = LOWER_15_BITS << 15;
|
|
const UPPER_2_BITS: u32 = u32::MAX << 30;
|
|
let mut o = y & LOWER_15_BITS; // lower 15 bits are correct
|
|
o |= ((o << 15) & C) ^ (y & MID_15_BITS); // lower 30 bits are correct
|
|
o |= (((o << 15) & C) & UPPER_2_BITS) ^ (y & UPPER_2_BITS); // all 32 bits are correct
|
|
y = o;
|
|
|
|
// reverse y = y ^ ((y << S) & B); S = 7
|
|
const LOWER_7_BITS: u32 = u32::MAX >> 25;
|
|
const SECOND_7_BITS: u32 = LOWER_7_BITS << 7;
|
|
const THIRD_7_BITS: u32 = SECOND_7_BITS << 7;
|
|
const FOURTH_7_BITS: u32 = THIRD_7_BITS << 7;
|
|
const UPPER_4_BITS: u32 = u32::MAX << 28;
|
|
let mut o = y & LOWER_7_BITS; // lower 7 bits are correct
|
|
o |= ((o << 7) & B) ^ (y & SECOND_7_BITS); // lower 14 bits are correct
|
|
o |= (((o << 7) & B) & THIRD_7_BITS) ^ (y & THIRD_7_BITS); // lower 21 bits are correct
|
|
o |= (((o << 7) & B) & FOURTH_7_BITS) ^ (y & FOURTH_7_BITS); // lower 28 bits are correct
|
|
o |= (((o << 7) & B) & UPPER_4_BITS) ^ (y & UPPER_4_BITS); // all 32 bits are correct
|
|
y = o;
|
|
|
|
// reverse y = y ^ (y >> U); U = 11;
|
|
const UPPER_11_BITS: u32 = u32::MAX << 21;
|
|
const SECOND_11_BITS: u32 = UPPER_11_BITS >> 11;
|
|
const LOWER_10_BITS: u32 = u32::MAX >> 22;
|
|
let mut o = y & UPPER_11_BITS; // upper 11 bits are correct
|
|
o |= ((o & UPPER_11_BITS) >> 11) ^ (y & SECOND_11_BITS); // upper 22 bits are correct
|
|
o |= ((o & SECOND_11_BITS) >> 11) ^ (y & LOWER_10_BITS); // all 32 bits are correct
|
|
y = o;
|
|
|
|
y
|
|
}
|
|
|
|
// untemper test code
|
|
// let a: u32 = 0x12345678;
|
|
// let b = _temper(a);
|
|
// let c = untemper(b);
|
|
// println!("{:#010x} -> {:#010x} -> {:#010x}", a, b, c);
|
|
|
|
// Once you have "untemper" working, create a new MT19937 generator, tap it for 624 outputs,
|
|
let mut mt = mt19937::MT19937::new();
|
|
let seed: u32 = rand::thread_rng().gen::<u32>();
|
|
mt.seed(seed);
|
|
let outputs: Vec<u32> = (0..624).map(|_| mt.extract_number()).collect();
|
|
|
|
// untemper each of them to recreate the state of the generator,
|
|
let outputs = outputs.iter().map(|o| untemper(*o)).collect();
|
|
|
|
// and splice that state into a new instance of the MT19937 generator.
|
|
let mut spliced_mt = mt19937::MT19937::new();
|
|
spliced_mt.splice(outputs);
|
|
|
|
// The new "spliced" generator should predict the values of the original.
|
|
for _ in 0..2000 {
|
|
assert_eq!(mt.extract_number(), spliced_mt.extract_number());
|
|
}
|
|
|
|
println!("[okay] Challenge 23: MT19937 RNG successfully cloned from output");
|
|
}
|
|
|
|
pub fn challenge24() {
|
|
// Verify that you can encrypt and decrypt properly. This code should look
|
|
// similar to your CTR code.
|
|
let key: u16 = 111;
|
|
let cleartext = Bytes::from_utf8("Let's see if we can get the party started hard my friends.");
|
|
let cipher = mtcipher::encrypt(key, &cleartext);
|
|
let roundtrip = mtcipher::decrypt(key, &cipher);
|
|
assert_eq!(cleartext, roundtrip);
|
|
|
|
// Use your function to encrypt a known plaintext (say, 14 consecutive 'A'
|
|
// characters) prefixed by a random number of random characters.
|
|
fn get_plaintext() -> Bytes {
|
|
let length: usize = rand::thread_rng().gen_range(30..100);
|
|
let mut data = Bytes::random(length);
|
|
data.0.append(&mut Bytes(vec![b'A'; 14]).0);
|
|
data
|
|
}
|
|
let key: u16 = rand::thread_rng().gen::<u16>();
|
|
let plaintext = get_plaintext();
|
|
let cipher = mtcipher::encrypt(key, &plaintext);
|
|
|
|
// From the ciphertext, recover the "key" (the 16 bit seed).
|
|
fn recover_key(cipher: &Bytes) -> u16 {
|
|
let cipher_len = cipher.len();
|
|
// brute force bb!
|
|
for key in 0..u16::MAX {
|
|
let mut found_key = true;
|
|
let roundtrip = mtcipher::decrypt(key, cipher);
|
|
// check if the last 14 chars are 'A' - if yes, we found the key
|
|
for i in (cipher_len - 14)..cipher_len {
|
|
if roundtrip.0[i] != b'A' {
|
|
found_key = false;
|
|
break;
|
|
}
|
|
}
|
|
if found_key {
|
|
return key;
|
|
}
|
|
}
|
|
0
|
|
}
|
|
let recovered_key = recover_key(&cipher);
|
|
assert_eq!(key, recovered_key);
|
|
|
|
// Use the same idea to generate a random "password reset token" using
|
|
// MT19937 seeded from the current time.
|
|
fn get_reset_token(time: Option<u32>) -> Bytes {
|
|
const TOKEN_LENGTH: usize = 16;
|
|
let time = match time {
|
|
Some(time) => time,
|
|
None => utils::unix_timestamp(),
|
|
};
|
|
let mut token = vec![];
|
|
let mut mt = mt19937::MT19937::new();
|
|
mt.seed(time);
|
|
|
|
while token.len() < (TOKEN_LENGTH - 1) {
|
|
for b in mt.extract_bytes() {
|
|
if token.len() >= TOKEN_LENGTH {
|
|
break;
|
|
}
|
|
if b.is_ascii_alphanumeric() {
|
|
token.push(b);
|
|
}
|
|
}
|
|
}
|
|
Bytes(token)
|
|
}
|
|
|
|
let token = get_reset_token(None);
|
|
// println!("{}", token.to_utf8());
|
|
|
|
// Write a function to check if any given password token is actually the
|
|
// product of an MT19937 PRNG seeded with the current time.
|
|
fn is_time_token(token: &Bytes) -> bool {
|
|
let current_time = utils::unix_timestamp();
|
|
for time in (current_time - 10)..(current_time + 10) {
|
|
let time_token = get_reset_token(Some(time));
|
|
if *token == time_token {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
assert!(is_time_token(&token));
|
|
let non_token = Bytes(vec![b'z', 16]);
|
|
assert!(!is_time_token(&non_token));
|
|
|
|
println!("[okay] Challenge 24: MT19937 stream cipher implemented and cracked");
|
|
}
|