924 words
5 minutes
CryptoNite CTF 2026 - The Epstein Files - Forensics Writeup

Category: Forensics Flag: TACHYON{PDF_St3g4n0gr4phy_i5_kool_5tau36}

Challenge Description#

Welp, a person of interest seems to have hidden the flag in the given extract of the epstein files. But something feels off, almost like things have been erased from plain view. Can you find what’s going on underneath and recover the flag?

Analysis#

The file looked like an ordinary PDF at first, so I started with basic type confirmation.

file /home/rei/Downloads/output.pdf
/home/rei/Downloads/output.pdf: PDF document, version 1.7, 3 page(s)

The challenge hint about something being erased from plain view made me check the page tree itself. The /Pages object said there were only 3 kids, but object 6 was also a full /Type /Page with its own content stream and resources. That is exactly the kind of “hidden in structure” trick this challenge was pointing at.

qpdf --show-object=4 /home/rei/Downloads/output.pdf | rg "/Count|/Kids|/Type"
<< /Count 3 /Kids [ 7 0 R 8 0 R 9 0 R ] /Type /Pages >>
qpdf --show-object=6 /home/rei/Downloads/output.pdf | rg "/Type /Page|/Contents|/Parent|/F8|/Image38|/Image40"
<< /Contents 10 0 R ... /Parent 4 0 R ... /Font << /F4 15 0 R /F8 16 0 R >> ... /XObject << /Image38 17 0 R /Image40 18 0 R >> ... /Type /Page >>

From visible text, the document literally leaked an AES key string, and from raw strings it leaked an aes-iv key: marker. Those two values became the crypto parameters.

pdftotext /home/rei/Downloads/output.pdf - | rg -n "AES-128 Key|3f9c2a7b8d4e1f609a2b3c4d5e6f7081"
141:Password reference string recovered from notebook corresponding AES-128 Key:
142:3f9c2a7b8d4e1f609a2b3c4d5e6f7081
strings /home/rei/Downloads/output.pdf | rg -n "aes-iv key|a1b2c3d4e5f60718293a4b5c6d7e8f90"
2:%aes-iv key:a1b2c3d4e5f60718293a4b5c6d7e8f90

Then I decoded the hidden orphan-page text stream (10 0 R) by parsing the F8 ToUnicode CMap and translating CID pairs to hex digits. This extracted the real hidden ciphertext.

# extract_hidden_hex.py
import re
import subprocess

pdf = "/home/rei/Downloads/output.pdf"


def show_object(obj_id: int, stream: bool = False) -> str:
    cmd = ["qpdf", f"--show-object={obj_id}", pdf]
    if stream:
        cmd = ["qpdf", f"--show-object={obj_id}", "--filtered-stream-data", pdf]
    return subprocess.run(cmd, capture_output=True, text=True, check=False).stdout


obj6 = show_object(6)
f8_obj = int(re.search(r"/F8\s+(\d+)\s+0\s+R", obj6).group(1))
font = show_object(f8_obj)
tounicode_obj = int(re.search(r"/ToUnicode\s+(\d+)\s+0\s+R", font).group(1))
cmap = show_object(tounicode_obj, stream=True)

cid_to_char = {}
for a, b, c in re.findall(r"<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>", cmap):
    start = int(a, 16)
    end = int(b, 16)
    uni = int(c, 16)
    for cid in range(start, end + 1):
        cid_to_char[cid.to_bytes(2, "big")] = chr(uni + (cid - start))

content = show_object(10, stream=True)
segments = []
current_font = ""

for line in content.splitlines():
    tf = re.search(r"/(F\d+)\s+[0-9.]+\s+Tf", line)
    if tf:
        current_font = tf.group(1)

    if current_font != "F8":
        continue

    arrays = re.findall(r"\[(.*?)\]\s*TJ", line)
    for arr in arrays:
        out = ""
        for hx in re.findall(r"<([0-9A-Fa-f]+)>", arr):
            raw = bytes.fromhex(hx)
            for i in range(0, len(raw), 2):
                out += cid_to_char.get(raw[i : i + 2], "?")
        if out:
            segments.append(out)

for s in segments:
    print(s)

print("CIPHERTEXT=" + "".join(segments))
python /home/rei/Downloads/extract_hidden_hex.py
4fc80625b049f68462f7d02e7
9a8cbc1875ecd11a2b331eacc
c998fc9ffb3647d0adb35e9930
CIPHERTEXT=4fc80625b049f68462f7d02e79a8cbc1875ecd11a2b331eaccc998fc9ffb3647d0adb35e9930

At this point the challenge became a trolly rabbit-hole mix of PDF stego + crypto alignment weirdness because that ciphertext is 38 bytes, not an even CBC block count for a clean final decrypt path.

tableflip

The way out was to treat the extra rendered hex noise as a candidate tail source and then validate candidates mathematically, not by vibes. I generated nearby tail candidates, decrypted with the discovered key/IV, and enforced strict conditions: required known prefix, valid PKCS#7, fully printable plaintext, and proper TACHYON{...} regex. Only one candidate survived.

# solve_epstein_files.py
from Crypto.Cipher import AES
import itertools
import re

