---
# System prepended metadata

title: Pragyan CTF 2025 - TinyBunnie

---

# 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!}`