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

426 lines
14 KiB
Python

"""Raw socket packet injection for out-of-window fake SNI.
Implements the seq_id trick from the Go reference:
1. Sniff the outbound SYN to record the ISN (Initial Sequence Number)
2. Sniff the outbound 3rd ACK (handshake complete)
3. Inject a fake TLS ClientHello with seq = ISN+1 - len(fake)
This puts it BEFORE the server's receive window, so the server drops it,
but DPI sees and parses the fake SNI.
4. Wait for the server to ACK with ack == ISN+1, confirming the fake was
ignored and the server still expects the real data.
Linux only. Requires CAP_NET_RAW (run as root).
"""
import logging
import os
import socket
import struct
import threading
import time
from typing import Optional, Dict
logger = logging.getLogger("snispf")
ETH_P_IP = 0x0800
ETH_P_ALL = 0x0003
IPPROTO_TCP = 6
# TCP flags
FIN = 0x01
SYN = 0x02
RST = 0x04
PSH = 0x08
ACK = 0x10
def _htons(v):
return socket.htons(v)
def _ip_hdr_len(ip_bytes):
return (ip_bytes[0] & 0x0F) * 4
def _checksum_fold(s):
while s >> 16:
s = (s & 0xFFFF) + (s >> 16)
return (~s) & 0xFFFF
def _sum16(data):
s = 0
for i in range(0, len(data) - 1, 2):
s += (data[i] << 8) | data[i + 1]
if len(data) % 2 == 1:
s += data[-1] << 8
while s >> 16:
s = (s & 0xFFFF) + (s >> 16)
return s
def _ip_checksum(iph):
return _checksum_fold(_sum16(iph))
def _tcp_checksum(iph, tcp_with_payload):
ihl = _ip_hdr_len(iph)
pseudo = bytearray(12)
pseudo[0:4] = iph[12:16] # src IP
pseudo[4:8] = iph[16:20] # dst IP
pseudo[9] = 6 # TCP protocol
struct.pack_into("!H", pseudo, 10, len(tcp_with_payload))
return _checksum_fold(_sum16(pseudo) + _sum16(tcp_with_payload))
def _build_fake_frame(template_pkt, isn, fake_payload):
"""Build the injection frame from a captured 3rd-ACK packet template.
Takes the captured Ethernet+IP+TCP headers from the 3rd handshake ACK,
appends the fake TLS ClientHello as payload, and sets:
- seq = ISN + 1 - len(fake_payload) (out of window for the server)
- PSH flag added
- Proper IP and TCP checksums recalculated
"""
ip_off = 14 # Ethernet header is 14 bytes
ihl = _ip_hdr_len(template_pkt[ip_off:])
tcp_off = ip_off + ihl
tcp_hdr_len = (template_pkt[tcp_off + 12] >> 4) * 4
# Copy headers (Ethernet + IP + TCP) and append fake payload
headers = bytearray(template_pkt[:tcp_off + tcp_hdr_len])
out = headers + fake_payload
# Update IP total length
struct.pack_into("!H", out, ip_off + 2, len(out) - ip_off)
# Increment IP ID
old_id = struct.unpack("!H", out[ip_off + 4:ip_off + 6])[0]
struct.pack_into("!H", out, ip_off + 4, (old_id + 1) & 0xFFFF)
# Recalculate IP checksum
out[ip_off + 10] = 0
out[ip_off + 11] = 0
ip_cksum = _ip_checksum(out[ip_off:ip_off + ihl])
struct.pack_into("!H", out, ip_off + 10, ip_cksum)
# Set PSH flag
out[tcp_off + 13] |= PSH
# Set out-of-window sequence number: ISN + 1 - len(fake)
seq = (isn + 1 - len(fake_payload)) & 0xFFFFFFFF
struct.pack_into("!I", out, tcp_off + 4, seq)
# Recalculate TCP checksum
out[tcp_off + 16] = 0
out[tcp_off + 17] = 0
tcp_cksum = _tcp_checksum(
out[ip_off:ip_off + ihl],
bytes(out[tcp_off:]),
)
struct.pack_into("!H", out, tcp_off + 16, tcp_cksum)
return bytes(out)
class PortState:
"""Per-connection state tracked by the sniffer."""
def __init__(self, syn_seq, fake_hello):
self.syn_seq = syn_seq
self.fake_hello = fake_hello
self.fake_sent = False
self.confirmed = threading.Event()
self.lock = threading.Lock()
class RawInjector:
"""Raw socket sniffer and injector for out-of-window fake SNI.
This is the core mechanism that makes the seq_id trick work:
- Monitors all TCP traffic between local and target IPs
- When a new outbound SYN is detected, records the ISN
- When the 3rd handshake ACK is seen, injects the fake ClientHello
- Waits for server confirmation (ACK with ack == ISN+1)
"""
def __init__(self, local_ip, remote_ip, remote_port, fake_sni_builder):
self.local_ip = socket.inet_aton(local_ip)
self.remote_ip = socket.inet_aton(remote_ip)
self.remote_port = remote_port
self.fake_sni_builder = fake_sni_builder
self.ports: Dict[int, PortState] = {}
self.ports_lock = threading.Lock()
self.raw_fd = None
self.iface_idx = None
self.iface_name = None
self.running = False
self._sniffer_thread = None
def start(self):
"""Open the raw socket and start the sniffer loop."""
try:
self.raw_fd = socket.socket(
socket.AF_PACKET,
socket.SOCK_RAW,
socket.htons(ETH_P_ALL),
)
except (PermissionError, OSError) as e:
logger.warning(f"Cannot open AF_PACKET socket: {e}")
logger.warning("Raw injection unavailable - need root/CAP_NET_RAW")
return False
# Find the interface
iface_info = self._find_interface()
if iface_info is None:
logger.warning("Cannot determine outgoing interface for raw injection")
self.raw_fd.close()
self.raw_fd = None
return False
self.iface_name, self.iface_idx = iface_info
try:
self.raw_fd.bind((self.iface_name, ETH_P_ALL))
except OSError as e:
logger.warning(f"Cannot bind raw socket to {self.iface_name}: {e}")
logger.warning("Raw injection unavailable on this platform")
self.raw_fd.close()
self.raw_fd = None
return False
self.running = True
self._sniffer_thread = threading.Thread(
target=self._sniff_loop, daemon=True
)
self._sniffer_thread.start()
logger.info("Raw packet injector started")
return True
def stop(self):
"""Stop the sniffer."""
self.running = False
if self.raw_fd:
try:
self.raw_fd.close()
except Exception:
pass
def _find_interface(self):
"""Find the network interface name and index for the target IP.
Returns:
Tuple of (interface_name, interface_index) or None if not found.
"""
import fcntl
import array
try:
# Use a UDP connect to find which interface is used
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect((socket.inet_ntoa(self.remote_ip), 53))
local_addr = s.getsockname()[0]
s.close()
# Get all interfaces and find the matching one
# Using SIOCGIFCONF
max_bytes = 8096
buf = array.array("B", b"\0" * max_bytes)
ifconf = struct.pack("iL", max_bytes, buf.buffer_info()[0])
result = fcntl.ioctl(
self.raw_fd.fileno(), 0x8912, ifconf # SIOCGIFCONF
)
out_bytes = struct.unpack("iL", result)[0]
offset = 0
while offset < out_bytes:
name = buf[offset:offset + 16].tobytes().split(b"\0", 1)[0]
ip_bytes = buf[offset + 20:offset + 24].tobytes()
ip_str = socket.inet_ntoa(ip_bytes)
if ip_str == local_addr:
iface_name = name.decode("ascii", errors="replace")
# Get interface index
ifreq = struct.pack("16sI", name, 0)
result = fcntl.ioctl(
self.raw_fd.fileno(), 0x8933, ifreq # SIOCGIFINDEX
)
idx = struct.unpack("16sI", result)[1]
logger.debug(f"Using interface {iface_name} (index {idx})")
return (iface_name, idx)
offset += 40 # struct ifreq size
except Exception as e:
logger.debug(f"Interface detection error: {e}")
return None
def register_port(self, local_port, fake_hello):
"""Register a port for monitoring (called before connect)."""
with self.ports_lock:
self.ports[local_port] = PortState(0, fake_hello)
def wait_for_confirmation(self, local_port, timeout=2.0):
"""Wait for the server to confirm it ignored the fake packet.
Returns True if confirmed, False on timeout.
"""
with self.ports_lock:
ps = self.ports.get(local_port)
if ps is None:
return False
return ps.confirmed.wait(timeout=timeout)
def cleanup_port(self, local_port):
"""Clean up state for a port."""
with self.ports_lock:
self.ports.pop(local_port, None)
def _inject_frame(self, frame):
"""Inject a raw Ethernet frame."""
try:
addr = (
self.iface_name or "", # interface name
ETH_P_IP,
0, # packet type
0, # arp hardware type
frame[0:6], # destination MAC
)
self.raw_fd.sendto(frame, addr)
return True
except Exception as e:
logger.debug(f"Inject error: {e}")
# Fallback: try sendto with sockaddr_ll style
try:
sll = struct.pack(
"HH I BB 8s",
socket.htons(ETH_P_IP), # protocol
self.iface_idx, # ifindex
0, # pkttype
6, # halen
0,
frame[0:8], # addr
)
os.write(self.raw_fd.fileno(), frame)
return True
except Exception as e2:
logger.debug(f"Inject fallback error: {e2}")
return False
def _sniff_loop(self):
"""Main sniffer loop - watches TCP handshakes and injects fake packets."""
while self.running:
try:
pkt, _ = self.raw_fd.recvfrom(65536)
except (OSError, socket.error):
if not self.running:
break
continue
if len(pkt) < 14 + 20 + 20:
continue
# Check Ethernet type is IPv4
eth_type = struct.unpack("!H", pkt[12:14])[0]
if eth_type != ETH_P_IP:
continue
ip = pkt[14:]
if (ip[0] >> 4) != 4 or ip[9] != IPPROTO_TCP:
continue
ihl = _ip_hdr_len(ip)
src_ip = ip[12:16]
dst_ip = ip[16:20]
tcp = ip[ihl:]
if len(tcp) < 20:
continue
flags = tcp[13]
tcp_hdr_len = (tcp[12] >> 4) * 4
payload_len = len(tcp) - tcp_hdr_len
outbound = (src_ip == self.local_ip and dst_ip == self.remote_ip)
inbound = (src_ip == self.remote_ip and dst_ip == self.local_ip)
if outbound:
src_port = struct.unpack("!H", tcp[0:2])[0]
seq = struct.unpack("!I", tcp[4:8])[0]
# SYN (no ACK): new outbound connection
if (flags & SYN) and not (flags & ACK):
with self.ports_lock:
ps = self.ports.get(src_port)
if ps is not None:
with ps.lock:
ps.syn_seq = seq
logger.debug(
f"[sniff] SYN port={src_port} isn={seq}"
)
continue
# 3rd-handshake ACK: ACK only, no payload
if (flags & ACK) and not (flags & (SYN | FIN | RST)) and payload_len == 0:
with self.ports_lock:
ps = self.ports.get(src_port)
if ps is None:
continue
with ps.lock:
if ps.fake_sent:
continue
ps.fake_sent = True
syn_seq = ps.syn_seq
fake = ps.fake_hello
# Inject after a tiny delay (like the Go version's 1ms)
tpl_copy = bytearray(pkt)
def _do_inject(tpl=tpl_copy, isn=syn_seq, payload=fake, port=src_port):
time.sleep(0.001)
frame = _build_fake_frame(bytes(tpl), isn, payload)
if self._inject_frame(frame):
out_seq = (isn + 1 - len(payload)) & 0xFFFFFFFF
logger.debug(
f"[inject] port={port} fake seq={out_seq} "
f"(ISN={isn}, fake_len={len(payload)})"
)
else:
logger.debug(f"[inject] port={port} injection failed")
threading.Thread(target=_do_inject, daemon=True).start()
if inbound:
dst_port = struct.unpack("!H", tcp[2:4])[0]
ack_num = struct.unpack("!I", tcp[8:12])[0]
# Server's ACK confirming fake was ignored
if (flags & ACK) and not (flags & (SYN | FIN | RST)) and payload_len == 0:
with self.ports_lock:
ps = self.ports.get(dst_port)
if ps is None:
continue
with ps.lock:
if ps.fake_sent and ack_num == (ps.syn_seq + 1) & 0xFFFFFFFF:
if not ps.confirmed.is_set():
ps.confirmed.set()
logger.debug(
f"[sniff] port={dst_port} CONFIRMED "
f"server acked ISN+1={ack_num}"
)
def is_raw_available():
"""Check if raw socket injection is available on this system."""
try:
s = socket.socket(
socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL)
)
s.close()
return True
except (PermissionError, OSError, AttributeError):
return False