739 words
4 minutes
UniVsThreats 26 Quals CTF - Where is everything? - Steganography Writeup

Category: Steganography
Flag: UVT{N0th1nG_iS_3mp7y_1n_sP4c3}

Challenge Description#

HTTP 404: Everything Not Found

Analysis#

I started with basic archive triage to see whether the challenge was truly “empty” or just layered. The ZIP held three files: a PNG, a whitespace-heavy TXT, and a JS artifact, which immediately suggested a multi-stage stego chain rather than a single hidden string.

file "empty.zip"
7z l "empty.zip"
empty.zip: Zip archive data, made by v2.0 UNIX, extract using at least v2.0, ...

Listing archive: empty.zip
...
2026-02-27 01:16:51 .....         1486          779  empty.png
2026-02-27 01:16:51 .....         8700         1024  empty.txt
2026-02-27 01:16:51 .....        24126         2529  empty.js
...

The JS file looked like mostly decoy text, but it contained a giant VOID_PAYLOAD string made of zero-width characters. Decoding just U+200B/U+200C as bits gave an actual ZIP stream with PK\x03\x04 magic, so that was the first real pivot point.

import re
from pathlib import Path

js = Path("empty_work/empty.js").read_text("utf-8", errors="ignore")
p = re.search(r"VOID_PAYLOAD\\s*=\\s*`(.*?)`\\s*;", js, re.S).group(1)
zw = [c for c in p if c in "\\u200b\\u200c"]
bits = "".join("0" if c == "\\u200b" else "1" for c in zw)
out = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits) - 7, 8))

print("zero_width_chars", len(zw))
print("decoded_len", len(out))
print("magic", out[:4])
zero_width_chars 7664
decoded_len 958
magic b'PK\x03\x04'

Once that hidden ZIP was carved, listing it showed a single encrypted flag.png using AES-256 Deflate, so the remaining problem was password recovery.

7z l -slt "empty_work/decoded/map_200b0_200c1.bin"
Path = empty_work/decoded/map_200b0_200c1.bin
Type = zip
Physical Size = 958

Path = flag.png
Size = 8237
Packed Size = 786
Encrypted = +
Method = AES-256 Deflate
Characteristics = NTFS WzAES : Encrypt

The whitespace document was not junk either. Decoding each line as 8 bits (space=0, tab=1) revealed operator notes explicitly hinting that the signal lives in blue-channel faint bits and must be sampled with an every-third cadence.

from pathlib import Path

raw = Path("empty_work/empty.txt").read_bytes().splitlines()
out = []
for ln in raw:
    ws = [b for b in ln if b in (0x20, 0x09)]
    if len(ws) == 8:
        bits = "".join("0" if b == 0x20 else "1" for b in ws)
        out.append(int(bits, 2))

text = bytes(out).decode("utf-8", "replace")
for i, line in enumerate(text.splitlines(), 1):
    if any(k in line for k in ["CAPTAIN", "faintest", "blue starlight", "every third heartbeat", "void is hiding"]):
        print(f"{i}:{line}")
6:CAPTAIN'S NOTE
9:The real clue is in the faintest part of the signal, not the color you see,
12:We only saw it when sampling the blue starlight... and not at every point.
13:A pattern. A cadence. Like taking every third heartbeat along the grid.
15:Once you recover the whisper from the image, it opens what the void is hiding.

That clue matched the image perfectly: pulling blue LSB bits with the row-wise cadence row0 x=0::3, row1 x=2::3, row2 x=1::3 produced a short plaintext containing the ZIP password.

from PIL import Image
import numpy as np

img = np.array(Image.open("empty_work/empty.png").convert("RGB"))
b = img[:, :, 2] & 1
seq = np.concatenate([b[0, 0::3], b[1, 2::3], b[2, 1::3]])
msg = bytes(int("".join(str(int(x)) for x in seq[i:i+8]), 2) for i in range(0, 256, 8))
print(msg.decode("latin1"))
\x00\x1dZIP_PASSWORD=D4rKm47T3rrr;END\xff

After stripping framing bytes, the password D4rKm47T3rrr decrypted the hidden archive cleanly and flag.png contained the literal flag string.

7z x -y -p"D4rKm47T3rrr" "empty_work/decoded/map_200b0_200c1.bin" -o"empty_work/final"
Extracting archive: empty_work/decoded/map_200b0_200c1.bin
...
Everything is Ok
strings -a "empty_work/final/flag.png" | rg -o 'UVT\{[^}]+\}'
UVT{N0th1nG_iS_3mp7y_1n_sP4c3}

The satisfying part here was how each artifact carried one precise clue for the next layer: zero-width payload to hidden ZIP, whitespace note to cadence rule, cadence rule to password, then final extraction.

dance

Solution#

# solve.py
#!/usr/bin/env python3.12
import re
import subprocess
from pathlib import Path

import numpy as np
from PIL import Image


def decode_void_zip(js_path: Path, out_zip: Path) -> None:
    js = js_path.read_text("utf-8", errors="ignore")
    payload = re.search(r"VOID_PAYLOAD\s*=\s*`(.*?)`\s*;", js, re.S).group(1)
    zw = [c for c in payload if c in ("\u200b", "\u200c")]
    bits = "".join("0" if c == "\u200b" else "1" for c in zw)
    raw = bytes(int(bits[i:i + 8], 2) for i in range(0, len(bits) - 7, 8))
    out_zip.write_bytes(raw)


def recover_password_from_blue_lsb(png_path: Path) -> str:
    img = np.array(Image.open(png_path).convert("RGB"))
    b = img[:, :, 2] & 1
    seq = np.concatenate([b[0, 0::3], b[1, 2::3], b[2, 1::3]])
    msg = bytes(int("".join(str(int(x)) for x in seq[i:i + 8]), 2) for i in range(0, 256, 8)).decode("latin1")

    m = re.search(r"ZIP_PASSWORD=([^;]+);", msg)
    if not m:
        raise RuntimeError("password marker not found")
    return m.group(1)


def main() -> None:
    work = Path("empty_work")
    decoded_zip = work / "decoded" / "map_200b0_200c1.bin"
    decoded_zip.parent.mkdir(parents=True, exist_ok=True)

    decode_void_zip(work / "empty.js", decoded_zip)
    password = recover_password_from_blue_lsb(work / "empty.png")

    final_dir = work / "final"
    final_dir.mkdir(parents=True, exist_ok=True)
    subprocess.run(
        [
            "7z",
            "x",
            "-y",
            f"-p{password}",
            str(decoded_zip),
            f"-o{final_dir}",
        ],
        check=True,
    )

    out = subprocess.check_output(
        "strings -a empty_work/final/flag.png | rg -o 'UVT\\{[^}]+\\}'",
        shell=True,
        text=True,
    ).strip()
    print(out)


if __name__ == "__main__":
    main()
python3.12 solve.py
UVT{N0th1nG_iS_3mp7y_1n_sP4c3}
UniVsThreats 26 Quals CTF - Where is everything? - Steganography Writeup
https://blog.rei.my.id/posts/53/univsthreats-26-quals-ctf-where-is-everything-steganography-writeup/
Author
Reidho Satria
Published at
2026-02-27
License
CC BY-NC-SA 4.0