diff --git a/README.md b/README.md index 0eb2966..48225a7 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ TCP connection will spawn a separate short-lived session that will be closed aft behave like GoQuiet. This maybe useful for people with unstable connections. `BrowserSig` is the browser you want to **appear** to be using. It's not relevant to the browser you are actually using. -Currently, `chrome` and `firefox` are supported. +Currently, `chrome`, `firefox` and `safari` are supported. `KeepAlive` is the number of seconds to tell the OS to wait after no activity before sending TCP KeepAlive probes to the Cloak server. Zero or negative value disables it. Default is 0 (disabled). Warning: Enabling it might make your server diff --git a/internal/client/TLS.go b/internal/client/TLS.go index d30c982..11cd8a5 100644 --- a/internal/client/TLS.go +++ b/internal/client/TLS.go @@ -2,6 +2,7 @@ package client import ( "encoding/binary" + "encoding/hex" "net" "github.com/cbeuw/Cloak/internal/common" @@ -17,6 +18,14 @@ type clientHelloFields struct { serverName string } +func decodeHex(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return b +} + type browser interface { composeClientHello(clientHelloFields) []byte } diff --git a/internal/client/safari.go b/internal/client/safari.go new file mode 100644 index 0000000..b75378e --- /dev/null +++ b/internal/client/safari.go @@ -0,0 +1,88 @@ +// Fingerprint of Safari 16.4 + +package client + +import ( + "encoding/binary" +) + +type Safari struct{} + +func (s *Safari) composeExtensions(serverName string, keyShare []byte) []byte { + makeSupportedGroups := func() []byte { + suppGroupListLen := []byte{0x00, 0x0a} + ret := make([]byte, 2+2+8) + copy(ret[0:2], suppGroupListLen) + copy(ret[2:4], makeGREASE()) + copy(ret[4:], []byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19}) + return ret + } + + makeKeyShare := func(hidden []byte) []byte { + ret := make([]byte, 43) + ret[0], ret[1] = 0x00, 0x29 // length 41 + copy(ret[2:4], makeGREASE()) + ret[4], ret[5] = 0x00, 0x01 // length 1 + ret[6] = 0x00 + ret[7], ret[8] = 0x00, 0x1d // group x25519 + ret[9], ret[10] = 0x00, 0x20 // length 32 + copy(ret[11:43], hidden) + return ret + } + + // extension length is always 393, and server name length is variable + var ext [16][]byte + ext[0] = addExtRec(makeGREASE(), nil) // First GREASE + ext[1] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication + ext[2] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret + ext[3] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info + ext[4] = addExtRec([]byte{0x00, 0x0a}, makeSupportedGroups()) // supported groups + ext[5] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats + ext[6] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation + ext[7] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request + ext[8] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001604030804040105030203080508050501080606010201")) // Signature Algorithms + ext[9] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp + ext[10] = addExtRec([]byte{0x00, 0x33}, makeKeyShare(keyShare)) // key share + ext[11] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes + suppVersions := decodeHex("0a5a5a0304030303020301") // 5a5a needs to be a GREASE + copy(suppVersions[1:3], makeGREASE()) + ext[12] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions + ext[13] = addExtRec([]byte{0x00, 0x1b}, []byte{0x02, 0x00, 0x01}) // compress certificate + ext[14] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE + // len(ext[1]) + len(all other ext) + len(ext[15]) = 393 + // len(all other ext) = 174 + // len(ext[15]) = 219 - len(ext[1]) + // 2+2+len(padding) = 219 - len(ext[1]) + // len(padding) = 215 - len(ext[1]) + ext[15] = addExtRec([]byte{0x00, 0x15}, make([]byte, 215-len(ext[1]))) // padding + var ret []byte + for _, e := range ext { + ret = append(ret, e...) + } + return ret +} + +func (s *Safari) composeClientHello(hd clientHelloFields) (ch []byte) { + var clientHello [12][]byte + clientHello[0] = []byte{0x01} // handshake type + clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508 + clientHello[2] = []byte{0x03, 0x03} // client version + clientHello[3] = hd.random // random + clientHello[4] = []byte{0x20} // session id length 32 + clientHello[5] = hd.sessionId // session id + clientHello[6] = []byte{0x00, 0x2a} // cipher suites length 42 + clientHello[7] = append(makeGREASE(), decodeHex("130113021303c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a")...) // cipher suites + clientHello[8] = []byte{0x01} // compression methods length 1 + clientHello[9] = []byte{0x00} // compression methods + + extensions := s.composeExtensions(hd.serverName, hd.x25519KeyShare) + clientHello[10] = []byte{0x00, 0x00} + binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length + clientHello[11] = extensions + + var ret []byte + for _, c := range clientHello { + ret = append(ret, c...) + } + return ret +} diff --git a/internal/client/safari_test.go b/internal/client/safari_test.go new file mode 100644 index 0000000..3d3e22b --- /dev/null +++ b/internal/client/safari_test.go @@ -0,0 +1,42 @@ +package client + +import ( + "github.com/cbeuw/Cloak/internal/common" + "github.com/dreadl0ck/ja3" + "github.com/dreadl0ck/tlsx" + "github.com/stretchr/testify/assert" + "testing" +) + +var safariHd = clientHelloFields{ + random: decodeHex("977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc17"), + sessionId: decodeHex("c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3"), + x25519KeyShare: decodeHex("c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a"), + serverName: "github.com", +} + +func TestSafariJA3(t *testing.T) { + result := common.AddRecordLayer((&Safari{}).composeClientHello(safariHd), common.Handshake, common.VersionTLS11) + + hello := tlsx.ClientHelloBasic{} + err := hello.Unmarshal(result) + assert.Nil(t, err) + + digest := ja3.DigestHex(&hello) + assert.Equal(t, "773906b0efdefa24a7f2b8eb6985bf37", digest) +} + +func TestSafariComposeClientHello(t *testing.T) { + result := (&Safari{}).composeClientHello(safariHd) + target := decodeHex("010001fc0303977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc1720c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3002acaca130113021303c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000189fafa00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000c000a7a7a001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d0018001604030804040105030203080508050501080606010201001200000033002b00297a7a000100001d0020c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a002d00020101002b000b0a2a2a0304030303020301001b00030200017a7a000100001500c400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + for p := 0; p < len(result); p++ { + if result[p] != target[p] { + if result[p]&0x0F == 0xA && target[p]&0x0F == 0xA && + ((p > 0 && result[p-1] == result[p] && target[p-1] == target[p]) || + (p < len(result)-1 && result[p+1] == result[p] && target[p+1] == target[p])) { + continue + } + t.Errorf("inequality at %v", p) + } + } +} diff --git a/internal/client/state.go b/internal/client/state.go index dfda038..587c83d 100644 --- a/internal/client/state.go +++ b/internal/client/state.go @@ -242,6 +242,8 @@ func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (local Loca switch strings.ToLower(raw.BrowserSig) { case "firefox": browser = &Firefox{} + case "safari": + browser = &Safari{} case "chrome": fallthrough default: