TLS 指紋:JA3 與 JA4 完整指南

從 TLS 握手與 ClientHello 基礎講起,解釋為何加密連線的第一個訊息就能識別客戶端軟體,以及 JA3 到 JA4 的演算法演進。

📚 系列導覽:本篇是「Cloudflare 反爬蟲機制」系列第 2 篇,對應總覽篇六層偵測中的第 2 層(TLS 層)。

# 筆記 主題
01 反爬蟲偵測總覽 六層偵測全景、請求時序地圖
02 TLS 指紋:JA3 與 JA4(本篇) ClientHello 為何洩漏身分、兩代演算法
03 HTTP/2 指紋與 Akamai Fingerprint 連線層參數指紋、三方交叉驗證
04 Managed Challenge 與 Bot Score 主動挑戰機制、PAT、Cookie 綁定

目錄


什麼是 TLS 指紋?

TLS 指紋是把客戶端在 TLS 握手時送出的參數組合,濃縮成一個可比對的短字串,用來識別客戶端使用的是哪一套軟體。

一句話理解:TLS 指紋不看你「說」自己是誰,而看你「怎麼」開始加密連線。

為什麼需要 TLS 指紋?

只靠 HTTP 標頭判斷客戶端的問題:

  • User-Agent 是純字串,任何程式都能填任意值,零成本
  • 標頭由應用層程式碼產生,跟底層網路實作完全脫鉤
  • 沒有任何機制能驗證標頭的真實性

TLS 指紋提供的東西:

  • 資訊來源是 TLS 函式庫的行為,不是應用程式碼可以隨手改的
  • 在 HTTP 請求送出之前就已採集完成(見總覽篇的請求時序)
  • 可與 User-Agent 的宣稱交叉比對,矛盾即為訊號

核心特點

TLS 指紋 = ClientHello 的參數集合 + 參數的順序 → 雜湊

基礎補課:TLS 握手在做什麼

在談指紋之前,需要先理解 TLS 握手的目的。如果你對 TLS 已經熟悉,可以跳到為什麼 ClientHello 會洩漏身分。

握手要達成三件事

當你連上一個 https:// 網址,在傳送任何實際資料之前,雙方必須先完成三件事:

1. 驗證伺服器身分
   「你真的是 example.com 嗎?」
   → 伺服器出示憑證,客戶端驗證憑證鏈

2. 協商加密演算法
   「我們用哪一種加密方式溝通?」
   → 客戶端列出自己支援的,伺服器從中挑一個

3. 產生共用的對稱金鑰
   「接下來用這把鑰匙加密。」
   → 透過金鑰交換演算法,雙方各自算出同一把金鑰

第 2 件事是 TLS 指紋的來源。因為「列出自己支援的演算法」這個動作,必然要把客戶端的能力清單攤在檯面上。

握手的訊息順序

客戶端                                    伺服器
   │                                         │
   │──────── ClientHello ──────────────────▶ │  ← 明文
   │         「我支援這些演算法,            │
   │           我要連 example.com」          │
   │                                         │
   │ ◀────── ServerHello ──────────────────  │  ← 明文
   │         「就用其中這一個」               │
   │ ◀────── Certificate ──────────────────  │
   │         「這是我的憑證」                 │
   │                                         │
   │──────── 金鑰交換、驗證 ───────────────▶ │
   │ ◀────── 完成 ─────────────────────────  │
   │                                         │
   │════════ 之後的通訊全部加密 ════════════ │

關鍵:為什麼第一個訊息是明文

ClientHello 必須是明文,因為此時還沒有任何金鑰。

這是一個無法迴避的邏輯順序:要加密就得先有金鑰,要有金鑰就得先協商演算法,要協商演算法就得先把清單送出去。所以清單本身不可能被加密。

這個結構性事實是所有 TLS 指紋技術的基礎:任何位於連線路徑上的設備——CDN、防火牆、負載平衡器、ISP——都能完整讀取 ClientHello,即使它無法解密後續的通訊內容。

💡 常見誤解:「HTTPS 是加密的,所以中間的設備看不到任何東西。」

實際上,握手階段的 ClientHello 與 ServerHello 都是明文。加密只保護握手完成之後的應用資料。這也是為什麼 SNI(你要連哪個網域)長年是個隱私問題,並促成了後來的 Encrypted ClientHello 提案。


