← Back to Writeups
CUSTOMMediumSteganography

Странное видео (Strange Video)

XESXOR9/2/20266 min read
#steganography#custom#medium#braille_dot_position_decoding#hidden_url_discovery#morse_code_decoding#mp4_atom_parsing#multi_layer_encoding_chain#video_metadata_extraction#whitespace_binary_encoding#linux

Странное видео (Strange Video)

Platform: hackerlab | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-03 | Status: Solved Techniques: braille_dot_position_decoding, hidden_url_discovery, morse_code_decoding, mp4_atom_parsing, multi_layer_encoding_chain, video_metadata_extraction, whitespace_binary_encoding

Summary

Task: a ZIP with msg.txt containing a hidden Vimeo URL among empty lines, leading to an MP4 with whitespace data appended after video atoms. Solution: 6-layer decode chain — whitespace binary → ASCII → binary bytes → base64 → Morse code (digits 1-6) → Braille dot positions → flag.

Странное видео (Strange Video) — Hackerlab

Description

Странное видео, это точно. Но жутко интересное! Подсказка 1: Иногда даже с помощью пустоты можно записать текст Подсказка 2: В 1838 году появился знаменитый способ кодирования сообщений - это ...

English summary: A "strange video" challenge with two hints — one about writing text with emptiness (whitespace steganography), and one referencing 1838 (Morse code). The task provides a ZIP file containing msg.txt. The goal is to follow a multi-layer trail of hidden data through decoy Rick Rolls to the actual encoded flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

The challenge is a 6-layer steganography puzzle combining misdirection (Rick Roll decoys), whitespace steganography, Morse code, and Braille encoding.

Layer 0 — Hidden URL in msg.txt: The file is 6881 bytes with 6727 lines. Only 2 lines contain visible text: a "no flag here" message and a YouTube Rick Roll link. Hidden at line 3222 (buried among 6725 empty lines) is the real URL: https://vimeo.com/911593386?share=copy.

Layer 1 — Vimeo video metadata: The Vimeo video (title: "Strange", uploader: "Not a strange person") is also a Rick Roll, but its description contains a Mega download link: https://mega.nz/file/JRYFXaSK#_WuZVG_JdKdR_kmocV3_iZ1B0hhEDuwKwE4OPPZuceE. Extracted using yt-dlp --dump-json. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Layer 2 — Appended data in MP4: The downloaded strange.mp4 (1,304,672 bytes) has MP4 atoms totaling only 1,185,883 bytes (ftyp: 24 + moov: 4954 + mdat: 1,180,905). The remaining 118,789 bytes after the video data consist exclusively of three byte values: space (0x20), tab (0x09), and newline (0x0a).

Layers 3-6 — Multi-layer decoding chain: The whitespace data encodes through four successive layers: whitespace binary → ASCII characters → binary bytes → base64 → Morse-encoded digits → Braille letters. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Extract hidden URL from msg.txt

python
# msg.txt has 6727 lines, mostly empty. Find non-empty lines:
with open('msg.txt') as f:
    for i, line in enumerate(f, 1):
        if line.strip():
            print(f"Line {i}: {line.strip()}")
# Line 1: There is no flag in this file :)
# Line 2: But so be it, I'll tell you where he is: https://www.youtube.com/watch?v=QNXvE1BZu8g
# Line 3222: https://vimeo.com/911593386?share=copy

The YouTube link is a Rick Roll (decoy). The Vimeo link is the real next step. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Extract Mega link from Vimeo description

bash
yt-dlp --dump-json "https://vimeo.com/911593386?share=copy" > meta.json
# Description contains: "If you want to download: https://mega.nz/file/JRYFXaSK#_WuZVG_JdKdR_kmocV3_iZ1B0hhEDuwKwE4OPPZuceE"
megadl "https://mega.nz/file/JRYFXaSK#_WuZVG_JdKdR_kmocV3_iZ1B0hhEDuwKwE4OPPZuceE"

Step 3: Extract whitespace data from MP4

python
# Parse MP4 atoms to find where video data ends
import struct

with open('strange.mp4', 'rb') as f:
    data = f.read()
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Parse atoms: ftyp (24) + moov (4954) + mdat (1180905) = 1185883 bytes
offset = 0
while offset < len(data):
    size = struct.unpack('>I', data[offset:offset+4])[0]
    atype = data[offset+4:offset+8].decode('ascii', errors='replace')
    print(f"Atom: {atype} at {offset}, size {size}")
    if size == 0:
        break
    offset += size

# Extract trailing data after last atom
trailing = data[1185883:]  # 118,789 bytes of whitespace

Step 4: Decode whitespace → binary → ASCII → binary bytes → base64

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

python
#!/usr/bin/env python3
"""Complete decode chain from whitespace to Morse+Braille digits."""
import base64

with open('strange.mp4', 'rb') as f:
    raw = f.read()

# Extract trailing whitespace data (after MP4 atoms end at 1185883)
trailing = raw[1185883:]

# Split on tab+newline separators to get data lines
lines = trailing.split(b'\t\n')

