Redacted Blueprint
Overview
A PDF of a classified facility floor plan. It renders normally: a blueprint
with black redaction bars over sensitive measurements and [CLASSIFIED] where
the verification code should be. The flag is not in the rendered output, not
in strings, and not visible to a PDF reader. It's hidden in an orphaned Form
XObject stream (object 10) that isn't referenced by the page tree and its
stream data is compressed.
Step 1: Establish that the rendered output hides something
pdftotext redacted_blueprint.pdf -
Output includes DOCUMENT VERIFICATION CODE: [CLASSIFIED], that's a
placeholder. The real code is somewhere else in the file.
strings redacted_blueprint.pdf
No flag. The interesting content is compressed.
Step 2: Enumerate all objects
A simple one-page PDF typically has 6-8 objects. Check how many this one has:
# Option A: pdf-parser.py (from Didier Stevens tools)
pdf-parser.py redacted_blueprint.pdf --stats
# Option B: mutool
mutool show redacted_blueprint.pdf trailer
# Option C: manual grep
python3 -c "
import re
d = open('redacted_blueprint.pdf','rb').read()
for m in re.finditer(rb'(\d+) 0 obj', d):
print(f'obj {m.group(1).decode()} at offset {m.start()}')
"
There are 10 objects (1–10). Page documents rarely need more than 8.
Object 10 is a /XObject of subtype /Form: a Form XObject. It's not
referenced anywhere in the page dictionary, meaning it never renders.
Step 3: Dump object 10's stream
# pdf-parser.py: dump and decompress stream
pdf-parser.py -o 10 -f redacted_blueprint.pdf
# mutool: extract stream
mutool show redacted_blueprint.pdf 10 stream
# Python: decompress manually
python3 -c "
import zlib, re
data = open('redacted_blueprint.pdf', 'rb').read()
# Find obj 10
idx = data.find(b'10 0 obj')
s_start = data.find(b'\nstream\n', idx) + len(b'\nstream\n')
s_end = data.find(b'\nendstream', s_start)
raw = data[s_start:s_end]
content = zlib.decompress(raw)
print(content.decode())
"
Output:
% DEADROP Document System v2.0 - Form XObject (Internal)
% Generated: D:20260303114152+00'00'
% Classification metadata block - do not modify
% doc_id: 7a3f-9c2d-4e1a-b8f0
% revision: 14
% author_sig: a3f9c1d2beef4242
% build_env: deadrop-ci-runner-07
% content_digest: REVBRFJPUHtwZGZfZm9yZW5zaWNzX2lzX2Ffc3BlY2lhbF9raW5kX29mX3N1ZmZlcmluZ30=
% seal: KOVACS-EYES-ONLY
% end metadata
Step 4: Decode the content_digest
content_digest is base64. Decode it:
echo 'REVBRFJPUHtwZGZfZm9yZW5zaWNzX2lzX2Ffc3BlY2lhbF9raW5kX29mX3N1ZmZlcmluZ30=' | base64 -d | xxd
Flag: DEADROP{pdf_forensics_is_a_special_kind_of_suffering}
Key Takeaway
PDF's object model allows content to exist in the file without being connected to the visible page tree. Form XObjects are especially useful for this because they're a legitimate PDF feature (used for repeated graphics, watermarks, etc) and their streams are compressed, so casual string searches miss them entirely. Thorough PDF forensics always involves enumerating all objects, not just reading what renders.