Дар (Dar / Gift)
Дар (Dar / Gift)
Platform: duckerz | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-02-09 | Status: Solved Techniques: frame0_anomaly_detection, gif_palette_index_lsb, hidden_file_in_palette_indices, structured_palette_analysis
Summary
Task: Animated GIF with a cat, hint about frames. Solution: Discovered artificially structured 8x8x4 palette with values divisible by 4; frame 0 histogram anomaly revealed hidden PNG extracted via LSB of palette indices.
Дар (Dar / Gift) — duckerz CTF
Description
"Этот кот выглядит спокойным, но он пришел не с пустыми лапами. Кажется, он принес тебе небольшой подарок. Только не потеряй его в суете кадров."
"This cat looks calm, but he didn't come empty-pawed. It seems he brought you a small gift. Just don't lose it in the bustle of frames."
File: cat.gif (GIF89a, 702×394, 129 frames, 4.3s animation, 4.4 MB)
Image: animated GIF with a cat in a basket and meme text "ПЕРЕРЫВ НА КОШКУ" (break for a cat).
250 points, 11 teams solved. Author: racwano.
Analysis
Standard recon — nothing obvious
- file — GIF89a, 702×394
- exiftool — standard GIF metadata, 129 frames, nothing unusual
- binwalk — nothing found (no embedded files)
- strings | grep DUCKERZ — no flag
- Data after GIF trailer — no extra data
Red herrings (rabbit holes)
- XOR of all frames — showed meme text overlay, not the flag
- Transparent color index — frame 0 has tc=31, others have tc=3 (some tc=2). Turned out to be a side effect of encoding
- Frame delay pattern — periodic 30ms/40ms every 3rd frame. Also not data
- Per-pixel differences between frames — normal animation changes
- Standard LSB of pixel RGB values — showed dithering patterns, not data
Key discovery: structured palette
The GIF uses a specially constructed uniform 8×8×4 palette:
| Channel | Levels | Values | Step |
|---|---|---|---|
| R | 8 | 0, 36, 72, 108, 144, 180, 216, 252 | 36 |
| G | 8 | 0, 36, 72, 108, 144, 180, 216, 252 | 36 |
| B | 4 | 0, 85, 170, 255 | 85 |
Palette index formula: index = r_level + g_level × 8 + b_level × 64
Critical indicator: all R and G values in the palette have lower 2 bits = 0 (all divisible by 4). A natural image palette would never be so perfectly structured — this is an artificial palette created for steganography.
Frame 0 anomaly
- Frame 0 uses 84 unique palette indices (significantly more diversity than other frames)
- Frame 0 histogram differs drastically from all other frames (diff = 1,038,157 vs ~20,000 for others)
- 73 palette indices are used ONLY in frame 0
- This means frame 0 has additional information encoded through palette index selection
LSB of palette indices
Since the palette is structured so that adjacent indices produce visually indistinguishable colors, the least significant bit (LSB) of each index can be used to store data without visible changes to the image.
Extracting LSB (bit 0) of each palette index in frame 0, reading pixels left to right, top to bottom:
- First 8 bytes:
89 50 4e 47 0d 0a 1a 0a— PNG file signature!
Solution
python read-only • copy allowed#!/usr/bin/env python3 """ Extract hidden PNG from LSB of GIF frame 0 palette indices. Technique: GIF Palette Index LSB Steganography - GIF uses artificial uniform 8x8x4 palette - Adjacent indices produce visually indistinguishable colors - LSB of each index in frame 0 encodes a bit of the hidden file - Bits are assembled into bytes in LSB-first order """ from PIL import Image import numpy as np # Open GIF and seek to frame 0 gif = Image.open('cat.gif') gif.seek(0) frame0 = gif.copy() # Get palette indices (not RGB, but indices!) indices = np.array(frame0) # Extract LSB of each palette index flat_indices = indices.flatten() bits = flat_indices & 1 # Assemble bits into bytes (LSB-first order) data = bytearray() for i in range(0, len(bits) - 7, 8): byte = 0 for j in range(8): byte |= (bits[i + j] << j) data.append(byte) # Verify PNG signature assert data[:8] == b'\x89PNG\r\n\x1a\n', "Not a PNG!" print(f"[+] Found PNG signature!") print(f"[+] Extracted {len(data)} bytes") # Save hidden PNG with open('hidden.png', 'wb') as f: f.write(data) print("[+] Saved hidden.png — open it to see the flag!")
Result
The extracted hidden.png is a valid 1000×1000 RGBA image with the flag text in large black letters on a white background.