← Back to Writeups
CUSTOMMediumSteganography

Stickers (9ff6401f-2c68-4b01-a557-5d01fe1e3954)

XESXOR9/2/20264 min read
#steganography#custom#medium#color-channel-separation-(gree#frame-by-frame-resolution-anal#gif-header-repair-(corrupted-m#hidden-canvas-area-extraction-#linux

Stickers (9ff6401f-2c68-4b01-a557-5d01fe1e3954)

Platform: hackerlab | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2025-12-23 | Status: Solved Techniques: Color channel separation (green channel), Frame-by-frame resolution analysis, GIF header repair (corrupted magic bytes), Hidden canvas area extraction (beyond visible dimensions)

Summary

Task: GIF file with corrupted header (first 6 bytes zeroed) and hidden sticker pack. Solution: Restore GIF89a header, extract frames, discover frames 10-12 have larger dimensions (600x600 vs 512x512), extract flag from green channel in the hidden canvas area below 512px.

Stickers — hackerlab.pro

Description

Недавно мы выпустили свой собственный стикерпак. Я немного изменил то, что прислал мне дизайнер. Пожалуйста, оцените его и не упускайте ничего из виду

File: stickers.zip contains Stickers.gif

Analysis

Step 1: Corrupted GIF Header

The GIF file doesn't open in standard viewers. Checking the header:

bash
xxd Stickers.gif | head -1
# 00000000: 0000 0000 0000 0002 0002 f70f 00ff ffff  ................

The first 6 bytes are zeroed! Should be GIF89a (47 49 46 38 39 61). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This is the "modification" mentioned in the task description.

Step 2: Header Restoration

bash
printf 'GIF89a' | dd of=fixed.gif bs=1 conv=notrunc
dd if=Stickers.gif of=fixed.gif bs=1 skip=6 seek=6

Or via Python:

python
with open('Stickers.gif', 'rb') as f:
    data = bytearray(f.read())

data[0:6] = b'GIF89a'

with open('fixed.gif', 'wb') as f:
    f.write(data)

Step 3: Frame Analysis

After restoration, extract the frames:

bash
convert fixed.gif -coalesce frames/frame_%03d.png

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

Analyzing dimensions:

python
from PIL import Image
import glob

for f in sorted(glob.glob("frames/frame_*.png")):
    img = Image.open(f)
    print(f"{f}: {img.size}")

Key discovery:

  • Frames 0-9: 512x512 pixels
  • Frames 10-12: 600x600 pixels (larger!)

Step 4: Investigating the Extended Area

Frames 10-12 have an additional 88 pixels in height and width. When viewing the GIF normally, this area is cropped to the first frame's dimensions (512x512).

python
from PIL import Image

def analyze_extra_area(frame_path):
    img = Image.open(frame_path).convert("RGBA")
    width, height = img.size
    
    # Check the area below 512 pixels
    bottom_area = []
    for y in range(512, height):
        for x in range(width):
            pixel = img.getpixel((x, y))
            if pixel[3] > 0:  # Non-transparent
                bottom_area.append((x, y, pixel))
    
    return bottom_area
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Frame 10 contains data in the bottom area!

Step 5: Extracting the Flag from the Green Channel

The flag is hidden in the green channel of the area from Y=512 to Y=600:

python
from PIL import Image

img = Image.open("frames/frame_010.png").convert("RGBA")

# Extract the bottom area (rows 512-600)
bottom_img = Image.new("L", (600, 88))
for y in range(512, 600):
    for x in range(600):
        pixel = img.getpixel((x, y))
        # Use the green channel
        bottom_img.putpixel((x, y - 512), pixel[1])
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bottom_img.save("frame10_bottom_green.png")

# Visualize the text
for y in range(73, 84):
    line = ""
    for x in range(600):
        p = bottom_img.getpixel((x, y))
        if p > 200:
            line += "#"
        else:
            line += "."
    print(line)

The result shows the flag text in ASCII-art format.

Solution

Complete script:

python
#!/usr/bin/env python3
from PIL import Image
import os

# Step 1: Fix corrupted GIF header
with open('Stickers.gif', 'rb') as f:
    data = bytearray(f.read())
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

data[0:6] = b'GIF89a'

with open('fixed.gif', 'wb') as f:
    f.write(data)

# Step 2: Extract frames using ImageMagick
os.system('convert fixed.gif -coalesce frames/frame_%03d.png')

# Step 3: Analyze frame 10's extended area
img = Image.open("frames/frame_010.png").convert("RGBA")
width, height = img.size
print(f"Frame 10 size: {width}x{height}")

# Step 4: Extract green channel from bottom area
bottom_img = Image.new("L", (600, 88))
for y in range(512, 600):
    for x in range(600):
        pixel = img.getpixel((x, y))
        bottom_img.putpixel((x, y - 512), pixel[1])
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bottom_img.save("frame10_bottom_green.png")

# Step 5: Visualize the flag
print("\nFlag area (rows 73-84):")
for y in range(73, 84):
    line = ""
    for x in range(600):
        p = bottom_img.getpixel((x, y))
        line += "#" if p > 200 else " "
    print(line.strip())

Interesting Details

  • The flag uses leetspeak: spl1t (split), g1f (gif), ch4ng3 (change), r3s (res)
  • The flag itself is a hint to the solution: "split gif or change res?" — you needed to split the GIF into frames AND notice the resolution change
  • The green channel was chosen because it has the highest sensitivity for the human eye (and is often used for hiding data) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Alternative Approaches

  1. GIMP — open as layers, check the dimensions of each layer
  2. ImageMagick identify — identify -verbose fixed.gif will show dimensions of all frames
  3. Python imageio — imageio.mimread() for reading all frames with metadata ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