From 565817a2d601813615312544b55abaf1b15632ab Mon Sep 17 00:00:00 2001 From: SkeletalDemise <29117662+SkeletalDemise@users.noreply.github.com> Date: Mon, 21 Nov 2022 02:10:37 -0800 Subject: [PATCH] Improve Citrix CTX1 (#86) * Improve Citrix CTX1 * Citrix CTX1 decoder now handles subtraction overflows * Removed uppercase check, now it can decode lowercase Citrix CTX1 strings * Added 3 more tests * Removed 2 redundant tests * Added docs to every test * Improve tests and error handling * Renamed tests to be more clear * Errors now propagate up and get printed * Fix docs Added docs for errors --- Cargo.lock | 68 ++++++++++++++++ Cargo.toml | 1 + src/decoders/citrix_ctx1_decoder.rs | 120 ++++++++++++++++------------ 3 files changed, 137 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5582544c..c6996ece 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,6 +524,73 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -660,6 +727,7 @@ dependencies = [ "include_dir", "lemmeknow", "log", + "num", "once_cell", "rayon", "text_io", diff --git a/Cargo.toml b/Cargo.toml index ae45d8e9..8ac464d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ once_cell = "1.16.0" text_io = "0.1.12" data-encoding = "2.3.2" bs58 = "0.4.0" +num = "0.4" crossbeam = "0.8" base65536 = "1.0.1" diff --git a/src/decoders/citrix_ctx1_decoder.rs b/src/decoders/citrix_ctx1_decoder.rs index d280267d..f0ae4681 100644 --- a/src/decoders/citrix_ctx1_decoder.rs +++ b/src/decoders/citrix_ctx1_decoder.rs @@ -10,6 +10,19 @@ use log::{debug, info, trace}; ///! Citrix CTX1 Decoder pub struct CitrixCTX1Decoder; +///! Error enum +#[derive(Debug)] +enum Error { + ///! Error when the input is not divisible by 4 + InvalidLength, + ///! Error with left-hand side subtraction + LhsOverflow, + ///! Error with right-hand side subtraction + RhsOverflow, + ///! Error if the result isn't UTF-8 + InvalidUtf8, +} + impl Crack for Decoder { fn new() -> Decoder { Decoder { @@ -31,16 +44,17 @@ impl Crack for Decoder { /// Else the Option returns nothing and the error is logged in Trace fn crack(&self, text: &str, checker: &CheckerTypes) -> CrackResult { trace!("Trying citrix_ctx1 with text {:?}", text); - let decoded_text: Option = decode_citrix_ctx1(text); + let decoded_text: Result = decode_citrix_ctx1(text); - trace!("Decoded text for citrix_ctx1: {:?}", decoded_text); let mut results = CrackResult::new(self, text.to_string()); - if decoded_text.is_none() { - debug!("Failed to decode citrix_ctx1 because the length is not a multiple of 4"); + if decoded_text.is_err() { + debug!("Failed to decode citrix_ctx1: {:?}", decoded_text); return results; } + trace!("Decoded text for citrix_ctx1: {:?}", decoded_text); + let decoded_text = decoded_text.unwrap(); if !check_string_success(&decoded_text, text) { info!( @@ -60,13 +74,9 @@ impl Crack for Decoder { } /// Decodes Citrix CTX1 -fn decode_citrix_ctx1(text: &str) -> Option { +fn decode_citrix_ctx1(text: &str) -> Result { if text.len() % 4 != 0 { - return None; - } - - if text.to_uppercase() != text || !text.chars().all(|c| c.is_ascii()) { - return None; + return Err(Error::InvalidLength); } let mut rev = text.as_bytes().to_vec(); @@ -78,16 +88,19 @@ fn decode_citrix_ctx1(text: &str) -> Option { if i + 2 >= rev.len() { temp = 0; } else { - temp = ((rev[i + 2] - 0x41) & 0xF) ^ (((rev[i + 3] - 0x41) << 4) & 0xF0); + temp = ((rev[i + 2].checked_sub(0x41)).ok_or(Error::LhsOverflow)? & 0xF) + ^ (((rev[i + 3].checked_sub(0x41)).ok_or(Error::RhsOverflow)? << 4) & 0xF0); } - temp ^= (((rev[i] - 0x41) & 0xF) ^ (((rev[i + 1] - 0x41) << 4) & 0xF0)) ^ 0xA5; + temp ^= (((rev[i].checked_sub(0x41)).ok_or(Error::LhsOverflow)? & 0xF) + ^ (((rev[i + 1].checked_sub(0x41)).ok_or(Error::RhsOverflow)? << 4) & 0xF0)) + ^ 0xA5; result.push(temp); } result.retain(|&x| x != 0); result.reverse(); - String::from_utf8(result).ok() + String::from_utf8(result).map_err(|_| Error::InvalidUtf8) } #[cfg(test)] @@ -109,7 +122,8 @@ mod tests { } #[test] - fn test_citrix_ctx1() { + fn citrix_ctx1_decodes_successfully() { + // This tests if Citrix CTX1 can decode Citrix CTX1 successfully let decoder = Decoder::::new(); let result = decoder.crack( "MNGIKIANMEGBKIANMHGCOHECJADFPPFKINCIOBEEIFCA", @@ -117,19 +131,47 @@ mod tests { ); assert_eq!(result.unencrypted_text.unwrap(), "hello world"); } + + #[test] + fn citrix_ctx1_decodes_lowercase_successfully() { + // This tests if Citrix CTX1 can decode lowercase strings + let decoder = Decoder::::new(); + let result = decoder.crack( + "pbfejjdmpaffidcgkdagmkgpljbmjjdmpffajkdponeiiicnpkfpjjdmpifnilcoooelmoglincioeebjadfocehilcopdfgndhgjadfmegbjmdjknai", + &get_athena_checker(), + ); + assert_eq!( + result.unencrypted_text.unwrap(), + "This is lowercase Citrix CTX1" + ); + } + #[test] - fn citrix_ctx1_decode_empty_string() { - // Citrix_ctx1 returns an empty string, this is a valid citrix_ctx1 string - // but returns False on check_string_success + fn citrix_ctx1_handles_substraction_overflow() { + // This tests if Citrix CTX1 can handle substraction overflows + // It should return None and not panic let citrix_ctx1_decoder = Decoder::::new(); let result = citrix_ctx1_decoder - .crack("", &get_athena_checker()) + .crack("NUWEN43XR44TLAYHSU4DVI2ISF======", &get_athena_checker()) + .unencrypted_text; + assert!(result.is_none()); + } + + #[test] + fn citrix_ctx1_handles_length_not_divisible_by_4() { + // This tests if Citrix CTX1 can handle strings with length that are not divisible by 4 + // It should return None + let citrix_ctx1_decoder = Decoder::::new(); + let result = citrix_ctx1_decoder + .crack("AAA", &get_athena_checker()) .unencrypted_text; assert!(result.is_none()); } #[test] fn citrix_ctx1_decode_handles_panics() { + // This tests if Citrix CTX1 can handle panics + // It should return None let citrix_ctx1_decoder = Decoder::::new(); let result = citrix_ctx1_decoder .crack( @@ -137,51 +179,25 @@ mod tests { &get_athena_checker(), ) .unencrypted_text; - if result.is_some() { - panic!("Decode_citrix_ctx1 did not return an option with Some.") - } else { - // If we get here, the test passed - // Because the citrix_ctx1_decoder.crack function returned None - // as it should do for the input - assert_eq!(true, true); - } + assert!(result.is_none()); } #[test] fn citrix_ctx1_handle_panic_if_empty_string() { + // This tests if Citrix CTX1 can handle an empty string + // It should return None let citrix_ctx1_decoder = Decoder::::new(); let result = citrix_ctx1_decoder .crack("", &get_athena_checker()) .unencrypted_text; - if result.is_some() { - assert_eq!(true, true); - } - } - - #[test] - fn citrix_ctx1_work_if_string_not_citrix_ctx1() { - // You can citrix_ctx1 decode a string that is not citrix_ctx1 - // This string decodes to: - // ```.ée¢ - // (uÖ²``` - // https://gchq.github.io/CyberChef/#recipe=From_Base58('A-Za-z0-9%2B/%3D',true)&input=aGVsbG8gZ29vZCBkYXkh - let citrix_ctx1_decoder = Decoder::::new(); - let result = citrix_ctx1_decoder - .crack("hello good day!", &get_athena_checker()) - .unencrypted_text; - if result.is_some() { - assert_eq!(true, true); - } + assert!(result.is_none()); } #[test] - fn citrix_ctx1_handle_panic_if_emoji() { + fn citrix_ctx1_decodes_emoji_successfully() { + // This tests if Citrix CTX1 can decode an emoji let citrix_ctx1_decoder = Decoder::::new(); - let result = citrix_ctx1_decoder - .crack("😂", &get_athena_checker()) - .unencrypted_text; - if result.is_some() { - assert_eq!(true, true); - } + let result = citrix_ctx1_decoder.crack("😂", &get_athena_checker()); + assert_eq!(result.unencrypted_text.unwrap(), "[*"); } }