---
# System prepended metadata

title: KashiCTF 2025 - Absolutely Encrypted Shenanigans

---

# KashiCTF 2025 - Absolutely Encrypted Shenanigans
**Title:** Absolutely Encrypted Shenanigans

**Description:** Lets see you break this. I even gave you the key

`nc kashictf.iitbhucybersec.in 56958`

**Files:** [AES.py](https://github.com/xtasy94/CTFW/blob/main/CTF_Files/KashiCTF/AES.py) · [server.py](https://github.com/xtasy94/CTFW/blob/main/CTF_Files/KashiCTF/server.py)

## Analysis
### Custom AES Implementation:

**AES Functions:**  
- The AES module provided reimplements standard AES-128 operations like byte substitution (`sub_bytes`), row shifting (`shift_rows`), column mixing (`mix_columns`), and key expansion. While the overall structure adheres to AES principles, the non-standard details (like the modified `shift_rows`) play a role in the overall attack.
### Vulnerability: Faulty IV Generation
- **IV Structure:**  
  The server generated the IV using the expression:
  ```python
  iv = os.urandom(8) * 2
  ```
  This results in a 16-byte IV that consists of 8 random bytes repeated twice.  
- **Known Plaintext Leak:**  
  The flag (or fake flag) always starts with a known prefix (`"KashiCTF{"`). In CBC mode, the first block of ciphertext is computed like:
  ```
  C₁ = encrypt_block(key, IV ⊕ P₁)
  ```
  During decryption, recovering the following:
  ```
  P₁ = decrypt_block(key, C₁) ⊕ IV
  ```
  allows us to deduce the first `8 bytes` of the `IV` by XORing with the prefix we know already (`"KashiCTF{"`). Since the IV is repeated, the full IV becomes known.

Here's a diagram explaining the encryption process:
![Untitled Diagram.drawio (3)](https://hackmd.io/_uploads/rkryDpu5yx.png)

### Exploitation
1. **IV Recovery:**  
   - The exploit script needs to first decrypt the first ciphertext block.
   - It then computes:
     ```python
     candidate_X = xor(D[:8], b"KashiCTF")
     candidate_iv = candidate_X + candidate_X
     ```
     to reconstruct the complete IV.
2. **Validation via Fake Rounds:**  
   - The service encrypts a fake flag for 10 rounds. We use these rounds to verify that the recovered IV correctly reveals the expected plaintext.
3. **Real Flag Decryption:**  
   - Finally, after 10 rounds, the server sends an encryption of the secret flag. Using the same IV recovery method, the script decrypts all ciphertext blocks to obtain the real flag.

Here's a diagram a diagram explaining exploitation - IV Recovery and Flag Decryption:
![Untitled Diagram.drawio (4)](https://hackmd.io/_uploads/rkchLpu91l.png)

 

## Exploit/Solution Script

Below is the complete attack script that automates IV recovery and flag decryption:

```python
#!/usr/bin/env python3
import json
from pwn import remote

# ---------------- Helper Functions ----------------

def xor(b1, b2):
    return bytes(a ^ b for a, b in zip(b1, b2))

def bytes2matrix(text):
    return [list(text[i:i+4]) for i in range(0, len(text), 4)]

def matrix2bytes(matrix):
    return b"".join(bytes(row) for row in matrix)

def unpad(s):
    return s[:-s[-1]]

# ---------------- AES S-box & Inverse ----------------

s_box = (
    0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5,
    0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
    # ... (rest of the S-box values) ...
)
inv_s_box = [0]*256
for i, b in enumerate(s_box):
    inv_s_box[b] = i

def sub_bytes(state):
    return [[s_box[b] for b in row] for row in state]

def inv_sub_bytes(state):
    return [[inv_s_box[b] for b in row] for row in state]

# ---------------- Shift Rows & Inverse ----------------

def shift_rows(state):
    s = state
    s[2][2], s[2][1], s[0][3], s[2][0], s[3][3], s[2][3], s[3][1], s[1][3], \
    s[0][2], s[1][0], s[0][1], s[0][0], s[1][1], s[3][0], s[3][2], s[1][2] = \
    s[2][2], s[3][3], s[0][0], s[1][1], s[2][1], s[1][2], s[3][0], s[2][3], \
    s[0][3], s[0][2], s[3][2], s[0][1], s[3][1], s[1][0], s[2][0], s[1][3]
    return s

def inv_shift_rows(state):
    flat = sum(state, [])
    new_flat = [
         flat[3],  flat[0],  flat[4],  flat[2],
         flat[12], flat[8],  flat[11], flat[6],
         flat[14], flat[15], flat[10], flat[7],
         flat[13], flat[5],  flat[1],  flat[9]
    ]
    return [new_flat[i:i+4] for i in range(0, 16, 4)]

# ---------------- Mix Columns & Inverse ----------------

def xtime(a):
    return (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)

def mix_single_column(a):
    t = a[0] ^ a[1] ^ a[2] ^ a[3]
    u = a[0]
    a0 = a[0] ^ t ^ xtime(a[0] ^ a[1])
    a1 = a[1] ^ t ^ xtime(a[1] ^ a[2])
    a2 = a[2] ^ t ^ xtime(a[2] ^ a[3])
    a3 = a[3] ^ t ^ xtime(a[3] ^ u)
    return [a0, a1, a2, a3]

def mix_columns(state):
    return [mix_single_column(row) for row in state]

def inv_mix_single_column(a):
    def gf_mul(a, b):
        res = 0
        for i in range(8):
            if b & 1:
                res ^= a
            hi_bit = a & 0x80
            a = (a << 1) & 0xFF
            if hi_bit:
                a ^= 0x1B
            b >>= 1
        return res
    b0 = gf_mul(a[0], 14) ^ gf_mul(a[1], 11) ^ gf_mul(a[2], 13) ^ gf_mul(a[3], 9)
    b1 = gf_mul(a[0], 9)  ^ gf_mul(a[1], 14) ^ gf_mul(a[2], 11) ^ gf_mul(a[3], 13)
    b2 = gf_mul(a[0], 13) ^ gf_mul(a[1], 9)  ^ gf_mul(a[2], 14) ^ gf_mul(a[3], 11)
    b3 = gf_mul(a[0], 11) ^ gf_mul(a[1], 13) ^ gf_mul(a[2], 9)  ^ gf_mul(a[3], 14)
    return [b0, b1, b2, b3]

def inv_mix_columns(state):
    return [inv_mix_single_column(row) for row in state]

# ---------------- Key Expansion & Add Round Key ----------------

def expand_key(master_key):
    r_con = (
        0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
        0x80, 0x1B, 0x36,
    )
    key_columns = bytes2matrix(master_key)
    iteration_size = len(master_key) // 4
    i = 1
    while len(key_columns) < (10 + 1) * 4:
        word = list(key_columns[-1])
        if len(key_columns) % iteration_size == 0:
            word = word[1:] + word[:1]
            word = [s_box[b] for b in word]
            word[0] ^= r_con[i]
            i += 1
        elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
            word = [s_box[b] for b in word]
        word = [a ^ b for a, b in zip(word, key_columns[-iteration_size])]
        key_columns.append(word)
    return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]

def add_round_key(state, key_matrix):
    new_state = []
    for i in range(4):
        row = []
        for j in range(4):
            row.append(state[i][j] ^ key_matrix[j][i])
        new_state.append(row)
    return new_state

# ---------------- Block Decryption ----------------

def decrypt_block(key, ct_block):
    round_keys = expand_key(key)
    state = bytes2matrix(ct_block)
    state = add_round_key(state, round_keys[10])
    for i in range(9, 0, -1):
        state = inv_shift_rows(state)
        state = inv_sub_bytes(state)
        state = add_round_key(state, round_keys[i])
        state = inv_mix_columns(state)
    state = inv_shift_rows(state)
    state = inv_sub_bytes(state)
    state = add_round_key(state, round_keys[0])
    return matrix2bytes(state)

# ---------------- Main Exploit Routine ----------------

def main():
    r = remote("kashictf.iitbhucybersec.in", 56958)
    fake_flag = None

    # Process 10 rounds of fake encryption to validate IV recovery
    for rnd in range(10):
        line = r.recvline().strip()
        data = json.loads(line)
        key = bytes.fromhex(data['key'])
        ciphertext = bytes.fromhex(data['ciphertext'])
        blocks = [ciphertext[i:i+16] for i in range(0, len(ciphertext), 16)]
        
        # Decrypt first block to recover IV:
        D = decrypt_block(key, blocks[0])
        candidate_X = xor(D[:8], b"KashiCTF")
        candidate_iv = candidate_X + candidate_X
        p0 = xor(decrypt_block(key, blocks[0]), candidate_iv)
        
        if p0.startswith(b"KashiCTF{"):
            print(f"[Round {rnd+1}] Recovered IV: {candidate_iv.hex()}")
            p1 = xor(decrypt_block(key, blocks[1]), blocks[0])
            p2 = xor(decrypt_block(key, blocks[2]), blocks[1])
            candidate_plain = unpad(p0 + p1 + p2)
            try:
                flag_str = candidate_plain.decode()
            except:
                flag_str = candidate_plain.hex()
            print(f"[Round {rnd+1}] Candidate fake flag: {flag_str}")
            fake_flag = flag_str
        else:
            print(f"[Round {rnd+1}] IV recovery failed!")
        
        # Send the recovered IV back to the server
        r.sendline(candidate_iv.hex().encode())
        r.recvline()  # Consume extra prompt

    # Process final secret encryption containing the real flag
    final_line = r.recvline().strip()
    print("\nFinal secret encryption JSON:")
    print(final_line.decode())
    final_data = json.loads(final_line)
    final_key = bytes.fromhex(final_data['key'])
    final_ct = bytes.fromhex(final_data['ciphertext'])
    blocks = [final_ct[i:i+16] for i in range(0, len(final_ct), 16)]
    
    # Recover IV from final encryption
    D = decrypt_block(final_key, blocks[0])
    candidate_X = xor(D[:8], b"KashiCTF")
    candidate_iv = candidate_X + candidate_X
    
    # Decrypt all blocks to reveal the real flag
    p0 = xor(decrypt_block(final_key, blocks[0]), candidate_iv)
    p1 = xor(decrypt_block(final_key, blocks[1]), blocks[0])
    p2 = xor(decrypt_block(final_key, blocks[2]), blocks[1])
    p3 = xor(decrypt_block(final_key, blocks[3]), blocks[2])
    real_plain = unpad(p0 + p1 + p2 + p3)
    try:
        real_flag = real_plain.decode()
    except:
        real_flag = real_plain.hex()
    print("\nRecovered REAL flag:", real_flag)
    r.close()

if __name__ == "__main__":
    main()
```

Running the script, this is the output we get: 

```bash
$ python3 solve.py
[+] Opening connection to kashictf.iitbhucybersec.in on port 56958: Done
[Round 1] Recovered IV: ed3c6dd69dd7055bed3c6dd69dd7055b
[Round 1] Candidate flag (fake): KashiCTF{AES??_HeHe_Fake_Flag;)}
[Round 2] Recovered IV: ec5804c3f05998d9ec5804c3f05998d9
[Round 2] Candidate flag (fake): KashiCTF{AES??_HeHe_Fake_Flag;)}
[Round 3] Recovered IV: 38fb4f04cd16e09f38fb4f04cd16e09f
[Round 3] Candidate flag (fake): KashiCTF{AES??_HeHe_Fake_Flag;)}
[Round 4] Recovered IV: 1ffdb424775d6d351ffdb424775d6d35
[Round 4] Candidate flag (fake): KashiCTF{AES??_HeHe_Fake_Flag;)}
[Round 5] Recovered IV: 16662774d985826316662774d9858263
[Round 5] Candidate flag (fake): KashiCTF{AES??_HeHe_Fake_Flag;)}
[Round 6] Recovered IV: 0af90149d7b5817a0af90149d7b5817a
[Round 6] Candidate flag (fake): KashiCTF{AES??_HeHe_Fake_Flag;)}
[Round 7] Recovered IV: 306d340460326b8c306d340460326b8c
[Round 7] Candidate flag (fake): KashiCTF{AES??_HeHe_Fake_Flag;)}
[Round 8] Recovered IV: d1b0fb3c592e20b6d1b0fb3c592e20b6
[Round 8] Candidate flag (fake): KashiCTF{AES??_HeHe_Fake_Flag;)}
[Round 9] Recovered IV: 3af147983199c3d03af147983199c3d0
[Round 9] Candidate flag (fake): KashiCTF{AES??_HeHe_Fake_Flag;)}
[Round 10] Recovered IV: 68bfedb4513a6a0e68bfedb4513a6a0e
[Round 10] Candidate flag (fake): KashiCTF{AES??_HeHe_Fake_Flag;)}

Final secret encryption JSON:
{"key": "ff57d6259ff7ef32e008ea9c2d023e9a", "ciphertext": "6858d5e8141ddb2eb79be53732d61fe116ba25ea22f39ede5af992faa119f1f1dcb104c934e1130fe87570973ecebb1759032da10f2807097591544e9e068a88"}

Recovered REAL flag: KashiCTF{AES_Unbr34KAbl3_but_t0T4lly_br3Akable_mAyb3_OvdGTNWe}
[*] Closed connection to kashictf.iitbhucybersec.in port 56958
```

Which gives us our real flag: `KashiCTF{AES_Unbr34KAbl3_but_t0T4lly_br3Akable_mAyb3_OvdGTNWe}` 

---
## Conclusion

- **Flawed IV Construction:**  
  The use of a repeated 8-byte IV (`os.urandom(8)*2`) in CBC mode is a critical vulnerability, as it introduces predictable redundancy.

- **Exploitation Flow:**  
  By leveraging the known plaintext prefix, the attacker recovers the IV from the first ciphertext block, which is used to decrypt subsequent blocks. This technique is applied first on fake flags to validate the approach and at last on the real flag encryption to retrieve the secret.

This challenge highlights the importance of secure IV generation and the risks of even minor cryptographic mistakes.