📚 系列導覽:本篇是「Cloudflare 反爬蟲機制」系列第 2 篇,對應總覽篇六層偵測中的第 2 層(TLS 層)。
# 筆記 主題 01 反爬蟲偵測總覽 六層偵測全景、請求時序地圖 02 TLS 指紋:JA3 與 JA4(本篇) ClientHello 為何洩漏身分、兩代演算法 03 HTTP/2 指紋與 Akamai Fingerprint 連線層參數指紋、三方交叉驗證 04 Managed Challenge 與 Bot Score 主動挑戰機制、PAT、Cookie 綁定
目錄
- 什麼是 TLS 指紋?
- 基礎補課:TLS 握手在做什麼
- 基礎補課:ClientHello 裡有什麼
- 為什麼 ClientHello 會洩漏身分
- JA3:第一代指紋
- JA3 的三個破口
- JA4:第二代指紋
- JA4 字串逐碼拆解
- 為什麼 Python 必然不同於 Chrome
- 對比分析:JA3 vs JA4
- 防禦端如何使用 TLS 指紋
- 常見問題
- 總結
什麼是 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 是標準值,誰都可以用。有辨識價值的是:
- 支援哪些(集合)
- 不支援哪些(集合的補集同樣有訊息量)
- 排列的順序(偏好次序)
- 送出哪些擴充欄位
- 擴充欄位的排列順序
這五項組合起來的可能性極大,而每一套 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 的能力)。
但有兩個現實限制:
- 必須持續追版本:目標瀏覽器每次改版都可能改變 ClientHello,模仿的一方需要跟上
- 只解決一層: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 與完整參數
要理解的兩點:
- 外層仍可指紋化:外層 ClientHello 依然是明文,依然有可辨識的參數組合
- 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 → 跨網域穩定
理解重點
-
明文是結構性必然:要加密先要金鑰,要金鑰先要協商,要協商先要送清單。ClientHello 不可能加密,這是 TLS 指紋成立的前提。
-
洩漏的是組合與順序,不是內容:單一加密套件編號沒有辨識力;「支援哪些、不支援哪些、什麼順序」的組合才有。
-
指紋識別軟體堆疊,不識別人:它回答「你用哪一套 TLS 函式庫與設定」。同版本瀏覽器的所有使用者共享指紋,這使它不可能用於個人追蹤。
-
JA4 的核心創新是「排序 + 可讀」:排序讓它免疫 Chrome 的隨機化,a 段可讀讓它能對未知客戶端做語意判斷——這是 JA3 完全做不到的。
-
應用程式碼碰不到這一層:這是「改標頭無效」的根本原因。不是還沒調對,而是控制權不在那裡。
快速記憶
| 方面 | 說明 |
|---|---|
| 是什麼 | 把 ClientHello 的參數組合濃縮成可比對的短字串 |
| 為什麼看得到 | ClientHello 是明文,因為握手時還沒有金鑰 |
| 為什麼難偽造 | 參數由 TLS 函式庫決定,應用程式碼無存取權 |
| 識別粒度 | 軟體堆疊層級(群組),非個人 |
| JA3 的死穴 | Chrome 110+ 的擴充順序隨機化 |
| JA4 的解法 | 排序後才雜湊,並保留 10 字元可讀的 a 段 |
學習路徑
1. 建立六層全景 ✓ 系列 01
2. 理解 TLS 指紋為何無法用標頭修補 ✓ 本篇
3. 理解 HTTP/2 連線層的第二份證據 → 系列 03
4. 理解主動挑戰在檢驗什麼 → 系列 04
延伸閱讀
- 反爬蟲偵測總覽 - 六層偵測全景與請求時序
- HTTP/2 指紋與 Akamai Fingerprint - 第二份獨立證據
- Managed Challenge 與 Bot Score - 主動挑戰機制
- SSL/TLS 憑證完全指南 - 憑證、信任鏈與握手驗證
- 雜湊與加密完全指南 - MD5、SHA256 與雜湊特性
- AES 加密模式比較 - 加密套件中的對稱加密部分
建立日期:2026-09-07 最後更新:2026-09-07