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.
Download and extract hiddencipher.zip.
The hint says the binary can be unpacked with a tool that's often pre-installed on Linux.
unzip hiddencipher.zipls -laSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Unpack the binary with UPXObservationI noticed the challenge hint mentioned a tool pre-installed on Linux for unpacking, and running 'file hiddencipher' would show a UPX-packed ELF signature, which pointed directly to using 'upx -d' before any further analysis was possible.Install UPX and use it to unpack the binary. Once unpacked, load it into Ghidra for analysis.bashsudo apt-get install upxbashupx -d hiddencipherbashfile hiddencipher # confirm it's now an uncompressed ELFbashghidra hiddencipher &Expected 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 compression replaces the real code with a decompression stub, so strings on the packed binary returns only stub artifacts and UPX version markers - not the key or cipher logic. The meaningful strings and logic are only visible after upx -d decompresses the binary back to its original form.
Tried: Skipping UPX and loading the packed binary directly into Ghidra.
Ghidra will disassemble the UPX decompression stub rather than the actual program code, showing a tight loop that inflates compressed data into memory - not the XOR cipher or get_secret() function. The decompiled output looks like noise because the real ELF sections are stored as compressed data blocks inside the UPX 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 2
Find the XOR key and ciphertext in GhidraObservationI noticed the decompiled main() in Ghidra contained a loop with a '^' (XOR) operator and a modular index 'key[i % 6]', which suggested the cipher was XOR with a 6-byte key stored in the nearby get_secret() function.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 in a slightly obfuscated form rather than as a plain null-terminated string, so strings will not print them as a readable sequence. You need Ghidra's decompiler to see the array initialisation or the arithmetic that constructs the key bytes before they are returned.
Tried: Assuming the encoded flag bytes are embedded inside the binary and searching for them in Ghidra's data view.
The binary reads the ciphertext from an external file at runtime via an fread call in main() - it is not stored as a data constant inside the ELF. Looking only at .rodata or .data sections in Ghidra will not surface the ciphertext; you need to locate the accompanying encoded flag file that was extracted from hiddencipher.zip.
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 3
Decrypt the flag with XOR key in CyberChefObservationI noticed that XOR is its own inverse (applying the same key twice recovers the plaintext), so once I had the 6-byte key from get_secret() and the ciphertext bytes from the external file, a simple Python XOR loop or CyberChef's XOR recipe would reconstruct the flag directly.Copy the encoded flag bytes from Ghidra and use CyberChef to XOR-decrypt them with the key you found. In CyberChef: (1) paste the hex bytes, (2) use 'From 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 = bytes.fromhex("CIPHERTEXT_HEX_FROM_GHIDRA") 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.
Vigenère operates on alphabetic characters using (ct - 'A' - key_offset) % 26 arithmetic, which only produces letters and breaks on non-alphabetic bytes. XOR operates on raw bytes with no alphabet constraint, so applying Vigenère logic to the XOR ciphertext yields garbage. The Ghidra decompiler output shows a ^ operator, not the % 26 subtraction chain that marks Vigenère.
Tried: Entering the key as an ASCII string in CyberChef's XOR operation instead of the raw hex bytes from get_secret().
The key bytes from get_secret() may include non-printable byte values that do not correspond to standard ASCII characters. If you type what you think the key string is rather than pasting the exact hex bytes from Ghidra, the XOR offsets shift and the output is garbled. Always use the hex representation of the key when working with raw byte XOR ciphers in CyberChef.
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{h1dd3n_c1ph3r_1_...}
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.