● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0

Multi-protocol network egress assessment and DLP verification framework featuring automated L4 sweeps, corporate proxy traversal, and covert protocol smuggling across 20+ channels.

$ git clone https://shehzade.io/repos/contraband.git Copy
Source ZIP (1.93 MB)
Code 174 Commits 1
contraband / pkg / client / exfil / tls_masquerade.go
← Back to directory
438 lines · 11.9 KB
Raw
1package exfil
2
3import (
4 "context"
5 "crypto/md5"
6 "crypto/sha256"
7 "crypto/tls"
8 "crypto/x509"
9 "encoding/hex"
10 "errors"
11 "fmt"
12 "net"
13 "net/http"
14 "sort"
15 "strings"
16 "time"
17
18 utls "github.com/refraction-networking/utls"
19 "golang.org/x/net/http2"
20 "google.golang.org/grpc/credentials"
21)
22
23// TLSConfig configures uTLS client masquerading.
24type TLSConfig struct {
25 ServerName string
26 InsecureSkipVerify bool
27 Fingerprint string // chrome, edge, safari, firefox, curl, default
28 ALPN []string
29 RootCAs *x509.CertPool
30 MinVersion uint16
31 MaxVersion uint16
32}
33
34// MasqueradeConn wraps a uTLS connection and exposes computed JA3/JA4 fingerprints.
35type MasqueradeConn struct {
36 net.Conn
37 uconn *utls.UConn
38 JA3Raw string
39 JA3Hash string
40 JA4 string
41 Profile string
42}
43
44// ConnectionState maps utls.ConnectionState to crypto/tls.ConnectionState for standard library callers.
45func (m *MasqueradeConn) ConnectionState() tls.ConnectionState {
46 if m.uconn == nil {
47 return tls.ConnectionState{}
48 }
49 return convertUTLSToCryptoTLS(m.uconn.ConnectionState())
50}
51
52// NegotiatedProtocol returns the ALPN string negotiated during handshake.
53func (m *MasqueradeConn) NegotiatedProtocol() string {
54 if m.uconn == nil {
55 return ""
56 }
57 return m.uconn.ConnectionState().NegotiatedProtocol
58}
59
60// GetJA3 returns raw JA3 and MD5 hash.
61func (m *MasqueradeConn) GetJA3() (string, string) {
62 return m.JA3Raw, m.JA3Hash
63}
64
65// GetJA4 returns the computed JA4 fingerprint.
66func (m *MasqueradeConn) GetJA4() string {
67 return m.JA4
68}
69
70// GetUConn returns underlying utls.UConn.
71func (m *MasqueradeConn) GetUConn() *utls.UConn {
72 return m.uconn
73}
74
75// ResolveClientHelloID maps string presets to utls.ClientHelloID.
76func ResolveClientHelloID(profile string) (utls.ClientHelloID, bool) {
77 switch strings.ToLower(profile) {
78 case "chrome":
79 return utls.HelloChrome_120, true
80 case "edge":
81 return utls.HelloEdge_Auto, true
82 case "safari":
83 return utls.HelloSafari_Auto, true
84 case "firefox":
85 return utls.HelloFirefox_Auto, true
86 case "curl":
87 return utls.HelloChrome_58, true
88 case "default":
89 return utls.HelloGolang, true
90 default:
91 return utls.HelloChrome_120, false
92 }
93}
94
95// isGREASE checks if a 16-bit value is a TLS GREASE identifier.
96func isGREASE(val uint16) bool {
97 return (val&0x0f0f) == 0x0a0a && (val>>8) == (val&0xff)
98}
99
100func extractExtensionID(ext utls.TLSExtension) (id uint16, ok bool, sigAlgs []uint16) {
101 switch e := ext.(type) {
102 case *utls.SNIExtension:
103 return 0x0000, true, nil
104 case *utls.StatusRequestExtension:
105 return 0x0005, true, nil
106 case *utls.SupportedCurvesExtension:
107 return 0x000a, true, nil
108 case *utls.SupportedPointsExtension:
109 return 0x000b, true, nil
110 case *utls.SignatureAlgorithmsExtension:
111 var sa []uint16
112 for _, s := range e.SupportedSignatureAlgorithms {
113 sa = append(sa, uint16(s))
114 }
115 return 0x000d, true, sa
116 case *utls.ALPNExtension:
117 return 0x0010, true, nil
118 case *utls.SCTExtension:
119 return 0x0012, true, nil
120 case *utls.ExtendedMasterSecretExtension:
121 return 0x0017, true, nil
122 case *utls.SessionTicketExtension:
123 return 0x0023, true, nil
124 case *utls.PSKKeyExchangeModesExtension:
125 return 0x002d, true, nil
126 case *utls.KeyShareExtension:
127 return 0x0033, true, nil
128 case *utls.SupportedVersionsExtension:
129 return 0x002b, true, nil
130 case *utls.RenegotiationInfoExtension:
131 return 0xff01, true, nil
132 case *utls.UtlsCompressCertExtension:
133 return 0x001b, true, nil
134 case *utls.ApplicationSettingsExtension:
135 return 0x44cd, true, nil
136 case *utls.UtlsPaddingExtension:
137 return 0x0015, true, nil
138 case *utls.GenericExtension:
139 return e.Id, true, nil
140 case *utls.UtlsGREASEExtension:
141 return 0, false, nil
142 case *utls.GREASEEncryptedClientHelloExtension:
143 return 0, false, nil
144 default:
145 return 0, false, nil
146 }
147}
148
149// CalculateJA3 computes raw JA3 and its MD5 hash from a configured uTLS connection.
150func CalculateJA3(uconn *utls.UConn) (string, string) {
151 if uconn == nil || uconn.HandshakeState.Hello == nil {
152 return "", ""
153 }
154 hello := uconn.HandshakeState.Hello
155 ver := hello.Vers
156
157 var ciphers []string
158 for _, c := range hello.CipherSuites {
159 if !isGREASE(c) {
160 ciphers = append(ciphers, fmt.Sprintf("%d", c))
161 }
162 }
163
164 var exts []string
165 for _, ext := range uconn.Extensions {
166 id, ok, _ := extractExtensionID(ext)
167 if ok && !isGREASE(id) {
168 exts = append(exts, fmt.Sprintf("%d", id))
169 }
170 }
171
172 var curves []string
173 for _, cv := range hello.SupportedCurves {
174 if !isGREASE(uint16(cv)) {
175 curves = append(curves, fmt.Sprintf("%d", cv))
176 }
177 }
178
179 var points []string
180 for _, pt := range hello.SupportedPoints {
181 points = append(points, fmt.Sprintf("%d", pt))
182 }
183
184 raw := fmt.Sprintf("%d,%s,%s,%s,%s",
185 ver,
186 strings.Join(ciphers, "-"),
187 strings.Join(exts, "-"),
188 strings.Join(curves, "-"),
189 strings.Join(points, "-"),
190 )
191 h := md5.Sum([]byte(raw))
192 return raw, hex.EncodeToString(h[:])
193}
194
195// CalculateJA4 computes the FoxIO standard JA4 string (JA4_a_JA4_b_JA4_c).
196func CalculateJA4(uconn *utls.UConn) string {
197 if uconn == nil || uconn.HandshakeState.Hello == nil {
198 return ""
199 }
200 hello := uconn.HandshakeState.Hello
201
202 proto := "t" // TCP
203 ver := "12"
204 for _, v := range hello.SupportedVersions {
205 if v == 0x0304 {
206 ver = "13"
207 break
208 }
209 }
210
211 sni := "i"
212 if hello.ServerName != "" && net.ParseIP(hello.ServerName) == nil {
213 sni = "d"
214 }
215
216 var cipherList []string
217 for _, c := range hello.CipherSuites {
218 if !isGREASE(c) {
219 cipherList = append(cipherList, fmt.Sprintf("%04x", c))
220 }
221 }
222
223 var ja4ExtList []string
224 var allExtList []string
225 var sigAlgs []string
226
227 for _, ext := range uconn.Extensions {
228 id, ok, sa := extractExtensionID(ext)
229 if !ok || isGREASE(id) {
230 continue
231 }
232 allExtList = append(allExtList, fmt.Sprintf("%04x", id))
233 if id != 0x0000 && id != 0x0010 { // JA4 excludes SNI (0000) and ALPN (0010)
234 ja4ExtList = append(ja4ExtList, fmt.Sprintf("%04x", id))
235 }
236 for _, s := range sa {
237 if !isGREASE(s) {
238 sigAlgs = append(sigAlgs, fmt.Sprintf("%04x", s))
239 }
240 }
241 }
242
243 alpn := "00"
244 if len(hello.AlpnProtocols) > 0 && len(hello.AlpnProtocols[0]) >= 2 {
245 first := hello.AlpnProtocols[0]
246 alpn = string(first[0]) + string(first[len(first)-1])
247 }
248
249 partA := fmt.Sprintf("%s%s%s%02d%02d%s", proto, ver, sni, len(cipherList), len(allExtList), alpn)
250
251 sort.Strings(cipherList)
252 bHash := sha256.Sum256([]byte(strings.Join(cipherList, ",")))
253 partB := hex.EncodeToString(bHash[:])[:12]
254
255 sort.Strings(ja4ExtList)
256 sort.Strings(sigAlgs)
257 cRaw := strings.Join(ja4ExtList, ",")
258 if len(sigAlgs) > 0 {
259 cRaw += "_" + strings.Join(sigAlgs, ",")
260 }
261 cHash := sha256.Sum256([]byte(cRaw))
262 partC := hex.EncodeToString(cHash[:])[:12]
263
264 return fmt.Sprintf("%s_%s_%s", partA, partB, partC)
265}
266
267// DialTLS dials a target address over TCP and completes a uTLS masquerade handshake.
268func DialTLS(ctx context.Context, network, addr string, cfg TLSConfig) (*MasqueradeConn, error) {
269 var d net.Dialer
270 rawConn, err := d.DialContext(ctx, network, addr)
271 if err != nil {
272 return nil, fmt.Errorf("tcp dial failed: %w", err)
273 }
274 return WrapConn(ctx, rawConn, addr, cfg)
275}
276
277// WrapConn wraps an established net.Conn with uTLS masquerader.
278func WrapConn(ctx context.Context, rawConn net.Conn, addr string, cfg TLSConfig) (*MasqueradeConn, error) {
279 helloID, _ := ResolveClientHelloID(cfg.Fingerprint)
280
281 serverName := cfg.ServerName
282 if serverName == "" {
283 host, _, err := net.SplitHostPort(addr)
284 if err == nil {
285 serverName = host
286 } else {
287 serverName = addr
288 }
289 }
290
291 utlsCfg := &utls.Config{
292 ServerName: serverName,
293 InsecureSkipVerify: cfg.InsecureSkipVerify,
294 RootCAs: cfg.RootCAs,
295 MinVersion: cfg.MinVersion,
296 MaxVersion: cfg.MaxVersion,
297 NextProtos: cfg.ALPN,
298 }
299
300 uconn := utls.UClient(rawConn, utlsCfg, helloID)
301 _ = uconn.BuildHandshakeState()
302
303 if len(cfg.ALPN) > 0 {
304 found := false
305 for _, ext := range uconn.Extensions {
306 if alpnExt, ok := ext.(*utls.ALPNExtension); ok {
307 alpnExt.AlpnProtocols = cfg.ALPN
308 found = true
309 break
310 }
311 }
312 if !found {
313 uconn.Extensions = append(uconn.Extensions, &utls.ALPNExtension{AlpnProtocols: cfg.ALPN})
314 }
315 _ = uconn.BuildHandshakeState()
316 }
317
318 ja3Raw, ja3Hash := CalculateJA3(uconn)
319 ja4 := CalculateJA4(uconn)
320
321 if err := uconn.HandshakeContext(ctx); err != nil {
322 rawConn.Close()
323 return nil, fmt.Errorf("utls handshake failed: %w", err)
324 }
325
326 return &MasqueradeConn{
327 Conn: uconn,
328 uconn: uconn,
329 JA3Raw: ja3Raw,
330 JA3Hash: ja3Hash,
331 JA4: ja4,
332 Profile: cfg.Fingerprint,
333 }, nil
334}
335
336// NewHTTPTransport creates an *http.Transport configured with uTLS DialTLSContext.
337func NewHTTPTransport(cfg TLSConfig) *http.Transport {
338 c1 := cfg
339 if len(c1.ALPN) == 0 {
340 c1.ALPN = []string{"http/1.1"}
341 }
342 return &http.Transport{
343 DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
344 return DialTLS(ctx, network, addr, c1)
345 },
346 ForceAttemptHTTP2: false,
347 MaxIdleConns: 100,
348 IdleConnTimeout: 90 * time.Second,
349 TLSHandshakeTimeout: 10 * time.Second,
350 }
351}
352
353// NewHTTP2Transport creates an *http2.Transport configured with uTLS DialTLSContext.
354func NewHTTP2Transport(cfg TLSConfig) *http2.Transport {
355 c2 := cfg
356 c2.ALPN = []string{"h2"}
357 return &http2.Transport{
358 DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
359 return DialTLS(ctx, network, addr, c2)
360 },
361 AllowHTTP: false,
362 }
363}
364
365// convertUTLSToCryptoTLS converts utls.ConnectionState into standard crypto/tls.ConnectionState.
366func convertUTLSToCryptoTLS(s utls.ConnectionState) tls.ConnectionState {
367 return tls.ConnectionState{
368 Version: s.Version,
369 HandshakeComplete: s.HandshakeComplete,
370 DidResume: s.DidResume,
371 CipherSuite: s.CipherSuite,
372 NegotiatedProtocol: s.NegotiatedProtocol,
373 NegotiatedProtocolIsMutual: s.NegotiatedProtocolIsMutual,
374 ServerName: s.ServerName,
375 PeerCertificates: s.PeerCertificates,
376 VerifiedChains: s.VerifiedChains,
377 SignedCertificateTimestamps: s.SignedCertificateTimestamps,
378 OCSPResponse: s.OCSPResponse,
379 }
380}
381
382// utlsGRPCWrap implements credentials.TransportCredentials for gRPC client dialing over uTLS.
383type utlsGRPCWrap struct {
384 helloID utls.ClientHelloID
385 config *utls.Config
386}
387
388// NewGRPCTransportCredentials creates gRPC transport credentials using uTLS masquerading.
389func NewGRPCTransportCredentials(cfg TLSConfig) credentials.TransportCredentials {
390 helloID, _ := ResolveClientHelloID(cfg.Fingerprint)
391 utlsCfg := &utls.Config{
392 ServerName: cfg.ServerName,
393 InsecureSkipVerify: cfg.InsecureSkipVerify,
394 RootCAs: cfg.RootCAs,
395 MinVersion: cfg.MinVersion,
396 MaxVersion: cfg.MaxVersion,
397 NextProtos: []string{"h2"},
398 }
399 return &utlsGRPCWrap{
400 helloID: helloID,
401 config: utlsCfg,
402 }
403}
404
405func (c *utlsGRPCWrap) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
406 cfg := c.config.Clone()
407 if cfg.ServerName == "" {
408 host, _, err := net.SplitHostPort(authority)
409 if err == nil {
410 cfg.ServerName = host
411 } else {
412 cfg.ServerName = authority
413 }
414 }
415 uconn := utls.UClient(rawConn, cfg, c.helloID)
416 if err := uconn.HandshakeContext(ctx); err != nil {
417 rawConn.Close()
418 return nil, nil, fmt.Errorf("utls grpc handshake failed: %w", err)
419 }
420 return uconn, credentials.TLSInfo{State: convertUTLSToCryptoTLS(uconn.ConnectionState())}, nil
421}
422
423func (c *utlsGRPCWrap) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
424 return nil, nil, errors.New("utlsGRPCWrap is client-only")
425}
426
427func (c *utlsGRPCWrap) Info() credentials.ProtocolInfo {
428 return credentials.ProtocolInfo{SecurityProtocol: "tls", ServerName: c.config.ServerName}
429}
430
431func (c *utlsGRPCWrap) Clone() credentials.TransportCredentials {
432 return &utlsGRPCWrap{helloID: c.helloID, config: c.config.Clone()}
433}
434
435func (c *utlsGRPCWrap) OverrideServerName(serverNameOverride string) error {
436 c.config.ServerName = serverNameOverride
437 return nil
438}