┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Silent Stream — picoCTF 2026
~ Imattas aka Zemi
 Category: Reverse Engineering
 Difficulty: Medium
 Points: 200
 Author: Imattas aka Zemi

────────────────────────────────────────────────────────────────────────────────

--[ Challenge Description ]--

 We recovered a suspicious packet capture file that seems to contain a
transferred file. The sender was kind enough to allow us to analyze their
transfer tool.

────────────────────────────────────────────────────────────────────────────────

--[ Recon / Initial Analysis ]--

This challenge provides two artifacts:
1. A PCAP file containing captured network traffic of a file transfer
2. A binary/tool used to perform the transfer (the "transfer tool" we can
analyze)

By reverse engineering the transfer tool, we can understand the custom
encoding/protocol used to transmit the file. The tool uses a simple byte-level
encoding scheme:
-- python --
def encode_byte(b, key):
    return (b + key) % 256
Each byte of the file is encoded by adding a key value (default: 42) and taking
the result modulo 256. This is essentially a Caesar cipher operating on bytes.

────────────────────────────────────────────────────────────────────────────────

--[ Vulnerability / Observation ]--

The encoding is trivially reversible. By reverse engineering the binary (Ghidra,
IDA, or strings) we identify the encoding function:

- Encoding: encoded = (original + key) % 256
- Decoding: original = (encoded - key) % 256

The default key is 42, but it may also be visible in the binary's strings or
hardcoded constants, or transmitted as part of the protocol header. Because the
keyspace is only 0–255, the key can also be brute-forced by checking for the
flag format in the decoded output.

────────────────────────────────────────────────────────────────────────────────

--[ Exploitation / Solution ]--

1. Analyze the binary: Identify the encoding function. The tool reads a file,
encodes each byte by adding a key, and sends the encoded bytes over the network.
2. Identify the encoding: encoded = (original + key) % 256; the inverse is
original = (encoded - key) % 256.
3. Determine the key: The default key is 42; check the binary's
strings/constants or brute-force all 256 values.
4. Extract from PCAP: Open the PCAP in Wireshark and identify the TCP stream
carrying the file transfer. Follow TCP Stream -> Save as Raw (or use
tshark/Scapy to extract the TCP payload bytes).
5. Decode: Apply decoded = (encoded - 42) % 256 to each byte.
6. Recover the flag: The decoded output is the original file containing the
flag.

────────────────────────────────────────────────────────────────────────────────

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Silent Stream - picoCTF 2026 (Reverse Engineering, 200 pts)

A file was transferred using a custom tool that encodes each byte as:
    encoded = (original_byte + key) % 256

The default key is 42. We need to:
  1. Extract the payload from the PCAP file
  2. Reverse the encoding to recover the original file
  3. Find the flag in the recovered data

Usage:
  python3 solve.py <pcap_file> [--key KEY]
  e.g.: python3 solve.py capture.pcap
        python3 solve.py capture.pcap --key 42

