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:
- Use Ghidra to decompile the program.
- Understand how the input is processed and validated.
- 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 usingkey[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)
- You type a number.
- The program breaks that number into 4 bytes (like a PIN code).
- It XORs a 25-byte ciphertext using those 4 key bytes (repeated over and over).
- The result is hashed with SHA-256.
- 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:
- Extract the 25 encrypted bytes (from
abStack_1b1
in Ghidra). - Brute-force every 4-byte key (i.e., try every
u32
from 0 to 2Β³Β²). - 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.
- 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()
, andsha256
. - 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.