penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

fuzz/devtools/gen_seeds_stream_sniff.py

raw ยท 4375 bytes

#!/usr/bin/env python3

from pathlib import Path
import struct

OUT = Path("seeds/stream_sniff")
OUT.mkdir(parents=True, exist_ok=True)


def write(name, data):
    (OUT / name).write_bytes(data)


def ext_sni(hostname: bytes) -> bytes:
    # extension_type: server_name = 0
    # extension_data:
    #   server_name_list length
    #   name_type = host_name
    #   host_name length
    #   host_name bytes
    server_name = b"\x00" + struct.pack("!H", len(hostname)) + hostname
    server_name_list = struct.pack("!H", len(server_name)) + server_name
    return struct.pack("!HH", 0, len(server_name_list)) + server_name_list


def ext_supported_versions() -> bytes:
    # supported_versions extension, client side:
    #   extension_type = 43
    #   extension_data = length-prefixed list of versions
    body = b"\x04\x03\x04\x03\x03"  # TLS 1.3, TLS 1.2
    return struct.pack("!HH", 43, len(body)) + body


def ext_alpn(proto: bytes = b"http/1.1") -> bytes:
    # ALPN extension:
    #   protocol_name_list length
    #   protocol_name length
    #   protocol_name
    name = bytes([len(proto)]) + proto
    body = struct.pack("!H", len(name)) + name
    return struct.pack("!HH", 16, len(body)) + body


def client_hello(hostname: bytes | None = b"example.com", extra_exts: bytes = b"") -> bytes:
    legacy_version = b"\x03\x03"  # TLS 1.2
    random = bytes(range(32))
    session_id = b""

    cipher_suites = b"".join([
        b"\x13\x01",  # TLS_AES_128_GCM_SHA256
        b"\x13\x02",  # TLS_AES_256_GCM_SHA384
        b"\xc0\x2f",  # TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    ])

    compression_methods = b"\x00"

    extensions = b""
    if hostname is not None:
        extensions += ext_sni(hostname)
    extensions += ext_supported_versions()
    extensions += ext_alpn()
    extensions += extra_exts

    body = (
        legacy_version +
        random +
        bytes([len(session_id)]) + session_id +
        struct.pack("!H", len(cipher_suites)) + cipher_suites +
        bytes([len(compression_methods)]) + compression_methods +
        struct.pack("!H", len(extensions)) + extensions
    )

    handshake = (
        b"\x01" +                         # HandshakeType client_hello
        len(body).to_bytes(3, "big") +     # handshake length
        body
    )

    record = (
        b"\x16" +                         # ContentType handshake
        b"\x03\x01" +                     # TLS record legacy version
        struct.pack("!H", len(handshake)) +
        handshake
    )

    return record


# 1. Non-TLS / junk
write("junk-empty", b"")
write("junk-http-get", b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
write("junk-ssh-banner", b"SSH-2.0-OpenSSH_9.9\r\n")
write("junk-random-small", bytes([0x00, 0xff, 0x13, 0x37, 0x42]))

# 2. Partial TLS records
good = client_hello(b"example.com")
for n in [1, 2, 3, 4, 5, 6, 9, 16, 32, 64]:
    write(f"partial-{n}", good[:n])

# 3. Valid-ish ClientHello with SNI
write("tls-sni-example-com", client_hello(b"example.com"))
write("tls-sni-localhost", client_hello(b"localhost"))
write("tls-sni-long-valid-253", client_hello(b"a" * 253))
write("tls-sni-idn-punycode", client_hello(b"xn--r8jz45g.xn--zckzah"))

# 4. ClientHello without SNI
write("tls-no-sni", client_hello(None))

# 5. Malformed TLS-ish things
write("tls-record-says-zero-len", b"\x16\x03\x01\x00\x00")
write("tls-record-says-too-big", b"\x16\x03\x01\xff\xff" + b"\x01\x00\x00\x00")
write("tls-alert-not-handshake", b"\x15\x03\x03\x00\x02\x02\x28")
write("tls-appdata-not-handshake", b"\x17\x03\x03\x00\x05hello")

# 6. Bad handshake length fields
bad = bytearray(good)
bad[6:9] = b"\xff\xff\xff"  # handshake length too large
write("tls-bad-handshake-len-huge", bytes(bad))

bad = bytearray(good)
bad[6:9] = b"\x00\x00\x01"  # handshake length too small
write("tls-bad-handshake-len-small", bytes(bad))

# 7. SNI edge cases

# Declared SNI hostname length larger than available.
trunc_sni = bytearray(client_hello(b"abc"))
idx = trunc_sni.find(b"\x00\x03abc")
if idx != -1:
    trunc_sni[idx:idx + 2] = b"\x01\x00"
write("tls-sni-declared-len-too-large", bytes(trunc_sni))

# Actual hostname is 254 bytes; should hit NAME_TOO_LONG or parse error,
# depending on your implementation.
write("tls-sni-too-long-254", client_hello(b"a" * 254))

# Empty hostname.
write("tls-sni-empty", client_hello(b""))

print(f"wrote seeds to {OUT}")