Skip to content

Forensics

Flag: SAS{1_d1d_my_f1rst_add_5_when_1_w4s_7} (submit verbatim — the drawn image has no _ between a and dd_5)Flag: RTRTNI26{43.431, 39.933} — server-validated First Blood, 300 base points + 20 bonus.

Created

Updated

6 min read

Reading time

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

Source-engine VTF alpha-mask multi-layer stego (.bsp decals) → hidden multiline flag

Event NameSAS CTF 2026
GitHub URL-
Challenge NameEscape from Guba II (game, TF2/FlagFortress2, id=16)
Attachments
  • Manual upload: solver.py, dirtmap_00.vtfdirtmap_09.vtf (from the bsp pakfile)
References

Flag: SAS{1_d1d_my_f1rst_add_5_when_1_w4s_7} (submit verbatim — the drawn image has no _ between a and dd_5)

  • Root cause: the TF2 map ctf_guba.bsp embeds (in its pakfile = a zip inside the .bsp) 10 custom decal textures materials/concrete/dirtmap_00.vtf … dirtmap_09.vtf (VTF v7.2, 1024×1024, BGRA8888, ONEBITALPHA, 6 mips). The base color looks like dirt; the payload is in the alpha channel of mip level 2 (256×256). Each alpha mask alone is ~50% random dither (noise).
  • Exploit chain: each dirtmap is placed in-map as an infodecal on the same wall plane (x=484) at slightly different (y,z) origins (from bsp entity lump 0). Align the 10 masks by their decal origins and keep pixels whose overlap count ∈ [2..8] → the carved region renders a multiline message; concatenating the drawn lines gives the flag.
  • Decal origins (y,z), all at x=484: 00(-274,111) 01(-253,101) 02(-304,109) 03(-247,108) 04(-302,106) 05(-240,77) 06(-247,139) 07(-270,134) 08(-247,130) 09(-286,89).
  • Caveat: naive same-coordinate overlay is just noise — alignment by the in-map decal origins is required. VTF mips are stored smallest→largest; this VTF has no low-res thumbnail so high-res data starts at offset 80. Exact pixel registration uses the in-engine decal world-scale (player read the [2..8] overlay directly in-engine on the x=484 wall); flag is player-validated.
why it is vulnerable
# 10 decals look like ordinary dirt, but the SECRET is in alpha @ the 256x256 mip,
# and is only legible when the masks are stacked at their in-map decal origins:
#   - VTF: BGRA8888 + ONEBITALPHA  -> alpha channel carries a 1-bit mask
#   - each mask ~50% coverage (noise) so a single texture leaks nothing
#   - overlap COUNT in [2..8] (i.e. "covered by some-but-not-all") draws the glyphs
# i.e. the message is encoded in the *agreement pattern* across 10 aligned noise masks.
exploit payload
# extract the 256x256 mip alpha of each dirtmap, then composite count in [2..8]
off = hdr_size(=80) + sum(smaller-mip byte sizes)      # mips stored smallest->largest
alpha = frombuffer(vtf[off:off+256*256*4]).reshape(256,256,4)[:,:,3]   # BGRA -> A is index 3
bin = alpha > 127
# place each bin mask at its (y,z) decal origin (x=484 plane), sum, keep 2<=count<=8 -> flag
solver
import struct, zipfile, numpy as np
from PIL import Image
z = zipfile.ZipFile("ctf_guba.bsp")            # Source bsp pakfile is a trailing zip
def mip256_alpha(d):
    hdr = struct.unpack_from("<I", d, 12)[0]; w,h = struct.unpack_from("<HH", d, 16)
    assert struct.unpack_from("<I", d, 0x34)[0] == 12   # BGRA8888
    mips = d[0x38]; off = hdr                            # no low-res thumbnail here
    for (mw, mh) in reversed([(w>>i, h>>i) for i in range(mips)]):   # smallest first
        if (mw, mh) == (256, 256):
            a = np.frombuffer(d[off:off+mw*mh*4], np.uint8).reshape(mh, mw, 4)[:, :, 3]
            return a
        off += mw*mh*4
masks = [mip256_alpha(z.read(f"materials/concrete/dirtmap_{i:02d}.vtf")) > 127 for i in range(10)]
ORIG = {0:(-274,111),1:(-253,101),2:(-304,109),3:(-247,108),4:(-302,106),
        5:(-240,77),6:(-247,139),7:(-270,134),8:(-247,130),9:(-286,89)}   # (y,z) @ x=484
