← Back to Writeups
CUSTOMMediumSteganography

Quick Response

XESXOR9/2/20262 min read
#steganography#custom#medium#finder_pattern_analysis#module_grid_extraction#qr_mask_pattern_removal#xor_deobfuscation

Quick Response

Platform: tamuctf | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-03-21 | Status: Solved Techniques: finder_pattern_analysis, module_grid_extraction, qr_mask_pattern_removal, xor_deobfuscation

Summary

Task: QR code image that cannot be decoded by standard readers due to applied XOR mask. Solution: Identified checkerboard XOR pattern (QR Mask Pattern 0) by comparing corrupted finder patterns with expected structure, applied inverse XOR to restore valid QR code.

Quick Response — TAMUctf

Description

If you're bored, check this one out!

Given: A PNG image (quick-response.png) that visually resembles a QR code but cannot be decoded by any standard QR reader. The goal is to recover the hidden flag by understanding and reversing the applied transformation.

Analysis

Initial reconnaissance revealed:

  • PNG image: 928×928 pixels, 2-bit colormap (palette mode), non-interlaced
  • Exactly 2 colors: RGB(20,22,27) dark and RGB(224,226,234) light
  • Dimensions: 928 / 32 = 29 modules → QR Version 3 (29×29 grid)
  • Each QR module is represented by a 32×32 pixel block ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key observation: The three corner finder patterns (normally 7×7 black-white-black squares) were NOT standard QR finder patterns. All three corners had the same non-standard 7×7 pattern.

Comparing the actual corner pattern with the expected QR finder pattern using XOR revealed a perfect checkerboard pattern: (row + col) % 2. This is QR Mask Pattern 0, one of the 8 standard QR data masking patterns.

The flag hint "timing bits" refers to the alternating black-white timing patterns in QR codes that follow this same checkerboard pattern. The entire QR code was XORed with this mask, corrupting the finder patterns and making it unreadable. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Extract the module grid

Read the PNG and extract a 29×29 binary grid by sampling each 32×32 pixel block:

python
from PIL import Image
import numpy as np
from pyzbar.pyzbar import decode

img = Image.open('quick-response.png')
arr = np.array(img)

# Extract 29x29 module grid (each module is 32x32 pixels)
modules = np.zeros((29, 29), dtype=int)
for r in range(29):
    for c in range(29):
        block = arr[r*32:(r+1)*32, c*32:(c+1)*32]
        modules[r, c] = int(np.mean(block) > 0.5)

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

Step 2: Undo the checkerboard XOR mask

Apply the inverse transformation (XOR is self-inverse):

python
# Undo checkerboard XOR mask (QR Mask Pattern 0)
for r in range(29):
    for c in range(29):
        modules[r, c] ^= (r + c) % 2

Step 3: Render and decode

Create a clean QR code image and decode it:

python
# Render clean QR code
scale = 10
qr_img = np.ones((29 * scale, 29 * scale), dtype=np.uint8) * 255
for r in range(29):
    for c in range(29):
        if modules[r, c] == 0:
            qr_img[r*scale:(r+1)*scale, c*scale:(c+1)*scale] = 0
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

out = Image.fromarray(qr_img)
out.save('qr_fixed.png')

results = decode(out)
print(results[0].data.decode())  # gigem{REDACTED}

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