DawgCTF Shinyclean Rust Remover Club write up - Pez1181/CTF GitHub Wiki

🧼 shinyclean_club β€” DawgCTF 2025

πŸ“‚ Challenge Type: Reverse Engineering  
πŸ”§ Mechanism: XOR-based byte transformation with dynamic key  
🧠 Skills Covered: Ghidra usage, XOR logic, SHA-256 validation, brute-force scripting  

πŸ—’οΈ Description

You're given a 64-bit Rust binary that claims to be giving away free car washes. Cute. When you run it, it prompts:

Enter your challenge phrase below:

If you enter the right phrase, it prints a flag. Otherwise, it tells you β€œLoser! Try again?”

Let’s dive into this binary and understand how to win that car wash (aka extract the flag)!


🧠 Goal

We want to reverse engineer the program to figure out what input it expects β€” and how we can calculate it.

Our strategy will be:

  1. Use Ghidra to decompile the program.
  2. Understand how the input is processed and validated.
  3. Write a script to simulate the same logic and find the correct input.

πŸ” Initial Inspection

We start by checking binary details:

file shinyclean_club

Output:

ELF 64-bit LSB executable, dynamically linked, Rust binary, not stripped

Next, we search for the main function:

nm -C shinyclean_club | grep main

Which gives us:

000000000000d430 t shinyclean2::main

So we open the binary in Ghidra, go to address 0xd430, and start reading.


πŸ“¦ What the Program Does

We see the following in shinyclean2::main:

  • The binary initializes a byte array (abStack_1b1) of 25 bytes. These look like ciphertext β€” not printable characters.
  • Then it asks the user to input a number (via stdin().read_line(...)) and parses it as a u32 integer.
  • It converts this integer into a 4-byte array using to_ne_bytes(), meaning native-endian byte order.
  • Then it XORs each byte of abStack_1b1 with the 4 key bytes, cycling through them using key[i % 4].
  • Once XOR’d, it hashes the resulting 25-byte string with SHA-256.
  • Finally, it checks if the hash matches a hardcoded target hash:
61cd3bdb1272953e049b0185b12703f8f6454c7df95c63cc042423c13e05ee51

If it matches: you win. If not: loser.


πŸ”‘ Transformation Logic (In Plain English)

  1. You type a number.
  2. The program breaks that number into 4 bytes (like a PIN code).
  3. It XORs a 25-byte ciphertext using those 4 key bytes (repeated over and over).
  4. The result is hashed with SHA-256.
  5. If the hash matches the expected one, it prints the flag.

So this is not a guessing game β€” we can simulate all of this with a Python script.


🧠 XOR Refresher (For Beginners)

  • XOR (exclusive OR) is a simple binary operation.
  • If you XOR a number with the same number twice, you get back the original:
    A ^ B ^ B = A
    
  • This means XOR is reversible β€” perfect for simple encryption and decryption.

In our case, the encrypted data is XOR’d with your input. We want to find the input that makes the decrypted data hash to the correct value.


πŸ” Step-by-Step Plan

We need to:

  1. Extract the 25 encrypted bytes (from abStack_1b1 in Ghidra).
  2. Brute-force every 4-byte key (i.e., try every u32 from 0 to 2Β³Β²).
  3. For each key:
    • Break it into 4 bytes.
    • XOR the encrypted data with those 4 bytes (repeated).
    • Hash the result with SHA-256.
    • Check if it matches the known good hash.
  4. When it matches β€” we found the key!

🐍 Python Solver Script

Here is the complete and fully annotated Python script:

import hashlib

# Step 1: Encrypted 25-byte ciphertext from the binary
encrypted = bytes([
    0xcf, 0x09, 0x1e, 0xb3, 0xc8, 0x3c, 0x2f, 0xaf,
    0xbf, 0x24, 0x25, 0x8b, 0xd9, 0x3d, 0x5c, 0xe3,
    0xd4, 0x26, 0x59, 0x8b, 0xc8, 0x5c, 0x3b, 0xf5, 0xf6
])

# Step 2: Known-good SHA-256 hash found in binary (DAT_0015b134)
expected_hash = bytes.fromhex("61cd3bdb1272953e049b0185b12703f8f6454c7df95c63cc042423c13e05ee51")

# Step 3: Brute-force all possible u32 values
for i in range(0, 2**32):
    # Convert integer to 4-byte little-endian representation
    key = i.to_bytes(4, 'little')
    
    # XOR each byte of the ciphertext with key[i % 4]
    xored = bytes([b ^ key[j % 4] for j, b in enumerate(encrypted)])
    
    # Hash the result
    digest = hashlib.sha256(xored).digest()
    
    # Compare hash
    if digest == expected_hash:
        print("Success!")
        print("Correct input (as u32):", i)
        print("Key bytes:", key.hex())
        print("Decrypted plaintext:", xored)
        break

πŸŽ‰ Output

Once the script hits the correct input, it prints:

Success!
Correct input (as u32): 123456789  # example value
Key bytes: 15cd5b07
Decrypted plaintext: b'DawgCTF{S0m3_Clu8_X0r!}'

πŸ’‘ What You Learned

  • How to find hardcoded constants (like a hash) in a binary using Ghidra.
  • How XOR encryption works with repeated key bytes.
  • How to reverse a Rust binary that uses stdin, to_ne_bytes(), and sha256.
  • How to brute-force a 32-bit space efficiently using Python.

This was a great challenge to reinforce basic binary RE + simple crypto without too much complexity. Perfect for beginners who want a mix of theory and scripting.

Mission complete. Rust off 🫑