Reconstructing a Claude AI-generated steganography tool from an API event stream to extract a hidden flag.

I was given two files: flag.png (the stego image) and messages.log. The description hints that the author asked an AI (Claude) to write a custom, overly complicated steganography algorithm to hide the flag.
Looking at messages.log, it's a massive JSONL file containing HTTP requests and responses to an Anthropic API gateway. The responses are streamed using Server-Sent Events (SSE) and wrapped in Base64.
The goal? Reconstruct Claude's custom stego script from the API logs and use it to decode flag.png.
My first thought was to just dump the conversation to see what Claude did. I wrote a quick Python script to decode the Base64 payloads and extract the text_delta chunks from the SSE stream.
This successfully gave me the conversational output. Claude proudly explained its 5-layer stego scheme:
It was an incredibly complex, CTF-worthy stego scheme. If I had to reverse-engineer this mathematically, it would be a nightmare. But there was a problem: the actual Python code was missing.
I quickly realized that Claude didn't output the code in standard markdown code blocks. Because it was operating as an agent via the command line, it used the Bash tool to create files using cat << 'EOF' > stego/... commands. Tool arguments stream as input_json_delta, which my first script completely ignored.
I wrote a second script to hook into the tool_use events and stitch the JSON chunks together. This spat out a massive wall of bash commands, file contents, and—most annoyingly—diff patches (--- old_string --- / --- new_string ---) that Claude used to fix bugs in its own code as it was developing the package.
import json
import base64
def extract_tools_from_logs(log_file):
print("[*] Extracting Claude's file generation commands...\n")
with open(log_file, 'r', encoding='utf-8') as f:
for line in f:
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
resp_body = entry.get('resp_body', '')
if not resp_body:
continue
try:
decoded_body = base64.b64decode(resp_body).decode('utf-8')
except Exception:
decoded_body = resp_body
tool_calls = {}
for sse_line in decoded_body.split('\n'):
if sse_line.startswith('data: '):
data_str = sse_line[6:].strip()
if not data_str or data_str == '[DONE]':
continue
try:
event = json.loads(data_str)
if event.get('type') == 'content_block_start' and event['content_block']['type'] == 'tool_use':
idx = event['index']
tool_calls[idx] = {
'name': event['content_block']['name'],
'args': ''
}
elif event.get('type') == 'content_block_delta' and event['delta']['type'] == 'input_json_delta':
idx = event['index']
if idx in tool_calls:
tool_calls[idx]['args'] += event['delta']['partial_json']
elif event.get('type') == 'content_block_stop':
idx = event['index']
if idx in tool_calls:
name = tool_calls[idx]['name']
try:
args = json.loads(tool_calls[idx]['args'])
if 'command' in args:
print(args['command'])
print("\n")
else:
for k, v in args.items():
print(f"--- {k} ---\n{v}\n")
except json.JSONDecodeError:
pass
except Exception:
continue
if __name__ == "__main__":
extract_tools_from_logs('messages.log')
Armed with the raw dump of Claude's tool executions, I got a bit ahead of myself. I tried to blindly run python3 -m stego extract flag.png and then wrote a custom solve.py script to bypass the CLI.
Python immediately threw an ImportError: cannot import name 'extract' from 'stego'.
I was trying to import a module that I hadn't even written to the disk yet! I had the instructions to build the stego package, but I hadn't actually rebuilt it.
To fix this, I wrote a rebuild.py script that took the raw text dump of Claude's tool calls and automatically rebuilt the package. The script was designed to:
--- content --- blocks and write them to their respective directories (stego/crypto, stego/coding, etc.).--- old_string --- / --- new_string --- patches to fix things like Claude's malformed EXTRACT_SALT.I piped the output to a text file and ran the builder... and immediately hit a UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff.
Because I used PowerShell to pipe the output (> output.txt), Windows helpfully saved the file in UTF-16 with a Byte Order Mark (BOM). I updated my script to try reading the file in UTF-16 first, falling back to UTF-8.
Finally, the script ran flawlessly. It rebuilt the entire stego/ Python package locally, including the golden goose: stego/secret.py, which contained all the hardcoded cryptographic salts, HKDF seeds, and S-box keys that Claude baked into the algorithm.
import os
import re
def rebuild_package(log_text_file):
print("[*] Rebuilding Claude's stego package...")
try:
with open(log_text_file, 'r', encoding='utf-16') as f:
content = f.read()
except UnicodeDecodeError:
with open(log_text_file, 'r', encoding='utf-8') as f:
content = f.read()
blocks = content.split('--- file_path ---')
for block in blocks[1:]:
lines = block.strip().split('\n')
filepath = lines[0].strip()
if 'stego/' not in filepath or not filepath.endswith('.py'):
continue
rel_path = filepath[filepath.find('stego/'):]
os.makedirs(os.path.dirname(rel_path), exist_ok=True)
if '--- content ---' in block:
raw_content = block.split('--- content ---')[1]
clean_content = re.split(r'\n(?=cd |python3 |echo |rm |---)', raw_content)[0]
with open(rel_path, 'w', encoding='utf-8') as f:
f.write(clean_content.strip('\n') + '\n')
print(f"[+] Created {rel_path}")
elif '--- old_string ---' in block and '--- new_string ---' in block:
old_str = block.split('--- old_string ---')[1].split('--- new_string ---')[0].strip('\n')
raw_new = block.split('--- new_string ---')[1]
new_str = re.split(r'\n(?=cd |python3 |echo |rm |---)', raw_new)[0].strip('\n')
if os.path.exists(rel_path):
with open(rel_path, 'r', encoding='utf-8') as f:
data = f.read()
if old_str in data:
with open(rel_path, 'w', encoding='utf-8') as f:
f.write(data.replace(old_str, new_str))
print(f"[*] Patched {rel_path}")
if __name__ == "__main__":
rebuild_package('output.txt')
Because the author explicitly told Claude not to use a passphrase (meaning all the security relied entirely on Kerckhoffs's principle being violated—the secrecy of the algorithm and hardcoded keys), I didn't need to do any reverse engineering.
With the stego/ package perfectly recreated in my directory, I simply used Claude's own tool against the image:
python -m stego extract flag.png
The script grabbed the hardcoded keys, generated the correct scattered slot sequence, decrypted the payloads, and spat out the flag:
(And the flag text even perfectly matches the fact that the API gateway routed the request to Opus instead of Mythos!)