← Back to Writeups
CUSTOMMediumSteganography

Writeup

XESXOR9/2/20264 min read
#steganography#custom#medium#hex_dump_invisible_character_d#morse_code_binary_decoding#zero_width_unicode_extraction

Writeup

Platform: hackerlab | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-04-08 | Status: Solved Techniques: hex_dump_invisible_character_detection, morse_code_binary_decoding, zero_width_unicode_extraction

Summary

Task: a text file with visible Russian text hides invisible zero-width Unicode characters (U+200B and U+2063) between lines. Solution: extract the two types of zero-width characters, interpret them as Morse code dashes and dots respectively, decode each line to get CODEBY{REDACTED}.

Writeup — Hackerlab

Description

Пытался сделать райтап на недавний таск с Киберколизея, но что-то пошло не так

English summary: we receive a ZIP archive containing task.txt (1619 bytes). The file contains visible Russian text that looks like a blog post about a CyberColiseum 2 CTF writeup. The goal is to find a hidden flag.

Analysis

The visible text is a short blog post (about 500 characters of visible content), but the file is 1619 bytes — suspiciously large. The text itself contains several hints: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. "CTRL + V" — the author mentions pasting, but "practically nothing was copied," suggesting invisible content was pasted alongside visible text.
  2. "Юни[код]" — the word "Unicode" is split with brackets, drawing attention to Unicode encoding specifically.

Running a hex dump reveals clusters of invisible bytes between lines of visible text. Only two types of invisible UTF-8 sequences appear:

  • E2 80 8B → U+200B (Zero-Width Space, ZWS) — 35 occurrences
  • E2 81 A3 → U+2063 (Invisible Separator, IS) — 28 occurrences ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Total: 63 invisible characters arranged in 17 groups (one group per line or between visible text lines).

Since exactly two distinct symbols are used, this is a binary encoding. The pattern matches Morse code: one symbol for dashes, one for dots.

Solution

Step 1: Extract invisible characters

Using hex analysis or a Python script, extract all zero-width characters from the file, preserving their grouping by line:

python
#!/usr/bin/env python3
with open("task.txt", "r", encoding="utf-8") as f:
    text = f.read()
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

ZWS = '\u200b'   # Zero-Width Space
IS  = '\u2063'   # Invisible Separator

# Extract only invisible characters, split by newlines
lines = text.split('\n')
for i, line in enumerate(lines, 1):
    hidden = ''.join(c for c in line if c in (ZWS, IS))
    if hidden:
        symbols = hidden.replace(ZWS, '-').replace(IS, '.')
        print(f"Line {i:2d}: {symbols}")

Step 2: Decode Morse code

Each group of invisible characters represents one Morse code character:

LinePatternMorseChar
2-.-.CC
4---OO
6-..DD
8.EE
12-...BB
14-.--YY
16-.--. ({
18--..ZZ
19...--33
20.-.RR
21-----00
22.--WW
23.----11
24-..DD
25-TT
26....HH
27-.--.-)}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: Assemble the flag

The decoded characters spell: CODEBY(Z3R0W1DTH)

Morse code uses -.--. for ( and -.--.- for ). Replacing parentheses with curly braces per the flag format: CODEBY{REDACTED}

The flag content "z3r0w1dth" is leetspeak for "zero width" — a self-referencing hint to the zero-width character steganography technique used in the challenge.

Full solve script

python
#!/usr/bin/env python3
"""Solve: Hackerlab - Writeup (Stego 350)
Extract zero-width Unicode chars from task.txt, decode as Morse code."""
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

MORSE = {
    '.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D', '.': 'E',
    '..-.': 'F', '--.': 'G', '....': 'H', '..': 'I', '.---': 'J',
    '-.-': 'K', '.-..': 'L', '--': 'M', '-.': 'N', '---': 'O',
    '.--.': 'P', '--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T',
    '..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X', '-.--': 'Y',
    '--..': 'Z', '.----': '1', '..---': '2', '...--': '3',
    '....-': '4', '.....': '5', '-....': '6', '--...': '7',
    '---..': '8', '----.': '9', '-----': '0',
    '-.--.': '(', '-.--.-': ')',
}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

ZWS = '\u200b'  # Zero-Width Space  → dash
IS  = '\u2063'  # Invisible Separator → dot

with open("task.txt", "r", encoding="utf-8") as f:
    text = f.read()

flag = ""
for line in text.split('\n'):
    hidden = ''.join(c for c in line if c in (ZWS, IS))
    if hidden:
        morse = hidden.replace(ZWS, '-').replace(IS, '.')
        char = MORSE.get(morse, '?')
        flag += char

# Replace Morse parentheses with curly braces for flag format
flag = flag.replace('(', '{').replace(')', '}')
print(f"Flag: {flag}")

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

Output: Flag: CODEBY{REDACTED} → lowercase: CODEBY{REDACTED} ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