用Go構建你專屬的JA3指紋

Gopher指北發表於2022-04-14
來自公眾號:Gopher指北

在這篇文章中將會簡單回顧https的握手流程,並基於讀者的提問題解釋什麼是JA3指紋以及如何用Go定製專屬的JA3指紋。

1.jpg

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

1.jpg

回顧HTTPS握手流程

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

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

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

1.jpg

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

1.jpg

單向認證和雙向認證的對比:

  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 ExtensionsElliptic CurvesElliptic Curve Formats。然後,它將這些值串聯起來,使用“,”來分隔各個欄位,同時使用“-”來分隔各個欄位中的值。最後,計算這些字串的md5雜湊值,即得到易於使用和共享的長度為32字元的指紋。

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

1.jpg

細心的同學可能已經發現了,根據前文描述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...) 可以在一定程度上修改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指紋如下。

1.jpg

我們已經得到了第一個JA3指紋,這個時候對程式碼稍加改動以期得到專屬的JA3指紋。例如我們將2333這個數值加入到CipherSuites列表中,最後得到結果如下。

1.jpg

最終,JA3指紋又發生了變化,並且可稱得上是自己專屬的指紋。不用我說,看標題就應該知道問題還沒有結束。從前面請求得到JA3指紋的結果圖也可以看出來,當前使用的協議為http1.1,因此老許從某度中找了一個支援http2的連結繼續驗證。

1.jpg

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

結果如下。

1.jpg

可以看到,最終在自定義JA3指紋後,http2的請求也能正常讀取。至此,在支援http2的請求中構建專屬的JA3指紋就完成了(生成JA3指紋的資訊在clientHelloMsg中,完成本部分僅是為了確保從發起請求到讀取響應都能夠正常進行)。

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

寫在最後

老許寫下本文不僅僅是帶大家瞭解ja3,更多的是期望各位讀者能夠通過自身的實踐加深對http底層的理解。

最後,衷心希望本文能夠對各位讀者有一定的幫助。

注:

寫本文時, 筆者所用go版本為: go1.17.7

文章中所用完整例子:https://github.com/Isites/go-...

相關文章