Rust fixme 2

Published: April 2, 2025

Description

Fix borrowing issues in a simple XOR decrypter: pass a mutable string to `decrypt`, unwrap the XORCryptor constructor safely, and mutate the buffer so the flag prints.

Extract the archive and `cd fixme2`.

Use `cargo run` to see the borrow checker complaints and compiler errors.

tar -xvf fixme2.tar.gz && cd fixme2
cargo run

Solution

  1. Step 1Borrow the string mutably
    Change the function signature to accept `&mut String` and call it with `&mut party_foul` so the helper can append to the string.
    Learn more

    Rust's ownership and borrowing system is the language's most distinctive feature and its primary mechanism for achieving memory safety without a garbage collector. Every value has exactly one owner. You can create references ("borrows") to a value - either many immutable references (&T) or exactly one mutable reference (&mut T) at a time, but never both simultaneously.

    When a function needs to modify a value owned by its caller, it must receive a &mut reference. Passing &T (immutable reference) to a function that tries to mutate the data is a compile-time error. This forces the programmer to be explicit about mutation intentions at every function boundary - a sharp contrast with C where any pointer can be silently used to modify data.

    This system prevents entire classes of bugs at compile time: use-after-free (the owner drops the value while a reference exists), data races (two threads holding mutable references simultaneously), and iterator invalidation (modifying a collection while iterating over it). These are among the most common and dangerous bugs in C and C++ codebases, and Rust eliminates them without runtime overhead.

  2. Step 2Handle the XORCryptor constructor
    Wrap the decrypt logic in `if let Ok(xrc) = XORCryptor::new(&key)` because the previous code tried to use `res` even if construction failed.
    Learn more

    Rust's Result<T, E> type is an enum with two variants: Ok(T) for success and Err(E) for failure. It forces callers to explicitly handle both outcomes - unlike exceptions in Python or Java, which can be silently uncaught. The if let Ok(value) = result { ... } pattern is a concise way to handle the success case and implicitly ignore the error case.

    Other idiomatic ways to unwrap a Result include: .unwrap() (panics on Err - useful in tests and prototypes, dangerous in production), .expect("message") (panics with a custom message), .unwrap_or(default) (returns a default value on error), and the ? operator (propagates errors to the caller). Each has its place depending on how fatal the error is and whether the function itself returns a Result.

    XOR-based encryption is simple but instructive: the same operation (XOR with the key) both encrypts and decrypts, making it a symmetric cipher with trivial implementation. Real-world XOR cipher weaknesses include key reuse (if the same key encrypts two plaintexts, XORing the ciphertexts reveals information about both plaintexts) and short keys (susceptible to frequency analysis). XOR is, however, the core operation inside stream ciphers like ChaCha20 and inside block cipher modes like CTR and OFB.

  3. Step 3Allow mutation in main
    Declare `let mut party_foul = ...` so the borrowed string can be mutated, then rerun `cargo run` to decrypt and print the flag.
    Learn more

    In Rust, variables are immutable by default. You must explicitly opt into mutability with the mut keyword: let mut x = 5;. This is the opposite of most languages and is a deliberate design choice - immutability makes code easier to reason about and prevents accidental state mutations. The compiler error "cannot borrow x as mutable, as it is not declared as mutable" is one of the most common Rust beginner errors and one of the most educational.

    Immutability by default also has performance implications: the compiler can make stronger aliasing assumptions for immutable data, potentially enabling more aggressive optimizations. In concurrent code, immutable data can be freely shared across threads without locks (Arc<T> instead of Arc<Mutex<T>>), simplifying concurrency logic significantly.

    The combination of skills this challenge reinforces - understanding mut, &mut references, and Result handling - covers a significant portion of Rust's learning curve. Developers who master these concepts find that the compiler becomes a powerful assistant that catches design mistakes before they become runtime bugs, making Rust code unusually reliable despite its initial steepness of learning.

Flag

picoCTF{4r3_y0u_h4v1n5_fun_...}

This task reinforces Rust's borrowing rules, and Fixme 3 builds on the same pattern with a slightly larger project.

Want more picoCTF 2025 writeups?

Useful tools for General Skills

Related reading

What to try next