Rainbow QR (3bdf7da0-991d-46bf-aa92-e4dcd4ef692f)
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
-
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
-
Visual analysis:
- Both images look like rainbow gradients
- Visually almost identical
- Dimensions match: 720x480
-
Hypothesis:
- QR code is hidden in the difference between images
- XOR or absolute pixel difference was used
Solution
Step 1: Computing Image Difference
python read-only • copy allowedimport 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 read-only • copy allowed# Threshold 50 to extract significant differences binary = (gray_diff > 50).astype(np.uint8) * 255
Result — a noisy QR code.
Step 3: Morphological Cleanup
python read-only • copy allowed# 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 read-only • copy allowed# 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 read-only • copy allowed#!/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 filenamesREDACTED— hints at XOR operation used to hide data
Alternative Approaches
-
XOR instead of difference:
python read-only • copy allowedxor_result = cv2.bitwise_xor(img1, img2) -
Different thresholds:
- Threshold 30-100 may give different results
- Adaptive threshold (cv2.adaptiveThreshold)
- Otsu's binarization (cv2.threshold with cv2.THRESH_OTSU)
-
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)