Ziran OS — Filesystem CTF · Tier 1 (in-browser)

A real from-scratch 64-bit kernel is booting below — QEMU compiled to WebAssembly, not a replay. Its RAM disk hides a FLAG{…} in bytes with no directory entry: ls will never show it, and cat can't name it. Your job is to make the filesystem hand it to you anyway — by crafting a disk image whose own rules betray it. ← the guided tour · plain live boot

loading…

🚩 Captured!

You made the filesystem hand you a flag it was built to hide:

That's the whole primitive — one dishonest length field turned a bounds check into an out-of-bounds read. Remember this is the white-box sandbox: the flag lived in your tab, so it was always in reach. The uncheatable version, where the flag lives only on a server and the exploit must land over the wire, is Tier 2.

Nice work — submit a writeup to get on the solvers list.

1 · Build a disk image

The kernel's load command mounts a base64 image you paste in. Start from the presets — each one rebuilds the base64 below and explains what it does. Then hit Load into the kernel and try ls / and cat /notes.txt.

① A normal, well-formed disk with two files. The secret sits past data_end, unlisted — ls won't show it and a normal cat can't reach it.

✎ Craft mode — set every field yourself (the on-ramp for Tier 2)

The presets build fixed exploits against this disk. Craft mode is a plain ZranFS serializer: set the superblock and directory fields to whatever you like and it emits the base64 — it does not solve anything for you. This is what Tier 2 needs: there the flag sits at a per-session offset the kernel prints, past your upload, so no fixed preset can reach it — you build a fresh image aimed at your instance's offset. Blank fields stay honest. Byte layout: the ZranFS format primer.

2 · Walkthrough (spoilers — try it first)

How the disk is laid out

ZranFS v3 is dead simple. A 16-byte superblock: "ZRFS", version 3, a file count, the total_size (must equal the image length), and a data_end — the declared ceiling of the "real" data region. Then a table of 32-byte directory entries (a 20-byte name, then a 32-bit offset, length, and kind). Then the file bytes. The flag is appended after data_end, with no entry pointing at it.

Where the bug is

The strict reader (Fs::mount) does confine every file's extent to [.., data_end) — so an entry can't normally reach the flag past it. But data_end is a field inside the image, and you supply the whole image, superblock included. Validating an extent against a number the attacker also controls isn't validation. Preset ② proves the confinement is real (an over-long extent with an honest data_end is rejected — ExtentEscapesData). Preset ③ removes the guard by lying.

The exploit

Forge data_end = total_size (now the whole image counts as "data"), and stretch a listed file's length so its extent runs off the end of its own bytes and over the flag region. Mount passes every check — because you moved the ceiling — and cat /notes.txt zero-copies from its declared offset for its declared length: its own bytes, then everything after them, including the flag. That's preset ③. One dishonest field turns a bounds check into an out-of-bounds read.

Why this is a sandbox — and what Tier 2 is

Everything here runs in your tab, so the flag was always in your reach; the lesson is the primitive, not the secret. Tier 2 makes it a real capture: the kernel runs on a server, the flag lives only in that process's RAM (never in this page, never in git), and the only way to get it is to land this same exploit over the wire — develop it here, capture it there. That's the version worth bragging about.

Tier 2 is live. Develop your exploit here first — the flag is your own, so you can iterate freely — then reproduce it against the per-session offset the remote kernel prints for your instance.

Scope — what this proves, what it doesn't, and where to learn more

Be clear-eyed: this is one vulnerability class — a bounds check that trusts a length you supply, so a read runs past a logical fence (data_end) into bytes it shouldn't return: a classic out-of-bounds read (CWE-125). Two honest caveats. First, it is not memory-unsafe — the kernel slices its backing buffer in safe Rust, so every read stays inside the allocation; this is information disclosure past a logical boundary, not memory corruption or a smashed stack (src/fs.rs notes this itself). Second, it is a single, deliberately-planted instance that the strict Fs::mount in the same file already rejects — real kernels layer memory-safe languages, continuous fuzzing, and mitigations precisely to keep this class out. One bug, one path — not a model of a hardened kernel's real attack surface.

Want the fuller picture? This class is alive and hunted in production software: Google Project Zero (real-world exploit writeups) · LangSec (why parsing untrusted input is the security boundary) · pwn.college (hands-on binary exploitation, well past this one primitive).