Skip to main content

Soulcrabber - Cyberapocalypse 2021 CTF

· 3 min read

This is a writeup for the Soulcrabber challenge, part of the Hack the box's Cyberapocalypse CTF 2021, category Crypto.

Propmpt

Aliens heard of this cool newer language called Rust, and hoped the safety it offers could be used to improve their stream cipher.

Recon

We get a Rust script which applies a seemingly random XOR cypher to a flag.txt file, and outputs it in hex to out.txt. We also get the output file, so all we have to do is reverse the procedure.

use rand::{Rng,SeedableRng};
use rand::rngs::StdRng;
use std::fs;
use std::io::Write;

fn get_rng() -> StdRng {
let seed = 13371337;
return StdRng::seed_from_u64(seed);
}

fn rand_xor(input : String) -> String {
let mut rng = get_rng();
return input
.chars()
.into_iter()
.map(|c| format!("{:02x}", (c as u8 ^ rng.gen::<u8>())))
.collect::<Vec<String>>()
.join("");
}

fn main() -> std::io::Result<()> {
let flag = fs::read_to_string("flag.txt")?;
let xored = rand_xor(flag);
println!("{}", xored);
let mut file = fs::File::create("out.txt")?;
file.write(xored.as_bytes())?;
Ok(())
}

Sequence of events:

  1. Open a file flag.txt.
  2. Generate a random key for XOR by using the seed 13371337.
  3. XOR the contents of flag.txt with the key.
  4. Write out the result onto out.txt.

Although the code for XOR is generated by a random number generator, we immediately notice that the RNG is supplied with a known seed. This automatically means, that the numbers generated by it will follow the same sequence every time.

So all we have to do to reverse the process is to supply out.txt as input (in hex) to the function, and we will get the original flag in hex as output.

Solution

Sequence:

  1. Read the hex value contents of out.txt.
  2. Generate the pseudo-random key by using the same seed.
  3. XOR the contents of out.txt.
  4. Write the XORed value onto flag.txt.
  5. Represent the hex value of the key in ASCII using xxd.
use rand::{Rng,SeedableRng};
use rand::rngs::StdRng;
use std::fs;
use std::io::Write;
use std::io::prelude::*;

fn get_rng() -> StdRng {
let seed = 13371337;
return StdRng::seed_from_u64(seed);
}

fn rand_xor(input :&mut [u8]) -> String {
let mut rng = get_rng();
return input
// .chars()
.into_iter()
.map(|c| format!("{:02x}", (*c ^ rng.gen::<u8>())))
.collect::<Vec<String>>()
.join("");
}

fn main() -> std::io::Result<()> {
// we now read the file contents into a vector of u8 values
let mut file_content = Vec::new();
let mut file = fs::File::open("out.txt").expect("Unable to open file");
file.read_to_end(&mut file_content).expect("Unable to read");
let xored = rand_xor(&mut file_content);
println!("{}", xored);
let mut file = fs::File::create("flag.txt")?;
file.write(xored.as_bytes())?;
Ok(())
}

Of course, since this is the first time I had to deal with Rust, I had to install a lot of stuff first. After that, to generate the needed files to build the project, we run:

  1. cargo init in the project folder to generate a templated Cargo.toml file.
  2. Add rand = '*' under [dependencies] inside Cargo.toml.
  3. Run cargo build to build the project.
  4. Run it with cargo run.

We now have the hex representation of the flag inside our flag.txt file, so we can simply run

cat flag.txt | xxd -r -ps

and we get our flag

CHTB{mem0ry_s4f3_crypt0_f41l}