From 467c3bdbefcc4f186c541727ea28972f86214e61 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Tue, 19 Jul 2022 19:35:38 -0400 Subject: [PATCH] Solve challenge 19 in Python because my Rust still sucks. --- data/19.txt | 40 ++++++++++++++++++++ data/19_enc.txt | 40 ++++++++++++++++++++ src/main.rs | 44 ++++++++++++---------- src/set3.rs | 30 ++++++++++++++- src/set3c19.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 21 deletions(-) create mode 100644 data/19.txt create mode 100644 data/19_enc.txt create mode 100644 src/set3c19.py diff --git a/data/19.txt b/data/19.txt new file mode 100644 index 0000000..affd17b --- /dev/null +++ b/data/19.txt @@ -0,0 +1,40 @@ +SSBoYXZlIG1ldCB0aGVtIGF0IGNsb3NlIG9mIGRheQ== +Q29taW5nIHdpdGggdml2aWQgZmFjZXM= +RnJvbSBjb3VudGVyIG9yIGRlc2sgYW1vbmcgZ3JleQ== +RWlnaHRlZW50aC1jZW50dXJ5IGhvdXNlcy4= +SSBoYXZlIHBhc3NlZCB3aXRoIGEgbm9kIG9mIHRoZSBoZWFk +T3IgcG9saXRlIG1lYW5pbmdsZXNzIHdvcmRzLA== +T3IgaGF2ZSBsaW5nZXJlZCBhd2hpbGUgYW5kIHNhaWQ= +UG9saXRlIG1lYW5pbmdsZXNzIHdvcmRzLA== +QW5kIHRob3VnaHQgYmVmb3JlIEkgaGFkIGRvbmU= +T2YgYSBtb2NraW5nIHRhbGUgb3IgYSBnaWJl +VG8gcGxlYXNlIGEgY29tcGFuaW9u +QXJvdW5kIHRoZSBmaXJlIGF0IHRoZSBjbHViLA== +QmVpbmcgY2VydGFpbiB0aGF0IHRoZXkgYW5kIEk= +QnV0IGxpdmVkIHdoZXJlIG1vdGxleSBpcyB3b3JuOg== +QWxsIGNoYW5nZWQsIGNoYW5nZWQgdXR0ZXJseTo= +QSB0ZXJyaWJsZSBiZWF1dHkgaXMgYm9ybi4= +VGhhdCB3b21hbidzIGRheXMgd2VyZSBzcGVudA== +SW4gaWdub3JhbnQgZ29vZCB3aWxsLA== +SGVyIG5pZ2h0cyBpbiBhcmd1bWVudA== +VW50aWwgaGVyIHZvaWNlIGdyZXcgc2hyaWxsLg== +V2hhdCB2b2ljZSBtb3JlIHN3ZWV0IHRoYW4gaGVycw== +V2hlbiB5b3VuZyBhbmQgYmVhdXRpZnVsLA== +U2hlIHJvZGUgdG8gaGFycmllcnM/ +VGhpcyBtYW4gaGFkIGtlcHQgYSBzY2hvb2w= +QW5kIHJvZGUgb3VyIHdpbmdlZCBob3JzZS4= +VGhpcyBvdGhlciBoaXMgaGVscGVyIGFuZCBmcmllbmQ= +V2FzIGNvbWluZyBpbnRvIGhpcyBmb3JjZTs= +SGUgbWlnaHQgaGF2ZSB3b24gZmFtZSBpbiB0aGUgZW5kLA== +U28gc2Vuc2l0aXZlIGhpcyBuYXR1cmUgc2VlbWVkLA== +U28gZGFyaW5nIGFuZCBzd2VldCBoaXMgdGhvdWdodC4= +VGhpcyBvdGhlciBtYW4gSSBoYWQgZHJlYW1lZA== +QSBkcnVua2VuLCB2YWluLWdsb3Jpb3VzIGxvdXQu +SGUgaGFkIGRvbmUgbW9zdCBiaXR0ZXIgd3Jvbmc= +VG8gc29tZSB3aG8gYXJlIG5lYXIgbXkgaGVhcnQs +WWV0IEkgbnVtYmVyIGhpbSBpbiB0aGUgc29uZzs= +SGUsIHRvbywgaGFzIHJlc2lnbmVkIGhpcyBwYXJ0 +SW4gdGhlIGNhc3VhbCBjb21lZHk7 +SGUsIHRvbywgaGFzIGJlZW4gY2hhbmdlZCBpbiBoaXMgdHVybiw= +VHJhbnNmb3JtZWQgdXR0ZXJseTo= +QSB0ZXJyaWJsZSBiZWF1dHkgaXMgYm9ybi4= diff --git a/data/19_enc.txt b/data/19_enc.txt new file mode 100644 index 0000000..f6fd8bb --- /dev/null +++ b/data/19_enc.txt @@ -0,0 +1,40 @@ +P/GjKtnHZo+G2yMpBHauUrOYTL/0AmG777V5s8uPCg== +Nb6mIsHFZpWK22t9Gnq1G7bMCr37CGE= +MKOkJo/BKZeN22YvTHyxUraJH7e4DH+xob0/9N2LCg== +M7isI9vHI4yXxy4+CX23B6CVTLT3GGG7vPQ= +P/GjKtnHZpKC3HA4CDO0G6aETL24A32677V5s9uGFjhFxe+v +OaPrO8DOL5aGj244DX2qHLWACa/rTWWxvb5svw== +OaPrI87UI8KPxm06CWGmFvKNG7TxAXf+rrR7s9yPGnw= +Jr6nItvHZo+Gzm00AnSvF6GfTKv3H3at4w== +N7+va9vKKZeEx3d9DnalHaCJTJW4BXO6775w/co= +ObfrKo/PKYGIxm06TGeiHrfMA664DDK5prh6 +Ir7rO8PHJ5GGj2J9D3yuArOCBbP2 +N6OkPsHGZpaLyiM7BWGmUrOYTKjwCDK9o699vw== +NLSiJciCJYeR22I0AjO3GrOYTKjwCGv+rrR7s+Y= +NKS/a8PLMIeHj3Q1CWGmUr+DGLD9FDK3vPpo/N2ASQ== +N72na8zKJ4yEymdxTHCrE7yLCbi4GGaqqqhz6pU= +N/G/Lt3QL4CPyiM/CXK2BqvMBa+4D32sofQ= +IrmqP4/VKY+CwSQuTHeiC6HMG7nqCDKtv79x5w== +P7/rIsjMKZCCwXd9C3ysFvKbBbD0QQ== +PrS5a8HLIYqX3CM0AjOiALWZAbn2GQ== +I7+/IsOCLoeRj3UyBXCmUrWeCau4HnqsprZzvQ== +IbmqP4/UKYuAyiMwA2GmUqGbCbnsTWa2rrQ/+8qcAA== +IbmuJY/bKZeNyCM8AnfjELeNGajxC2ey4w== +Jbmua93NIofD22x9BHKxALuJHq+n +IrmiOI/PJ4zDx2I5THimAqbMDfzrDnqxoLY= +N7+va93NIofDwHYvTGSqHLWJCPzwAmCtqvQ= +IrmiOI/NMoqG3SM1BWDjGreAHLnqTXOwq/p54caLHXw= +IbC4a8zNK4uNyCM0AmesUrqFH/z+AmC9quE= +PrTrJsbFLpbDx2IrCTO0HbzMCr31CDK3ofpr+8rOFnZJjA== +Jb7rOMrMNYuXxnU4THuqAfKCDajtH3f+vL96/sqKXw== +Jb7rL87QL4yEj2IzCDOwBbeJGPzwBGH+u7Jw5siGBzY= +IrmiOI/NMoqG3SMwDX3jO/KEDbi4CWC7rrd69w== +N/GvOdrMLYeNgyMrDXqtX7WAA67xAmet77Zw5tvA +PrTrI87GZoaMwWZ9AXywBvKOBajsCGD+uKhw/cg= +Ir7rOMDPI8KUx2x9DWGmUryJDa64AGv+p79+4dvC +L7S/a+aCKJeOzWYvTHuqH/KFAvzsBXf+vLVx9JQ= +PrTna9vNKc7Dx2IuTGGmAbuLArn8TXq3vPpv8t2a +P7/rP8fHZoGC3HY8ADOgHb+JCKWj +PrTna9vNKc7Dx2IuTHGmF7zMD7T5A3W7q/p2/Y+GGmsN1Pu5f1c= +IqOqJdzEKZCOymd9GWe3F6CAFeY= +N/G/Lt3QL4CPyiM/CXK2BqvMBa+4D32sofQ= diff --git a/src/main.rs b/src/main.rs index 70dd938..5fb88ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(); + } } diff --git a/src/set3.rs b/src/set3.rs index 02e7f3b..e00ce94 100644 --- a/src/set3.rs +++ b/src/set3.rs @@ -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 { + 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 = 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"); } diff --git a/src/set3c19.py b/src/set3c19.py new file mode 100644 index 0000000..fc44724 --- /dev/null +++ b/src/set3c19.py @@ -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)