Description
The flag is right in front of you, just slightly encrypted. Figure out the cipher and the key. Download the binary and the encoded flag.
unzip hiddencipher.zipls -laSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Unpack the binary with UPX
ObservationThe hint mentions a Linux tool for unpacking, and file reports a UPX-packed ELF. Decompress with upx -d before analyzing anything.Install UPX and use it to unpack the binary. Once unpacked, load it into Ghidra for analysis.bashsudo apt-get install upx-ucl # the Debian/Ubuntu package name; it provides /usr/bin/upxbashupx -d hiddencipherbashfile hiddencipher # confirm it's now an uncompressed ELFbashghidraRun & # then create a project and import the unpacked hiddencipherExpected output
hiddencipher: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
What didn't work first
Tried: Running strings on the packed binary before using UPX to unpack it.
UPX replaces the real code with a decompression stub, so strings on the packed file returns stub artifacts and version markers, not the key or the cipher. Everything meaningful appears only after decompression.
Tried: Skipping UPX and loading the packed binary directly into Ghidra.
Ghidra disassembles the UPX stub rather than the program, showing a tight loop inflating compressed data into memory and nothing of the cipher. The output looks like noise because the real ELF sections are compressed blocks inside the container.
Learn more
Classical ciphers are simple substitution or transposition schemes that predate modern cryptography. The most common ones in CTF challenges are:
- Caesar cipher: shifts each letter by a fixed number (e.g., shift 13 = ROT13)
- Vigenère cipher: a polyalphabetic cipher using a repeating keyword as the key
- XOR cipher: bytes are XORed with a key byte or key sequence
- Substitution cipher: each letter is replaced by a fixed different letter
- Atbash: reverses the alphabet (A↔Z, B↔Y, etc.)
Recognising which cipher was used is the first step. Ciphertext that contains only letters and spaces suggests an alphabetic cipher (Caesar, Vigenère, substitution). Ciphertext containing arbitrary bytes or hex suggests XOR. The key insight for this challenge is that "hidden" means the cipher type and key are embedded in the provided files (in constants, strings, or the program logic) rather than being something you need to crack.
Distinguishing XOR from Vigenère in pseudocode is a one-glance test. XOR loops look like
out[i] = ct[i] ^ key[i % keylen]with a bitwise XOR (and frequently a& 0xFFmask if the operands are wider than a byte). Vigenère loops look likeout[i] = ((ct[i] - 'A') - (key[i % keylen] - 'A')) % 26 + 'A': subtraction of letter offsets, modulo 26, no XOR, and the operands are guaranteed to be alphabetic. If you see a^operator or a hex constant XOR, it is XOR; if you see%26or character arithmetic on'A', it is Vigenère or a Caesar variant. See the encodings guide for the broader pattern catalogue and Ghidra reverse engineering for tips on decompiling these loops.Step 2Find the XOR key and ciphertext in Ghidra
ObservationThe decompiled main() runs a loop with an XOR and a modular index into a six-element key. That is repeating-key XOR, and the key comes from get_secret() nearby.In Ghidra, look at main(). You will see a get_secret() function and a loop that XORs the flag bytes with the secret key modulo the key length. The function get_secret() returns a 6-byte key. Look at the data in that function to find the key bytes - they are stored slightly obfuscated.bash# In Ghidra after unpacking:bash# 1. Browse to main() in the Listing viewbash# 2. Find the XOR loop: flag[i] ^ key[i % 6]bash# 3. Navigate to get_secret() to find the 6-byte keybash# The encoded flag bytes are read from an external file at runtime (fread call in main)What didn't work first
Tried: Using strings on the unpacked binary to find the key directly without opening Ghidra.
The key bytes in get_secret() are stored obfuscated rather than as a null-terminated string, so strings never prints them as a readable run. The decompiler shows the array initialization, or the arithmetic that builds them.
Tried: Assuming the encoded flag bytes are embedded inside the binary and searching for them in Ghidra's data view.
The ciphertext is read from an external file at runtime, not stored as a constant in the ELF, so searching .rodata and .data turns up nothing. Find the encoded flag file that came out of the archive alongside the binary.
Learn more
Static analysis extracts information from a binary without executing it.
stringsfinds printable ASCII sequences and is the fastest way to find hardcoded keys, passwords, or meaningful strings. Radare2 (r2) is a powerful open-source reverse engineering framework;-Aanalyses the binary automatically and identifies functions, cross-references, and strings. Ghidra (developed by the NSA) provides a GUI with decompilation - it translates assembly back into readable C-like pseudocode.Keys in CTF binaries are typically stored as: hardcoded string literals (found by
strings), integer constants in the assembly, arrays initialised at the start of a function, or values computed from program inputs. When you see an XOR loop in Ghidra output, look for the key value being loaded from a nearby variable or constant. For Vigenère, look for a string being used as a repeating index.In real malware analysis, extracting hardcoded encryption keys from binaries is a core skill. Malware families often XOR-encrypt their configuration (C2 server addresses, port numbers, campaign identifiers) with a static key embedded in the binary. Tools like FLOSS (FireEye Labs Obfuscated String Solver) automatically find and decode XOR-encoded strings in malware samples.
Step 3Decrypt the flag with XOR key in CyberChef
ObservationXOR is its own inverse, so with the six-byte key and the ciphertext in hand, one loop in Python or a CyberChef recipe reconstructs the flag.Take the encoded flag bytes from the encrypted file that shipped in the archive and use CyberChef to XOR-decrypt them with the key you found. In CyberChef: (1) paste the file contents, (2) use 'From Hex' if they are hex, then 'XOR' with the key. The key cycles through all its bytes for each flag character.pythonpython3 << 'EOF' key = bytes.fromhex("530000...") # replace with actual hex bytes from get_secret() in Ghidra ct = open("encrypted_flag", "rb").read() # the ciphertext file from the archive pt = bytes(c ^ key[i % len(key)] for i, c in enumerate(ct)) print(pt.decode()) EOFWhat didn't work first
Tried: Treating the cipher as Vigenère instead of XOR and applying a letter-offset modulo-26 decryption formula.
Vigenere works on letters with modulo-26 arithmetic, produces only letters, and breaks on non-alphabetic bytes. XOR works on raw bytes with no alphabet at all, so Vigenere logic over this ciphertext gives garbage. The decompiler shows an XOR operator, not the modulo-26 subtraction chain.
Tried: Entering the key as an ASCII string in CyberChef's XOR operation instead of the raw hex bytes from get_secret().
The key may include non-printable bytes with no ASCII equivalent. Type what you think the key says instead of pasting the exact hex from Ghidra and the offsets shift, garbling the output. Use the hex form for any raw-byte XOR.
Learn more
XOR encryption is symmetric: applying the same key twice returns the original plaintext (
P XOR K XOR K = P). This makes decryption identical to encryption - the same code that encrypts also decrypts. XOR with a repeated key is also called a Vigenère cipher in binary and has the same weaknesses: if any plaintext byte is known (like thepinpicoCTF), you can recover the corresponding key byte.The Python expression
bytes(c ^ k for c, k in zip(ct, key * N))decrypts by cycling the key over the ciphertext usingzip. The key is repeated withkey * Nto ensure it is at least as long as the ciphertext. This is a clean, Pythonic one-liner for any XOR cipher regardless of key length.For classical alphabetic ciphers, Python's
str.translate()andstr.maketrans()are the cleanest decryption tools: they define a character mapping table and apply it to the entire string in one call. Online tools like CyberChef("The Cyber Swiss Army Knife") support all common ciphers and encoding schemes and are excellent for rapid prototyping when writing custom code feels like overkill.
Interactive tools
- Cipher Identifier & Auto-DecoderPaste any ciphertext and the tool auto-runs every common decoder (base64, hex, Morse, ROT, Atbash, Bacon, binary, decimal, URL) and ranks the results by English-likeness.
Alternate Solution
Once you have identified the key, use the XOR Cipher tool on this site to decrypt without writing any Python. Paste the hex ciphertext, enter the key, and the plaintext flag appears instantly. If the cipher turns out to be a Caesar/ROT variant, use the ROT / Caesar Cipher tool instead.
Flag
Reveal flag
picoCTF{xor_unpack_4nalys1s_...}
First unpack with 'upx -d hiddencipher', then load in Ghidra. Find the XOR loop in main() and the key in get_secret(). Decrypt the flag bytes with the key using CyberChef or Python.