← Back to Writeups
CUSTOMEasyCrypto

AES-CTR

XESXOR9/10/20262 min read
#crypto#custom#easy#ctr_keystream_reuse#known_header_attack

AES-CTR

Platform: spbctf | Category: Crypto | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-03-09 | Status: Solved Techniques: ctr_keystream_reuse, known_header_attack

Summary

Task: AES-CTR encrypted PNG file with static counter implementation. Solution: Exploit keystream reuse by XORing known PNG header with ciphertext to recover keystream, then decrypt entire file.

AES-CTR — CryptoKids 2.0

Description

AES encryption in CTR mode. Encrypted PNG file provided.

Analysis

  • AES-CTR mode implementation with a critical flaw
  • Counter function is lambda: same_value - returns constant instead of incrementing
  • This means every 16-byte block is XORed with the SAME keystream
  • PNG files have known header (first 16 bytes are standard)
  • Recover keystream from known header, decrypt entire file

Solution

In CTR mode, plaintext is XORed with AES(key, counter). If counter never changes, all blocks use the same keystream - equivalent to simple XOR cipher. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

PNG header is always: \x89PNG\r\n\x1a\n + IHDR chunk header

python
#!/usr/bin/env python3
# AES-CTR with static counter - keystream reuse attack

def xor_bytes(a, b):
    return bytes([x ^ y for x, y in zip(a, b)])

# Read encrypted PNG
with open('encrypted.png', 'rb') as f:
    ct = f.read()

# Known PNG header (first 16 bytes)
png_header = bytes([
    0x89, 0x50, 0x4E, 0x47,  # \x89PNG
    0x0D, 0x0A, 0x1A, 0x0A,  # \r\n\x1a\n
    0x00, 0x00, 0x00, 0x0D,  # IHDR length (13)
    0x49, 0x48, 0x44, 0x52   # "IHDR"
])
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Recover keystream from first block
keystream = xor_bytes(ct[:16], png_header)
print(f"Keystream: {keystream.hex()}")

# Decrypt all blocks with same keystream
plaintext = b''
for i in range(0, len(ct), 16):
    block = ct[i:i+16]
    plaintext += xor_bytes(block, keystream)

# Save decrypted PNG
with open('decrypted.png', 'wb') as f:
    f.write(plaintext)

print("Decrypted PNG saved - flag is in the image")
# Flag: spbctf{REDACTED}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