基礎補課:ClientHello 裡有什麼

ClientHello 是一個結構化的二進位訊息。以下是與指紋相關的欄位:

欄位 內容 是否有序 指紋價值
Version 客戶端支援的最高 TLS 版本 — 低(值少)
Client Random 32 位元組隨機值 — 無(每次都不同)
Session ID 用於恢復先前的連線 — 無
Cipher Suites 支援的加密演算法清單 ✅ 有序 🔴 極高
Compression Methods 壓縮方式(現代 TLS 固定為「無」) — 低
Extensions 擴充欄位清單 ✅ 有序 🔴 極高

Cipher Suites(加密套件清單)

一個加密套件描述一整組演算法的搭配。例如 TLS_AES_128_GCM_SHA256 表示用 AES-128-GCM 做對稱加密、SHA256 做雜湊。

客戶端送出的是一個依偏好排序的清單:

Cipher Suites (示意,共 15 個):
  0x1301  TLS_AES_128_GCM_SHA256          ← 最偏好
  0x1302  TLS_AES_256_GCM_SHA384
  0x1303  TLS_CHACHA20_POLY1305_SHA256
  0xc02b  ECDHE-ECDSA-AES128-GCM-SHA256
  0xc02f  ECDHE-RSA-AES128-GCM-SHA256
  ...                                      ← 最不偏好

清單的內容與順序都不是使用者選的,而是由 TLS 函式庫(或使用它的應用程式)在編譯或初始化時決定。

Extensions(擴充欄位清單)

TLS 的擴充機制讓新功能能在不改版本號的前提下加入。常見的擴充:

編號 名稱 用途
0 server_name (SNI) 告知要連的網域名稱
10 supported_groups 支援的橢圓曲線 / 金鑰交換群組
11 ec_point_formats 橢圓曲線點的編碼格式
13 signature_algorithms 支援的簽章演算法清單(有序)
16 application_layer_protocol_negotiation (ALPN) 協商上層協定(h2 / http/1.1)
43 supported_versions TLS 1.3 用來宣告版本
51 key_share TLS 1.3 的金鑰交換材料
65281 renegotiation_info 重新協商相關

送出哪些擴充、以什麼順序送,是各家 TLS 實作的自由選擇。 RFC 並未規定順序,這正是指紋的空間所在。


為什麼 ClientHello 會洩漏身分

這一節是本篇的核心觀念。

洩漏的不是「內容」,而是「組合與順序」

單看一個加密套件編號,沒有任何辨識價值——0x1301 是標準值,誰都可以用。有辨識價值的是:

  1. 支援哪些(集合)
  2. 不支援哪些(集合的補集同樣有訊息量)
  3. 排列的順序(偏好次序)
  4. 送出哪些擴充欄位
  5. 擴充欄位的排列順序

這五項組合起來的可能性極大,而每一套 TLS 實作在其中佔據一個相當固定的位置。

為什麼會固定?

因為這些選擇寫在函式庫的原始碼裡,不是執行時的動態決定:

Chrome
  └─ 使用 BoringSSL(Google 從 OpenSSL 分支出來的實作)
     └─ 加密套件清單由 Chrome 原始碼明確指定
        └─ 同一版 Chrome 的所有使用者 → 相同清單

Firefox
  └─ 使用 NSS(Mozilla 的實作)
     └─ 不同的清單、不同的擴充集合

Python requests / urllib3
  └─ 使用 CPython 的 ssl 模組
     └─ 底層是系統的 OpenSSL
        └─ 清單來自 OpenSSL 的預設值或 Python 設定的字串

Go 標準庫
  └─ 使用 crypto/tls(純 Go 實作,不依賴 OpenSSL)
     └─ 又是完全不同的集合與順序

結論:TLS 指紋識別的是「你用哪一套 TLS 函式庫、什麼版本、什麼設定」。它不識別個人,也不識別應用程式——它識別軟體堆疊。

一個重要的推論

因為指紋來自函式庫,所以:

  • 改應用程式碼無法改變指紋——你在 Python 裡怎麼設定標頭,都不會影響 OpenSSL 送出的 ClientHello
  • 要改變指紋只能更換 TLS 實作——這是換零件,不是調參數
  • 同一版瀏覽器的所有使用者共享指紋——因此指紋是群組識別碼,不是個人識別碼

