sni/sni_spoofing/tls/__init__.py
2026-05-28 02:18:25 +04:30

434 lines
14 KiB
Python

"""TLS ClientHello builder and parser module.
Constructs TLS 1.3 ClientHello messages with customizable SNI fields
for DPI bypass purposes.
"""
import struct
import os
from typing import Optional
class ClientHelloBuilder:
"""Builds TLS ClientHello packets with spoofed SNI.
The ClientHello is the first message in a TLS handshake. DPI systems
inspect the SNI (Server Name Indication) extension to determine the
destination hostname. By sending a ClientHello with a fake SNI to an
allowed domain, we can bypass SNI-based filtering.
"""
# Pre-built template parts from the original tool
# TLS Record Header + Handshake Header + Client Version + ...
# Cipher suites, compression methods, and most extensions are static
# Only SNI, session_id, random, and key_share are dynamic
# TLS 1.3 cipher suites that look legitimate
CIPHER_SUITES = bytes.fromhex(
"0024" # length = 36 bytes (18 cipher suites x 2)
"1302" # TLS_AES_256_GCM_SHA384
"1303" # TLS_CHACHA20_POLY1305_SHA256
"1301" # TLS_AES_128_GCM_SHA256
"c02c" # TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
"c030" # TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
"c02b" # TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
"c02f" # TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
"cca9" # TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
"cca8" # TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
"c024" # TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
"c028" # TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
"c023" # TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
"c027" # TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
"009f" # TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
"009e" # TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
"006b" # TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
"0067" # TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
"00ff" # TLS_EMPTY_RENEGOTIATION_INFO_SCSV
)
# Supported groups extension
SUPPORTED_GROUPS = bytes.fromhex(
"000a" # extension type: supported_groups
"0016" # length
"0014" # list length
"001d" # x25519
"0017" # secp256r1
"001e" # x448
"0019" # secp521r1
"0018" # secp384r1
"0100" # ffdhe2048
"0101" # ffdhe3072
"0102" # ffdhe4096
"0103" # ffdhe6144
"0104" # ffdhe8192
)
# Signature algorithms extension
SIGNATURE_ALGORITHMS = bytes.fromhex(
"000d" # extension type: signature_algorithms
"002a" # length
"0028" # list length
"0403" # ecdsa_secp256r1_sha256
"0503" # ecdsa_secp384r1_sha384
"0603" # ecdsa_secp521r1_sha512
"0807" # ed25519
"0808" # ed448
"0809" # ...
"080a"
"080b"
"0804" # rsa_pss_rsae_sha256
"0805" # rsa_pss_rsae_sha384
"0806" # rsa_pss_rsae_sha512
"0401" # rsa_pkcs1_sha256
"0501" # rsa_pkcs1_sha384
"0601" # rsa_pkcs1_sha512
"0303" # ...
"0301"
"0302"
"0402"
"0502"
"0602"
)
# EC point formats
EC_POINT_FORMATS = bytes.fromhex(
"000b" # extension type: ec_point_formats
"0004" # length
"0300" # list length + uncompressed
"0102" # ansiX962_compressed_prime + ansiX962_compressed_char2
)
# Session ticket extension (empty)
SESSION_TICKET = bytes.fromhex(
"0023" # extension type: session_ticket
"0000" # length: 0
)
# ALPN extension (h2, http/1.1)
ALPN = bytes.fromhex(
"0010" # extension type: ALPN
"000e" # length
"000c" # protocols length
"0268" # length + 'h'
"3208" # '2' + length
"6874" # 'ht'
"7470" # 'tp'
"2f31" # '/1'
"2e31" # '.1'
)
# Encrypt then MAC
ENCRYPT_THEN_MAC = bytes.fromhex("0016" "0000")
# Extended master secret
EXTENDED_MASTER_SECRET = bytes.fromhex("0017" "0000")
# Supported versions extension (TLS 1.3, TLS 1.2)
SUPPORTED_VERSIONS = bytes.fromhex(
"002b" # extension type: supported_versions
"0005" # length: 5 bytes of data follow
"04" # supported_versions list length: 4 bytes (2 versions x 2 bytes)
"0304" # TLS 1.3
"0303" # TLS 1.2
)
# PSK key exchange modes
PSK_KEY_EXCHANGE = bytes.fromhex(
"002d" # extension type: psk_key_exchange_modes
"0002" # length
"0101" # psk_dhe_ke
)
@classmethod
def build_sni_extension(cls, sni: str) -> bytes:
"""Build the SNI (Server Name Indication) extension."""
sni_bytes = sni.encode("ascii")
sni_len = len(sni_bytes)
# Server name entry: type(1) + length(2) + name
entry = struct.pack("!BH", 0, sni_len) + sni_bytes
# Server name list: length(2) + entries
name_list = struct.pack("!H", len(entry)) + entry
# Extension: type(2) + length(2) + data
return struct.pack("!HH", 0x0000, len(name_list)) + name_list
@classmethod
def build_key_share_extension(cls, public_key: Optional[bytes] = None) -> bytes:
"""Build the key_share extension with x25519 key."""
if public_key is None:
public_key = os.urandom(32)
# Key share entry: group(2) + key_length(2) + key
entry = struct.pack("!HH", 0x001D, 32) + public_key
# Key share extension: length(2) + entries
data = struct.pack("!H", len(entry)) + entry
return struct.pack("!HH", 0x0033, len(data)) + data
@classmethod
def build_padding_extension(cls, target_length: int, current_length: int) -> bytes:
"""Build padding extension to reach target ClientHello size.
Padding is used to make the ClientHello a specific size, which helps
avoid fingerprinting and ensures consistent packet sizes.
"""
# Extension header is 4 bytes (type + length)
padding_needed = target_length - current_length - 4
if padding_needed < 0:
return b""
return struct.pack("!HH", 0x0015, padding_needed) + (b"\x00" * padding_needed)
@classmethod
def build_client_hello(
cls,
sni: str,
session_id: Optional[bytes] = None,
random_bytes: Optional[bytes] = None,
key_share: Optional[bytes] = None,
target_size: int = 517,
) -> bytes:
"""Build a complete TLS ClientHello record.
Args:
sni: The Server Name Indication to include
session_id: 32-byte session ID (random if None)
random_bytes: 32-byte client random (random if None)
key_share: 32-byte x25519 public key (random if None)
target_size: Target total size for the TLS record (default 517)
Returns:
Complete TLS record bytes ready to send
"""
if session_id is None:
session_id = os.urandom(32)
if random_bytes is None:
random_bytes = os.urandom(32)
# Client version: TLS 1.2 (0x0303) - real version in extensions
client_version = b"\x03\x03"
# Session ID
session_id_field = struct.pack("!B", len(session_id)) + session_id
# Compression methods: null only
compression = b"\x01\x00"
# Build extensions
sni_ext = cls.build_sni_extension(sni)
key_share_ext = cls.build_key_share_extension(key_share)
# Assemble extensions (order matters for fingerprint matching)
extensions = b"".join([
sni_ext,
cls.EC_POINT_FORMATS,
cls.SUPPORTED_GROUPS,
cls.SESSION_TICKET,
cls.ALPN,
cls.ENCRYPT_THEN_MAC,
cls.EXTENDED_MASTER_SECRET,
cls.SIGNATURE_ALGORITHMS,
cls.SUPPORTED_VERSIONS,
cls.PSK_KEY_EXCHANGE,
key_share_ext,
])
# Calculate size for padding
# Handshake body (without record header): version(2) + random(32) + session_id_field + cipher_suites + compression + extensions_header(2) + extensions
handshake_body_no_pad = (
client_version
+ random_bytes
+ session_id_field
+ cls.CIPHER_SUITES
+ compression
)
extensions_len_so_far = len(extensions)
# Total handshake msg = 4 (handshake header) + body + 2 (extensions length) + extensions
total_so_far = 4 + len(handshake_body_no_pad) + 2 + extensions_len_so_far
# TLS record = 5 (record header) + handshake
record_so_far = 5 + total_so_far
# Add padding to reach target size
padding_ext = cls.build_padding_extension(target_size, record_so_far)
extensions += padding_ext
# Extensions length prefix
extensions_with_len = struct.pack("!H", len(extensions)) + extensions
# Handshake body
handshake_body = handshake_body_no_pad + extensions_with_len
# Handshake message: type(1) + length(3) + body
handshake_len = len(handshake_body)
handshake = (
b"\x01" # ClientHello
+ struct.pack("!I", handshake_len)[1:] # 3-byte length
+ handshake_body
)
# TLS record: content_type(1) + version(2) + length(2) + data
record = (
b"\x16" # Handshake
+ b"\x03\x01" # TLS 1.0 (legacy for compatibility)
+ struct.pack("!H", len(handshake))
+ handshake
)
return record
@classmethod
def build_client_response(cls, random_bytes: Optional[bytes] = None) -> bytes:
"""Build a fake TLS client response (ChangeCipherSpec + ApplicationData).
This simulates the client's response after receiving ServerHello,
which is useful for making the connection look legitimate to DPI.
"""
if random_bytes is None:
random_bytes = os.urandom(32)
# Change Cipher Spec
ccs = b"\x14\x03\x03\x00\x01\x01"
# Application Data (fake encrypted payload)
app_data = (
b"\x17" # Application Data
+ b"\x03\x03" # TLS 1.2
+ struct.pack("!H", len(random_bytes))
+ random_bytes
)
return ccs + app_data
@staticmethod
def parse_client_hello(data: bytes) -> dict:
"""Parse a TLS ClientHello to extract SNI and other fields.
Args:
data: Raw TLS record bytes
Returns:
Dictionary with parsed fields
"""
result = {}
if len(data) < 5:
return result
# TLS Record header
content_type = data[0]
tls_version = struct.unpack("!H", data[1:3])[0]
record_len = struct.unpack("!H", data[3:5])[0]
result["content_type"] = content_type
result["tls_version"] = f"0x{tls_version:04x}"
if content_type != 0x16: # Not handshake
return result
pos = 5 # Skip record header
# Handshake header
if pos + 4 > len(data):
return result
hs_type = data[pos]
hs_len = struct.unpack("!I", b"\x00" + data[pos + 1 : pos + 4])[0]
pos += 4
if hs_type != 0x01: # Not ClientHello
return result
result["handshake_type"] = "ClientHello"
# Client version
client_version = struct.unpack("!H", data[pos : pos + 2])[0]
result["client_version"] = f"0x{client_version:04x}"
pos += 2
# Random (32 bytes)
result["random"] = data[pos : pos + 32].hex()
pos += 32
# Session ID
sess_len = data[pos]
pos += 1
result["session_id"] = data[pos : pos + sess_len].hex()
pos += sess_len
# Cipher suites
cs_len = struct.unpack("!H", data[pos : pos + 2])[0]
pos += 2 + cs_len
# Compression
comp_len = data[pos]
pos += 1 + comp_len
# Extensions
if pos + 2 > len(data):
return result
ext_len = struct.unpack("!H", data[pos : pos + 2])[0]
pos += 2
ext_end = pos + ext_len
while pos + 4 <= ext_end:
ext_type = struct.unpack("!H", data[pos : pos + 2])[0]
ext_data_len = struct.unpack("!H", data[pos + 2 : pos + 4])[0]
ext_data = data[pos + 4 : pos + 4 + ext_data_len]
pos += 4 + ext_data_len
if ext_type == 0x0000: # SNI
if len(ext_data) >= 5:
name_list_len = struct.unpack("!H", ext_data[0:2])[0]
name_type = ext_data[2]
name_len = struct.unpack("!H", ext_data[3:5])[0]
sni = ext_data[5 : 5 + name_len].decode("ascii", errors="replace")
result["sni"] = sni
return result
@staticmethod
def parse_server_hello(data: bytes) -> dict:
"""Parse a TLS ServerHello message."""
result = {}
if len(data) < 5:
return result
content_type = data[0]
if content_type != 0x16:
return result
pos = 5 # Skip record header
if pos + 4 > len(data):
return result
hs_type = data[pos]
pos += 4
if hs_type != 0x02: # Not ServerHello
return result
result["handshake_type"] = "ServerHello"
# Server version
server_version = struct.unpack("!H", data[pos : pos + 2])[0]
result["server_version"] = f"0x{server_version:04x}"
pos += 2
# Server random
result["random"] = data[pos : pos + 32].hex()
pos += 32
# Session ID
sess_len = data[pos]
pos += 1
result["session_id"] = data[pos : pos + sess_len].hex()
pos += sess_len
# Cipher suite
cipher = struct.unpack("!H", data[pos : pos + 2])[0]
result["cipher_suite"] = f"0x{cipher:04x}"
pos += 2
# Compression
result["compression"] = data[pos]
pos += 1
return result