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
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.
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.
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.
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.
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.
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.
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.
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).