最後這點很重要,也很常被誤解:TLS 指紋無法用來追蹤特定使用者,因為全世界數億個 Chrome 使用者的指紋是一樣的。它的用途是回答「這是什麼工具」,不是「這是誰」。


JA3:第一代指紋

JA3 由 Salesforce 於 2017 年提出,是第一個被廣泛採用的 TLS 客戶端指紋標準。名稱來自三位作者名字的首字母。

組成公式

JA3 取 ClientHello 的五個欄位,用逗號串成一個字串:

SSLVersion, Ciphers, Extensions, EllipticCurves, EllipticCurvePointFormats

規則:

  • 每個欄位內的多個數值用連字號 - 分隔
  • 所有數值轉為十進位
  • GREASE 保留值必須剔除(下節說明)
  • 欄位缺失時留空,但逗號保留

實際組成(示意)

輸入字串(示意值):
771,4865-4866-4867-49195-49199-52393,0-23-65281-10-11-16-5-13-43-51,29-23-24,0
 │   │                               │                              │        │
 │   │                               │                              │        └─ 點格式
 │   │                               │                              └─ 橢圓曲線
 │   │                               └─ 擴充欄位編號(有序)
 │   └─ 加密套件編號(有序)
 └─ TLS 版本(771 = 0x0303 = TLS 1.2)

           ↓ MD5

JA3 指紋:cd08e31494f9531f560d64c695473da9  (32 個十六進位字元)

⚠️ 上述數值為示意,實際值依軟體版本與設定而異。本篇所有指紋數值都僅供理解格式之用,不應當成特定軟體的識別依據。

版本號為什麼是 771

TLS 版本在協定中以兩位元組表示,0x0303 換成十進位就是 771。對照表:

十六進位 十進位 版本
0x0301 769 TLS 1.0
0x0302 770 TLS 1.1
0x0303 771 TLS 1.2
0x0304 772 TLS 1.3

有個容易困惑的細節:即使實際使用 TLS 1.3,ClientHello 的版本欄位通常仍寫 771。這是為了與舊設備相容——真正的 1.3 版本宣告放在 supported_versions 擴充(編號 43)裡。JA3 讀的是舊欄位,所以 TLS 1.3 連線的 JA3 版本段常常是 771。


JA3 的三個破口

JA3 至今仍被廣泛使用,但它有結構性的弱點。理解這些弱點,才能理解 JA4 為何要重新設計。

破口 1:GREASE 造成的雜訊

GREASE(Generate Random Extensions And Sustain Extensibility,RFC 8701)是一個防止協定僵化的機制。

問題背景:如果所有客戶端都只送已知的值,中間設備(防火牆、代理)可能會寫出「遇到不認識的值就報錯」的實作。日後協定要加新功能時,這些設備就會壞掉——這叫協定僵化(ossification)。

GREASE 的解法:客戶端故意隨機插入一些保留的無意義值(0x0a0a、0x1a1a、0x2a2a … 0xfafa),迫使所有設備學會忽略未知值。

含 GREASE 的加密套件清單(示意):
  0x3a3a  ← GREASE,隨機插入,無實際意義
  0x1301  TLS_AES_128_GCM_SHA256
  0x1302  TLS_AES_256_GCM_SHA384
  ...

對指紋的影響:GREASE 值每次連線都不同,若不剔除,同一個瀏覽器每次的指紋都會不一樣。JA3 標準要求剔除 GREASE,但各家實作的剔除是否完整並不一致,造成同一個客戶端在不同偵測系統得到不同指紋。

破口 2:Chrome 的擴充順序隨機化(致命)

2023 年初,Chrome 110 開始每次連線都隨機打亂 TLS 擴充欄位的順序。動機同樣是防止協定僵化——避免中間設備依賴特定順序。

Chrome 第 1 次連線的擴充順序:0-23-65281-10-11-16-5-13-43-51
Chrome 第 2 次連線的擴充順序:43-10-0-51-13-11-16-65281-5-23
Chrome 第 3 次連線的擴充順序:11-51-13-0-16-43-23-10-65281-5

