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.
Setup
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.
wget https://artifacts.picoctf.net/c_titan/18/enc_flag && \
wget https://artifacts.picoctf.net/c_titan/18/custom_encryption.pySolution
Walk me through it- Step 1Rebuild the shared keycustom_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 computesg^x mod p. When two parties computeg^a mod pandg^b mod pand swap results, each raises the received value to their own private exponent and lands on the same shared secretg^(ab) mod p.In this challenge
a,b,g, andpare all printed by the script (search forprint('a:', a)and the matching b line), so the "key exchange" is trivially reversible. Real DH keepsaandbprivate; onlyg^a mod pandg^b mod pever 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.
- 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 thechr()call downstream because character codes have to be integers.The value
311is 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. - Step 3Reverse dynamic_xor_encryptCreate 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_decryptLearn 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, thenplain = cipher XOR key.The script's
text_keyvariable holds the repeating key bytes, derived from the literal string"trudeau". The encrypt function walkstext_keyforward 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
Python for CTF
The solver lives or dies by knowing where to put // instead of / and how ord/chr behave. The Python guide covers the pieces this challenge leans on.
AES for CTF
For context on what real symmetric crypto looks like (and why this homemade scheme falls apart), the AES guide walks through ECB/CBC/GCM and the bugs that show up in CTF AES challenges.
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
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.