Description
The provided blockchain script hashes the key with SHA-256 and XORs every 16-byte block with that digest. With the key and ciphertext in hand, decrypting is as simple as repeating the XOR.
Download both block_chain.py and enc_flag from the challenge files.
Read the script to see how xor_bytes and the block loop work; the same routine can be run in reverse.
The key is not in block_chain.py's source; block_chain.py generates a random key at runtime and prints it. Open enc_flag in a text editor (or run head -1 enc_flag): the first line reads Key: b'\x...'. Copy that byte string verbatim into your solve script.
Confirm enc_flag is a flat binary blob: file enc_flag should print something like data (not ASCII text). If file reports it as text, the bytes are hex-encoded and you'll need bytes.fromhex(Path('enc_flag').read_text().strip()) instead of read_bytes().
Confirm enc_flag length is a multiple of 16 with wc -c enc_flag so block alignment is fine.
wget https://challenge-files.picoctf.net/c_verbal_sleep/ba64ef56074be4d9f1b047eb451185d84de7c490e264b9ea6e645bd9b0956c01/block_chain.pywget https://challenge-files.picoctf.net/c_verbal_sleep/ba64ef56074be4d9f1b047eb451185d84de7c490e264b9ea6e645bd9b0956c01/enc_flagfile enc_flagwc -c enc_flagSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Reimplement xor_bytes
ObservationThe source includes an xor_bytes helper that XORs paired bytes from two sequences. XOR is its own inverse, so that same function decrypts without a single change.Copy the helper function from the source: it simply XORs each ciphertext byte with the correspondingkey_hashbyte. You'll use the same function to decrypt.Learn more
XOR (exclusive OR) is the foundation of most stream cipher constructions. Its critical property is self-inverse:
A XOR B XOR B = A. This means the exact same operation encrypts and decrypts. XOR the plaintext with the keystream to get ciphertext; XOR the ciphertext with the same keystream to recover the plaintext. No separate "decrypt" function is needed.The
xor_byteshelper operates byte-by-byte using Python'szip()to pair corresponding bytes from two sequences. This is a clean, Pythonic pattern for byte-level operations. The equivalent in a single line is:bytes(a ^ b for a, b in zip(ciphertext, key)). When the keystream is shorter than the data,zipsilently truncates, a potential bug in production code that should useitertools.cycleto repeat the key if needed.The challenge name "tap into hash" hints at using a hash function as a key derivation step. This is conceptually similar to how HMAC-based key derivation (HKDF) works in real protocols; a hash function expands a master secret into keying material. Here, SHA-256 of the key produces exactly 32 bytes of uniform-looking output, which is then used as the XOR mask repeated across blocks. See stream ciphers in CTFs for why this construction is broken.
Step 2Derive the keystream
ObservationThe script runs the key through SHA-256 before using it as the mask, and the first line of enc_flag holds the raw key bytes. Hash those and you have the exact keystream.Hash the provided key with SHA-256 (key_hash = hashlib.sha256(key).digest()) so you have the exact keystream the challenge used.Learn more
Using a hash of the key rather than the raw key has two purposes: first, it produces a fixed-length output (32 bytes for SHA-256) regardless of key length, making the XOR mask size predictable. Second, it applies a one-way transformation; someone who sees the keystream cannot directly recover the key. This is a primitive form of key derivation.
However, this construction is not a secure cipher. The fundamental flaw is keystream reuse: every 16-byte block is XORed with the same 32-byte key hash (reusing the first 16 bytes for each block). If an attacker knows any 16 bytes of plaintext, they can recover the keystream for that position and decrypt any other block at the same offset. This is the same weakness that broke the Lorenz cipher in WWII and the RC4 stream cipher in WPA-TKIP.
Secure stream cipher design uses a unique keystream segment for each block, typically by combining the key with a nonce (number used once) that changes per message. Modern authenticated encryption like AES-GCM or ChaCha20-Poly1305 handles both encryption and integrity verification, preventing both decryption without the key and undetected message tampering. The AES for CTF guide covers GCM in depth.
Step 3Loop through the blocks
ObservationEncryption splits the file into 16-byte chunks and XORs each against the key hash independently. Run the same loop with the recovered keystream and append the results.Iterate over the ciphertext in 16-byte chunks, XOR each block withkey_hash, and append the result. Printing the combined buffer reveals the flag embedded among hex strings; grep it for convenience.pythonpython3 - <<'PY' import hashlib from pathlib import Path def xor_bytes(a, b): return bytes(x ^ y for x, y in zip(a, b)) encrypted = Path('enc_flag').read_bytes() # Paste the exact key bytes from the FIRST LINE of enc_flag (the 'Key: b"..."' line) key = b"\x8b\x9a\x00G\xfe\xb3\xf3\x93\xdb\xa8yT\xfe\x15\x87a\xf4\xdf\x00\x8d\xee\xab\xd9\t^|\x04(%\x81\x9e\xf8" block_size = 16 key_hash = hashlib.sha256(key).digest() flag = b'' for i in range(0, len(encrypted), block_size): block = encrypted[i:i+block_size] flag += xor_bytes(block, key_hash) print(flag.decode(errors='ignore')) PYbash# Or save the snippet to solve.py and pipe through grep: # python3 solve.py | grep -oE 'picoCTF\{[^}]+\}'Expected output
picoCTF{block_3SRhViRbT1qcX_XUjM0r49cH_qCzmJZzBK_6064...}What didn't work first
Tried: XOR each ciphertext block directly against the raw key bytes instead of the SHA-256 digest of the key.
The output is garbage: no readable ASCII, no flag prefix. The script hashes the key before using it as the mask, so the keystream is the 32-byte digest and not the key. Skip the hash and every block gets the wrong bytes, with no decode error to tell you why.
Tried: Load enc_flag with Path('enc_flag').read_text() and call bytes.fromhex() on it, treating the file as a hex-encoded ciphertext.
bytes.fromhex raises a value error, because enc_flag is a raw binary blob rather than a hex string; the setup step already showed file reporting it as data. read_bytes loads it directly. The fromhex path only applies to files that really are hex-encoded.
Learn more
Iterating in fixed-size chunks is a fundamental pattern in block-oriented cryptography. Python's
range(0, len(data), block_size)with slice notationdata[i:i+block_size]is the idiomatic way to split data into blocks. The last chunk may be shorter thanblock_sizeif the data length isn't a multiple of the block size; this is handled automatically byzip's truncation behavior, though production code should handle padding explicitly.The name "block_chain.py" is a pun; it chains together the XOR blocks but has nothing to do with blockchain technology (distributed ledger systems). This kind of thematic naming is common in CTF challenges to add flavor without complicating the actual puzzle. The
enc_flagfile being a flat binary rather than an encoded format meansPath.read_bytes()is the correct way to load it; no base64 or hex decoding needed before the XOR step.The
decode(errors='ignore')argument tells Python to silently skip bytes that aren't valid UTF-8. You need it here because the encryption padded the flag's last block with arbitrary bytes to reach a 16-byte boundary; decrypting that block yields a few non-UTF-8 trailers after the closing}. Withouterrors='ignore',.decode()would raiseUnicodeDecodeErroron those trailers and you'd never see the readable flag at all. The subsequentgrep -oE 'picoCTF\{[^}]+\}'step extracts just the flag text from the mixed output. In a production script, you'd use Python'sre.searchto do the same extraction programmatically.
Interactive tools
- XOR CipherXOR-decrypt hex or text ciphertext with a known key, or brute-force the single-byte key automatically.
- Cipher Identifier & Auto-DecoderPaste any ciphertext and the tool auto-runs every common decoder (base64, hex, Morse, ROT, Atbash, Bacon, binary, decimal, URL) and ranks the results by English-likeness.
- Frequency AnalysisAnalyze letter frequencies in a substitution cipher and interactively build the decryption mapping with auto-filled guesses.
Alternate Solution
After deriving the SHA-256 key hash, use the XOR Cipher tool on this site to XOR each 16-byte block of the ciphertext against the key hash manually - paste the hex bytes, enter the hash as the key, and the plaintext appears without writing any Python.
Flag
Reveal flag
picoCTF{block_3SRhViRbT1qcX_XUjM0r49cH_qCzmJZzBK_6064...}
Padding bytes leave stray characters, so piping the output through `grep -o 'picoCTF{...}'` is the quickest way to isolate the flag.