Tap into Hash

Published: April 2, 2025

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.

wget https://challenge-files.picoctf.net/c_verbal_sleep/ba64ef56074be4d9f1b047eb451185d84de7c490e264b9ea6e645bd9b0956c01/block_chain.py
wget https://challenge-files.picoctf.net/c_verbal_sleep/ba64ef56074be4d9f1b047eb451185d84de7c490e264b9ea6e645bd9b0956c01/enc_flag

Solution

  1. Step 1Reimplement xor_bytes
    Copy the helper function from the source: it simply XORs each ciphertext byte with the corresponding `key_hash` byte. 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_bytes helper operates byte-by-byte using Python's zip() 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, zip silently truncates - a potential bug in production code that should use itertools.cycle to 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.

  2. Step 2Derive the 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.

  3. Step 3Loop through the blocks
    Iterate over the ciphertext in 16-byte chunks, XOR each block with `key_hash`, and append the result. Printing the combined buffer reveals the flag embedded among hex strings; grep it for convenience.
    python3 - <<'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()
    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'))
    PY
    python3 solve.py | grep -o 'picoCTF{...}' --color=none
    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 notation data[i:i+block_size] is the idiomatic way to split data into blocks. The last chunk may be shorter than block_size if the data length isn't a multiple of the block size - this is handled automatically by zip'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_flag file being a flat binary rather than an encoded format means Path.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, which is necessary because the padding at the end of the last block may produce garbage bytes after decryption. The subsequent grep -o 'picoCTF{.*}' step extracts just the flag text from the mixed output. In a production script, you'd use Python's re.search to do the same extraction programmatically.

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

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.

Want more picoCTF 2025 writeups?

Useful tools for Reverse Engineering

Related reading

What to try next