# ApoorvCTF 2025 - Kowareta Cipher **Title:** Kowareta Cipher **Description:** In Tokyo’s cyber arena, Kowareta Cipher has left secrets exposed. Coding prodigy Lain Iwakura encrypted her messages with AES—but without an IV, patterns emerge, revealing cracks in her defenses. Can you break the cipher and uncover the truth? 🔐💻 🔗 **Files:** [chall.py](https://github.com/xtasy94/CTFW/blob/main/CTF_Files/ApoorvCTF/chall.py) · [solve.py](https://github.com/xtasy94/CTFW/blob/main/CTF_Files/ApoorvCTF/solve.py) ## Solution: The challenge leverages a classic vulnerability in AES when used in ECB mode. The server encrypts data in the following format: ``` userinput || flag || userinput ``` using AES in ECB mode with no IV. Due to ECB’s deterministic nature, identical plaintext blocks result in identical ciphertext blocks, making it susceptible to a byte‑at‑a‑time decryption attack. 1. **ECB Vulnerability & Oracle Setup:** The server accepts hex-encoded input, appends the flag between two copies of that input, pads the result, and then encrypts it using AES-ECB. This predictable behavior allows us to craft inputs that reveal parts of the secret flag. 2. **Forcing Flag Alignment:** To ensure that the flag is aligned with the block boundaries, we prepend a fixed 16‑byte block (i.e., `b"A" * 16`) to our input. With additional carefully calculated padding, we guarantee that the unknown flag byte will always be the last byte of a block. 3. **Byte‑at‑a‑Time Decryption:** By querying the oracle with our chosen prefix and then constructing a dictionary of possible ciphertext blocks (each corresponding to a candidate byte appended to our known plaintext), we can determine the next unknown byte when the resulting block matches the target. Repeating this process byte‑by‑byte eventually recovers the complete flag. The following script automates the attack. It connects to the remote service, interacts with the encryption oracle, and performs a byte‑at‑a‑time attack to recover the secret flag: ```python #!/usr/bin/env python3 from pwn import remote import sys HOST = "chals1.apoorvctf.xyz" PORT = 4001 r = remote(HOST, PORT) print(f"[+] Connected to {HOST}:{PORT}") def oracle(data: bytes) -> bytes: """ Sends hex-encoded data to the oracle and returns the raw ciphertext. The service prints a line like: "Ciphertext: <hex ciphertext>" """ r.recvuntil(b"Enter your input:") r.sendline(data.hex().encode()) line = r.recvline() if b"Ciphertext:" in line: ct_hex = line.split(b"Ciphertext:")[1].strip() return bytes.fromhex(ct_hex.decode()) else: print("Error in response:", line) sys.exit(1) block_size = 16 # Fixed prefix: choose one full block to force flag alignment. fixed = b"A" * block_size secret = b"" max_len = 50 # Adjust maximum expected flag length print("[*] Starting byte-at-a-time attack...") for i in range(max_len): # Determine the additional pad length so that: # (len(fixed) + len(pad) + i) mod block_size == block_size - 1. n = (block_size - 1 - ((len(fixed) + i) % block_size)) % block_size pad = b"A" * n prefix = fixed + pad # Query the oracle with our current prefix. ct = oracle(prefix) # Split ciphertext into 16-byte blocks. blocks = [ct[j:j+block_size] for j in range(0, len(ct), block_size)] # The unknown flag starts immediately after our fixed+pad. target_block_index = (len(prefix) + i) // block_size if target_block_index >= len(blocks): print("[-] Target block index out of range, breaking...") break target_block = blocks[target_block_index] # Build dictionary for candidate bytes. dictionary = {} for c in range(256): test_input = prefix + secret + bytes([c]) ct_test = oracle(test_input) blocks_test = [ct_test[j:j+block_size] for j in range(0, len(ct_test), block_size)] dictionary[blocks_test[target_block_index]] = c if target_block not in dictionary: print("[-] No matching candidate found (perhaps reached padding); breaking...") break next_byte = dictionary[target_block] secret += bytes([next_byte]) print(f"[*] Recovered so far: {secret}") # Stop if the flag appears to be complete. if secret.endswith(b"}"): break print("\n[+] Flag:", secret.decode()) r.close() ``` ### How It Works: - **Exploiting ECB Mode:** The deterministic nature of ECB mode ensures that identical plaintext blocks result in identical ciphertext. By controlling the input, we can observe these patterns and infer the unknown bytes of the flag. - **Alignment via Fixed Prefix:** A fixed block (`b"A" * 16`) is prepended to force the flag to begin at a block boundary. Calculated padding is then added so that each unknown flag byte falls at the end of a block, making it predictable. - **Dictionary Construction & Byte Recovery:** For every unknown byte, the script builds a dictionary mapping every possible candidate (0–255) to its corresponding ciphertext block. When the oracle’s response matches one of these blocks, the candidate byte is confirmed as the next byte of the flag. - **Iterative Decryption:** This process is repeated until the complete flag is recovered, indicated by the trailing `}`. Running this script, this is the output we get in some time: ```console $ python3 solve.py [+] Opening connection to chals1.apoorvctf.xyz on port 4001: Done [+] Connected to chals1.apoorvctf.xyz:4001 [*] Starting byte-at-a-time attack... [*] Recovered so far: b'a' ... [*] Recovered so far: b'apoo' ... [*] Recovered so far: b'apoorvctf{3cb_345y_crypt0_' [*] Recovered so far: b'apoorvctf{3cb_345y_crypt0_b' ... [*] Recovered so far: b'apoorvctf{3cb_345y_crypt0_br3' [*] Recovered so far: b'apoorvctf{3cb_345y_crypt0_br34' [*] Recovered so far: b'apoorvctf{3cb_345y_crypt0_br34k' [*] Recovered so far: b'apoorvctf{3cb_345y_crypt0_br34k}' [+] Flag: apoorvctf{3cb_345y_crypt0_br34k} [*] Closed connection to chals1.apoorvctf.xyz port 4001 ``` By exploiting the lack of an IV and the deterministic behavior of ECB mode, we successfully broke the cipher and uncovered the secret message.