# Pragyan CTF 2025 - TinyBunnie **Title:** TinyBunnie **Description:** I have been working on my very own blockchain , seems like i got it working. Took a lot of inspiration from microsoft as well , just to make sure things dont go wrong :) **Files:** [tiny.py](https://ctf.prgy.in/files/b4b7571c719ddc5cb2f0830945e5f392/tiny.py?token=eyJ1c2VyX2lkIjo1MDksInRlYW1faWQiOjMwNiwiZmlsZV9pZCI6MzJ9.Z6hNRQ.Zrm7I2aWvC8f7h8i0q73F9938NM), [custom_block.py](https://ctf.prgy.in/files/668c83b8ee79cf19eb1d8f3fa72c4ab9/custom_block.py?token=eyJ1c2VyX2lkIjo1MDksInRlYW1faWQiOjMwNiwiZmlsZV9pZCI6MzN9.Z6hNRQ.u112-CzgUGPdfj6O40mcLBwl0lY) ## Solution: The challenge centers on a modified version of the [TEA](https://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm) cipher. Here’s what we found upon examining the source: 1. **TEA’s Key Structure and Rounds:** The cipher in `tiny.py` splits a 16‑byte key into four 32‑bit words and runs 32 rounds of operations. Each round involves additions modulo 2³², bit shifts, and XOR operations. 2. **The Equivalent-Keys Property:** A critical observation is that addition modulo 2³² and XOR interact in a way that, for any 32‑bit word `x`, the following holds: ``` x + 0x80000000 (mod 2^32) == x XOR 0x80000000 ``` Thus, if you have a key: ``` K = (K₀, K₁, K₂, K₃) ``` you can derive an equivalent key by XORing each 32‑bit word with `0x80000000`: ``` K' = (K₀ ⊕ 0x80000000, K₁ ⊕ 0x80000000, K₂ ⊕ 0x80000000, K₃ ⊕ 0x80000000) ``` Although `K` and `K'` are different as byte strings, the encryption rounds process them identically, yielding the same ciphertext. This is exactly what the server checks: if encrypting a given block with PoW₁ and PoW₂ produces identical outputs, the block is “validated.” 3. **Avoiding Reuse:** The server also rejects a key pair if it has already been used (in either order). To overcome this, we must generate a fresh equivalent key pair for every block. One easy method is to use a counter as a seed, converting it to a unique 16‑byte PoW₁ and then deriving its equivalent PoW₂. The following script automates this process. It connects to the remote server, reads the block challenge, and for each block generates a new unique PoW pair using the equivalent-key trick: ```python from pwn import remote def compute_equiv_pair(seed): pow1 = seed.to_bytes(16, 'big') pow2 = b''.join((int.from_bytes(pow1[i:i+4], 'big') ^ 0x80000000).to_bytes(4, 'big') for i in range(0, 16, 4)) return pow1.hex(), pow2.hex() def main(): r = remote("block.ctf.prgy.in", 1337, ssl=True) seed = 0 while True: try: if not (line := r.recvline(timeout=5)): break line = line.decode().strip() print(line) if line.startswith("Validate this block:"): pow1, pow2 = compute_equiv_pair(seed) seed += 1 print(f"Sending PoW1: {pow1}\nSending PoW2: {pow2}") r.sendline(pow1) r.sendline(pow2) except Exception as e: print(f"Error or EOF: {e}") break r.close() if __name__ == "__main__": main() ``` ### How It Works: - **Generating PoW₁:** The function `compute_equiv_pair(seed)` converts the given `seed` (an integer) into a 16‑byte big‑endian value. This serves as PoW₁. - **Deriving PoW₂:** The same function processes PoW₁ in 4‑byte chunks. Each chunk is XORed with `0x80000000` to produce the equivalent 32‑bit word, and the results are concatenated to form PoW₂. Despite being different from PoW₁, this key is equivalent under the TEA encryption used. - **Sending Unique Pairs:** A counter (`seed`) is incremented for every block so that the key pair is never reused. The script waits for the "Validate this block:" prompt, then sends the newly generated PoW pair. - **Validation and Flag Retrieval:** The server accepts the block when both keys produce identical ciphertexts. Once all blocks are validated, the flag is eventually printed. By exploiting TEA’s equivalent-key property—where XORing each key word with `0x80000000` yields a different key that encrypts identically—we bypass the server’s requirement. The script automates generating unique and valid PoW pairs for each block, leading us to retrieve the flag: ``` $ python3 solve.py [+] Opening connection to block.ctf.prgy.in on port 1337: Done **************************************************************** ___________.__ _________ .__ .__ \__ ___/|__| ____ ___.__.\_ ___ \| |__ _____ |__| ____ | | | |/ < | |/ \ \/| | \\__ \ | |/ \ | | | | | \___ |\ \___| Y \/ __ \| | | \ |____| |__|___| / ____| \______ /___| (____ /__|___| / \/\/ \/ \/ \/ \/ **************************************************************** Validate this block: c8fdee0e9a5b943cf3c9349074b3daa1e6edc40c4420194878914fa06d3ac668 Sending PoW1: 00000000000000000000000000000000 Sending PoW2: 80000000800000008000000080000000 /mnt/e/CTF/pragyanctf/tiny/solve.py:23: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes r.sendline(pow1) /mnt/e/CTF/pragyanctf/tiny/solve.py:24: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes r.sendline(pow2) Enter first ProofOfWork :Enter second ProofOfWork :Good job validating the block! Validate this block: fb7d8c559e45cc6afb386e67709712ca44bd0245474b10ad7f5e71ba086e2e3e Sending PoW1: 00000000000000000000000000000001 Sending PoW2: 80000000800000008000000080000001 Enter first ProofOfWork :Enter second ProofOfWork :Good job validating the block! Validate this block: 5b92f3c0d8ab00008455805f02cbeb6ba492f6adeb294ab06cd916ac9062301d Sending PoW1: 00000000000000000000000000000002 Sending PoW2: 80000000800000008000000080000002 Enter first ProofOfWork :Enter second ProofOfWork :Good job validating the block! Validate this block: 9e962d3c42acde05ac7969759294cd66fe52ed63da9e4614a07a27b5522bb919 Sending PoW1: 00000000000000000000000000000003 Sending PoW2: 80000000800000008000000080000003 Enter first ProofOfWork :Enter second ProofOfWork :Good job validating the block! Validate this block: e32bedda93141e19cf30e96f38323fc78e719da0b6d7fd5017cc7b5c3725a674 Sending PoW1: 00000000000000000000000000000004 Sending PoW2: 80000000800000008000000080000004 Enter first ProofOfWork :Enter second ProofOfWork :Good job validating the block! Validate this block: 86d3af23ae717180000f57e55865e9e4dfeed2e629ae1c80b91f0785da6d5f61 Sending PoW1: 00000000000000000000000000000005 Sending PoW2: 80000000800000008000000080000005 Enter first ProofOfWork :Enter second ProofOfWork :Good job validating the block! Wait Noooooooooo: p_ctf{0Hh_No00_I5_T(-)i$_value_Ov3rFl0W!} Error or EOF: [*] Closed connection to block.ctf.prgy.in port 1337 ``` Which gives us our flag: `p_ctf{0Hh_No00_I5_T(-)i$_value_Ov3rFl0W!}`