# Layer 3: Each line encodes one ASCII char (space=0, tab=1 → binary → char)
chars = []
for line in lines:
    if not line or line == b'\n':
        continue
    line = line.strip(b'\n')
    if not line:
        continue
    binary = ''
    for b in line:
        if b == 0x20:    # space = 0
            binary += '0'
        elif b == 0x09:  # tab = 1
            binary += '1'
    if binary:
        chars.append(chr(int(binary, 2)))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

ascii_text = ''.join(chars)  # "01001101 01010100 01000001..."

# Layer 4: Binary bytes → ASCII → base64
binary_groups = ascii_text.split(' ')
raw_bytes = bytes(int(bg, 2) for bg in binary_groups if bg)
b64_text = raw_bytes.decode('ascii')

# Base64 decode
decoded = base64.b64decode(b64_text).decode('ascii')
print(decoded)
# Output: "10000    01111 00001    01111 00011 00000    ..."

Step 5: Morse code → digits 1-6

The decoded data contains 5-digit binary groups (and one 6-digit group for underscore), with { and } as literal delimiters. Each group uses 0=dot, 1=dash in Morse code: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
MORSE_NUMBERS = {
    '.----': '1', '..---': '2', '...--': '3',
    '....-': '4', '.....': '5', '-....': '6',
    '..--.-': '_',  # underscore in Morse
}

# Parse: 4 spaces separate "words", single space separates groups within a word
words = decoded.split('    ')
digit_words = []
for word in words:
    word = word.strip()
    if word in ['{', '}']:
        digit_words.append(word)
        continue
    groups = word.split(' ')
    digits = ''
    for g in groups:
        morse = g.replace('0', '.').replace('1', '-')
        digits += MORSE_NUMBERS.get(morse, '?')
    digit_words.append(digits)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Result: ['6', '14', '135', '145', '15', '12', '13456', '{',
#           '2345', '125', '24', '234', '_', '24', '234', '_',
#           '12', '1235', '1', '24', '123', '123', '15', '_',
#           '1', '1345', '145', '_', '134', '135', '1235', '234', '15', '_',
#           '134', '234', '1245', '}']

Step 6: Braille dot positions → letters

The digits 1-6 correspond to the 6 dot positions in a Braille cell:

  1 4
  2 5
  3 6

Each "word" specifies which dots are raised:

python
#!/usr/bin/env python3
"""Braille decoding: digit sequences → raised dot positions → letters."""
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

BRAILLE = {
    frozenset([1]): 'A',           frozenset([1,2]): 'B',
    frozenset([1,4]): 'C',         frozenset([1,4,5]): 'D',
    frozenset([1,5]): 'E',         frozenset([1,2,4]): 'F',
    frozenset([1,2,4,5]): 'G',     frozenset([1,2,5]): 'H',
    frozenset([2,4]): 'I',         frozenset([2,4,5]): 'J',
    frozenset([1,3]): 'K',         frozenset([1,2,3]): 'L',
    frozenset([1,3,4]): 'M',       frozenset([1,3,4,5]): 'N',
    frozenset([1,3,5]): 'O',       frozenset([1,2,3,4]): 'P',
    frozenset([1,2,3,4,5]): 'Q',   frozenset([1,2,3,5]): 'R',
    frozenset([2,3,4]): 'S',       frozenset([2,3,4,5]): 'T',
    frozenset([1,3,6]): 'U',       frozenset([1,2,3,6]): 'V',
    frozenset([2,4,5,6]): 'W',     frozenset([1,3,4,6]): 'X',
    frozenset([1,3,4,5,6]): 'Y',   frozenset([1,3,5,6]): 'Z',
    frozenset([6]): 'CAP',         # Capital letter indicator (⠠)
}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

words = ["6", "14", "135", "145", "15", "12", "13456",
         "{",
         "2345", "125", "24", "234", "_", "24", "234", "_",
         "12", "1235", "1", "24", "123", "123", "15", "_",
         "1", "1345", "145", "_", "134", "135", "1235", "234", "15", "_",
         "134", "234", "1245",
         "}"]

result = ''
for w in words:
    if w in ['{', '}', '_']:
        result += w
        continue
    dots = frozenset(int(d) for d in w)
    char = BRAILLE.get(dots, '?')
    if char == 'CAP':
        continue  # Capital indicator, skip
    result += char
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

print(f"Flag: {result}")
# Flag: CODEBY{REDACTED}

Decoding table:

Digit wordDots raisedBrailleLetter
6{6}⠠(capital indicator)
14{1,4}⠉C
135{1,3,5}⠕O
145{1,4,5}⠙D
15{1,5}⠑E
12{1,2}⠃B
13456{1,3,4,5,6}⠽Y
2345{2,3,4,5}⠞T
125{1,2,5}⠓H
24{2,4}⠊I
234{2,3,4}⠎S
1235{1,2,3,5}⠗R
1{1}⠁A
123{1,2,3}⠇L
1345{1,3,4,5}⠝N
134{1,3,4}⠍M
1245{1,2,4,5}⠛G
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