Skip to content

Commit

Permalink
Cleanup and preparation for challenge 13
Browse files Browse the repository at this point in the history
  • Loading branch information
aochagavia committed Sep 24, 2019
1 parent ae86842 commit 020e5db
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 122 deletions.
133 changes: 12 additions & 121 deletions src/aes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,18 @@ pub fn count_repetitions(bytes: &[u8]) -> u32 {
freq.iter().filter(|&(_, &v)| v > 1).map(|(_, v)| v).sum()
}

pub fn break_aes_ecb() -> Vec<u8> {
use rand::RngCore;

let mut random_key = vec![0; 16];
rand::thread_rng().fill_bytes(&mut random_key);

// Note: this function assumes that the encryption function is deterministic
pub fn detect_block_size_and_padding_bytes<F>(encrypt: F) -> (usize, usize)
where F: Fn(&[u8]) -> Vec<u8> {
// We can discover the block size even without knowing which cipher it is by seeing
// the effect of our input on the padding
let len = encrypt_aes_ecb_with_appendix(b"", &random_key).len();

let len = encrypt(b"").len();
let mut block_size = 0;
let mut padding_bytes = 0;
for added_bytes in 1.. {
let bytes = vec![0; added_bytes];
let new_len = encrypt_aes_ecb_with_appendix(&bytes, &random_key).len();
let new_len = encrypt(&bytes).len();
if new_len > len {
// New block added!
padding_bytes = added_bytes;
Expand All @@ -110,107 +108,14 @@ pub fn break_aes_ecb() -> Vec<u8> {
}
}

assert_eq!(block_size, 16); // Sanity check

// Detect that this is indeed a case of ECB
let plaintext = vec![b'A'; block_size * 6];
let ciphertext = encrypt_aes_ecb_with_appendix(&plaintext, &random_key);
if count_repetitions(&ciphertext) < 4 {
panic!("Not ECB!");
}

// Find the length of the secret text
let secret_text = encrypt_aes_ecb_with_appendix(b"", &random_key);
let secret_text_len = secret_text.len() - padding_bytes;
let input_len = secret_text_len + padding_bytes;

assert!(input_len % block_size == 0);

let mut discovered_bytes = Vec::new();
while discovered_bytes.len() < secret_text_len {
// Find all combinations for the last char
let mut input = vec![0; input_len - discovered_bytes.len() - 1];
input.extend_from_slice(&discovered_bytes);
input.push(0);

let mut map = std::collections::HashMap::new();
for x in 0..=255 {
input[input_len - 1] = x;
let encrypted = encrypt_aes_ecb_with_appendix(&input, &random_key);

let block = encrypted[input_len - block_size..input_len].to_owned();
assert_eq!(block.len(), block_size); // Sanity check
map.insert(block, x);
}

// Craft an input block that allows an undiscovered byte to be included in the block
let input = vec![0; input_len - discovered_bytes.len() - 1];
let encrypted = encrypt_aes_ecb_with_appendix(&input, &random_key);
let discovered_byte = map[&encrypted[input_len - block_size..input_len]];
discovered_bytes.push(discovered_byte);
}

discovered_bytes
}

pub fn encrypt_aes_ecb_with_appendix(plaintext: &[u8], key: &[u8]) -> Vec<u8> {
let mut plaintext = plaintext.to_owned();
let appendix = crate::base64::decode(b"Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK");
plaintext.extend_from_slice(&appendix);

encrypt_aes_ecb(&plaintext, key)
}

pub fn aes_oracle() -> (BlockCipherMode, BlockCipherMode) {
let plaintext = vec![b'A'; 16 * 6];
let (ciphertext, mode) = encrypt_aes_cbc_or_ecb(&plaintext);

// If the encryption function is using ECB, there will be at least
// 4 repeated blocks
if count_repetitions(&ciphertext) >= 4 {
(BlockCipherMode::ECB, mode)
} else {
(BlockCipherMode::CBC, mode)
}
}

#[derive(Debug, PartialEq, Eq)]
pub enum BlockCipherMode {
ECB,
CBC
(block_size, padding_bytes)
}

fn encrypt_aes_cbc_or_ecb(plaintext: &[u8]) -> (Vec<u8>, BlockCipherMode) {
use rand::{RngCore, Rng};

// Generate random key
let mut key = vec![0; 16];
let mut rng = rand::thread_rng();
rng.fill_bytes(&mut key);

// Modify the plaintext
let prepend_count: usize = rng.gen_range(5, 11);
let mut prepend_bytes = vec![0; prepend_count];
rng.fill_bytes(&mut prepend_bytes);

let append_count: usize = rng.gen_range(5, 11);
let mut append_bytes = vec![0; append_count];
rng.fill_bytes(&mut append_bytes);

let mut new_plaintext = prepend_bytes;
new_plaintext.extend_from_slice(plaintext);
new_plaintext.extend_from_slice(&append_bytes);

// Encrypt!
if rng.gen_bool(0.5) {
// CBC
let mut iv = vec![0; 16];
rng.fill_bytes(&mut iv);
(encrypt_aes_cbc(&new_plaintext, &key, &iv), BlockCipherMode::CBC)
} else {
// ECB
(encrypt_aes_ecb(&new_plaintext, &key), BlockCipherMode::ECB)
}
pub fn is_ecb<F>(encrypt: F, block_size: usize) -> bool
where F: Fn(&[u8]) -> Vec<u8> {
let plaintext = vec![b'A'; block_size * 6];
let ciphertext = encrypt(&plaintext);
count_repetitions(&ciphertext) >= 4
}

#[test]
Expand Down Expand Up @@ -256,17 +161,3 @@ fn test_encrypt_decrypt_aes_cbc() {
assert_eq!(decrypted, plaintext);
}

#[test]
fn test_detect_aes_ecb() {
for _ in 0..20 {
let (detected, real) = aes_oracle();
assert_eq!(detected, real);
}
}

#[test]
fn test_break_aes_ecb() {
let secret = break_aes_ecb();
let expected = crate::base64::decode(b"Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK");
assert_eq!(secret, expected)
}
61 changes: 61 additions & 0 deletions src/challenge11.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::aes;

pub fn aes_oracle() -> (BlockCipherMode, BlockCipherMode) {
let plaintext = vec![b'A'; 16 * 6];
let (ciphertext, mode) = encrypt_aes_cbc_or_ecb(&plaintext);

// If the encryption function is using ECB, there will be at least
// 4 repeated blocks
if aes::count_repetitions(&ciphertext) >= 4 {
(BlockCipherMode::ECB, mode)
} else {
(BlockCipherMode::CBC, mode)
}
}

#[derive(Debug, PartialEq, Eq)]
pub enum BlockCipherMode {
ECB,
CBC
}

fn encrypt_aes_cbc_or_ecb(plaintext: &[u8]) -> (Vec<u8>, BlockCipherMode) {
use rand::{RngCore, Rng};

// Generate random key
let mut key = vec![0; 16];
let mut rng = rand::thread_rng();
rng.fill_bytes(&mut key);

// Modify the plaintext
let prepend_count: usize = rng.gen_range(5, 11);
let mut prepend_bytes = vec![0; prepend_count];
rng.fill_bytes(&mut prepend_bytes);

let append_count: usize = rng.gen_range(5, 11);
let mut append_bytes = vec![0; append_count];
rng.fill_bytes(&mut append_bytes);

let mut new_plaintext = prepend_bytes;
new_plaintext.extend_from_slice(plaintext);
new_plaintext.extend_from_slice(&append_bytes);

// Encrypt!
if rng.gen_bool(0.5) {
// CBC
let mut iv = vec![0; 16];
rng.fill_bytes(&mut iv);
(aes::encrypt_aes_cbc(&new_plaintext, &key, &iv), BlockCipherMode::CBC)
} else {
// ECB
(aes::encrypt_aes_ecb(&new_plaintext, &key), BlockCipherMode::ECB)
}
}

#[test]
fn test_detect_aes_ecb() {
for _ in 0..20 {
let (detected, real) = aes_oracle();
assert_eq!(detected, real);
}
}
69 changes: 69 additions & 0 deletions src/challenge12.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::aes;

pub fn break_aes_ecb() -> Vec<u8> {
// Set up a random key
use rand::RngCore;
let mut random_key = vec![0; 16];
rand::thread_rng().fill_bytes(&mut random_key);

let (block_size, padding_bytes) = aes::detect_block_size_and_padding_bytes(
|bytes| encrypt_aes_ecb_with_appendix(bytes, &random_key)
);

assert_eq!(block_size, 16); // Sanity check

// Detect that this is indeed a case of ECB
if !aes::is_ecb(
|bytes| encrypt_aes_ecb_with_appendix(bytes, &random_key),
block_size) {
panic!("Not ECB!");
}

// Find the length of the secret text
let secret_text = encrypt_aes_ecb_with_appendix(b"", &random_key);
let secret_text_len = secret_text.len() - padding_bytes;
let input_len = secret_text_len + padding_bytes;

assert!(input_len % block_size == 0);

let mut discovered_bytes = Vec::new();
while discovered_bytes.len() < secret_text_len {
// Find all combinations for the last char
let mut input = vec![0; input_len - discovered_bytes.len() - 1];
input.extend_from_slice(&discovered_bytes);
input.push(0);

let mut map = std::collections::HashMap::new();
for x in 0..=255 {
input[input_len - 1] = x;
let encrypted = encrypt_aes_ecb_with_appendix(&input, &random_key);

let block = encrypted[input_len - block_size..input_len].to_owned();
assert_eq!(block.len(), block_size); // Sanity check
map.insert(block, x);
}

// Craft an input block that allows an undiscovered byte to be included in the block
let input = vec![0; input_len - discovered_bytes.len() - 1];
let encrypted = encrypt_aes_ecb_with_appendix(&input, &random_key);
let discovered_byte = map[&encrypted[input_len - block_size..input_len]];
discovered_bytes.push(discovered_byte);
}

discovered_bytes
}

pub fn encrypt_aes_ecb_with_appendix(plaintext: &[u8], key: &[u8]) -> Vec<u8> {
let mut plaintext = plaintext.to_owned();
let appendix = crate::base64::decode(b"Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK");
plaintext.extend_from_slice(&appendix);

aes::encrypt_aes_ecb(&plaintext, key)
}

#[test]
fn test_break_aes_ecb() {
let secret = break_aes_ecb();
let expected = crate::base64::decode(b"Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK");
assert_eq!(secret, expected)
}
28 changes: 28 additions & 0 deletions src/challenge13.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use crate::aes;
use crate::profile::Profile;

fn profile_ciphertext(email: String, key: &[u8]) -> Vec<u8> {
let p = Profile::new(email);
let plaintext = p.encode_as_string();
aes::encrypt_aes_ecb(plaintext.as_bytes(), &key)
}

#[test]
fn ecb_cut_and_paste() {
// Set up a random key
use rand::RngCore;
let mut random_key = vec![0; 16];
rand::thread_rng().fill_bytes(&mut random_key);

let ciphertext = profile_ciphertext("user@email.com".into(), &random_key);

// Do magic to create a ciphertext containing role=admin instead of role=user
// Only calls to `profile_ciphertext` are allowed
// Are we allowed to change the email? The wording of the challenge is a bit vague... Probably irrelevant

let decrypted = aes::decrypt_aes_ecb(&ciphertext, &random_key);
let decrypted_p = Profile::from_string(&String::from_utf8_lossy(&decrypted));
assert_eq!(decrypted_p.email, "user@email.com");
assert_eq!(decrypted_p.uid, 0);
assert_eq!(decrypted_p.role, "admin");
}
6 changes: 5 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

mod aes;
mod base64;
mod challenge11;
mod challenge12;
mod challenge13;
mod hamming_distance;
mod histogram;
mod ordf32;
mod pkcs7;
mod profile;
mod util;
mod xor;

fn main() {
aes::break_aes_ecb();
// aes::break_aes_ecb();
}
Loading

0 comments on commit 020e5db

Please sign in to comment.