stained-glass
tjctf
Task: 7488-byte binary file encrypted with three per-channel XOR keys plus a residual whole-file XOR layer. Solution: autocorrelation to find key lengths (10/6/14), exploit zero-plaintext region for key recovery, PNG signature known-plaintext for residual key, full decryption reveals flag image.
$ ls tags/ techniques/
stained-glass — TJCTF 2026
Description
three brushes painted over the window
Given a single file window.bin (7488 bytes). The goal is to decrypt it and recover the flag. The hint "three brushes" suggests three layers of encryption applied to a "window" (PNG image).
Analysis
File structure
The 7488-byte file can be viewed as three interleaved channels (byte positions mod 3), each encrypted with a different repeating XOR key. This matches the "three brushes" hint — each brush paints one channel.
Key length determination via autocorrelation
Autocorrelation analysis on each channel revealed distinct repeating key periods:
- Channel 0 (file positions 0, 3, 6, ...): key period 10
- Channel 1 (file positions 1, 4, 7, ...): key period 6
- Channel 2 (file positions 2, 5, 8, ...): key period 14
Zero-plaintext region
A 304-byte region at file positions 160–463 showed extremely low byte diversity per-channel per-key-position — exactly 1 unique value at each (channel, key_position) slot. This indicated the plaintext was zero (null bytes) in that region, meaning the ciphertext bytes there directly reveal the per-channel XOR keys.
Residual XOR layer
After per-channel decryption, the first 8 bytes were 89 FB 6F 57 0F 0A 1A A1 — close to the PNG signature 89 50 4E 47 0D 0A 1A 0A but not exact. XORing the two revealed a period-6 residual key: [0x00, 0xAB, 0x21, 0x10, 0x02, 0x00]. This was confirmed against the IEND chunk at the file's end.
Solution
Step 1: Per-channel key recovery from zero region
The zero region starts at channel index 53 (file position 160 / 3 ≈ 53). Key position offsets must account for 53 % key_length:
#!/usr/bin/env python3 import struct with open("window.bin", "rb") as f: data = bytearray(f.read()) n = len(data) # 7488 # Split into 3 channels channels = [data[i::3] for i in range(3)] # Zero region: file positions 160-463 → channel indices 53-154 (approx) # Each channel has ~101 bytes in this region key_lengths = [10, 6, 14] keys = [None, None, None] # Channel 0: key length 10 # Zero region starts at channel index 53, so key offset = 53 % 10 = 3 ch0 = channels[0] key0 = [0] * 10 for i in range(53, 155): pos = i % 10 key0[pos] = ch0[i] keys[0] = key0 # [190, 46, 30, 232, 144, 174, 62, 14, 248, 128] # Channel 1: key length 6 # Zero region starts at channel index 53, so key offset = 53 % 6 = 5 ch1 = channels[1] key1 = [0] * 6 for i in range(53, 155): pos = i % 6 key1[pos] = ch1[i] keys[1] = key1 # [94, 110, 103, 241, 137, 207] # Channel 2: key length 14 # Zero region starts at channel index 53, so key offset = 53 % 14 = 11 ch2 = channels[2] key2 = [0] * 14 for i in range(53, 155): pos = i % 14 key2[pos] = ch2[i] keys[2] = key2 # [235, 8, 35, 159, 152, 15, 23, 202, 41, 2, 190, 185, 46, 54]
Step 2: Per-channel decryption
# Decrypt each channel for ch_idx in range(3): ch = channels[ch_idx] klen = key_lengths[ch_idx] key = keys[ch_idx] for i in range(len(ch)): ch[i] ^= key[i % klen] # Reassemble decrypted = bytearray(n) for i in range(3): decrypted[i::3] = channels[i]
Step 3: Residual XOR removal
# PNG signature: 89 50 4E 47 0D 0A 1A 0A png_sig = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) residual_key = [decrypted[i] ^ png_sig[i] for i in range(6)] # Result: [0x00, 0xAB, 0x21, 0x10, 0x02, 0x00] # Apply residual key (period 6) for i in range(n): decrypted[i] ^= residual_key[i % 6] with open("window_decrypted.png", "wb") as f: f.write(decrypted)
Step 4: Read the flag
The decrypted PNG (600×140 pixels, RGB) shows the flag text rendered upside-down (rotated 180°). After rotation:
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [crypto][Pro]Поврежденная расшифровка (Corrupted Decryption)— hackerlab
- [crypto][Pro]Микс из символов (Mix of Symbols)— bug-makers
- [reverse][Pro]Cursed Steganography— duckerz
- [crypto][Pro]XORDECRYPT— bluehensctf
- [stego][Pro]Мозаика внутри PNG (Mosaic inside PNG)— bug-makers