← Back to Writeups
CUSTOMMediumSteganography

Rainbow QR (3bdf7da0-991d-46bf-aa92-e4dcd4ef692f)

XESXOR9/2/20263 min read
#steganography#custom#medium#image-difference-analysis-(pix#threshold-binarization-to-extr#morphological-operations-(dila#qr-code-detection-and-decoding

Rainbow QR (3bdf7da0-991d-46bf-aa92-e4dcd4ef692f)

Platform: hackerlab.pro | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2025-12-22 | Status: Solved Techniques: Image difference analysis (pixel-by-pixel comparison), Threshold binarization to extract hidden pattern, Morphological operations (dilation/erosion) for noise removal, QR code detection and decoding

Summary

Files: resources.zip contains rainbow1.jpg and rainbow2.jpg (both 720x480 JPEG)

Rainbow QR — hackerlab.pro

Description

Увидев цены на антистресс-раскраски, я решил, что просто буду красить каждый пиксель в понравившийся мне цвет. Меня хватило только на два листа x_x

Files: resources.zip contains rainbow1.jpg and rainbow2.jpg (both 720x480 JPEG)

Analysis

  1. Hints in the description:

    • "coloring each pixel" — hints at pixel-by-pixel operation
    • "two sheets" — two images for comparison
    • Name "rainbow" + flag contains "REDACTED" — hints at XOR
  2. Visual analysis:

    • Both images look like rainbow gradients
    • Visually almost identical
    • Dimensions match: 720x480 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
  3. Hypothesis:

    • QR code is hidden in the difference between images
    • XOR or absolute pixel difference was used

Solution

Step 1: Computing Image Difference

python
import cv2
import numpy as np
from PIL import Image

# Load images
img1 = np.array(Image.open('rainbow1.jpg'))
img2 = np.array(Image.open('rainbow2.jpg'))

# Calculate absolute difference
difference = np.abs(img1.astype(np.int16) - img2.astype(np.int16))

# Convert to grayscale (average channels)
gray_diff = np.mean(difference, axis=2)

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

Step 2: Threshold Binarization

python
# Threshold 50 to extract significant differences
binary = (gray_diff > 50).astype(np.uint8) * 255

Result — a noisy QR code.

Step 3: Morphological Cleanup

python
# Create kernel for morphological operations
kernel = np.ones((5,5), np.uint8)

# Dilation (expand white areas) — fills gaps
dilated = cv2.dilate(binary, kernel, iterations=1)

# Erosion (shrink) — removes noise
cleaned = cv2.erode(dilated, kernel, iterations=1)

Step 4: Inversion and Decoding

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

python
# QR codes require black code on white background
img_inv = cv2.bitwise_not(cleaned)

# Decode QR code
detector = cv2.QRCodeDetector()
data, _, _ = detector.detectAndDecode(cv2.cvtColor(img_inv, cv2.COLOR_GRAY2BGR))
print(data)  # CODEBY{REDACTED}

Full Script

python
#!/usr/bin/env python3
import cv2
import numpy as np
from PIL import Image

# Load images
img1 = np.array(Image.open('rainbow1.jpg'))
img2 = np.array(Image.open('rainbow2.jpg'))

# Calculate absolute difference
difference = np.abs(img1.astype(np.int16) - img2.astype(np.int16))
gray_diff = np.mean(difference, axis=2)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Threshold to binary
binary = (gray_diff > 50).astype(np.uint8) * 255

# Morphological cleanup
kernel = np.ones((5,5), np.uint8)
dilated = cv2.dilate(binary, kernel, iterations=1)
cleaned = cv2.erode(dilated, kernel, iterations=1)

# Invert and decode QR
img_inv = cv2.bitwise_not(cleaned)
detector = cv2.QRCodeDetector()
data, _, _ = detector.detectAndDecode(cv2.cvtColor(img_inv, cv2.COLOR_GRAY2BGR))
print(f"Flag: {data}")

Flag Analysis

  • ra1nb0w — "rainbow", matches the filenames
  • REDACTED — hints at XOR operation used to hide data ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Alternative Approaches

  1. XOR instead of difference:

    python
    xor_result = cv2.bitwise_xor(img1, img2)
  2. Different thresholds:

    • Threshold 30-100 may give different results
    • Adaptive threshold (cv2.adaptiveThreshold)
    • Otsu's binarization (cv2.threshold with cv2.THRESH_OTSU)
  3. Other morphological operations:

    • Opening (erosion → dilation) for noise removal
    • Closing (dilation → erosion) for filling gaps

Tools

  • OpenCV — morphological operations, QR decoding
  • NumPy — mathematical operations with arrays
  • PIL/Pillow — image loading
  • pyzbar — alternative QR decoder (more robust to noise) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