首頁 >後端開發 >Golang >用Go建構你專屬的JA3指紋

用Go建構你專屬的JA3指紋

Go语言进阶学习
Go语言进阶学习轉載
2023-07-24 16:20:53875瀏覽
在這篇文章中將會簡單回顧https的握手流程,並基於讀者的提問題解釋什麼是JA3指紋以及如何用Go定制專屬的JA3指紋。
用Go建構你專屬的JA3指紋

本文大綱如下,請各位讀者跟著老許的思路逐步建構自己專屬的JA3指紋。

用Go建構你專屬的JA3指紋

回顧HTTPS握手流程

在正式開始了解什麼是JA3指紋之前,我們先回顧一下HTTPS的握手流程,這將有助於對後文的理解。

在碼了2000多行程式碼就是為了講清楚TLS握手流程這篇文章中主要分析了HTTPS單向認證和雙向認證的流程(TLS1.3)。

單向認證中,用戶端不需要證書,只需驗證服務端證書合法即可。其握手流程和交換的msg如下。

用Go建構你專屬的JA3指紋

雙向認證中,服務端和用戶端均需驗證對方憑證的合法性。其握手流程和交換的msg如下。

用Go建構你專屬的JA3指紋

單向認證與雙向認證的比較:

  1. 單向認證與雙向認證中,總的資料收發僅三次,單次傳送的資料中包含一個或多個訊息

  2. clientHelloMsgserverHelloMsg未經過加密,之後傳送的訊息都做了加密處理

  3. #Client和Server會各自計算兩次金鑰,運算時機分別是讀取到對方的HelloMsgfinishedMsg之後

  4. #雙向認證和單向認證相比,服務端多發送了certificateRequestMsgTLS13訊息

  5. 雙向認證和單向認證相比,客戶端多發送了certificateMsgTLS13certificateVerifyMsg兩個訊息

無論是單向認證或雙向認證,Server對於Client的基本資訊了解完全依賴Client主動告知Server,而其中比較關鍵的資訊分別是客戶端支援的TLS版本客戶端支援的加密套件(cipherSuites)客戶端支援的簽章演算法和客戶端支援的金鑰交換協定以及其對應的公鑰 。這些資訊皆在包含clientHelloMsg中,而這些資訊也是產生JA3指紋的關鍵訊息,且clientHelloMsgserverHelloMsg#未經過加密 。未加密意味著修改難度降低,這也為我們自訂自己專屬的JA3指紋提供了可能。

如果有興趣了解HTTPS握手流程的更多細節,請閱讀下面文章:

#碼了2000多行程式碼就是為了講清楚TLS握手流程

碼了2000多行程式碼就是為了講清楚TLS握手流程(續)

什麼是JA3指紋

前面說了這麼多,那到底什麼是JA3指紋。根據Open Sourcing JA3這篇文章,老許簡單將其理解為JA3就是一種在線識別TLS客戶端指紋的方法。

此方法用於收集clientHelloMsg封包中以下欄位的十進位位元組值:TLS VersionAccepted CiphersList of Extensions Elliptic CurvesElliptic Curve Formats。然後,它將這些值串聯起來,使用“,”來分隔各個字段,同時使用“-”來分隔各個字段中的值。最後,計算這些字串的md5雜湊值,即得到易於使用和共享的長度為32字元的指紋。

為了更近一步描述清楚這些資料的來源,老許將John Althouse文章中的抓包圖結合Go源碼中的clientHelloMsg結構體做了字段一一映射。

用Go建構你專屬的JA3指紋

細心的同學可能已經發現了,根據前文描述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的专属指纹

前文提到clientHelloMsgserverHelloMsg未经过加密,这为定制自己专属的JA3指纹提供了可能,而在github上面有一个库(https://github.com/refraction-networking/utls)可以在一定程度上修改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
}

上述代码总结起来分为三步。

  1. 创建TCP连接

  2. 构建clientHelloMsg需要的信息

  3. 完成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指纹如下。

用Go建構你專屬的JA3指紋

我们已经得到了第一个JA3指纹,这个时候对代码稍加改动以期得到专属的JA3指纹。例如我们将2333这个数值加入到CipherSuites列表中,最后得到结果如下。

用Go建構你專屬的JA3指紋

最终,JA3指纹又发生了变化,并且可称得上是自己专属的指纹。不用我说,看标题就应该知道问题还没有结束。从前面请求得到JA3指纹的结果图也可以看出来,当前使用的协议为http1.1,因此老许从某度中找了一个支持http2的链接继续验证。

用Go建構你專屬的JA3指紋

看过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)

结果如下。

用Go建構你專屬的JA3指紋

可以看到,最终在自定义JA3指纹后,http2的请求也能正常读取。至此,在支持http2的请求中构建专属的JA3指纹就完成了(生成JA3指纹的信息在clientHelloMsg中,完成本部分仅是为了确保从发起请求到读取响应都能够正常进行)。

額外補充幾句,透過手動NewClientConn這種方式完成http2請求具有很大的限制。例如,需要自行管理連線的生命週期、無法自動重連等。當然,這些都是後話,真有這方面需求的時候,可能就需要開發者從go源碼將net包fork一份自己維護了。

#

以上是用Go建構你專屬的JA3指紋的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:Go语言进阶学习。如有侵權,請聯絡admin@php.cn刪除