← Back to Writeups
CUSTOMMediumCrypto

stained-glass

XESXOR9/10/20264 min read
#crypto#custom#medium#autocorrelation_key_length#known_plaintext_attack#multi_layer_xor_decryption#per_channel_xor_key_recovery

stained-glass

Platform: CUSTOM | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: autocorrelation_key_length, known_plaintext_attack, multi_layer_xor_decryption, per_channel_xor_key_recovery

Summary

Walkthrough of the CUSTOM Crypto stained-glass. 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 k

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:

python
#!/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

python
# 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

python
# 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:

Key Takeaways / Lessons

  • Mastering autocorrelation_key_length, known_plaintext_attack, multi_layer_xor_decryption, per_channel_xor_key_recovery was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-15 • CUSTOM • stained-glass • Difficulty: Medium