因為 JA3 的第三個欄位是有序的擴充清單,順序一變,MD5 就完全不同。

結果:JA3 對 Chrome 徹底失效。同一個瀏覽器每次連線產生不同的 JA3 指紋,無法比對、無法建立清單、無法統計。

這是 JA4 誕生的直接原因。

破口 3:MD5 不可讀

JA3 的輸出是一個 32 字元的 MD5 值:

cd08e31494f9531f560d64c695473da9

這個值只能做完全比對。它無法回答任何有意義的問題:

  • ❌ 「這個指紋像不像 Chrome?」——無法判斷,雜湊沒有相似性
  • ❌ 「這個客戶端支援 TLS 1.3 嗎?」——資訊被雜湊掉了
  • ❌ 「這個客戶端只支援 5 種加密套件,是不是太少?」——看不出來
  • ❌ 「這個客戶端有沒有協商 HTTP/2?」——JA3 根本不含 ALPN

要做任何規則判斷,都必須先建立一份龐大的「已知指紋資料庫」,然後純比對。這使得 JA3 對未知的新客戶端完全無能——沒見過就是沒見過,得不到任何線索。

附帶問題:JA3 不含 ALPN

ALPN 擴充(編號 16)本身會出現在擴充清單裡,但它的內容(是 h2 還是 http/1.1)不進入 JA3。這損失了一個高價值訊號:一個聲稱是現代瀏覽器卻不協商 HTTP/2 的客戶端,本身就很可疑。


JA4:第二代指紋

JA4 由 FoxIO 於 2023 年提出,直接針對 JA3 的三個破口重新設計。

JA4 實際上是一整個系列(統稱 JA4+),涵蓋不同的觀察點:

名稱 觀察對象
JA4 TLS 客戶端(ClientHello)← 本篇主題
JA4S TLS 伺服器(ServerHello)
JA4H HTTP 請求(標頭與 cookie 特徵)
JA4L 連線延遲與距離推估
JA4X X.509 憑證特徵

📄 授權注意:JA4+ 採用 FoxIO License 1.1。內部使用與研究通常無限制,但若要把它做成商業產品的功能販售,需要取得授權。實作前值得確認條款。

三段式結構

JA4 的輸出不是單一雜湊,而是三段:

t13d1516h2 _ 8daaf6152771 _ b0da82dd1658
└────┬───┘   └─────┬────┘   └─────┬────┘
   a 段          b 段           c 段
 可讀中介資料   加密套件雜湊    擴充欄位雜湊
  (10 字元)    (12 字元)     (12 字元)

⚠️ 上述為示意值,僅用於說明格式。

三個關鍵改進

改進 1:排序後才雜湊(解決破口 2)

JA4 在計算 b 段與 c 段之前,先把清單依數值排序,才做雜湊。

Chrome 第 1 次:擴充順序 0-23-65281-10-11-...
Chrome 第 2 次:擴充順序 43-10-0-51-13-...
                    ↓ 排序
兩次都變成:        0-10-11-13-23-43-51-65281-...
                    ↓ SHA256
兩次都得到:        相同的 c 段

這一步讓 JA4 對 Chrome 的順序隨機化完全免疫。

代價:損失了「順序」這個訊號。JA4 用兩個方式補償——a 段保留可讀的中介資料,且 c 段裡的 signature_algorithms 刻意不排序(見下節)。

改進 2:a 段可讀(解決破口 3)

a 段是十個可直接閱讀的字元,不經雜湊。這讓防禦方可以寫出「有語意」的規則,例如:

  • 「TLS 1.3 + 協商 h2,但只支援 5 種加密套件」→ 真瀏覽器不會這樣,可疑
  • 「沒有 SNI」→ 直接用 IP 連線,一般瀏覽行為不會如此
  • 「擴充欄位只有 4 個」→ 遠少於任何現代瀏覽器

即使從未見過這個指紋,a 段本身就提供了判斷依據。這是 JA4 相對 JA3 最實質的躍進。

改進 3:排除 SNI 與 ALPN 出 c 段雜湊

server_name(SNI)的內容隨目標網域而變,ALPN 的資訊已經進了 a 段。JA4 把這兩個擴充排除在 c 段的雜湊之外。

