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:3f9c2a7b8d4e1f609a2b3c4d5e6f7081strings /home/rei/Downloads/output.pdf | rg -n "aes-iv key|a1b2c3d4e5f60718293a4b5c6d7e8f90"2:%aes-iv key:a1b2c3d4e5f60718293a4b5c6d7e8f90Then 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.py4fc80625b049f68462f7d02e7
9a8cbc1875ecd11a2b331eacc
c998fc9ffb3647d0adb35e9930
CIPHERTEXT=4fc80625b049f68462f7d02e79a8cbc1875ecd11a2b331eaccc998fc9ffb3647d0adb35e9930At 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.

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.pyVALID_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.

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.pyVALID_PKCS7_COUNT 1
TAIL 15f4aa88c894c09a9a67 PAD 7
PLAINTEXT TACHYON{PDF_St3g4n0gr4phy_i5_kool_5tau36}
FLAGS ['TACHYON{PDF_St3g4n0gr4phy_i5_kool_5tau36}']