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, the XOR key, 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
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Rebuild the shared key
ObservationThe test() routine in custom_encryption.py prints a and b outright, so the private Diffie-Hellman exponents are handed to you. Rebuild the shared key with generator(g, x, p) and skip the discrete log entirely.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 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()
Observationencrypt() multiplies each character ordinal by key * 311 to build the cipher list. Divide by the same factor to get the intermediate semi_cipher values back.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.bashsemi_cipher = [c // (key * 311) for c in cipher]What didn't work first
Tried: Use regular division (/) instead of integer division (//) when inverting the multiplication
Python's / returns a float, so each entry lands as 112.0 rather than 112, and chr() then refuses it with a TypeError. Use // for floor division and you get an int.
Tried: Guess that the magic constant 311 is a red herring and try dividing by just the key alone
Leave out the 311 and every value comes back 311 times too large, giving ordinals far above 127 that either overflow chr() or decode to garbage Unicode. The constant is hard-coded on the multiplication line in encrypt(), so it belongs in the divisor too.
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_encrypt
Observationdynamic_xor_encrypt() reverses the plaintext first, then XORs each character against the repeating key 'trudeau'. So XOR semi_cipher the same way, forward through the key, and reverse the result at the end.Write a dynamic_xor_decrypt that applies the same forward-cycling repeating-key XOR (using text_key[i % key_length]) to the semi_cipher string, then reverses the result with [::-1]. The key walk direction is unchanged from encrypt; the only difference is where the reversal happens. Applying it to semi_cipher with the key "trudeau" reveals the flag.pythonpython3 solver.py # uses decrypt + dynamic_xor_decryptExpected output
picoCTF{custom_d2cr0pt6d_751a...}What didn't work first
Tried: Skip reversing the string at the end and just return the XOR result directly
encrypt() reverses the plaintext before XORing, so the XOR ran against the reversed string. Forget to reverse afterwards and the flag comes out backwards, starting with } instead of picoCTF{. The reversal goes after the XOR, undoing the one that happened before it.
Tried: Try the XOR key in reverse (walking text_key backward) instead of cycling forward through it
encrypt() cycles the key forward with i % key_length no matter what the plaintext reversal did. XOR is its own inverse, so walk the key forward here too. Walk it backwards and every position gets the wrong key byte.
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, thenplain = cipher XOR key.The script's
text_keyvariable holds the repeating key bytes, derived from the literal string"trudeau". The encrypt function reverses the plaintext string first (plaintext[::-1]) and then XORs each character withtext_key[i % key_length], cycling forward. Decryption applies the same forward-cycling key XOR to the intermediate string, then reverses the result with[::-1]. There is no chained state; each position XORs independently withtext_key[i % key_length].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).
Interactive tools
- XOR CipherXOR-decrypt hex or text ciphertext with a known key, or brute-force the single-byte key automatically.
Flag
Reveal 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.
Key takeaway
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.