cy = (min(o[0] for o in ORIG.values())+max(o[0] for o in ORIG.values()))/2
cz = (min(o[1] for o in ORIG.values())+max(o[1] for o in ORIG.values()))/2
SCALE = 1.0; pad = 64; H = W = 256 + 2*pad; acc = np.zeros((H, W), int)
for i in range(10):
    y, z = ORIG[i]; dx = round((y-cy)*SCALE); dy = round(-(z-cz)*SCALE)
    c = np.zeros((H, W), int); c[pad+dy:pad+dy+256, pad+dx:pad+dx+256] = masks[i]; acc += c
Image.fromarray((((acc>=2)&(acc<=8))*255).astype("uint8")).save("reveal_aligned.png")  # read flag; tune SCALE
# -> SAS{1_d1d_my_f1rst_add_5_when_1_w4s_7}

Eye of the Witness — phone-video POV geolocation and validator-format trap

Event NameRTR II Cyber Competition - 2026
Source URLChallenge page
Challenge NameEye of the Witness (OSINT, 300 points)
Attachments
  • Original filename: solver.py
  • Rename solver.txt to solver.py after download
  • Corrected solver SHA-256: 4ce4fee4da4b0e9bb30a310320f728a398ee0f93117cc87c8b0698c151ab8ee8
  • Challenge artifact Drone.mp4 SHA-256: 1d8d606687e584e176a700ca426b237dc3077ea075acec3d8f8114f307690273
References

Flag: RTRTNI26{43.431, 39.933} — server-validated First Blood, 300 base points + 20 bonus.

  • Triage: the MP4 is a re-encoded vertical phone video with H.264/AAC only. It contains no GPS, telemetry, useful creation timestamp, subtitle, hidden data stream, or recoverable sign text.
  • Source match: Exilenova+ 11112 contains the exact sequence. Post 11115 replies with the same two-coach frame inset on satellite imagery and prints the operator point as Lat,Long 43.431717894702786, 39.93346527459446.
  • Physical corroboration: the Yandex panorama reproduces the unusual E97 acoustic barrier, railway catenary, service lane, and coach yard. Independent registration of the yellow POV marker lands at about 43.431756, 39.933403.
  • Range check: the new flash appears around 24.333 s and its report begins around 25.618 s. The 1.28–1.32 s light-to-sound delay implies roughly 441–452 m, agreeing with the approximately 445 m POV-to-tank geometry.
  • Validator trap: the description says RTRTNI26{Long, Lat}, but the live checker expects Lat, Long, truncation rather than rounding, the wrapper, and a literal space after the comma:
    • latitude 43.431717...43.431
    • longitude 39.933465...39.933
    • exact accepted string → RTRTNI26{43.431, 39.933}
why it is vulnerable

The attachment leaks no direct coordinates. The solve pivots from a distinctive source-frame fingerprint (two coaches, transparent E97 barrier, catenary, blue-roofed yard) to a public reply that discloses the witness/operator point. The final obstacle is a contradictory validator: obeying the displayed longitude-first order or normal rounding produces a wrong flag even with the correct physical location.

exploit payload
published point (Lat,Long): 43.431717894702786, 39.93346527459446
truncate to 3 dp:             43.431, 39.933
wrap exactly:                RTRTNI26{43.431, 39.933}
server:                      CHALLENGE SOLVED! +320 Pts
solver
#!/usr/bin/env python3
"""Verify the supplied artifact and reproduce the server-accepted CTF flag."""

from __future__ import annotations

import argparse
import hashlib
from decimal import Decimal, ROUND_DOWN
from pathlib import Path


EXPECTED_SHA256 = "1d8d606687e584e176a700ca426b237dc3077ea075acec3d8f8114f307690273"

# Exilenova+ post 11115 labels these as the phone operator's Lat,Long.
OPERATOR_LAT = Decimal("43.431717894702786")
OPERATOR_LON = Decimal("39.93346527459446")


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def truncate_three_decimals(value: Decimal) -> str:
    """Match the challenge validator: truncate positive coordinates to 3 dp."""
    return str(value.quantize(Decimal("0.001"), rounding=ROUND_DOWN))


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "video",
        nargs="?",
        type=Path,
        default=Path(__file__).with_name("Drone.mp4"),
    )
    args = parser.parse_args()

    actual = sha256(args.video)
    if actual != EXPECTED_SHA256:
        raise SystemExit(
            f"unexpected attachment SHA-256: {actual}\nexpected: {EXPECTED_SHA256}"
        )

    # The live validator contradicts the displayed Long,Lat instruction. It
    # accepts Lat, Long, truncated to three decimals, including this spacing.
    print(
        "RTRTNI26{"
        f"{truncate_three_decimals(OPERATOR_LAT)}, "
        f"{truncate_three_decimals(OPERATOR_LON)}"
        "}"
    )


if __name__ == "__main__":
    main()

Share this note

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.