Solve challenge 19 in Python because my Rust still sucks.

This commit is contained in:
2022-07-19 19:35:38 -04:00
parent e70c6c470c
commit 467c3bdbef
5 changed files with 232 additions and 21 deletions

View File

@@ -9,23 +9,29 @@ mod set2;
mod set3;
fn main() {
set1::challenge1();
set1::challenge2();
set1::challenge3();
set1::challenge4();
set1::challenge5();
set1::challenge6();
set1::challenge7();
set1::challenge8();
set2::challenge9();
set2::challenge10();
set2::challenge11();
set2::challenge12();
set2::challenge13();
set2::challenge14();
set2::challenge15();
set2::challenge16();
set3::challenge17();
set3::challenge18();
set3::challenge19();
const RUN_ALL: bool = true;
if RUN_ALL {
set1::challenge1();
set1::challenge2();
set1::challenge3();
set1::challenge4();
set1::challenge5();
set1::challenge6();
set1::challenge7();
set1::challenge8();
set2::challenge9();
set2::challenge10();
set2::challenge11();
set2::challenge12();
set2::challenge13();
set2::challenge14();
set2::challenge15();
set2::challenge16();
set3::challenge17();
set3::challenge18();
set3::challenge19();
set3::challenge20();
} else {
set3::challenge20();
}
}

View File

@@ -3,6 +3,7 @@ use crate::bytes_base64::BytesBase64;
use crate::cbc;
use crate::ctr;
use rand::Rng;
use std::fs;
use std::io::{BufRead, BufReader};
pub fn challenge17() {
@@ -100,7 +101,7 @@ pub fn challenge18() {
let cleartext = Bytes::from_utf8("Let's see if we can get the party started hard my friends.");
let cipher = ctr::encrypt(&key, 42351234, &cleartext);
let roundtrip = ctr::encrypt(&key, 42351234, &cipher);
let roundtrip = ctr::decrypt(&key, 42351234, &cipher);
assert_eq!(cleartext, roundtrip);
let cipher = BytesBase64::from_base64(
@@ -112,5 +113,30 @@ pub fn challenge18() {
}
pub fn challenge19() {
println!("[xxxx] Challenge 19: TBD");
fn read(path: &str) -> Vec<Bytes> {
let file = std::fs::File::open(path).unwrap();
let br = BufReader::new(file);
br.lines()
.map(|line| BytesBase64::from_base64(&line.unwrap()).to_bytes())
.collect()
}
let plaintexts = read("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(|ct| encrypt(&ct)).collect();
let mut data = String::new();
for cipher in ciphers.iter() {
let copy_cipher = Bytes(cipher.0.to_vec());
let cipher_base64 = BytesBase64::from_bytes(copy_cipher);
data.push_str(&cipher_base64.to_string());
data.push_str("\n");
}
fs::write("data/19_enc.txt", data).expect("Unable to write file");
println!("[okay] Challenge 19: encrypted to data/19_enc.txt, attack in set3c19.py.");
}
pub fn challenge20() {
println!("[xxxx] Challenge 20: TBD");
}

99
src/set3c19.py Normal file
View File

@@ -0,0 +1,99 @@
import binascii
import string
from typing import List, Dict, Set
def load_ciphers() -> List[bytes]:
with open("data/19_enc.txt", "r") as f:
r = [binascii.a2b_base64(line) for line in f]
return r
def xor_lookup_set(letters: str) -> Dict[int, Set[str]]:
d = {i: set() for i in range(256)}
for c1 in letters:
for c2 in letters:
xored = ord(c1) ^ ord(c2)
d[xored].add(c1)
d[xored].add(c2)
return d
def attack(ciphers: List[bytes]) -> List[List[str]]:
""" Find out possible characters for each cipher pair and hope that there
is only one possible character. If no character was found add '?' and
if more than one was found add '^'. """
ciphers_len = len(ciphers)
deciphered = [[] for _ in range(ciphers_len)]
max_cipher_len = max(map(len, ciphers))
for byte_index in range(0, max_cipher_len):
# Certain bytes only work with certain letters (found empirically).
if byte_index == 10:
LETTERS = string.ascii_letters + " _-.,;:'"
elif byte_index == 20:
LETTERS = string.ascii_letters + " _-.,;:?"
else:
LETTERS = string.ascii_letters + " _-.,;:"
lookup = xor_lookup_set(LETTERS)
ciphers_len = len(ciphers)
target_bytes = [cipher[byte_index]
if len(cipher) > byte_index
else None
for cipher in ciphers]
possible_chars = [set(LETTERS) for _ in range(ciphers_len)]
for i in range(ciphers_len):
for j in range(i, ciphers_len):
if target_bytes[i] is None or target_bytes[j] is None:
continue
xored = target_bytes[i] ^ target_bytes[j]
chars = lookup[xored]
possible_chars[i] &= chars
possible_chars[j] &= chars
for cipher_index in range(ciphers_len):
if len(ciphers[cipher_index]) <= byte_index:
continue
chars = list(possible_chars[cipher_index])
match len(chars):
case 0:
# print(f"No chars for {cipher_index=} {byte_index=}")
deciphered[cipher_index].append('?')
case 1:
deciphered[cipher_index].append(chars[0].lower())
case 2:
if chars[0].lower() == chars[1].lower():
deciphered[cipher_index].append(chars[0].lower())
else:
# print(f"Two {chars=} {cipher_index=} {byte_index=}")
deciphered[cipher_index].append('^')
case _:
# print(f"Too many {chars=} {cipher_index=} {byte_index=}")
deciphered[cipher_index].append('^')
return deciphered
def manual(decrypts: List[List[str]]) -> List[bytes]:
""" Manually add guessed letters. """
decrypts[0][30] = 'y'
decrypts[2][30] = 'y'
decrypts[4][30] = 'e'
decrypts[4][32] = 'h'
decrypts[4][33] = 'e'
decrypts[4][34] = 'a'
decrypts[4][35] = 'd'
decrypts[6][30] = 'i'
decrypts[13][30] = ' '
decrypts[20][30] = ' '
decrypts[25][30] = 'n'
decrypts[28][30] = ' '
decrypts[29][30] = 't'
decrypts[37][30] = 'i'
decrypts = list(map(lambda l: "".join(l), decrypts))
return decrypts
if __name__ == "__main__":
ciphers = load_ciphers()
decrypts = manual(attack(ciphers))
for d in decrypts:
print(d)