← Back to Writeups
CUSTOMEasyForensics

Визуальный код (Visual Code)

XESXOR9/2/20264 min read
#forensics#custom#easy#frame_extraction#gif_header_analysis#multi_part_flag#qr_decoding#linux

Визуальный код (Visual Code)

Platform: hackerlab | Category: Forensics | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-01-06 | Status: Solved Techniques: frame_extraction, gif_header_analysis, multi_part_flag, qr_decoding

Summary

Task: corrupted GIF file that does not open. Solution: repair GIF header (byte 5), extract frames from animated GIF, scan QR codes from each frame, assemble multi-part flag.

Визуальный код (Visual Code) — HackerLab

Description

Вам кажется, что это сигнал? Может какой-то код? Вы правы.

(You think this is a signal? Maybe some code? You're right.)

Files: visual_code.zip containing file "Visual Code"

Analysis

1. Initial File Analysis

After extracting the archive, we get a file without extension. Check its type:

bash
$ file "Visual Code"
Visual Code: data

The file is not recognized. Let's look at the header:

bash
$ xxd "Visual Code" | head -3
00000000: 4749 4638 3916 0800 0800 f700 00ff ffff  GIF89...........
00000010: 0000 001a 1a1a e2e2 e201 0101 0202 0203  ................
00000020: 0303 0404 0405 0505 0606 0608 0808 0909  ................

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

We see GIF89 — this is a GIF file, but the header is corrupted!

2. Corruption Analysis

Standard GIF header: GIF89a (6 bytes: 47 49 46 38 39 61)

In our file: GIF89\x16 — the 6th byte is 0x16 instead of 0x61 ('a')

Position: 0  1  2  3  4  5
Expected: 47 49 46 38 39 61  (GIF89a)
Actual:   47 49 46 38 39 16  (GIF89.)

3. Header Repair

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

# Fix the 6th byte (index 5)
data[5] = 0x61  # 'a'

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

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

After fixing:

bash
$ file visual_code_fixed.gif
visual_code_fixed.gif: GIF image data, version 89a, 229 x 229

4. GIF Analysis

bash
$ identify visual_code_fixed.gif
visual_code_fixed.gif[0] GIF 229x229 229x229+0+0 8-bit sRGB 256c 22399B 0.000u 0:00.000
visual_code_fixed.gif[1] GIF 229x229 229x229+0+0 8-bit sRGB 256c 22399B 0.000u 0:00.000
...
visual_code_fixed.gif[7] GIF 229x229 229x229+0+0 8-bit sRGB 256c 22399B 0.000u 0:00.000

Animated GIF with 8 frames (229x229 pixels).

Solution

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

Step 1: Frame Extraction

python
from PIL import Image

gif = Image.open('visual_code_fixed.gif')

frame_num = 0
while True:
    try:
        gif.seek(frame_num)
        # Convert to RGB for full display
        frame = gif.convert('RGB')
        frame.save(f'frame_{frame_num}.png')
        frame_num += 1
    except EOFError:
        break

print(f"Extracted {frame_num} frames")

Or using ImageMagick:

bash
magick visual_code_fixed.gif -coalesce frame_%d.png

Step 2: Scanning QR Codes

bash
for i in {0..7}; do
    echo "Frame $i:"
    zbarimg -q frame_$i.png 2>/dev/null || echo "  (no QR code)"
done

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

Result:

Frame 0: (empty white frame)
Frame 1: QR-Code:CODEBY{
Frame 2: QR-Code:b1p_
Frame 3: QR-Code:bip_
Frame 4: QR-Code:1ts
Frame 5: QR-Code:_qr
Frame 6: QR-Code:_c0d3
Frame 7: QR-Code:_l000l}

Step 3: Assembling the Flag

python
import subprocess

flag_parts = []
for i in range(8):
    result = subprocess.run(
        ['zbarimg', '-q', f'frame_{i}.png'],
        capture_output=True, text=True
    )
    if result.stdout.strip():
        # Format: "QR-Code:DATA"
        data = result.stdout.strip().split(':', 1)[1]
        flag_parts.append(data)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

flag = ''.join(flag_parts)
print(f"Flag: {flag}")

Complete Solution Script

python
#!/usr/bin/env python3
"""
Visual Code - HackerLab Forensics
Solution: fix GIF header + extract QR codes from frames
"""

from PIL import Image
import subprocess
import os

# 1. Fix the GIF header
with open('visual_code.gif', 'rb') as f:
    data = bytearray(f.read())

# GIF89a - 6th byte should be 0x61 ('a')
if data[5] != 0x61:
    print(f"[*] Corrupted header: byte 5 = 0x{data[5]:02x}, fixing to 0x61")
    data[5] = 0x61
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

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

# 2. Extract frames
gif = Image.open('visual_code_fixed.gif')
frames = []
frame_num = 0

while True:
    try:
        gif.seek(frame_num)
        frame = gif.convert('RGB')
        filename = f'frame_{frame_num}.png'
        frame.save(filename)
        frames.append(filename)
        frame_num += 1
    except EOFError:
        break

print(f"[*] Extracted {frame_num} frames")

# 3. Scan QR codes
flag_parts = []
for i, frame_file in enumerate(frames):
    result = subprocess.run(
        ['zbarimg', '-q', frame_file],
        capture_output=True, text=True
    )
    output = result.stdout.strip()
    if output:
        # QR-Code:DATA
        data = output.split(':', 1)[1] if ':' in output else output
        flag_parts.append(data)
        print(f"  Frame {i}: {data}")
    else:
        print(f"  Frame {i}: (empty)")
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# 4. Assemble the flag
flag = ''.join(flag_parts)
print(f"\n[+] Flag: {flag}")

GIF Header Structure

Offset  Size  Description
0       3     Signature: "GIF"
3       3     Version: "87a" or "89a"
6       2     Logical Screen Width
8       2     Logical Screen Height
10      1     Packed Fields
11      1     Background Color Index
12      1     Pixel Aspect Ratio

Corrupting the version byte (position 5) is a common CTF technique for disguising GIF files. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