共计 6223 个字符,预计需要花费 16 分钟才能阅读完成。
来自公众号:Gopher 指北
在这篇文章中将会简略回顾 https 的握手流程,并基于读者的提问题解释什么是 JA3 指纹以及如何用 Go 定制专属的 JA3 指纹。
本文纲要如下,请各位读者跟着老许的思路逐渐构建本人专属的 JA3 指纹。
回顾 HTTPS 握手流程
在正式开始理解什么是 JA3 指纹之前,咱们先回顾一下 HTTPS 的握手流程,这将有助于对后文的了解。
在码了 2000 多行代码就是为了讲清楚 TLS 握手流程这篇文章中次要剖析了 HTTPS 单向认证和双向认证流程(TLS1.3)。
在 单向认证 中,客户端不须要证书,只需验证服务端证书非法即可。其握手流程和替换的 msg 如下。
在 双向认证 中,服务端和客户端均需验证对方证书的合法性。其握手流程和替换的 msg 如下。
单向认证和双向认证的比照:
- 单向认证和双向认证中,总的数据收发仅三次,单次发送的数据中蕴含一个或者多个音讯
clientHelloMsg
和serverHelloMsg
未通过加密,之后发送的音讯均做了加密解决- Client 和 Server 会各自计算两次密钥,计算机会别离是读取到对方的
HelloMsg
和finishedMsg
之后 - 双向认证和单向认证相比,服务端多发送了
certificateRequestMsgTLS13
音讯 - 双向认证和单向认证相比,客户端多发送了
certificateMsgTLS13
和certificateVerifyMsg
两个音讯
无论是单向认证还是双向认证,Server 对于 Client 的根本信息理解齐全依赖于 Client 被动告知 Server,而其中比拟要害的信息别离是 客户端反对的 TLS 版本
、 客户端反对的加密套件(cipherSuites)
、客户端反对的签名算法和客户端反对的密钥替换协定以及其对应的公钥
。这些信息均在蕴含clientHelloMsg
中,而这些信息也是生成 JA3 指纹的要害信息,并且 clientHelloMsg
和serverHelloMsg
未通过加密。未加密意味着批改难度升高,这也就为咱们定制本人专属的 JA3 指纹提供了可能。
如果有趣味理解 HTTPS 握手流程的更多细节,请浏览上面文章:
码了 2000 多行代码就是为了讲清楚 TLS 握手流程
码了 2000 多行代码就是为了讲清楚 TLS 握手流程(续)
什么是 JA3 指纹
后面说了这么多,那么到底什么是 JA3 指纹呢。依据 Open Sourcing JA3 这篇文章,老许简略将其了解为 JA3 就是一种在线辨认 TLS 客户端指纹的办法。
该办法用于收集 clientHelloMsg
数据包中以下字段的十进制字节值:TLS Version
、Accepted Ciphers
、List of Extensions
、Elliptic Curves
和Elliptic Curve Formats
。而后,它将这些值串联起来,应用“,”来分隔各个字段,同时应用“-”来分隔各个字段中的值。最初,计算这些字符串的 md5 哈希值,即失去易于应用和共享的长度为 32 字符的指纹。
为了更近一步形容分明这些数据的起源,老许将 John Althouse
文章中的抓包图联合 Go 源码中的 clientHelloMsg
构造体做了字段一一映射。
仔细的同学可能曾经发现了,依据前文形容 JA3 指纹总共有 5 个数据字段,而上图却只映射了 4 个。那是因为 TLS 的 extension 字段比拟多,老许就不一一整顿了。尽管没有一一列举,但老许筹备了一个单元测试,有趣味深入研究的同学能够通过这个单元测试进行调试剖析。
https://github.com/Isites/go-coder/blob/master/http2/tls/handsh/msg_test.go
JA3 指纹用处
依据前文的形容,JA3 指纹就是一个 md5 字符串。请大家回忆一下在平时的开发中 md5 的用处。
- 判断内容是否统一
- 作为惟一标识
md5 尽管不平安,然而 JA3 抉择 md5 作为哈希的次要起因是为了更好的向后兼容
很显著,JA3 指纹也有其相似用处。举个简略的例子,攻击者构建了一个可执行文件,那么该文件的 JA3 指纹很有可能是惟一的。因而,咱们能通过 JA3 指纹识别出一些恶意软件。
在本大节的最初,老许给大家举荐一个网站,该网站挂出了很多歹意 JA3 指纹列表。
https://sslbl.abuse.ch/ja3-fingerprints/
构建专属的 JA3 指纹
http1.1 的专属指纹
前文提到 clientHelloMsg
和serverHelloMsg
未通过加密,这为定制本人专属的 JA3 指纹提供了可能,而在 github 下面有一个库(https://github.com/refraction…) 能够在肯定水平上批改clientHelloMsg
。上面咱们将通过这个库构建一个本人专属的 JA3 指纹。
// 要害 import
import (
xtls "github.com/refraction-networking/utls"
"crypto/tls"
)
// 克隆一个 Transport
tr := http.DefaultTransport.(*http.Transport).Clone()
// 自定义 DialTLSContext 函数,此函数会用于创立 tcp 连贯和 tls 握手
tr.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {dialer := net.Dialer{}
// 创立 tcp 连贯
con, err := dialer.DialContext(ctx, network, addr)
if err != nil {return nil, err}
// 依据地址获取 host 信息
host, _, err := net.SplitHostPort(addr)
if err != nil {return nil, err}
// 构建 tlsconf
xtlsConf := &xtls.Config{
ServerName: host,
Renegotiation: xtls.RenegotiateNever,
}
// 构建 tls.UConn
xtlsConn := xtls.UClient(con, xtlsConf, xtls.HelloCustom)
clientHelloSpec := &xtls.ClientHelloSpec{
// hellomsg 中的最大最小 tls 版本
TLSVersMax: tls.VersionTLS12,
TLSVersMin: tls.VersionTLS10,
// ja3 指纹须要的 CipherSuites
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
// tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
// tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
CompressionMethods: []byte{0,},
// ja3 指纹须要的 Extensions
Extensions: []xtls.TLSExtension{&xtls.RenegotiationInfoExtension{Renegotiation: xtls.RenegotiateOnceAsClient},
&xtls.SNIExtension{ServerName: host},
&xtls.UtlsExtendedMasterSecretExtension{},
&xtls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []xtls.SignatureScheme{
xtls.ECDSAWithP256AndSHA256,
xtls.PSSWithSHA256,
xtls.PKCS1WithSHA256,
xtls.ECDSAWithP384AndSHA384,
xtls.ECDSAWithSHA1,
xtls.PSSWithSHA384,
xtls.PSSWithSHA384,
xtls.PKCS1WithSHA384,
xtls.PSSWithSHA512,
xtls.PKCS1WithSHA512,
xtls.PKCS1WithSHA1}},
&xtls.StatusRequestExtension{},
&xtls.NPNExtension{},
&xtls.SCTExtension{},
&xtls.ALPNExtension{AlpnProtocols: []string{"h2", "http/1.1"}},
// ja3 指纹须要的 Elliptic Curve Formats
&xtls.SupportedPointsExtension{SupportedPoints: []byte{1}}, // uncompressed
// ja3 指纹须要的 Elliptic Curves
&xtls.SupportedCurvesExtension{Curves: []xtls.CurveID{
xtls.X25519,
xtls.CurveP256,
xtls.CurveP384,
xtls.CurveP521,
},
},
},
}
// 定义 hellomsg 的加密套件等信息
err = xtlsConn.ApplyPreset(clientHelloSpec)
if err != nil {return nil, err}
// TLS 握手
err = xtlsConn.Handshake()
if err != nil {return nil, err}
fmt.Println("以后申请应用协定:", xtlsConn.HandshakeState.ServerHello.AlpnProtocol)
return xtlsConn, err
}
上述代码总结起来分为三步。
- 创立 TCP 连贯
- 构建
clientHelloMsg
须要的信息 - 实现 TLS 握手
有了上述代码后,咱们通过申请 https://ja3er.com/json
来失去本人的 JA3 指纹。
c := http.Client{Transport: tr,}
resp, err := c.Get("https://ja3er.com/json")
if err != nil {fmt.Println(err)
return
}
bts, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println(string(bts), err)
最初失去的 JA3 指纹如下。
咱们曾经失去了第一个 JA3 指纹,这个时候对代码稍加改变以期失去 专属
的 JA3 指纹。例如咱们将 2333
这个数值退出到 CipherSuites
列表中,最初失去后果如下。
最终,JA3 指纹又产生了变动,并且可称得上是本人专属的指纹。不必我说,看题目就应该晓得问题还没有完结。从后面申请失去 JA3 指纹的后果图也能够看进去,以后应用的协定为http1.1
,因而老许从某度中找了一个反对 http2 的链接持续验证。
看过 Go 发动 HTTP2.0 申请流程析 (前篇) 这篇文章的同学应该晓得,http2 连贯在建设时须要发送 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
这么一个字符串。很显著,在自定义了 DialTLSContext
函数之后相干流程缺失。此时,咱们该如何构建 http2 的专属指纹呢?
http2 的专属指纹
通过 DialTLSContext
拨号之后只能失去一个曾经实现 TLS 握手的连贯,此时它还不反对 http2 的 数据帧
、 多路复用
等个性。所以,咱们须要本人构建一个反对 http2 各种个性的连贯。
上面,咱们通过 golang.org/x/net/http2
来实现自定义 TLS 握手流程后的 http2 申请。
// 手动拨号,失去一个曾经实现 TLS 握手后的连贯
con, err := tr.DialTLSContext(context.Background(), "tcp", "dss0.bdstatic.com:443")
if err != nil {fmt.Println("DialTLSContext", err)
return
}
// 构建一个 http2 的连贯
tr2 := http2.Transport{}
// 这一步很要害,不可缺失
h2Con, err := tr2.NewClientConn(con)
if err != nil {fmt.Println("NewClientConn", err)
return
}
req, _ := http.NewRequest("GET", "https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/topnav/newzhidao-da1cf444b0.png", nil)
// 向一个反对 http2 的链接发动申请并读取申请状态
resp2, err := h2Con.RoundTrip(req)
if err != nil {fmt.Println("RoundTrip", err)
return
}
io.CopyN(io.Discard, resp2.Body, 2<<10)
resp2.Body.Close()
fmt.Println("响应 code:", resp2.StatusCode)
后果如下。
能够看到,最终在自定义 JA3 指纹后,http2 的申请也能失常读取。至此,在反对 http2 的申请中构建专属的 JA3 指纹就实现了(生成 JA3 指纹的信息在 clientHelloMsg
中,实现本局部仅是为了确保从发动申请到读取响应都可能失常进行)。
额定补充几句,通过手动 NewClientConn
这种形式实现 http2 申请具备很大的局限性。比方,须要本人治理连贯的生命周期、无奈主动重连等。当然,这些都是后话,真有这方面需要的时候,可能就须要开发者从 go 源码将 net 包 fork 一份本人保护了。
写在最初
老许写下本文不仅仅是带大家理解 ja3,更多的是冀望各位读者可能通过本身的实际加深对 http 底层的了解。
最初,衷心希望本文可能对各位读者有肯定的帮忙。
注:
写本文时,笔者所用 go 版本为: go1.17.7
文章中所用残缺例子:https://github.com/Isites/go-…