If no pcap file is given, the script searches the current directory for .pcap/.pcapng files.
"""

import sys
import os
import argparse
import re

def decode_byte(b, key):
    """Reverse the encoding: original = (encoded - key) % 256"""
    return (b - key) % 256

def extract_tcp_payload_scapy(pcap_file):
    """Extract TCP payload data from a PCAP file using Scapy."""
    try:
        from scapy.all import rdpcap, TCP, Raw
        print(f"[*] Reading PCAP file: {pcap_file}")
        packets = rdpcap(pcap_file)

        # Collect all TCP payloads, grouped by stream
        streams = {}
        for pkt in packets:
            if pkt.haslayer(TCP) and pkt.haslayer(Raw):
                # Use (src_ip, src_port, dst_ip, dst_port) as stream key
                src = pkt[TCP].sport
                dst = pkt[TCP].dport
                key = (src, dst) if src > dst else (dst, src)
                if key not in streams:
                    streams[key] = b""
                streams[key] += bytes(pkt[Raw].load)

        if not streams:
            print("[!] No TCP payload data found in PCAP")
            return None

        # Return the largest stream (most likely the file transfer)
        largest = max(streams.values(), key=len)
        print(f"[*] Found {len(streams)} TCP stream(s)")
        print(f"[*] Largest payload: {len(largest)} bytes")
        return largest

    except ImportError:
        print("[!] Scapy not installed, trying dpkt...")
        return None

def extract_tcp_payload_dpkt(pcap_file):
    """Extract TCP payload data using dpkt as fallback."""
    try:
        import dpkt

        print(f"[*] Reading PCAP file with dpkt: {pcap_file}")
        payloads = {}

        with open(pcap_file, "rb") as f:
            try:
                pcap = dpkt.pcap.Reader(f)
            except ValueError:
                f.seek(0)
                pcap = dpkt.pcapng.Reader(f)

            for ts, buf in pcap:
                try:
                    eth = dpkt.ethernet.Ethernet(buf)
                    if not isinstance(eth.data, dpkt.ip.IP):
                        continue
                    ip = eth.data
                    if not isinstance(ip.data, dpkt.tcp.TCP):
                        continue
                    tcp = ip.data
                    if tcp.data:
                        key = (tcp.sport, tcp.dport)
                        if key not in payloads:
                            payloads[key] = b""
                        payloads[key] += tcp.data
                except Exception:
                    continue

        if not payloads:
            return None

        largest = max(payloads.values(), key=len)
        print(f"[*] Found {len(payloads)} stream(s), largest: {len(largest)} bytes")
        return largest

    except ImportError:
        print("[!] dpkt not installed")
        return None

def extract_tcp_payload_tshark(pcap_file):
    """Extract TCP payload using tshark as last resort."""
    import subprocess

    print(f"[*] Extracting payload with tshark...")
    try:
        # Get raw TCP payload bytes as hex
        result = subprocess.run(
            ["tshark", "-r", pcap_file, "-T", "fields",
             "-e", "tcp.payload", "-Y", "tcp.payload"],
            capture_output=True, text=True, timeout=30
        )

        if result.returncode != 0:
            print(f"[!] tshark error: {result.stderr}")
            return None

        hex_data = result.stdout.strip().replace("\n", "").replace(":", "")
        if not hex_data:
            return None

        payload = bytes.fromhex(hex_data)
        print(f"[*] Extracted {len(payload)} bytes via tshark")
        return payload

    except FileNotFoundError:
        print("[!] tshark not found")
        return None
    except Exception as e:
        print(f"[!] tshark error: {e}")
        return None

def find_pcap_file():
    """Search current directory for PCAP files."""
    for f in os.listdir("."):
        if f.endswith((".pcap", ".pcapng")):
            return f
    return None

def try_all_keys(encoded_data):
    """
    Brute-force the encoding key by trying all 256 possible values
    and checking for the flag format in the decoded output.
    """
    print("[*] Brute-forcing encoding key (0-255)...")
    for key in range(256):
        decoded = bytes([decode_byte(b, key) for b in encoded_data])
        if b"picoCTF{" in decoded:
            print(f"[+] Found key: {key}")
            return key, decoded
    return None, None

def main():
    parser = argparse.ArgumentParser(description="Silent Stream solver - picoCTF 2026")
    parser.add_argument("pcap_file", nargs="?", help="Path to the PCAP file")
    parser.add_argument("--key", type=int, default=None,
                        help="Encoding key (default: auto-detect, fallback 42)")
    parser.add_argument("--output", "-o", default="recovered_file",
                        help="Output filename for recovered data")
    args = parser.parse_args()

    pcap_file = args.pcap_file
    if pcap_file is None:
        pcap_file = find_pcap_file()
        if pcap_file is None:
            print("[!] No PCAP file specified and none found in current directory")
            print("Usage: python3 solve.py <pcap_file> [--key KEY]")
            sys.exit(1)

    if not os.path.exists(pcap_file):
        print(f"[!] File not found: {pcap_file}")
        sys.exit(1)

    print("=" * 60)
    print("  Silent Stream - picoCTF 2026 Solver")
    print("=" * 60)
    print()

    # Step 1: Extract TCP payload from PCAP
    payload = None
    for extractor in [extract_tcp_payload_scapy, extract_tcp_payload_dpkt, extract_tcp_payload_tshark]:
        payload = extractor(pcap_file)
        if payload:
            break

    if payload is None:
        print("[!] Could not extract payload from PCAP")
        print("[!] Try opening in Wireshark: Follow TCP Stream -> Save as Raw")
        sys.exit(1)

    print(f"[*] Extracted {len(payload)} bytes of encoded data")

    # Step 2: Decode the payload
    if args.key is not None:
        key = args.key
        decoded = bytes([decode_byte(b, key) for b in payload])
        print(f"[*] Decoded with key={key}")
    else:
        # First try brute force to find the correct key
        key, decoded = try_all_keys(payload)
        if key is None:
            # Fall back to default key of 42
            key = 42
            decoded = bytes([decode_byte(b, key) for b in payload])
            print(f"[*] Using default key={key}")

    # Step 3: Search for the flag
    flag_match = re.search(rb"picoCTF\{[^}]+\}", decoded)
    if flag_match:
        flag = flag_match.group().decode()
        print(f"\n[+] FLAG FOUND: {flag}")
    else:
        print("[*] Flag pattern not directly found, checking decoded content...")
        # Show printable characters
        printable = "".join(chr(b) if 32 <= b < 127 else "." for b in decoded)
        print(f"[*] Decoded preview (first 500 chars):")
        print(printable[:500])

    # Step 4: Save recovered file
    with open(args.output, "wb") as f:
        f.write(decoded)
    print(f"\n[*] Recovered data saved to: {args.output}")

    # Also check if it's a known file type
    if decoded[:4] == b"\x89PNG":
        new_name = args.output + ".png"
        os.rename(args.output, new_name)
        print(f"[*] Detected PNG image, renamed to: {new_name}")
    elif decoded[:2] == b"PK":
        new_name = args.output + ".zip"
        os.rename(args.output, new_name)
        print(f"[*] Detected ZIP archive, renamed to: {new_name}")
    elif decoded[:4] == b"%PDF":
        new_name = args.output + ".pdf"
        os.rename(args.output, new_name)
        print(f"[*] Detected PDF document, renamed to: {new_name}")

    print("\n[*] Done!")

if __name__ == "__main__":
    main()
────────────────────────────────────────────────────────────────────────────────

--[ Key Takeaways ]--

- Reverse engineering a transfer tool can reveal a trivial custom encoding —
here, an additive (Caesar-on-bytes) cipher.
- The decode is the exact inverse of the encode: original = (encoded - key) %
256.
- With only 256 possible keys, brute-forcing and checking for picoCTF{ is faster
than recovering the key from the binary.
- Extracting a transferred file from a PCAP is done via Wireshark's "Follow TCP
Stream -> Save as Raw", or programmatically with Scapy/dpkt/tshark.
- Checking magic bytes (\x89PNG, PK, %PDF) helps identify the recovered file
type.