来自公众号: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指纹。
// 要害importimport ( xtls "github.com/refraction-networking/utls" "crypto/tls")// 克隆一个Transporttr := 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-...