Custom encryption picoCTF 2024 Solution

Published: April 3, 2024

Description

Can you get sense of this code file and write the function that will decode the given encrypted file content. Find the encrypted file here flag_info and code file might be good to analyze and get the flag.

Local script

Download enc_flag and custom_encryption.py locally.

Inspect the script to understand the generator parameters, XOR key ("trudeau"), and how the cipher list was produced.

bash
wget https://artifacts.picoctf.net/c_titan/18/enc_flag && \
wget https://artifacts.picoctf.net/c_titan/18/custom_encryption.py
This custom cryptography challenge involves reversing Diffie-Hellman and XOR operations. For another custom cipher challenge, check out C3, which uses a cyclical differential cipher.
  1. Step 1Rebuild the shared key
    custom_encryption.py prints a and b during test() (look for the print('a:', a) and print('b:', b) calls). Plug them into generator(g, x, p) to recover the same shared key that encrypt() used.
    Learn more

    Diffie-Hellman key exchange lets two parties agree on a shared secret over an insecure channel. The generator(g, x, p) function computes g^x mod p. When two parties compute g^a mod p and g^b mod p and swap results, each raises the received value to their own private exponent and lands on the same shared secret g^(ab) mod p.

    In this challenge a, b, g, and p are all printed by the script (search for print('a:', a) and the matching b line), so the "key exchange" is trivially reversible. Real DH keeps a and b private; only g^a mod p and g^b mod p ever go on the wire. Its security rests on the discrete logarithm problem.

    Modern DH shows up in TLS handshakes, the Signal protocol, and WireGuard. The elliptic-curve variant (ECDH) gives equivalent security with much smaller keys and is the default in modern systems.

  2. Step 2Invert encrypt()
    Write a decrypt() that integer-divides each cipher entry by key * 311 with //. This yields the "semi_cipher" string prior to the dynamic XOR stage.
    bash
    semi_cipher = [c // (key * 311) for c in cipher]
    Learn more

    The encryption multiplies each character's ordinal by key * 311. Decryption divides. Use Python's integer division operator // here, not /: regular division returns a float, which then breaks the chr() call downstream because character codes have to be integers.

    The value 311 is hard-coded in the encryption function. Custom ciphers often add "complexity" through multiplication by a magic constant, but this provides zero security once the script is in your hands. Security through obscurity is not a primitive.

    The cipher is layered: DH key exchange, then multiplication, then XOR. Decryption undoes each layer in reverse: derive the key, divide out key * 311, then reverse the XOR. This compositional pattern is exactly how block-cipher modes and AEAD schemes are reasoned about.

  3. Step 3Reverse dynamic_xor_encrypt
    Create dynamic_xor_decrypt that walks text_key in the opposite order from the encrypt path. Applying it to semi_cipher with the key "trudeau" reveals the flag.
    python
    python3 solver.py  # uses decrypt + dynamic_xor_decrypt
    Learn more

    XOR encryption with a repeating key is a simple stream cipher. Each character of plaintext is XORed with the corresponding character of the key, cycling if the key is shorter. XOR is its own inverse: if cipher = plain XOR key, then plain = cipher XOR key.

    The script's text_key variable holds the repeating key bytes, derived from the literal string "trudeau". The encrypt function walks text_key forward and applies a running XOR; "reverse" here means walking the same key bytes in the opposite direction (and using the previous decrypted byte as the running state instead of the previous plaintext byte). Each XOR is its own inverse, so applying the operations in opposite sequence undoes the chain.

    The key "trudeau" is a weak key: a short dictionary word. Real stream ciphers (RC4, ChaCha20) use much longer pseudorandom key streams. A repeating ASCII key is trivially broken by frequency analysis once the key length is guessed (Kasiski examination, index of coincidence).

Alternate Solution

Once you have recovered the semi_cipher and know the repeating XOR key is "trudeau", use the XOR Cipher tool on this site to apply the final XOR step. Paste the semi_cipher bytes and enter the key to reveal the flag without writing any additional Python.

Related guides

Flag

picoCTF{custom_d2cr0pt6d_751a...}

The decrypted semi_cipher plus the reversed XOR routine yields the flag above. If the output doesn't start with picoCTF{, recheck the DH key recovery and confirm you used // (integer division) when undoing key * 311.

How to prevent this

Do not roll your own crypto. The history of broken homemade ciphers is the entire field of cryptanalysis.

  • Use a vetted library: libsodium (cross-language, opinionated), Tink (Google), or RustCrypto. These provide AEAD primitives (XChaCha20-Poly1305, AES-GCM-SIV) that handle key derivation, nonces, and authentication for you.
  • If you absolutely must implement crypto for a constrained environment, get the design reviewed by a real cryptographer and use NIST-approved primitives only. Test against known answer test vectors.
  • Authentication is non-negotiable. XOR + custom transform without a MAC means an attacker can also forge messages, not just decrypt them. AEAD modes solve confidentiality and integrity in one primitive.

Want more picoCTF 2024 writeups?

Tools used in this challenge

Related reading

Do these first

What to try next