Fix macOS tlsspoof
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# tls spoof
|
||||
|
||||
idea from https://github.com/therealaleph/sni-spoofing-rust
|
||||
@@ -84,8 +84,16 @@ func tcpdumpObserver(t *testing.T, iface string, port uint16, needle string, do
|
||||
}
|
||||
|
||||
func dialLocalEchoServer(t *testing.T) (client net.Conn, serverPort uint16) {
|
||||
return dialLocalEchoServerFamily(t, "tcp4", "127.0.0.1:0")
|
||||
}
|
||||
|
||||
func dialLocalEchoServerIPv6(t *testing.T) (client net.Conn, serverPort uint16) {
|
||||
return dialLocalEchoServerFamily(t, "tcp6", "[::1]:0")
|
||||
}
|
||||
|
||||
func dialLocalEchoServerFamily(t *testing.T, network, address string) (client net.Conn, serverPort uint16) {
|
||||
t.Helper()
|
||||
listener, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
listener, err := net.Listen(network, address)
|
||||
require.NoError(t, err)
|
||||
|
||||
accepted := make(chan net.Conn, 1)
|
||||
@@ -97,7 +105,7 @@ func dialLocalEchoServer(t *testing.T) (client net.Conn, serverPort uint16) {
|
||||
close(accepted)
|
||||
}()
|
||||
addr := listener.Addr().(*net.TCPAddr)
|
||||
client, err = net.Dial("tcp4", addr.String())
|
||||
client, err = net.Dial(network, addr.String())
|
||||
require.NoError(t, err)
|
||||
server := <-accepted
|
||||
require.NotNil(t, server)
|
||||
|
||||
@@ -48,11 +48,56 @@ func TestIntegrationSpoofer_WrongSequence(t *testing.T) {
|
||||
require.True(t, captured, "injected fake ClientHello must be observable on loopback")
|
||||
}
|
||||
|
||||
func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) {
|
||||
requireRoot(t)
|
||||
client, serverPort := dialLocalEchoServerIPv6(t)
|
||||
spoofer, err := NewSpoofer(client, MethodWrongChecksum)
|
||||
require.NoError(t, err)
|
||||
defer spoofer.Close()
|
||||
|
||||
payload, err := hex.DecodeString(realClientHello)
|
||||
require.NoError(t, err)
|
||||
fake, err := rewriteSNI(payload, "letsencrypt.org")
|
||||
require.NoError(t, err)
|
||||
|
||||
captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() {
|
||||
require.NoError(t, spoofer.Inject(fake))
|
||||
}, 3*time.Second)
|
||||
require.True(t, captured, "injected fake ClientHello must be observable on loopback")
|
||||
}
|
||||
|
||||
func TestIntegrationSpoofer_IPv6_WrongSequence(t *testing.T) {
|
||||
requireRoot(t)
|
||||
client, serverPort := dialLocalEchoServerIPv6(t)
|
||||
spoofer, err := NewSpoofer(client, MethodWrongSequence)
|
||||
require.NoError(t, err)
|
||||
defer spoofer.Close()
|
||||
|
||||
payload, err := hex.DecodeString(realClientHello)
|
||||
require.NoError(t, err)
|
||||
fake, err := rewriteSNI(payload, "letsencrypt.org")
|
||||
require.NoError(t, err)
|
||||
|
||||
captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() {
|
||||
require.NoError(t, spoofer.Inject(fake))
|
||||
}, 3*time.Second)
|
||||
require.True(t, captured, "injected fake ClientHello must be observable on loopback")
|
||||
}
|
||||
|
||||
// Loopback bypasses TCP checksum validation, so wrong-sequence is used instead.
|
||||
func TestIntegrationConn_InjectsThenForwardsRealCH(t *testing.T) {
|
||||
requireRoot(t)
|
||||
runInjectsThenForwardsRealCH(t, "tcp4", "127.0.0.1:0")
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
func TestIntegrationConn_IPv6_InjectsThenForwardsRealCH(t *testing.T) {
|
||||
requireRoot(t)
|
||||
runInjectsThenForwardsRealCH(t, "tcp6", "[::1]:0")
|
||||
}
|
||||
|
||||
func runInjectsThenForwardsRealCH(t *testing.T, network, address string) {
|
||||
t.Helper()
|
||||
listener, err := net.Listen(network, address)
|
||||
require.NoError(t, err)
|
||||
|
||||
serverReceived := make(chan []byte, 1)
|
||||
@@ -69,7 +114,7 @@ func TestIntegrationConn_InjectsThenForwardsRealCH(t *testing.T) {
|
||||
|
||||
addr := listener.Addr().(*net.TCPAddr)
|
||||
serverPort := uint16(addr.Port)
|
||||
client, err := net.Dial("tcp4", addr.String())
|
||||
client, err := net.Dial(network, addr.String())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
client.Close()
|
||||
|
||||
+26
-10
@@ -74,20 +74,36 @@ func encodeTCP(frame []byte, ipHeaderLen int, src, dst netip.AddrPort, seqNum, a
|
||||
}
|
||||
|
||||
func buildSpoofFrame(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) {
|
||||
var sequence uint32
|
||||
corrupt := false
|
||||
switch method {
|
||||
case MethodWrongSequence:
|
||||
sequence = sendNext - uint32(len(payload))
|
||||
case MethodWrongChecksum:
|
||||
sequence = sendNext
|
||||
corrupt = true
|
||||
default:
|
||||
return nil, E.New("tls_spoof: unknown method ", method)
|
||||
sequence, corrupt, err := resolveSpoofSequence(method, sendNext, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildTCPSegment(src, dst, sequence, receiveNext, payload, corrupt), nil
|
||||
}
|
||||
|
||||
// buildSpoofTCPSegment returns a TCP segment without an IP header, for
|
||||
// platforms where the kernel synthesises the IP header (darwin IPv6).
|
||||
func buildSpoofTCPSegment(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) {
|
||||
sequence, corrupt, err := resolveSpoofSequence(method, sendNext, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
segment := make([]byte, tcpHeaderLen+len(payload))
|
||||
encodeTCP(segment, 0, src, dst, sequence, receiveNext, payload, corrupt)
|
||||
return segment, nil
|
||||
}
|
||||
|
||||
func resolveSpoofSequence(method Method, sendNext uint32, payload []byte) (uint32, bool, error) {
|
||||
switch method {
|
||||
case MethodWrongSequence:
|
||||
return sendNext - uint32(len(payload)), false, nil
|
||||
case MethodWrongChecksum:
|
||||
return sendNext, true, nil
|
||||
default:
|
||||
return 0, false, E.New("tls_spoof: unknown method ", method)
|
||||
}
|
||||
}
|
||||
|
||||
func applyTCPChecksum(tcp header.TCP, srcAddr, dstAddr netip.Addr, payload []byte, corrupt bool) {
|
||||
tcpLen := tcpHeaderLen + len(payload)
|
||||
pseudo := header.PseudoHeaderChecksum(header.TCPProtocolNumber, srcAddr.AsSlice(), dstAddr.AsSlice(), uint16(tcpLen))
|
||||
|
||||
@@ -59,7 +59,7 @@ func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, sockaddr, err := openDarwinRawSocket(dst)
|
||||
fd, sockaddr, err := openDarwinRawSocket(src, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,31 +119,51 @@ func readDarwinTCPSequence(src, dst netip.AddrPort) (uint32, uint32, error) {
|
||||
return 0, 0, E.New("tls_spoof: connection ", src, "->", dst, " not found in pcblist_n")
|
||||
}
|
||||
|
||||
func openDarwinRawSocket(dst netip.AddrPort) (int, unix.Sockaddr, error) {
|
||||
if !dst.Addr().Is4() {
|
||||
// macOS does not expose IPV6_HDRINCL; raw AF_INET6 injection would
|
||||
// require either BPF link-layer writes or kernel-side IPv6 header
|
||||
// synthesis, neither of which is implemented here.
|
||||
return -1, nil, E.New("tls_spoof: IPv6 not supported on darwin")
|
||||
func openDarwinRawSocket(src, dst netip.AddrPort) (int, unix.Sockaddr, error) {
|
||||
if dst.Addr().Is4() {
|
||||
return openIPv4RawSocket(dst)
|
||||
}
|
||||
return openIPv4RawSocket(dst)
|
||||
// macOS does not accept IPV6_HDRINCL on AF_INET6 SOCK_RAW IPPROTO_TCP
|
||||
// sockets, so the kernel builds the IPv6 header itself. Bind to the real
|
||||
// connection's source address so in6_selectsrc returns it, and rely on
|
||||
// in6p_cksum defaulting to -1 so the user-supplied TCP checksum is
|
||||
// preserved (including deliberately corrupted ones).
|
||||
fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_RAW, unix.IPPROTO_TCP)
|
||||
if err != nil {
|
||||
return -1, nil, E.Cause(err, "open AF_INET6 SOCK_RAW")
|
||||
}
|
||||
err = unix.Bind(fd, &unix.SockaddrInet6{Addr: src.Addr().As16()})
|
||||
if err != nil {
|
||||
unix.Close(fd)
|
||||
return -1, nil, E.Cause(err, "bind AF_INET6 SOCK_RAW")
|
||||
}
|
||||
sockaddr := &unix.SockaddrInet6{Port: int(dst.Port()), Addr: dst.Addr().As16()}
|
||||
return fd, sockaddr, nil
|
||||
}
|
||||
|
||||
func (s *darwinSpoofer) Inject(payload []byte) error {
|
||||
if !s.src.Addr().Is4() {
|
||||
segment, err := buildSpoofTCPSegment(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = unix.Sendto(s.rawFD, segment, 0, s.rawSockAddr)
|
||||
if err != nil {
|
||||
return E.Cause(err, "sendto raw socket")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Darwin inherits the historical BSD quirk: with IP_HDRINCL the kernel
|
||||
// expects ip_len and ip_off in host byte order, not network byte order.
|
||||
// Apple's rip_output swaps them back before transmission. This does not
|
||||
// apply to IPv6.
|
||||
if s.src.Addr().Is4() {
|
||||
totalLen := binary.BigEndian.Uint16(frame[2:4])
|
||||
binary.NativeEndian.PutUint16(frame[2:4], totalLen)
|
||||
fragOff := binary.BigEndian.Uint16(frame[6:8])
|
||||
binary.NativeEndian.PutUint16(frame[6:8], fragOff)
|
||||
}
|
||||
// Apple's rip_output swaps them back before transmission.
|
||||
totalLen := binary.BigEndian.Uint16(frame[2:4])
|
||||
binary.NativeEndian.PutUint16(frame[2:4], totalLen)
|
||||
fragOff := binary.BigEndian.Uint16(frame[6:8])
|
||||
binary.NativeEndian.PutUint16(frame[6:8], fragOff)
|
||||
err = unix.Sendto(s.rawFD, frame, 0, s.rawSockAddr)
|
||||
if err != nil {
|
||||
return E.Cause(err, "sendto raw socket")
|
||||
|
||||
Reference in New Issue
Block a user