Skip to main content

Hidden Cipher 1 picoCTF 2026 Solution

A binary hides the flag behind a simple cipher. Identify the encryption scheme and recover the plaintext.

Published: March 20, 2026Updated: September 20, 2026

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.
bash
unzip hiddencipher.zip
bash
ls -la

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Unpack the binary with UPX
    Observation
    The 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.
    bash
    sudo apt-get install upx-ucl   # the Debian/Ubuntu package name; it provides /usr/bin/upx
    bash
    upx -d hiddencipher
    bash
    file hiddencipher   # confirm it's now an uncompressed ELF
    bash
    ghidraRun &   # then create a project and import the unpacked 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 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 & 0xFF mask if the operands are wider than a byte). Vigenère loops look like out[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 %26 or 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.

  2. Step 2Find the XOR key and ciphertext in Ghidra
    Observation
    The 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 view
    bash
    # 2. Find the XOR loop: flag[i] ^ key[i % 6]
    bash
    # 3. Navigate to get_secret() to find the 6-byte key
    bash
    # 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. strings finds 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; -A analyses 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.

  3. Step 3Decrypt the flag with XOR key in CyberChef
    Observation
    XOR 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.
    python
    python3 << '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())
    EOF
    What 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 the p in picoCTF), 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 using zip. The key is repeated with key * N to 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() and str.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.

Key takeaway

When a key is baked into a binary rather than derived from user input, static analysis recovers it without running anything. Decompiling shows the algorithm and the key constants together, which turns an apparent cryptography problem into code reading. Malware analysts do the same to pull hardcoded XOR keys from packed samples and decrypt configuration data such as command-and-control addresses.

Related reading

Tools used in this challenge

Where to go next