prefix_pt = b"TACHYON{PDF_St3g4n0gr4phy_i5_koo"
base_ct = bytes.fromhex(
    "4fc80625b049f68462f7d02e79a8cbc1875ecd11a2b331eaccc998fc9ffb3647d0adb35e9930"
)
key = bytes.fromhex("3f9c2a7b8d4e1f609a2b3c4d5e6f7081")
iv = bytes.fromhex("a1b2c3d4e5f60718293a4b5c6d7e8f90")

seeds = [
    "15f4aa88c894c09a9967",
    "15f4aa88c894c09a9a67",
    "15f4aa88cc894c09a9a6",
    "15f4aa88c894c094c09a",
]
hexchars = "0123456789abcdef"

candidates = set(seeds)
for s in seeds:
    for i, ch in enumerate(s):
        for repl in hexchars:
            if repl != ch:
                candidates.add(s[:i] + repl + s[i + 1 :])
    for i, j in itertools.combinations(range(len(s)), 2):
        for r1 in hexchars:
            if r1 == s[i]:
                continue
            for r2 in hexchars:
                if r2 == s[j]:
                    continue
                candidates.add(s[:i] + r1 + s[i + 1 : j] + r2 + s[j + 1 :])

valid = []
for tail in candidates:
    ct = base_ct + bytes.fromhex(tail)
    pt = AES.new(key, AES.MODE_CBC, iv).decrypt(ct)

    if not pt.startswith(prefix_pt):
        continue

    pad = pt[-1]
    if not (1 <= pad <= 16 and pt.endswith(bytes([pad]) * pad)):
        continue

    unpadded = pt[:-pad]
    if not all(32 <= b < 127 for b in unpadded):
        continue

    text = unpadded.decode()
    flags = re.findall(r"TACHYON\{[A-Za-z0-9_]+\}", text)
    valid.append((tail, pad, text, flags))

print("VALID_PKCS7_COUNT", len(valid))
for tail, pad, text, flags in sorted(valid, key=lambda x: x[2]):
    print("TAIL", tail, "PAD", pad)
    print("PLAINTEXT", text)
    print("FLAGS", flags)
python /home/rei/Downloads/solve_epstein_files.py
VALID_PKCS7_COUNT 1
TAIL 15f4aa88c894c09a9a67 PAD 7
PLAINTEXT TACHYON{PDF_St3g4n0gr4phy_i5_kool_5tau36}
FLAGS ['TACHYON{PDF_St3g4n0gr4phy_i5_kool_5tau36}']

Once that strict uniqueness check hit, the solve was done with confidence instead of guesswork.

smile

Solution#

# solve_epstein_files.py
from Crypto.Cipher import AES
import itertools
import re

prefix_pt = b"TACHYON{PDF_St3g4n0gr4phy_i5_koo"
base_ct = bytes.fromhex(
    "4fc80625b049f68462f7d02e79a8cbc1875ecd11a2b331eaccc998fc9ffb3647d0adb35e9930"
)
key = bytes.fromhex("3f9c2a7b8d4e1f609a2b3c4d5e6f7081")
iv = bytes.fromhex("a1b2c3d4e5f60718293a4b5c6d7e8f90")

seeds = [
    "15f4aa88c894c09a9967",
    "15f4aa88c894c09a9a67",
    "15f4aa88cc894c09a9a6",
    "15f4aa88c894c094c09a",
]
hexchars = "0123456789abcdef"

candidates = set(seeds)
for s in seeds:
    for i, ch in enumerate(s):
        for repl in hexchars:
            if repl != ch:
                candidates.add(s[:i] + repl + s[i + 1 :])
    for i, j in itertools.combinations(range(len(s)), 2):
        for r1 in hexchars:
            if r1 == s[i]:
                continue
            for r2 in hexchars:
                if r2 == s[j]:
                    continue
                candidates.add(s[:i] + r1 + s[i + 1 : j] + r2 + s[j + 1 :])

valid = []
for tail in candidates:
    ct = base_ct + bytes.fromhex(tail)
    pt = AES.new(key, AES.MODE_CBC, iv).decrypt(ct)

    if not pt.startswith(prefix_pt):
        continue

    pad = pt[-1]
    if not (1 <= pad <= 16 and pt.endswith(bytes([pad]) * pad)):
        continue

    unpadded = pt[:-pad]
    if not all(32 <= b < 127 for b in unpadded):
        continue

    text = unpadded.decode()
    flags = re.findall(r"TACHYON\{[A-Za-z0-9_]+\}", text)
    valid.append((tail, pad, text, flags))

print("VALID_PKCS7_COUNT", len(valid))
for tail, pad, text, flags in sorted(valid, key=lambda x: x[2]):
    print("TAIL", tail, "PAD", pad)
    print("PLAINTEXT", text)
    print("FLAGS", flags)
python /home/rei/Downloads/solve_epstein_files.py
VALID_PKCS7_COUNT 1
TAIL 15f4aa88c894c09a9a67 PAD 7
PLAINTEXT TACHYON{PDF_St3g4n0gr4phy_i5_kool_5tau36}
FLAGS ['TACHYON{PDF_St3g4n0gr4phy_i5_kool_5tau36}']
CryptoNite CTF 2026 - The Epstein Files - Forensics Writeup
https://blog.rei.my.id/posts/75/cryptonite-ctf-2026-the-epstein-files-forensics-writeup/
Author
Reidho Satria
Published at
2026-03-10
License
CC BY-NC-SA 4.0