unpackme.py picoCTF 2022 Solution

Published: July 20, 2023

Description

The provided Python script base64-decodes an embedded payload and execs it. Intercept the decoded string to review the logic and recover the flag/password without blindly executing unknown code.

Download unpackme.flag.py and open it in your editor.

Before the exec(plain.decode()) line, insert a print(plain.decode()) (or store the decoded string) to view what will execute.

Wrap exec in try/except so a malformed payload doesn't kill the process silently. If the decode path itself fails, fall back to manual base64 + XOR with the embedded key.

Run the script locally to print the hidden password and flag.

bash
wget https://artifacts.picoctf.net/c/48/unpackme.flag.py
bash
less unpackme.flag.py
python
python3 unpackme.flag.py
python
python3 - <<'PY'
# Defensive variant: print the payload, fall back to manual decode on error.
import base64
src = open('unpackme.flag.py').read()
try:
    g = {'__name__': '__main__'}
    exec(src, g)  # original loader runs print(); replace exec(plain) with print(plain) inside the file first
except Exception as e:
    print('Loader failed:', e)
    # Manual fallback: extract the base64 blob and XOR key from the source by eye, then:
    # blob = base64.b64decode(B64_BLOB)
    # plain = bytes(b ^ KEY[i % len(KEY)] for i, b in enumerate(blob))
    # print(plain.decode())
PY
  1. Step 1Inspect the decoder
    The script reads a base64 blob, XORs it, and finally calls exec on the decrypted source. Printing plain.decode() reveals the cleartext code.
    Learn more

    Self-modifying or self-decoding scripts use a two-stage approach: the outer script contains an encoded payload and decryption logic; when run, it decodes the payload and executes it dynamically. Python's exec() function runs a string as Python code, making this pattern possible in a single file.

    This technique is widely used in malware obfuscation: malicious scripts are encoded (Base64, XOR, zlib, etc.) to evade signature-based antivirus detection. The outer loader decodes and executes the real payload in memory. Multiple layers of encoding are common - each layer decodes the next. Security analysts "unpack" each layer by intercepting the decoded payload before execution, exactly as this challenge demonstrates.

    The safe approach is to replace exec(payload) with print(payload) - this reveals what the script would execute without actually running it. In a sandboxed environment, you can run the original safely, but in real incident response, you always analyze before executing unknown code.

    When PyCDC or uncompyle6 actually matter. They are only needed if the challenge ships compiled bytecode (a .pyc file or a frozen executable like PyInstaller output) without the matching source. This particular challenge ships plain Python source with a base64 + XOR string inside, so neither tool is required: the "decompile" step is just printing the decoded string. Save PyCDC/uncompyle6 for cases where you only have .pyc bytecode, and use Python's built-in dis module if you want to disassemble code objects you produced with compile().

  2. Step 2Recover the credentials
    Executing the modified file prints a message containing both the password (batteryhorse) and flag. Revert to the original script if desired and supply the password to reproduce the flag output.
    Learn more

    The XOR operation used in the decoding step is a symmetric operation: A XOR B XOR B = A. This makes XOR useful for simple obfuscation - XOR each byte of the payload with a key byte to "encrypt," then XOR again with the same key to decrypt. While trivially reversible if the key is embedded in the same script, it's enough to defeat simple string searches.

    Dynamic analysis - running code in a controlled environment and observing behavior - often reveals secrets that static analysis misses. For obfuscated malware, analysts run samples in sandboxes (like Any.run, Cuckoo Sandbox, or CAPE Sandbox) and capture: network connections, file system changes, registry modifications, and spawned processes. Replacing exec with print is the manual equivalent.

    Python's compile() and dis.dis() provide another approach: compile the string to a code object and disassemble it to bytecode, which is readable without execution. This is safer for analyzing potentially dangerous payloads and is the foundation of Python reverse engineering tools.

Flag

picoCTF{175_chr157m45_5274...}

Always inspect self-modifying scripts before running them; a simple print statement exposes the payload safely.

Want more picoCTF 2022 writeups?

Useful tools for Reverse Engineering

Related reading

Do these first

What to try next