Hash Based Cryptography - author: overllama - crypto

Here’s our solution script.

from pwn import remote


def split_into_blocks(data: bytes, block_size: int = 16) -> list[bytes]:
    """
    Split data into blocks of block_size bytes.
    """
    return [data[i : i + block_size] for i in range(0, len(data), block_size)]


def crack_block(conn, ciphertext_block: bytes, block_size: int, iteration: int) -> bytes:
    """
    Perform a byte-by-byte padding oracle attack on a single block.
    Returns the recovered intermediate (zero IV) bytes.
    """
    recovered = bytearray()

    # Work from last byte to first
    for pad_len in range(1, len(ciphertext_block) + 1):
        for guess in range(256):
            # Build forged IV:
            # - Leading zeros for previous blocks
            # - Zeros to align current pad
            # - Guess byte
            # - Known intermediate bytes XORed with pad length
            prefix = b"\x00" * (block_size * iteration)
            padding = b"\x00" * (block_size - pad_len)
            guess_byte = bytes([guess])
            suffix = bytes((b ^ pad_len) for b in recovered)

            forged_iv = prefix + padding + guess_byte + suffix

            # Send payload as hex string
            conn.recvuntil(b"> ")
            conn.sendline(forged_iv.hex().encode())
            response = conn.recvline()

            # Check for successful padding
            if b"Padding error" not in response:
                # Recover this intermediate byte
                recovered.insert(0, guess ^ pad_len)
                break

    return bytes(recovered)


def break_repeating(conn, ciphertext_blocks: list[bytes], block_size: int) -> bytes:
    """
    Orchestrate the attack over multiple ciphertext blocks (here simplified to first block).
    """
    # Wait for prompt and send a dummy to sync
    conn.recvuntil(b" > ")
    conn.sendline(b"00" * block_size)
    conn.recvline()

    # Attack only the first block in this challenge
    return crack_block(conn, ciphertext_blocks[0], block_size, iteration=1)


def main():
    host = 'hash.chal.cyberjousting.com'
    port = 1351

    conn = remote(host, port)
    # Read initial banner and ciphertext
    conn.recvlines(2)
    hex_ct = conn.recvline().strip().decode()
    ciphertext = bytes.fromhex(hex_ct)

    # Split into blocks if needed; here block_size is 20
    block_size = 20
    ct_blocks = split_into_blocks(ciphertext, block_size)

    # Recover zero IV for the first block
    zero_iv = break_repeating(conn, ct_blocks, block_size)

    # Append known prefix to form the full zero IV
    known_prefix = bytes.fromhex('541b5e3a73645cd0f64f07a5c96c0c7a1887fdc')
    full_zero_iv = known_prefix + zero_iv

    # Compute plaintext by XORing zero IV with ciphertext prefix
    plaintext = bytes(z ^ c for z, c in zip(full_zero_iv, ciphertext[: len(full_zero_iv)]))

    print(f"Zero IV: {full_zero_iv!r}")
    print(plaintext)
    conn.close()


if __name__ == "__main__":
    main()