效果:同一個客戶端連線到不同網域時,產生相同的 JA4 指紋。這讓跨網域的統計與比對變得可行——JA3 則會因 SNI 不同而產生不同的雜湊。

(註:SNI 與 ALPN 仍然被計入 a 段的擴充數量,只是不進入 c 段的雜湊內容。)


JA4 字串逐碼拆解

以示意值 t13d1516h2_8daaf6152771_b0da82dd1658 為例。

a 段:t13d1516h2(10 字元)

t  13  d  15  16  h2
│  │   │  │   │   │
│  │   │  │   │   └─ 第 9-10 碼:ALPN 首尾字元
│  │   │  │   │        h2       → "h2"
│  │   │  │   │        http/1.1 → "h1"(取首字 h、尾字 1)
│  │   │  │   │        無 ALPN  → "00"
│  │   │  │   │
│  │   │  │   └─ 第 7-8 碼:擴充欄位數量(十進位補零,上限 99)
│  │   │  │        此例為 16 個(含 SNI 與 ALPN,剔除 GREASE)
│  │   │  │
│  │   │  └─ 第 5-6 碼:加密套件數量(十進位補零,上限 99)
│  │   │       此例為 15 個(剔除 GREASE)
│  │   │
│  │   └─ 第 4 碼:SNI 狀態
│  │        d = 有網域名稱(domain)
│  │        i = 無 SNI,直接連 IP
│  │
│  └─ 第 2-3 碼:TLS 版本
│       13 = TLS 1.3    12 = TLS 1.2
│       11 = TLS 1.1    10 = TLS 1.0
│       (優先讀 supported_versions 擴充,故能正確反映 1.3)
│
└─ 第 1 碼:傳輸協定
     t = TCP(一般 TLS)
     q = QUIC(HTTP/3)
     d = DTLS

t13d1516h2 讀作:「走 TCP、TLS 1.3、有 SNI、15 個加密套件、16 個擴充欄位、ALPN 協商 h2」。

注意 JA4 的第 1 碼保留了 q,這是為 QUIC / HTTP/3 準備的——同一套框架可以延伸到新協定。

b 段:8daaf6152771(12 字元)

取得加密套件清單 → 剔除 GREASE → 依數值排序 → 逗號分隔
  ↓
SHA256 雜湊
  ↓
取前 12 個十六進位字元

b 段代表「這個客戶端支援哪些加密套件」這個集合(不含順序)。

c 段:b0da82dd1658(12 字元)

c 段的計算比較特別,它把兩部分接在一起:

第一部分:擴充欄位清單
  → 剔除 GREASE、剔除 SNI(0)、剔除 ALPN(16)
  → 依數值排序
  → 逗號分隔

第二部分:signature_algorithms 的內容
  → 保持原始順序,不排序!
  → 逗號分隔

  兩部分用底線連接 → SHA256 → 取前 12 個十六進位字元

為什麼 signature_algorithms 不排序?

這是 JA4 設計上一個刻意的權衡。Chrome 的隨機化只打亂擴充欄位之間的順序,並不打亂單一擴充內部的內容順序。因此 signature_algorithms 裡的簽章演算法排列仍然是穩定且高度可辨識的——不同函式庫的偏好次序差異很大。

JA4 保留這個順序,等於在「免疫隨機化」的前提下,把僅存的順序訊號完整留下。這是一個很精緻的設計取捨。

補充:JA4_r(原始版本)

除了雜湊版本,JA4 還定義了 JA4_r(raw),把 b、c 兩段的內容原樣列出而不雜湊。它較長但完全可讀,適合分析與除錯——你可以直接看到客戶端支援哪些演算法,而不必反查資料庫。


為什麼 Python 必然不同於 Chrome

回到最實務的問題:為什麼用 Python 寫的程式,即使把標頭抄得一字不差,TLS 指紋仍然對不上?

根本原因:不同的 TLS 實作

