Description
I have a friend that enjoys coding and he hasn't stopped talking about a snake recently He left this file on my computer and dares me to uncover a secret phrase from it. Can you assist?
Setup
Download the bytecode file snake and examine it with tools like uncompyle6 or Python's dis module.
wget https://artifacts.picoctf.net/c_titan/31/snake && \
python3 your_decrypt.pySolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Recover the key
ObservationThe download has no extension, and the challenge name points at Python. This is compiled bytecode, so unmarshal the code object and disassemble it with the dis module, then watch the LOAD_CONST sequence that builds the key character by character.A.pycfile is a small header followed by a marshalled code object, so read past the header before unmarshalling. Disassemble the result and scan for a sequence ofLOAD_CONSTops that push single characters onto the stack, ending inBUILD_STRING. That sequence reconstructskey_str(in this challenge it builds out tot_Jo3). Convert it to a list of integers for the XOR step:pythonpython3 -c "import dis, marshal; f = open('snake','rb'); f.read(16); dis.dis(marshal.load(f))" | lessbashkey_list = [ord(c) for c in 't_Jo3']What didn't work first
Tried: Run uncompyle6 directly on the snake file expecting clean source output with the key visible as a string literal.
uncompyle6 often fails on bytecode from versions it does not fully support, reporting an internal error or dropping exactly the constant-building section. dis works on the raw opcodes without trying to infer high-level structure, so the LOAD_CONST sequence stays visible whatever the Python version.
Tried: Use the strings utility on the snake file to look for the key printed as a plain ASCII sequence.
The key is built one character at a time by separate LOAD_CONST opcodes, so it never exists as a contiguous string on disk. strings only finds printable runs above a minimum length, and single characters scattered through bytecode never qualify. The disassembly is what shows them being assembled.
Learn more
Python source files (
.py) are compiled to bytecode (.pyc) by the CPython interpreter before execution. Bytecode is a lower-level, platform-independent instruction set for the Python Virtual Machine: not machine code, but not source code either. It sits in the middle: harder to read than Python, but much easier to recover than compiled C or C++.Tools like uncompyle6, decompyle3, or pycdc can reconstruct Python source from bytecode with high fidelity. Python's built-in
dismodule disassembles bytecode into human-readable opcode mnemonics; useful when decompilers fail on unusual or obfuscated bytecode.python3 -m dis snakedoes not work here: the module compiles its argument as source text, so a bytecode file raises a SyntaxError. Skip the 16-byte header and hand the marshalled code object todis.disinstead, as in the command above.uncompyle6 snakeattempts full source reconstruction (install withpip install uncompyle6).- Strings built character-by-character show up as repeated
LOAD_CONST 't',LOAD_CONST '_', ... followed by aBUILD_STRING Nop that joins them.
Step 2Recreate the input list
ObservationThe disassembly shows a BUILD_LIST preceded by a long run of LOAD_CONST instructions pushing integers. Those integers are the ciphertext the program XORs against the key.The bytecode stores the ciphertext integers in a list (input_list). Copy those numbers into your script.Learn more
In Python bytecode, list literals are stored as a series of
LOAD_CONSTinstructions (one per element) followed by aBUILD_LISTopcode. The disassembly makes these values directly visible - no decryption needed to extract the ciphertext list. This is a fundamental asymmetry in obfuscation via compilation: the data the program operates on must be present at runtime, so it is always recoverable from the bytecode.ord(c)converts a character to its ASCII integer value (e.g.,ord('A') == 65). The key is stored as a string but used as a list of integers during XOR operations, so converting it with[ord(c) for c in key_str]produces the correct numeric representation.Recognizing that a program stores integers in a list and XORs them against a key is a pattern found constantly in CTF reversing challenges and in real malware - it is one of the simplest ways to obfuscate strings like API keys, C2 addresses, or flags without using a proper cipher.
Step 3XOR decrypt
ObservationA BINARY_XOR joins elements of the two lists, so this is repeating-key XOR. Apply the same XOR with itertools.cycle and the plaintext comes back.XOR each ciphertext byte with the cycling key. Watch out:zip(a, b)stops at the shorter sequence, so if you pass the rawkey_listand it is shorter thaninput_listyou silently lose the tail of the message. Useitertools.cycleto repeat the key. The Python for CTF guide covers more idioms like this.pythonfrom itertools import cycle result = ''.join(chr(a ^ b) for a, b in zip(input_list, cycle(key_list))) print(result)Expected output
picoCTF{N0t_sO_coNfus1ng_sn@ke_30a...}What didn't work first
Tried: Use zip(input_list, key_list) without itertools.cycle, then wonder why the decoded output is truncated and missing the end of the flag.
zip stops at the shorter sequence, which is the five-character key. Only the first five bytes get decrypted and the rest is silently dropped. itertools.cycle turns the key into an endless repeating stream so zip runs the full length of the ciphertext.
Tried: XOR the ciphertext integers directly against the key string characters without calling ord(), getting a TypeError and assuming the key extraction step was done incorrectly.
Python's ^ works on integers, not characters, so convert the key string to code points first. A TypeError about int and str means that conversion was skipped, not that the key is wrong.
Learn more
XOR (exclusive OR) is the simplest symmetric cipher. XOR has a beautiful property: applying the same key twice returns the original value (
m XOR k XOR k = m). This makes XOR decryption identical to encryption: just XOR the ciphertext with the same key and you recover the plaintext.When the key is shorter than the message, it is typically repeated cyclically; this is called a repeating-key XOR or Vigenere-like XOR.
zipin Python pairs elements from two sequences and stops at the shorter one, which is the gotcha called out above.itertools.cycle(key_list)creates an infinitely repeating keystream so the zip stops at the ciphertext length instead.Repeating-key XOR was used in real cryptography before modern ciphers (the Vigenere cipher is a letter-based variant). It is broken by known-plaintext attacks (if you know any bytes of the plaintext, you immediately recover the corresponding key bytes) and by frequency analysis when the key is short relative to the message. AES in CTR mode is essentially XOR with a secure, non-repeating keystream; the security comes entirely from the quality of the keystream, not the XOR operation itself.
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.
Flag
Reveal flag
picoCTF{N0t_sO_coNfus1ng_sn@ke_30a...}
The decrypted string from the XOR routine is the final flag.