archlinux / r3t0x@portfolio
ALL SYSTEMS NOMINAL /
← Back to archive · Misc

Securinets Friendly CTF 2026 / Misc WRITEUP

APPROVED

No further action was requested.

EVENTSecurinets Friendly CTF 2026CATEGORYMiscDIFFICULTYmediumPOINTS500
misc

Walkthrough

Challenge

The player receives only the extensionless file final. It is a valid TeX DVI document composed entirely of rules, so it requires no external fonts.

Mechanism

DVI is a small page-description virtual machine. Its horizontal w and x register instructions both store a signed distance and move the cursor by that distance. Because this document never reuses either register with w0 or x0, the following instruction families render identically:

w1..w4 distance  -> hidden bit 0
x1..x4 distance  -> hidden bit 1

The operand width is independently varied between three and four bytes. NOPs, ordinary right/down movements, stack operations, and random y/z vertical choices provide unrelated bytecode noise.

The four physical pages are deliberately stored out of order. The first of the ten counters in each bop instruction gives its logical page number.

Solution

  1. Run file final to identify TeX DVI version 2.
  2. Parse the preamble and each bop/eop page body.
  3. Record 0 for explicit w1..w4 instructions and 1 for x1..x4.
  4. Sort the bit arrays by each page's first bop counter.
  5. Concatenate them and pack bits most-significant-bit first.

The resulting data is framed as:

"SEAL" | uint16 flag length | flag | CRC32(flag)

Run the reference solver:

python3 solver.py

Output:

Securinets{apparently_f1nal_d0es_n0t_mean_st0p_l00king}

Regenerate the handout with python3 make.py.

Reference solver

The reference solver supplied with the writeup:

#!/usr/bin/env python3

from pathlib import Path
import struct
import zlib


MAGIC = b"SEAL"


def unpack_bits(bits: list[int]) -> bytes:
    output = bytearray()
    for offset in range(0, len(bits) - 7, 8):
        value = 0
        for bit in bits[offset:offset + 8]:
            value = (value << 1) | bit
        output.append(value)
    return bytes(output)


def extract_pages(data: bytes) -> list[tuple[int, list[int]]]:
    if data[:2] != bytes([247, 2]):
        raise ValueError("not a version-2 DVI file")
    position = 14
    comment_length = data[position]
    position += 1 + comment_length
    pages = []

    while position < len(data):
        opcode = data[position]
        position += 1
        if opcode == 248:
            break
        if opcode == 138:
            continue
        if opcode != 139:
            raise ValueError(f"unexpected top-level opcode {opcode}")

        counters = struct.unpack(">10i", data[position:position + 40])
        position += 44  # ten counters plus previous-page pointer
        bits = []

        while True:
            opcode = data[position]
            position += 1
            if opcode == 140:
                break
            if opcode <= 127 or opcode in (138, 141, 142, 147, 152, 161, 166):
                continue
            if 128 <= opcode <= 131 or 133 <= opcode <= 136:
                position += (opcode - 127) if opcode <= 131 else (opcode - 132)
            elif opcode in (132, 137):
                position += 8
            elif 143 <= opcode <= 146:
                position += opcode - 142
            elif 148 <= opcode <= 151:
                bits.append(0)
                position += opcode - 147
            elif 153 <= opcode <= 156:
                bits.append(1)
                position += opcode - 152
            elif 157 <= opcode <= 160:
                position += opcode - 156
            elif 162 <= opcode <= 165:
                position += opcode - 161
            elif 167 <= opcode <= 170:
                position += opcode - 166
            elif 171 <= opcode <= 234:
                pass
            elif 235 <= opcode <= 238:
                position += opcode - 234
            elif 239 <= opcode <= 242:
                size = opcode - 238
                length = int.from_bytes(data[position:position + size], "big")
                position += size + length
            else:
                raise ValueError(f"unsupported page opcode {opcode}")

        pages.append((counters[0], bits))

    return pages


def solve(path: Path) -> bytes:
    pages = sorted(extract_pages(path.read_bytes()), key=lambda item: item[0])
    if [number for number, _ in pages] != list(range(len(pages))):
        raise ValueError("logical page counters are incomplete")
    packed = unpack_bits([bit for _, page_bits in pages for bit in page_bits])
    if not packed.startswith(MAGIC):
        raise ValueError("bad payload marker")
    length = struct.unpack(">H", packed[4:6])[0]
    flag = packed[6:6 + length]
    stored_crc = struct.unpack(">I", packed[6 + length:10 + length])[0]
    if zlib.crc32(flag) & 0xFFFFFFFF != stored_crc:
        raise ValueError("payload CRC mismatch")
    return flag


if __name__ == "__main__":
    print(solve(Path(__file__).with_name("final")).decode("ascii"))

Flag

Securinets{apparently_f1nal_d0es_n0t_mean_st0p_l00king}
Published 2026-09-28 · by r3t0x← Back to archive · Misc
0xSEARCH_INDEX

Trace a memory.

INDEX ONLINE0 addresses
↑↓ TRACEENTER OPENESC DISMISS