bloat.py picoCTF 2022 Solution

Published: July 20, 2023

Description

The provided Python script (bloat.flag.py) hides logic behind an array of printable characters before requesting a password to decrypt flag.txt.enc. Deobfuscate the script to recover the password, then run it to reveal the flag.

Download both bloat.flag.py and flag.txt.enc into the same working directory.

Read through the script to understand how the lookup table a[...] maps back to readable characters.

After uncovering the hard-coded password (happychance), run the script to decrypt the encrypted flag file.

bash
wget https://artifacts.picoctf.net/c/103/bloat.flag.py
bash
wget https://artifacts.picoctf.net/c/103/flag.txt.enc
python
python3 bloat.flag.py
python
python3 bloat.flag.py | tee output.txt
bash
sed -n '2p' output.txt

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Understand the lookup table
    Observation
    I noticed that every string in bloat.flag.py was assembled from indexed lookups into a single character array a rather than readable literals, which suggested the entire obfuscation was just array indexing and could be reversed by defining a in a Python REPL and evaluating the index expressions directly.
    Every string literal is built from indices into one character array a. Define a in a REPL and let Python evaluate the indexing for you.
    python
    python3 -c "
    # Paste the obfuscated array exactly as it appears in bloat.flag.py
    a = ['h','a','p','y','c','n','e','...']
    # Then walk one of the obfuscated index expressions, e.g.:
    print(''.join([a[i] for i in [0, 1, 2, 2, 3, 4, 5, 1, 6, 4, 7]]))
    "

    Expected output

    picoCTF{d30bfu5c4710n_f7w_b80...}
    What didn't work first

    Tried: Just run python3 bloat.flag.py immediately to see what the script does without reading it first.

    The script immediately asks for a password interactively. Without knowing the password first you cannot get past the prompt, and guessing or hitting Enter produces a decryption failure or garbled output. The correct approach is to read the source, evaluate the index expressions statically to recover the password, and only then run the script with the known credential.

    Tried: Use strings bloat.flag.py to search for the password as a plain text literal.

    strings extracts contiguous printable character sequences, but the password happychance is never stored as a single literal - it is assembled at runtime by joining individual characters from the array a via an index list. strings shows the raw character array elements and miscellaneous Python keywords but never the assembled word. You must evaluate the index expression yourself in a REPL to reconstruct the credential.

    Learn more

    The trick is "character-array string building": instead of "happychance" the code writes a[i0] + a[i1] + a[i2] + .... At runtime the interpreter joins the same string; statically it's just integers. Defining a in a REPL and running ''.join([a[i] for i in [...]]) on each obfuscated expression instantly reveals the plaintext.

    Don't run untrusted scripts on your host. Run inside a VM, container, or with python3 -c "..." capturing only the print output. Real malware uses the same character-array, base64, and eval-chain patterns you see here.

  2. Step 2
    Recover the password
    Observation
    I noticed the script contained a variable assigned by joining individual characters from the array a via an index list and passed it directly to the decryption routine, which suggested that walking those indices against the known array would spell out the plaintext password without any cryptanalysis.
    Walking the index expression for the credential variable spells happychance. That string is what flag.txt.enc was encrypted with.
    Learn more

    Specifically, the script's password variable is assigned something like password = a[i0] + a[i1] + ... . Walking the index list against your a array produces h-a-p-p-y-c-h-a-n-c-e. Because the array and the index sequence are both right there in the source, the obfuscation is purely cosmetic.

    Hardcoded credentials in source - even obfuscated ones - are a classic antipattern. Real systems store secrets outside source (environment variables, AWS Secrets Manager, HashiCorp Vault) or prompt the user at runtime and derive a key via PBKDF2/Argon2.

  3. Step 3
    Decrypt the flag
    Observation
    I noticed that flag.txt.enc was the encrypted output and the script contained the exact decryption routine keyed to the recovered password happychance, which meant running bloat.flag.py and supplying that credential was the only path to producing a readable picoCTF{...} flag.
    Run the script with the recovered password, then verify the output starts with picoCTF{ before submitting.
    python
    python3 bloat.flag.py | tee output.txt
    bash
    # enter: happychance
    bash
    sed -n '2p' output.txt
    bash
    grep -oE 'picoCTF\{[^}]+\}' output.txt
    What didn't work first

    Tried: Try to decrypt flag.txt.enc directly with openssl enc -d or gpg --decrypt without running the Python script.

    The script uses its own custom XOR or symmetric cipher logic, not a standard OpenSSL cipher suite or GPG encryption format. Running openssl enc -d -aes-256-cbc -in flag.txt.enc produces an error about a bad magic number or unrecognized header because the file format does not match any standard envelope. You must run the Python script, which contains the exact decryption routine used to produce flag.txt.enc.

    Tried: Press Enter (empty password) or type a guess like password when the script prompts, hoping to bypass the check.

    The script compares your input against the hard-coded obfuscated credential before calling the decryption function. A wrong password either produces an explicit failure message or decrypts to binary garbage - neither gives a valid flag. The only path forward is recovering the credential happychance by evaluating the index expression from the source.

    Learn more

    Decryption is success when the output is well-formed: starts with picoCTF{, ends with }, only printable ASCII inside. If the second line is binary garbage, the password is wrong or the script's decryption logic differs from what you reconstructed. The grep -oE regex is a robust alternative to sed -n '2p' when the output ordering shifts.

    tee writes stdin to both stdout and a file - useful when you want to see output live while also keeping a log. Combined with line-selection tools (sed, awk, grep), it beats copy-paste for any script that produces more than a couple of lines.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.

Flag

Reveal flag

picoCTF{d30bfu5c4710n_f7w_b80...}

Never run opaque scripts blindly-printing the decoded payload first keeps you safe and shows the password immediately.

Key takeaway

Code obfuscation replaces readable identifiers and string literals with encoded or indexed equivalents, but it cannot change the program's runtime behavior. Any transformation that the interpreter can reverse at runtime, a human can reverse statically by following the same steps in a REPL. The same character-array and eval-chain patterns appear in real-world malware loaders, JavaScript droppers, and packed shellcode stubs, so learning to mechanically undo them is a core skill in both CTF reversing and malware analysis.

Related reading

Want more picoCTF 2022 writeups?

Useful tools for Reverse Engineering

Do these first

What to try next