407 lines
17 KiB
Rust
407 lines
17 KiB
Rust
use crate::bytes::Bytes;
|
|
use crate::cbc;
|
|
use crate::ecb;
|
|
use crate::parser;
|
|
use crate::utils;
|
|
use rand::Rng;
|
|
use std::collections::HashMap;
|
|
|
|
pub fn challenge9() {
|
|
let mut bytes = Bytes::from_utf8("YELLOW SUBMARINE");
|
|
bytes.pad_pkcs7(20);
|
|
println!("[okay] Challenge 9: {:?}", bytes.to_utf8());
|
|
}
|
|
|
|
pub fn challenge10() {
|
|
let iv = Bytes(vec![0; 16]);
|
|
let key = Bytes::from_utf8("YELLOW SUBMARINE");
|
|
|
|
let text = Bytes::from_utf8("aaaabbbbccccddddeeeeffffgggghhhh");
|
|
let ciphertext = cbc::encrypt(&key, &iv, &text);
|
|
let roundtrip = cbc::decrypt(&key, &iv, &ciphertext);
|
|
if text == roundtrip {
|
|
let ciphertext = utils::read_base64("data/10.txt");
|
|
let cleartext = cbc::decrypt(&key, &iv, &ciphertext);
|
|
let output = cleartext.to_utf8()[..16].to_string();
|
|
println!("[okay] Challenge 10: {}", output);
|
|
} else {
|
|
println!("[fail] Challenge 10: rountrip failed");
|
|
}
|
|
}
|
|
|
|
pub fn challenge11() {
|
|
#[derive(Debug, PartialEq)]
|
|
enum EncryptionType {
|
|
Cbc,
|
|
Ecb,
|
|
}
|
|
|
|
fn pad_data(mut v: Vec<u8>) -> Bytes {
|
|
let mut pre_pad = Bytes::random_range(5, 10).0;
|
|
let mut post_pad = Bytes::random_range(5, 10).0;
|
|
pre_pad.append(&mut v);
|
|
pre_pad.append(&mut post_pad);
|
|
Bytes(pre_pad)
|
|
}
|
|
|
|
fn encryption_oracle(Bytes(data): &Bytes) -> (Bytes, EncryptionType) {
|
|
// Write a function that encrypts data under an unknown key --- that is, a
|
|
// function that generates a random key and encrypts under it.
|
|
let key = Bytes::random(16);
|
|
// Under the hood, have the function append 5-10 bytes (count chosen randomly)
|
|
// before the plaintext and 5-10 bytes after the plaintext.
|
|
let padded_data = pad_data(data.clone());
|
|
// Now, have the function choose to encrypt under ECB 1/2 the time, and under CBC
|
|
// the other half (just use random IVs each time for CBC). Use rand(2) to decide
|
|
// which to use.
|
|
let zero_or_one: u32 = rand::thread_rng().gen_range(0..2);
|
|
let (data, encryption_type) = if zero_or_one == 1 {
|
|
(ecb::encrypt(&key, &padded_data), EncryptionType::Ecb)
|
|
} else {
|
|
let iv = Bytes::random(16);
|
|
(cbc::encrypt(&key, &iv, &padded_data), EncryptionType::Cbc)
|
|
};
|
|
(data, encryption_type)
|
|
}
|
|
|
|
fn cbcecb_detection_oracle(data: &Bytes) -> EncryptionType {
|
|
// Detect the block cipher mode the function is using each time. You should end up
|
|
// with a piece of code that, pointed at a block box that might be encrypting ECB
|
|
// or CBC, tells you which one is happening.
|
|
if data.has_duplicated_cycle(16) {
|
|
EncryptionType::Ecb
|
|
} else {
|
|
EncryptionType::Cbc
|
|
}
|
|
}
|
|
|
|
fn run_oracle(count: usize) {
|
|
// I struggled for a while to understand how the detection oracle can
|
|
// work. The issue is that if we provide a random text to encrypt, we
|
|
// won't be able to use duplicated blocks as a method for detecting the
|
|
// cipher. I don't think there is a way around that, but if we can choose
|
|
// the plaintext it becomes trivial.
|
|
let text = Bytes::from_utf8("aaaabbbbccccddddaaaabbbbccccddddaaaabbbbccccdddd");
|
|
let mut correct: usize = 0;
|
|
for _ in 0..count {
|
|
let (ciphertext, et) = encryption_oracle(&text);
|
|
let get = cbcecb_detection_oracle(&ciphertext);
|
|
if et == get {
|
|
correct += 1;
|
|
}
|
|
}
|
|
if correct == count {
|
|
println!("[okay] Challenge 11: [{} / {}]", correct, count);
|
|
} else {
|
|
println!("[fail] Challenge 11: [{} / {}]", correct, count);
|
|
}
|
|
}
|
|
run_oracle(10);
|
|
}
|
|
|
|
pub fn challenge12() {
|
|
fn encryption_oracle(key: &Bytes, Bytes(data): &Bytes) -> Bytes {
|
|
// Copy your oracle function to a new function that encrypts buffers under ECB mode using a consistent but unknown key
|
|
// Now take that same function and have it append to the plaintext, BEFORE ENCRYPTING, the following string (from 12.txt):
|
|
let mut data = data.clone();
|
|
let mut string = utils::read_base64("data/12.txt");
|
|
data.append(&mut string.0);
|
|
ecb::encrypt(key, &Bytes(data))
|
|
}
|
|
|
|
fn get_block_size(key: &Bytes) -> usize {
|
|
// Detect cipher block size
|
|
let mut v = vec![];
|
|
let initial_cipher_len = encryption_oracle(key, &Bytes(v.clone())).0.len();
|
|
let mut new_cipher_len = initial_cipher_len;
|
|
while initial_cipher_len == new_cipher_len {
|
|
v.push(b'A');
|
|
let cipher = encryption_oracle(key, &Bytes(v.clone()));
|
|
new_cipher_len = cipher.0.len();
|
|
}
|
|
new_cipher_len - initial_cipher_len
|
|
}
|
|
|
|
fn is_encryption_ecb(key: &Bytes) -> bool {
|
|
let data = Bytes::from_utf8("aaaabbbbccccddddaaaabbbbccccdddd");
|
|
let cipher = encryption_oracle(key, &data);
|
|
cipher.has_duplicated_cycle(16)
|
|
}
|
|
|
|
fn decode(key: &Bytes) -> Bytes {
|
|
let block_size = get_block_size(key);
|
|
let block_count = encryption_oracle(key, &Bytes(vec![])).0.len() / block_size;
|
|
let mut cleartext = vec![];
|
|
|
|
for block_index in 0..block_count {
|
|
let mut cleartext_block = vec![];
|
|
for padding_length in (0..block_size).rev() {
|
|
let padding_text = vec![b'-'; padding_length];
|
|
let expected = encryption_oracle(key, &Bytes(padding_text.clone()));
|
|
let expected_block = expected.get_block(block_index, block_size);
|
|
|
|
let mut known_text = if block_index == 0 {
|
|
padding_text.clone()
|
|
} else {
|
|
let cleartext_offset =
|
|
((block_index - 1) * block_size) + (block_size - padding_length);
|
|
cleartext[cleartext_offset..(cleartext_offset + padding_length)].to_vec()
|
|
};
|
|
known_text.append(&mut cleartext_block.clone());
|
|
|
|
for i in 0..255 {
|
|
let mut guess_text = known_text.clone();
|
|
guess_text.push(i);
|
|
let cipher_block =
|
|
encryption_oracle(key, &Bytes(guess_text)).get_block(0, block_size);
|
|
if cipher_block.0 == expected_block.0 {
|
|
cleartext_block.push(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
cleartext.append(&mut cleartext_block);
|
|
}
|
|
Bytes(cleartext)
|
|
}
|
|
|
|
let key = Bytes::random(16); // consistent but unknown key
|
|
assert_eq!(get_block_size(&key), 16); // 1. discover block size
|
|
assert!(is_encryption_ecb(&key)); // 2. confirm oracle uses ecb
|
|
let roundtrip_text = decode(&key); // 3.-6.
|
|
let cleartext = utils::read_base64("data/12.txt");
|
|
|
|
// 138 (instead of 139); I think we get one additional byte because we guess
|
|
// the first padding byte. The right approach would be to remove the last
|
|
// 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_utf8()[..17]);
|
|
}
|
|
|
|
pub fn challenge13() {
|
|
fn profile_for(input: &str, key: &Bytes) -> Bytes {
|
|
let mut r = String::new();
|
|
for c in input.chars() {
|
|
assert!(
|
|
c.is_ascii_alphabetic() || c == '.' || c == '@',
|
|
"profile_for: invalid char {}",
|
|
c
|
|
);
|
|
}
|
|
r.push_str("email=");
|
|
r.push_str(input);
|
|
r.push_str("&uid=1337&role=user");
|
|
ecb::encrypt(key, &Bytes(r.as_bytes().to_vec()))
|
|
}
|
|
|
|
fn decrypt(key: &Bytes, data: &Bytes) -> HashMap<String, String> {
|
|
let c = ecb::decrypt(key, data);
|
|
parser::parse_key_value(&c.to_utf8())
|
|
}
|
|
|
|
fn attack(key: &Bytes) -> Bytes {
|
|
// Using only the user input to profile_for() (as an oracle to generate
|
|
// "valid" ciphertexts) and the ciphertexts themselves, make a
|
|
// role=admin profile.
|
|
// (FelixM) I assume ECB and block_size = 16; we could figure
|
|
// it out easily by adding enough 'a' to the email
|
|
let mut r = vec![];
|
|
|
|
// ________________________________
|
|
// 0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f
|
|
// email=aaaaa@a.com&uid=1337&role=user
|
|
let p = profile_for("aaaaa@a.com", key);
|
|
r.append(&mut p.0[0..32].to_vec());
|
|
|
|
// ----------------
|
|
// 0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f
|
|
// email=aaaaaaa@a.admin&uid=1337&role=user
|
|
let p = profile_for("aaaaaaa@a.admin", key);
|
|
r.append(&mut p.0[16..32].to_vec());
|
|
|
|
// ----------------
|
|
// 0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f
|
|
// email=aaaaaaaa@a.admin&uid=1337&role=user
|
|
let p = profile_for("aaaaaaaa@a.admin", key);
|
|
r.append(&mut p.0[32..48].to_vec());
|
|
|
|
Bytes(r)
|
|
}
|
|
|
|
let key = Bytes::random(16); // consistent but unknown key
|
|
let profile = attack(&key);
|
|
let dict = decrypt(&key, &profile);
|
|
let role = dict.get("role").unwrap();
|
|
assert_eq!(role, "admin");
|
|
println!("[okay] Challenge 13: role={}", role);
|
|
}
|
|
|
|
pub fn challenge14() {
|
|
fn encryption_oracle(
|
|
Bytes(random_prefix): &Bytes,
|
|
random_key: &Bytes,
|
|
Bytes(attacker_controlled): &Bytes,
|
|
) -> Bytes {
|
|
// AES-128-ECB(random-prefix || attacker-controlled || target-bytes, random-key)
|
|
// Thoughts: If I generate a random prefix for every encryption, then I don't
|
|
// know how to decode it because I cannot really run experiments. If I generate
|
|
// a single random prefix it becomes rather trivial. I just have to find out how
|
|
// long it is and then adjust the decoding routine.
|
|
let mut plaintext = random_prefix.clone();
|
|
plaintext.append(&mut attacker_controlled.clone());
|
|
let mut target_bytes = utils::read_base64("data/12.txt").0;
|
|
plaintext.append(&mut target_bytes);
|
|
ecb::encrypt(random_key, &Bytes(plaintext))
|
|
}
|
|
|
|
fn get_block_size(prefix: &Bytes, key: &Bytes) -> usize {
|
|
// Detect cipher block size this approach also confirms that it is ECB
|
|
let v = vec![b'a'; 256];
|
|
let cipher = encryption_oracle(prefix, key, &Bytes(v));
|
|
|
|
for i in 1..10 {
|
|
let block_size = i * 16;
|
|
if let Some((_, _)) = get_duplicated_block_indices(&cipher, block_size) {
|
|
return block_size;
|
|
}
|
|
}
|
|
0
|
|
}
|
|
|
|
fn get_duplicated_block_indices(cipher: &Bytes, block_size: usize) -> Option<(usize, usize)> {
|
|
let chunks: Vec<&[u8]> = cipher.0.chunks(block_size).collect();
|
|
for i in 0..chunks.len() {
|
|
for j in (i + 1)..chunks.len() {
|
|
if chunks[i] == chunks[j] {
|
|
return Some((i, j));
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn get_prefix_size(prefix: &Bytes, key: &Bytes) -> usize {
|
|
let block_size = get_block_size(prefix, key);
|
|
let duplicated_text = Bytes::from_utf8("aaaabbbbccccddddaaaabbbbccccdddd").0;
|
|
|
|
for i in 0..block_size {
|
|
let mut padding = vec![b'a'; i];
|
|
padding.append(&mut duplicated_text.clone());
|
|
let cipher = encryption_oracle(prefix, key, &Bytes(padding));
|
|
if let Some((first_block, _)) = get_duplicated_block_indices(&cipher, block_size) {
|
|
return block_size * first_block - i;
|
|
}
|
|
}
|
|
0
|
|
}
|
|
|
|
fn decode(prefix: &Bytes, key: &Bytes) -> Bytes {
|
|
let block_size = get_block_size(prefix, key);
|
|
let prefix_size = get_prefix_size(prefix, key);
|
|
let prefix_padding_size = block_size - (prefix_size % block_size);
|
|
let prefix_padding = Bytes(vec![b'a'; prefix_padding_size]);
|
|
let block_count = encryption_oracle(prefix, key, &prefix_padding).len() / block_size;
|
|
let first_block_index = (prefix_size + prefix_padding_size) / block_size;
|
|
let mut cleartext = vec![];
|
|
|
|
// AES-128-ECB(random-prefix || attacker-controlled || target-bytes, random-key)
|
|
// rrrrrrrrrrrrrrrrrrrpppppppppppppaaaaaaaaaaaaaaathe real text
|
|
// 0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f
|
|
// 0 1 ^ 2 4 5
|
|
// | ^ first_block_index
|
|
// \ prefix_padding
|
|
for block_index in first_block_index..block_count {
|
|
let mut cleartext_block = vec![];
|
|
for padding_length in (0..block_size).rev() {
|
|
let full_padding_text = vec![b'-'; prefix_padding_size + padding_length];
|
|
let expected = encryption_oracle(prefix, key, &Bytes(full_padding_text.clone()));
|
|
let expected_block = expected.get_block(block_index, block_size);
|
|
|
|
let mut known_text = if block_index == first_block_index {
|
|
full_padding_text.clone()
|
|
} else {
|
|
let mut prefix_padding = vec![b'-'; prefix_padding_size];
|
|
let cleartext_offset = ((block_index - first_block_index - 1) * block_size)
|
|
+ (block_size - padding_length);
|
|
prefix_padding.append(
|
|
&mut cleartext[cleartext_offset..(cleartext_offset + padding_length)]
|
|
.to_vec(),
|
|
);
|
|
prefix_padding
|
|
};
|
|
known_text.append(&mut cleartext_block.clone());
|
|
|
|
for i in 0..255 {
|
|
let mut guess_text = known_text.clone();
|
|
guess_text.push(i);
|
|
let cipher_block = encryption_oracle(prefix, key, &Bytes(guess_text))
|
|
.get_block(first_block_index, block_size);
|
|
if cipher_block.0 == expected_block.0 {
|
|
cleartext_block.push(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
cleartext.append(&mut cleartext_block);
|
|
}
|
|
// We get last byte from cbs padding so remove it.
|
|
Bytes(cleartext[0..(cleartext.len() - 1)].to_vec())
|
|
}
|
|
|
|
let prefix = Bytes::random_range(0, 200);
|
|
let key = Bytes::random(16);
|
|
assert_eq!(get_block_size(&prefix, &key), 16);
|
|
let prefix_len = get_prefix_size(&prefix, &key);
|
|
assert_eq!(prefix.len(), prefix_len);
|
|
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_utf8()[..17]);
|
|
}
|
|
|
|
pub fn challenge15() {
|
|
assert!(!Bytes::from_utf8("ICE ICE BABY\u{4}\u{4}\u{4}").has_valid_pkcs7(16));
|
|
assert!(!Bytes::from_utf8("ICE ICE BABY\u{3}\u{3}\u{4}\u{4}").has_valid_pkcs7(16));
|
|
assert!(!Bytes::from_utf8("ICE ICE BABY!!!\u{0}").has_valid_pkcs7(16));
|
|
assert!(Bytes::from_utf8("ICE ICE BABY!!!\u{1}").has_valid_pkcs7(16));
|
|
let mut bytes = Bytes::from_utf8("ICE ICE BABY\u{3}\u{3}\u{4}\u{4}");
|
|
bytes.pad_pkcs7(16);
|
|
assert!(bytes.has_valid_pkcs7(16));
|
|
println!("[okay] Challenge 15: PKCS7 works");
|
|
}
|
|
|
|
pub fn challenge16() {
|
|
fn encrypt(input: &str, key: &Bytes, iv: &Bytes) -> Bytes {
|
|
let mut r = String::new();
|
|
for c in input.chars() {
|
|
assert!(c != ';' && c != '=', "encrypt: invalid char {}", c);
|
|
}
|
|
r.push_str("comment1=cooking%20MCs;userdata=");
|
|
r.push_str(input);
|
|
r.push_str(";comment2=%20like%20a%20pound%20of%20bacon");
|
|
let mut cleartext = Bytes(r.as_bytes().to_vec());
|
|
cleartext.pad_pkcs7(16);
|
|
cbc::encrypt(key, iv, &cleartext)
|
|
}
|
|
|
|
let iv = Bytes::random(16);
|
|
let key = Bytes::random(16);
|
|
|
|
// 0 16 32 48 64
|
|
// 0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f
|
|
// comment1=cooking%20MCs;userdata=xxx=x;admin=true;comment2=%20like%20a%20pound%20of%20bacon
|
|
// flip_bit(2, '9') = '='; flip_bit(1, '9') = ';'
|
|
let mut cipher = encrypt("xxx9x9admin9true", &key, &iv);
|
|
cipher.flip_bit(19, 2); // results in flipping same bit in byte of the following block
|
|
cipher.flip_bit(21, 1);
|
|
cipher.flip_bit(27, 2);
|
|
let mut cleartext = cbc::decrypt(&key, &iv, &cipher);
|
|
cleartext.remove_pkcs7(16);
|
|
let cleartext_stripped = Bytes(cleartext.0[32..].to_vec());
|
|
let dict = parser::parse_key_value(&cleartext_stripped.to_utf8());
|
|
let admin_status = dict.get("admin").unwrap();
|
|
assert_eq!(admin_status, "true");
|
|
println!("[okay] Challenge 16: admin={}", admin_status);
|
|
}
|