客戶端 TLS 實作 特徵
Chrome / Edge BoringSSL 加密套件清單由 Chrome 原始碼指定;啟用 GREASE;擴充順序隨機化;ALPN 送 h2 + http/1.1
Firefox NSS 不同的加密套件集合與順序;擴充集合與 Chrome 明顯不同
Safari Apple Secure Transport / BoringSSL 又是另一組設定
Python requests / urllib3 CPython ssl → 系統 OpenSSL 清單來自 OpenSSL 預設值;無 GREASE;擴充集合較小;常包含 Chrome 已淘汰的舊套件
Go 標準庫 crypto/tls(純 Go) 完全獨立的實作,擴充集合與順序都與 OpenSSL 系不同
curl 依編譯選項(OpenSSL / GnuTLS / NSS / Schannel) 同一個 curl 指令在不同系統上指紋可能不同
Node.js 內建 OpenSSL 與 Python 同屬 OpenSSL 系,但版本與設定字串不同

具體會在哪一段對不上

以 Python 對照 Chrome 為例:

a 段:可能部分吻合
  版本、SNI 狀態或許相同
  但「加密套件數量」與「擴充數量」通常明顯不同
  → 例如 Chrome 有 16 個擴充,Python 可能只有 9 個
  → 且 Python 預設不送 ALPN,第 9-10 碼會是 "00" 而非 "h2"
     這一項單獨就足以判定「不是瀏覽器」

b 段:幾乎不可能吻合
  加密套件集合不同 → 雜湊必然不同
  Python/OpenSSL 通常包含 Chrome 早已移除的舊套件

c 段:幾乎不可能吻合
  擴充集合不同 → 雜湊必然不同
  且 signature_algorithms 的順序偏好差異很大

這解釋了什麼

「無法用標頭修補」的根本原因:標頭是應用層的字串,ClientHello 是傳輸層安全協定的訊息。它們由不同的軟體元件在不同的時間點產生。應用程式碼對後者沒有存取權。

要改變 TLS 指紋,唯一的方法是更換或改造 TLS 實作本身,讓它送出與目標軟體一致的 ClientHello。這類技術存在(例如可自訂 ClientHello 的 TLS 函式庫),但它是「換整個零件」的層級,而且——如系列第 03 篇會說明的——修好 TLS 層之後,HTTP/2 連線層還有一份獨立的證據等著比對。


對比分析:JA3 vs JA4

特性 JA3 JA4
提出者 / 年份 Salesforce / 2017 FoxIO / 2023
輸出格式 單一 MD5(32 字元) 三段 a_b_c(10 + 12 + 12)
雜湊演算法 MD5 SHA256(截斷取前 12 碼)
是否可讀 ❌ 完全不可讀 ✅ a 段可讀
對抗順序隨機化 ❌ 完全失效 ✅ 排序後雜湊,免疫
含 ALPN 資訊 ❌ 只知擴充存在,不知內容 ✅ a 段第 9-10 碼
跨網域穩定 ❌ SNI 影響雜湊 ✅ SNI 排除在雜湊外
支援 QUIC ❌ 未設計 ✅ 第 1 碼 q
對未知客戶端 ❌ 無線索,只能比對 ✅ a 段提供語意判斷
保留順序訊號 ✅ 全部保留(但因此脆弱) 🟡 僅保留 signature_algorithms 順序
生態成熟度 ✅ 工具與資料庫豐富 🟡 較新,但已被主流採用
授權 開源,無限制 FoxIO License 1.1(商業產品需授權)

何時使用 JA3?

  • 需要與既有的舊資料或舊系統比對
  • 目標客戶端是不做順序隨機化的舊軟體
  • 已有大量歷史 JA3 資料需延續分析

何時使用 JA4?

  • 需要涵蓋現代瀏覽器(Chrome 110 以後)
  • 需要寫「有語意」的規則,而非純比對清單
  • 需要跨網域統計同一客戶端
  • 需要涵蓋 QUIC / HTTP/3 流量

對比總結

選擇 JA3
  ✅ 適合:歷史資料延續、非隨機化的舊客戶端
  ❌ 不適合:Chrome 110+、未知客戶端判斷、QUIC

選擇 JA4
  ✅ 適合:現代瀏覽器、規則式判斷、跨網域統計、QUIC
  ❌ 不適合:需要完整順序訊號的特殊分析、有授權顧慮的商業產品

實務上:兩者並存
  多數偵測系統同時計算 JA3 與 JA4,前者用於相容,後者用於判斷。

防禦端如何使用 TLS 指紋

