Challenge: Warmerer Up · CTF: DalCTF 2026 · Category: Forensics · Difficulty: Medium
TL;DR
A 4.9 MB “rules” PDF that should have been a few kilobytes turned out to be a Russian doll:
| |
Peel every layer and you get the flag.
The hook
The challenge name “Warmerer Up” and the description “What, what, the rules again again?” are doing a lot of work. There was an earlier challenge called Warmer Up that involved a rules.pdf, and this one ships a rules2.pdf. The repeated “again again” / “what what” is the author winking at you: yes, it’s the rules PDF again, but look harder this time.
So we start with a single file: rules2.pdf.
Step 1 — Trust your eyes on file size
The very first thing that should set off alarm bells is the size. This is a 3-page rules document, but the file is 4.9 MB:
| |
Three pages of plain text rendered by pdf-lib from a markdown file. There is no good reason for that to be 5 MB. Something is hiding in there.
Step 2 — Where is the weight?
binwalk shows the file is wall-to-wall zlib streams — hundreds of them — which is normal-ish for a PDF, but the sheer count is unusual:
| |
Two standard forensics checks for PDFs come back empty:
| |
So pdfimages says there are no images, yet the file is enormous. That contradiction is the whole challenge. Time to crack the PDF open by hand.
Step 3 — Reading the PDF object tree
Using mutool show to dump the raw objects, the document catalog (object 1) immediately stands out:
| |
The catalog references /X0 through /X359 — 360 XObjects (objects 292–651). This is bizarre for two reasons:
- XObjects normally live in a page’s
/Resources, not bolted directly onto the document catalog. Putting them on the catalog means they are part of the file but never drawn on any page — perfect for hiding data. - There are exactly 360 of them, which smells like chunked data rather than legitimate graphics.
Let’s look at one:
| |
A “1×1 grayscale image” whose stream is 13 KB of base64 text. That is not an image — a real 1×1 grayscale pixel is one byte. This is pdfimages’s blind spot too: it tried to interpret these as images, saw the dimensions made no sense, and silently skipped them.
Two details give the game away completely:
- The chunk is wrapped in markers:
@@0:at the start and@@endat the end. The number is an ordering index. - The base64 decodes to bytes starting
UEsDBBQ…. Decode the first few characters and you get50 4B 03 04—PK\x03\x04, the ZIP local-file-header magic. And right there in the readable part of the base64 you can spot the filenameimage.sif.
So the 360 XObjects are a base64-encoded ZIP file, split into 360 ordered pieces, each smuggled inside a fake 1×1 image attached to the PDF catalog.
💡 Why 1×1 images on the catalog? It’s a clean stego trick: the data is structurally valid PDF (it parses, it opens, it prints the rules fine), the objects are never rendered, and casual tools like
pdfimages/pdftotextshow nothing interesting. You only find it by reading the object graph.
Step 4 — Reassembling the ZIP
Now it’s just plumbing. For each object 292–651, pull the stream, strip the @@N: prefix and @@end suffix and any whitespace/newlines, sort by the index N, concatenate, and base64-decode:
| |
| |
A clean ZIP:
| |
Step 5 — The encrypted ZIP and the password hiding in plain sight
The archive is password-protected (the ZIP general-purpose bit flag 0x09 — encryption + deflate — is visible in the local header …\x14\x00\x09\x00…). Where’s the password?
Back to the actual content of the PDF. If you read the rules text all the way to the end with pdftotext, the document signs off with two out-of-place lines:
| |
teapot_2026 is a dangling token with no business being in a rules document. (It’s also a cute nod to HTTP 418 “I’m a teapot” — appropriate for a challenge all about knowing the rules.) Try it as the ZIP password:
| |
It works. We now have a Singularity / Apptainer container image (.sif).
Step 6 — Cracking open the SIF container
A SIF image is a header + descriptors wrapping a root filesystem. A quick strings grep tells us exactly what to look for, and reveals the container’s build definition embedded in its metadata:
| |
So the image was built from alpine:latest and copies a local flag.txt to /home/flag/flag.txt. The actual filesystem is a squashfs embedded inside the SIF:
| |
You don’t need Singularity installed at all — just carve out the squashfs at offset 0x9000 and extract it:
| |
Flag
| |
…and the flag itself delivers the punchline: now you really better know the rules. The whole challenge was hidden inside the rules document you were told to read.
Lessons / takeaways
- File size is a signal. A 5 MB three-page text PDF is a contradiction. Always sanity-check size against content.
pdfimagesandpdftotextare not the whole picture. They render the intended view. Real PDF forensics means reading the object graph (mutool show,qpdf --qdf,peepdf) and questioning anything attached to the catalog or to resources that never get drawn.- Watch for chunked/ordered markers.
@@N:…@@endacross 360 objects screams “reassemble me.” Index numbers are there to tell you the order. - Magic bytes guide every step.
PK\x03\x04→ ZIP;run-singularity/ SIF magic → container; squashfs signature → carve andunsquashfs. Identify the format, then reach for the right tool. - Passwords love to hide in the obvious place. A stray token at the bottom of the very document you’re analyzing (
teapot_2026) is exactly where a CTF author plants a key. - You rarely need the “official” tooling. No Singularity/Apptainer runtime required —
dd+unsquashfsgot us into the container directly.
Tools used
pdfinfo · pdftotext · pdfimages · pdfdetach · binwalk · mutool · python3 (base64 reassembly) · unzip · file · strings · dd · unsquashfs