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.png2026-02-27 01:16:51 ..... 8700 1024 empty.txt2026-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 refrom 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 7664decoded_len 958magic 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.binType = zipPhysical Size = 958
Path = flag.pngSize = 8237Packed Size = 786Encrypted = +Method = AES-256 DeflateCharacteristics = NTFS WzAES : EncryptThe 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 NOTE9: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 Imageimport numpy as np
img = np.array(Image.open("empty_work/empty.png").convert("RGB"))b = img[:, :, 2] & 1seq = 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\xffAfter 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 Okstrings -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.

Solution
# !/usr/bin/env python3.12import reimport subprocessfrom pathlib import Path
import numpy as npfrom 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.pyUVT{N0th1nG_iS_3mp7y_1n_sP4c3}