Lab Fabricator: decoding the telemetry bench
Step-by-step solution for the Monaco playground’s `lab-fabricator` challenge—inspect the trace, reverse the spiral salt, undo the XOR, rotate the bytes, and submit the recovered flag.
The Monaco playground doubles as a telemetry lab. Clicking the JSON button
loads a mocked firmware capture named lab-fabricator. Every two entries in the
frames array correspond to one byte of the hidden flag. The entire challenge
is self-contained: the dataset, the execution sandbox, and the SHA-256 flag
verifier all sit on the same page. This walkthrough matches the playground’s
behavior exactly and walks through every transformation until the final flag,
flag{monaco_fullstack_kernel_ctf}, appears.
Tools and fixtures in the playground
Once you open /playground, click Load dataset. The JSON panel should show:
{
"challenge": "lab-fabricator",
"frames": [39, 52, 103, 185, 236, 255, "..."],
"key": [19, 55, 192, 222, 66, 153],
"spiral": "n^2 + 11n + 7 (mod 256)"
}
Important observations:
- Frames arrive as
[A0, B0, A1, B1, ...]. Pair indexidx = i / 2. - The six-byte key repeats frequently and is intentionally short so that three different key schedules overlap.
- The
spiralfunction is deterministic per index:spiral(idx) = (idx² + 11·idx + 7) & 0xff. - The playground ships with
FLAG_HASH_HEX = 7f2d7991bf72a3640057e7cf08875921d4b5779e2e3a66396f146d7e31a9f994. Your recovered string must hash to that value.
Step 1 – Inspecting the capture
Before reversing anything, print the first few pairs and confirm the structure:
const ctx = typeof input === "object" ? input : {};
const frames = ctx.frames ?? [];
const key = ctx.key ?? [];
for (let i = 0; i < 6; i += 2) {
const idx = i / 2;
console.log(idx, "A:", frames[i], "B:", frames[i + 1]);
}
console.log("pairs:", frames.length / 2, "key length:", key.length);
The log confirms there are 34 pairs (68 numbers) and the key length is 6. From here the decoding pipeline is the reverse of the encoder’s order.
Step 2 – Frame integrity check
Each pair is encoded with a “sanity guard”. If the capture is untrusted, verify that guard before doing more work. The condition enforced during encoding is:
B ^ key[(idx * 3) % key.length] === A
Add this check to your loop and throw if any pair fails. It protects against
bitrot or tampering and mirrors the Frame integrity scan example bundled with
the playground.
Step 3 – Remove the spiral salt
After the guard, the encoder added an index-specific bias. Undo it by subtracting the spiral value and wrapping inside an 8-bit lane:
salted = (A - spiral(idx) + 256) & 0xff
The + 256 keeps the subtraction positive before masking. Implement spiral
exactly as declared in the dataset string:
const spiral = (i: number) => (i * i + 11 * i + 7) & 0xff;
Step 4 – Undo the XOR mask
The salted value was masked again with the primary key schedule. Strip it by
XORing with key[idx % key.length]:
masked = salted ^ key[idx % key.length]
At this point each masked byte is still rotated.
Step 5 – Rotate the byte back into place
The encoder rotated the byte three positions to the left. Undoing that is a right rotation by three. A small helper keeps the math readable:
const rotRight8 = (value: number, count: number) =>
((value >>> count) | (value << (8 - count))) & 0xff;
Applying rotRight8(masked, 3) yields the true ASCII byte.
Putting it together
Drop the following decoder into the Monaco editor (or load it from the Examples dropdown if you saved a share link). It mirrors the steps above and prints the final flag:
const ctx = typeof input === "object" && input !== null ? input : {};
const frames = Array.isArray(ctx.frames) ? ctx.frames : [];
const key = Array.isArray(ctx.key) ? ctx.key : [];
if (!frames.length || !key.length) {
throw new Error("Load the lab-fabricator dataset first.");
}
const spiral = (i) => (i * i + 11 * i + 7) & 0xff;
const rotRight8 = (value, count) =>
((value >>> count) | (value << (8 - count))) & 0xff;
const bytes = [];
for (let i = 0; i < frames.length; i += 2) {
const idx = i / 2;
const A = frames[i];
const B = frames[i + 1];
const guardKey = key[(idx * 3) % key.length];
if ((B ^ guardKey) !== A) {
throw new Error(\`Frame pair \${idx} failed integrity check\`);
}
const salted = (A - spiral(idx) + 256) & 0xff;
const masked = salted ^ key[idx % key.length];
bytes.push(rotRight8(masked, 3));
}
const flag = String.fromCharCode(...bytes);
console.log("Recovered flag:", flag);
return flag;
Running it prints:
Recovered flag: flag{monaco_fullstack_kernel_ctf}
Copy the string into the verifier underneath the console. The verifier hashes
the candidate with crypto.subtle.digest("SHA-256", value) and compares it to
FLAG_HASH_HEX. If everything matches, the status pill turns green and the hash
is echoed back for sanity.
Troubleshooting
- Integrity check fails immediately – make sure you are reading
frames[i]asAandframes[i + 1]asB. Off-by-one errors or iterating by 1 instead of 2 are the usual culprits. - Verifier rejects the flag – verify the rotation direction. Rotating left instead of right yields printable text but hashes to something else.
- Nothing prints – the Monaco worker aborts executions that exceed the time
limit. Keep the loop synchronous and avoid accidentally awaiting
console.
Going beyond the base puzzle
- Swap in your own dataset by editing the JSON panel. As long as you follow the
same shape—
frames,key,spiral—the decoder above can process it. - Modify the polynomial, the guard, or the rotation count to create harder variants. Share them via the Share button; the link encodes the current code, input, and time limit.
- Extend the verifier. Because the worker exposes the Web Crypto API you can build MACs, signature checks, or even embed a second stage challenge.
If you create a nastier telemetry log or expand the tooling, send it my way. The Monaco playground is meant to evolve with community-built traces, and I’ll keep linking the best ones from this article.