Solution to Bornhack 2020 CTF challenge caesar_with_a_twist

16/08/20 — capitol

caesar

Name:

caesar_with_a_twist

Category:

crypto

Points:

75

Writeup

The third challenge in the crypto category, this was the first one with some actual encryption.

We got an encrypted text:

CLLJE{QtLo_wF_r_YqOI_bXrH_gpJw_WxPh_Sm_QraVmo_Se_IoSvvn_XyR_GeU_Ack_EDRDTIosscf_rj}

And a Python program that described how it was generated, analysing that program showed that it was a regular substitution cipher where the substitution key was changed for each character. The key was a function of that characters position in the string.

We reimplemented it in rust and wrote a reverse implementation:

use std::fs::File;
use std::io::prelude::*;

#[derive(Debug, Clone)]
struct CaesarError;

fn caesar_encrypt(original: char, base: u32) -> Result<char, CaesarError> {
    let letter = original as u32;

    // Ensures the base rotation does not exceed 26
    // If the base is 28, it exceeds 26, and ends up being 2 (after modulus)
    let base = base % 26;

    // If the letter is a space, underscore or curly brackets, just return it without rotating
    if letter == 32 || letter == 95 || letter == 123 || letter == 125 {
        return Ok(std::char::from_u32(letter).unwrap());
    }

    // If capital letter
    if 'A' as u32 <= letter && letter <= 'Z' as u32 {
        // If the base exceeds the alphabet
        return if ('Z' as u32) < (letter + base) {
            Ok(std::char::from_u32(letter + base - 26).unwrap())
        } else {
            Ok(std::char::from_u32(letter + base).unwrap())
        }
    }

    // If non-capital letter
    if 'a' as u32 <= letter && letter <= 'z' as u32 {
        // If the base exceeds the alphabet
        return if ('z' as u32) < (letter + base) {
            Ok(std::char::from_u32(letter + base - 26).unwrap())
        } else {
            Ok(std::char::from_u32(letter + base).unwrap())
        }
    }

    Err(CaesarError {})
}

fn caesar_decrypt(original: char, base: u32) -> Result<char, CaesarError> {
    let letter = original as u32;

    // Ensures the base rotation does not exceed 26
    // If the base is 28, it exceeds 26, and ends up being 2 (after modulus)
    let base = base % 26;

    // If the letter is a space, underscore or curly brackets, just return it without rotating
    if letter == 32 || letter == 95 || letter == 123 || letter == 125 {
        return Ok(std::char::from_u32(letter).unwrap());
    }

    // If capital letter
    if 'A' as u32 <= letter && letter <= 'Z' as u32 {
        // If the base exceeds the alphabet
        return if ('A' as u32) > (letter - base) {
            Ok(std::char::from_u32(letter - base + 26).unwrap())
        } else {
            Ok(std::char::from_u32(letter - base).unwrap())
        }
    }

    // If non-capital letter
    if 'a' as u32 <= letter && letter <= 'z' as u32 {
        // If the base exceeds the alphabet
        return if ('a' as u32) > (letter - base) {
            Ok(std::char::from_u32(letter - base + 26).unwrap())
        } else {
            Ok(std::char::from_u32(letter - base).unwrap())
        }
    }

    Err(CaesarError {})
}

fn encrypt(s: &String) -> Result<String, CaesarError> {
    let mut result:Vec<char> = vec![];
    for (i, c) in s.chars().enumerate() {
        result.push(caesar_encrypt(c, ((i + 1) * (i + 1)) as u32)?);
    }
    Ok(result.iter().collect::<String>())
}

fn decrypt(s: &String) -> Result<String, CaesarError> {
    let mut result:Vec<char> = vec![];
    for (i, c) in s.chars().enumerate() {
        result.push(caesar_decrypt(c, ((i + 1) * (i + 1)) as u32)?);
    }
    Ok(result.iter().collect::<String>())
}

fn main() -> std::io::Result<()> {
    let mut file = File::open("cwat_encrypted_flag.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    println!("{}", decrypt(&contents).unwrap());
    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::CaesarError;
    use crate::caesar_decrypt;
    use crate::caesar_encrypt;
    use crate::encrypt;
    use crate::decrypt;

    static ASCII_LOWER: [char; 26] = [
        'a', 'b', 'c', 'd', 'e',
        'f', 'g', 'h', 'i', 'j',
        'k', 'l', 'm', 'n', 'o',
        'p', 'q', 'r', 's', 't',
        'u', 'v', 'w', 'x', 'y',
        'z',
    ];

    static ASCII_UPPER: [char; 26] = [
        'A', 'B', 'C', 'D', 'E',
        'F', 'G', 'H', 'I', 'J',
        'K', 'L', 'M', 'N', 'O',
        'P', 'Q', 'R', 'S', 'T',
        'U', 'V', 'W', 'X', 'Y',
        'Z',
    ];

    #[test]
    fn loop_all_ascii_chars() -> Result<(), CaesarError> {
        for base in 0..25 {
            println!("base = {}", base);
            for c in &ASCII_LOWER {
                assert_eq!(*c, caesar_decrypt(caesar_encrypt(*c, base)?, base)?);
            }
            for c in &ASCII_UPPER {
                assert_eq!(*c, caesar_decrypt(caesar_encrypt(*c, base)?, base)?);
            }
        }

        Ok(())
    }

    #[test]
    fn encrypt_test() -> Result<(), CaesarError> {
        assert_eq!("Ulri sp d ksfh", encrypt(&"This is a test".to_string())?);

        Ok(())
    }

    #[test]
    fn encrypt_loop() -> Result<(), CaesarError> {
        assert_eq!("This is a test", decrypt(&encrypt(&"This is a test".to_string())?)?);

        Ok(())
    }
}

Flag was BHCTF{ThIs_iS_a_VeRY_lOnG_flAg_MaDe_By_CaeSar_To_EnSure_YoU_DiD_Not_BRUTUSforce_it}.