理解防禦方怎麼用這個訊號,能反過來加深對它性質的理解。

用法 1:作為評分特徵(最主流)

TLS 指紋是 Bot Score 的其中一個輸入,不是唯一依據。系統把它與 IP 信譽、HTTP/2 指紋、標頭一致性等訊號一起餵給模型。

Cloudflare 在 WAF 規則中提供對應欄位,可用於自訂規則:

欄位 內容
cf.bot_management.ja3_hash JA3 指紋
cf.bot_management.ja4 JA4 指紋
cf.bot_management.score 綜合 Bot Score
cf.bot_management.verified_bot 是否為已驗證的良性爬蟲

用法 2:交叉驗證(辨識力最強)

User-Agent 宣稱:Chrome 140 on macOS
JA4 顯示:a 段無 ALPN、擴充數量偏少、b 段對應 OpenSSL 系
     ↓
結論:宣稱與底層實作矛盾 → 明確的偽裝意圖

這是總覽篇提到的主線。單一指紋只說明「你用什麼工具」,矛盾才說明「你在假裝」。

用法 3:統計異常偵測

不需要事先知道某個指紋是好是壞,只要觀察分布變化:

  • 某個罕見指紋的請求量在短時間內暴增
  • 某個指紋只出現在特定路徑(例如只打登入端點)
  • 某個指紋的請求完全不載入子資源

這種用法對全新的、從未見過的自動化工具也有效。

用法 4:直接封鎖清單(風險最高)

技術上可以對特定 JA4 直接封鎖,但這是風險最高的用法,原因回到指紋的本質:

TLS 指紋是群組識別碼。封鎖一個指紋,等於封鎖使用同一套 TLS 函式庫與設定的所有客戶端。

實際後果可能包含:

  • 封鎖某個 Python 指紋 → 連帶擋掉客戶合法的 API 整合、監控系統、內部工具
  • 封鎖某個舊瀏覽器指紋 → 擋掉使用舊裝置的真實使用者
  • 指紋隨軟體改版而變 → 清單需要持續維護,否則逐漸失效

因此成熟的做法是把指紋當成評分特徵與交叉驗證的依據,而不是單獨的封鎖條件。


常見問題

問題 1:TLS 指紋可以偽造嗎?

回答:可以,但門檻在於「更換零件」而非「調整參數」。

需要一個能讓你自訂 ClientHello 的 TLS 實作——指定確切的加密套件清單、擴充集合、擴充順序、GREASE 行為。這類工具存在(有些函式庫專門提供模仿主流瀏覽器 ClientHello 的能力)。

但有兩個現實限制:

  1. 必須持續追版本:目標瀏覽器每次改版都可能改變 ClientHello,模仿的一方需要跟上
  2. 只解決一層:TLS 指紋修好之後,HTTP/2 連線層還有一份獨立指紋(系列第 03 篇),JavaScript 環境還有一層(系列第 04 篇)

問題 2:使用 HTTP 代理會改變 JA4 嗎?

回答:取決於代理的類型。

代理類型 是否改變 TLS 指紋 原因
HTTP CONNECT 隧道 ❌ 不變 TLS 是端到端的,代理只轉發位元組,看不到也改不了 ClientHello
SOCKS5 ❌ 不變 同上,僅在 TCP 層轉發
TLS 終結型代理(企業 MITM、mitmproxy) ✅ 改變 代理自己重新建立 TLS 連線,伺服器看到的是代理的指紋

這帶出一個常被忽略的推論:如果你透過某些企業網路或會終結 TLS 的工具上網,伺服器看到的 TLS 指紋是那個中間設備的,不是你瀏覽器的。這也是這類設備有時會導致網站行為異常的原因之一。


問題 3:Encrypted ClientHello (ECH) 會讓 TLS 指紋失效嗎?

回答:不會讓指紋技術失效,但會改變誰能看到它。

ECH 的做法是把敏感欄位(尤其是 SNI)放進一個加密的「內層 ClientHello」,外面包一個通用的「外層 ClientHello」。

外層 ClientHello(明文)
  ├─ SNI 指向共用的「ECH 公開名稱」,不洩漏真實目標網域
  ├─ 仍有自己的加密套件與擴充清單 → 仍可產生指紋
  └─ encrypted_client_hello 擴充
        └─ 內層 ClientHello(加密)
              └─ 真實 SNI 與完整參數

