Home > Writeups > DEADROP Forensics 4 - Redacted Blueprint

DEADROP Forensics 4 - Redacted Blueprint

A PDF floor plan whose rooms spell OOPS from above, hiding a flag in an orphaned FlateDecode XObject with no page tree reference, invisible to all standard PDF viewers. Requires parsing raw PDF streams or hex editing to find and base64-decode the hidden object.

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.

< Back to All Writeups