impossible stego

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

MiscStegoSekaiCTFEasy

SEKAI CTF Logo

The Premise

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.


Step 1: The Initial Stumble (Parsing Text Deltas)

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.


Step 2: The Rabbit Hole (Extracting Tool Calls)

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')

Step 3: The "Wait, what are you on?" Phase

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.


Step 4: Reconstructing the Package (and battling Windows)

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:

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')

Step 5: Extracting the Flag

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:

Flag
SEKAI{th3y_d1dn7_4ctually_l3t_m3_us3_my7h0s_f0r_7h1s_0n3_s4dly_i_h4d_i7_r3r0ut3d_t0_0pus}

(And the flag text even perfectly matches the fact that the API gateway routed the request to Opus instead of Mythos!)

oneline6ryp7o