要理解的兩點:

  1. 外層仍可指紋化:外層 ClientHello 依然是明文,依然有可辨識的參數組合
  2. CDN 是解密端:對於使用 Cloudflare 的網站,Cloudflare 本身就是 ECH 的解密方——它讀得到內層。ECH 保護的是「路徑上的第三方」(ISP、中間網路設備)看不到你連哪個網域,而不是讓 CDN 看不到

所以 ECH 是隱私機制,不是反指紋機制。


問題 4:常見誤解整理

誤解 1:JA3 已經沒用了,只要看 JA4 就好。

正確理解:JA3 對 Chrome 這類做順序隨機化的客戶端失效,但對許多不做隨機化的自動化工具與舊軟體仍然穩定有效。實務上兩者並存,各有適用範圍。

誤解 2:TLS 指紋可以用來追蹤特定使用者。

正確理解:它是群組識別碼。同一版 Chrome、同一作業系統、同樣設定的所有使用者共享同一個指紋。用它追蹤個人在原理上就不可行——這是設計特性,不是缺陷。

誤解 3:只要改設定就能讓 Python 的指紋變成 Chrome。

正確理解:加密套件清單或許能透過設定字串部分調整,但擴充欄位的集合與順序、GREASE 行為、signature_algorithms 的排列,都是實作層寫死的。要一致必須更換 TLS 實作。

誤解 4:指紋不同就會被擋。

正確理解:指紋不同只是「不是瀏覽器」,這本身完全合法——監控系統、API 客戶端、搜尋引擎爬蟲的指紋都不是瀏覽器。真正提高風險的是指紋與宣稱矛盾。誠實的 API 客戶端通常不會有問題。


總結

核心要點

TLS 指紋 = ClientHello 的參數集合與順序 → 雜湊

為何有效:
  ├─ ClientHello 必須是明文(此時尚無金鑰)
  ├─ 參數由 TLS 函式庫寫死,非應用程式碼可改
  └─ 在 HTTP 標頭出現之前就已採集完成

JA3(2017)              JA4(2023)
  5 欄位 → MD5            a_b_c 三段
  有序 → 被隨機化擊破      排序後雜湊 → 免疫
  不可讀 → 只能比對        a 段可讀 → 可寫規則
  含 SNI → 跨網域不穩      排除 SNI → 跨網域穩定

理解重點

  1. 明文是結構性必然:要加密先要金鑰,要金鑰先要協商,要協商先要送清單。ClientHello 不可能加密,這是 TLS 指紋成立的前提。

  2. 洩漏的是組合與順序,不是內容:單一加密套件編號沒有辨識力;「支援哪些、不支援哪些、什麼順序」的組合才有。

  3. 指紋識別軟體堆疊,不識別人:它回答「你用哪一套 TLS 函式庫與設定」。同版本瀏覽器的所有使用者共享指紋,這使它不可能用於個人追蹤。

  4. JA4 的核心創新是「排序 + 可讀」:排序讓它免疫 Chrome 的隨機化,a 段可讀讓它能對未知客戶端做語意判斷——這是 JA3 完全做不到的。

  5. 應用程式碼碰不到這一層:這是「改標頭無效」的根本原因。不是還沒調對,而是控制權不在那裡。

快速記憶

方面 說明
是什麼 把 ClientHello 的參數組合濃縮成可比對的短字串
為什麼看得到 ClientHello 是明文,因為握手時還沒有金鑰
為什麼難偽造 參數由 TLS 函式庫決定,應用程式碼無存取權
識別粒度 軟體堆疊層級(群組),非個人
JA3 的死穴 Chrome 110+ 的擴充順序隨機化
JA4 的解法 排序後才雜湊,並保留 10 字元可讀的 a 段

學習路徑

1. 建立六層全景                    ✓ 系列 01
2. 理解 TLS 指紋為何無法用標頭修補   ✓ 本篇
3. 理解 HTTP/2 連線層的第二份證據    → 系列 03
4. 理解主動挑戰在檢驗什麼            → 系列 04

延伸閱讀


建立日期:2026-09-07 最後更新:2026-09-07

🔗相關文章