⭐ 推荐:社区规则条款 V1.0

如何用 JA3 指纹识别采集程序的 TLS 特征?

萝卜特棒
萝卜特棒 正式会员萌新站长
发布于 2026-10-17 12:09 ·1 浏览 ·0 回复
内容摘要

JA3 将 TLS ClientHello 的版本、密码套件、扩展、椭圆曲线和点格式拼接后取 MD5,可用 tcpdump/tshark 抓取并与浏览器基线比对;它能识别未伪装的采集程序,但遇到 curl_cffi、uTLS 等改指纹客户端

JA3 是把 TLS ClientHello 里的版本、密码套件、扩展列表、椭圆曲线、曲线点格式这五个字段拼成一条字符串再取 MD5,得到固定的 32 位十六进制值。结论:Python requests、Go net/http、Java OkHttp 这类采集程序用的是各自的默认 TLS 栈,JA3 哈希与 Chrome、Edge 有稳定差异,因此能作为识别"非浏览器流量"的筛子;但它只能筛出"没有伪装的采集程序",遇到用 curl_cffi、uTLS 改过指纹的客户端就失效。

JA3 指纹是怎么算出来的?

TLS ClientHello 是客户端发起 TLS 握手时的第一条消息,明文传输,里面列出它支持的加密套件、扩展、曲线等能力清单——JA3 读的就是这份清单。算法分两步:先把五个字段按 SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat 的固定顺序拼成字符串,各字段内用 - 连接,字段间用 , 连接;再对整个字符串取 MD5。

手算一次就能理解:

import hashlib
def ja3_str(ver, ciphers, exts, curves, fmts):
    return ",".join([str(ver),
        "-".join(map(str, ciphers)),
        "-".join(map(str, exts)),
        "-".join(map(str, curves)),
        "-".join(map(str, fmts))])

s = ja3_str(771, [4866,4867,4865], [0,11,10,35], [29,23,24], [0])
print(s)
print(hashlib.md5(s.encode()).hexdigest())

771 是 TLS 1.2 的版本号(0x0303),JA3 里记录的始终是这个值,哪怕客户端实际协商到 TLS 1.3。字符串本身可读,MD5 值便于入库比对和写规则。

怎么抓到自己采集程序的 JA3?

三步:抓 ClientHello、解析出 JA3、比对基线。第一步用 tcpdump 抓包,只留握手阶段:

tcpdump -i eth0 -w tls.pcap 'tcp port 443 and tcp[13] & 2 != 0'

第二步用 tshark 直接算,Wireshark 3.2 之后内置了 tls.handshake.ja3 字段,需要显式开启生成开关:

tshark -r tls.pcap -o tls.generate_ja3:TRUE \
  -Y "tls.handshake.type==1" -T fields -e tls.handshake.ja3

第三步把结果和自己的浏览器访问同一站点的 JA3 并排放。实测中 requests(OpenSSL 默认配置)的 JA3 与同版本 Chrome 的哈希不同,前者扩展数量少、不含 ALPN 的 h2 相关协商;Chrome 的 ClientHello 带 GREASE 值、带 application_layer_protocol_negotiation、带 signed_certificate_timestamp 等一批扩展。差异点就在扩展列表和曲线顺序上,肉眼能看出来。

JA3 和 JA3S 有什么区别?

JA3 是客户端指纹,JA3S 是服务端指纹,两者字段不同。JA3S 只取三项:SSLVersion,Cipher,SSLExtension,因为没有曲线和点格式可读。排查采集程序时主要看 JA3;JA3S 用来识别"服务端是否被同一套中间件接住",比如判断某批域名是不是挂在同一个 CDN 或反代后面。

线上落地用 Zeek 最省事,加载一行脚本:

@load policy/protocols/ssl/ja3

之后 ssl.log 每行带 ja3 和 ja3s 字段。Suricata 从 5.0 起支持 ja3.hash 关键字,可以直接写告警规则:

alert tls any any -> any any (msg:"known scraper JA3"; ja3.hash; content:"<32位哈希>"; sid:1000001;)

为什么同一个程序的 JA3 每次都不一样?

因为 GREASE 值和扩展顺序会让哈希漂移,需要改用 JA3N。Chrome、Firefox 会在密码套件和扩展里随机塞入 GREASE 值(0x0a0a、0x1a1a … 0xfafa,共 16 个),每次握手的取值不同,原始 JA3 字符串随之变化,MD5 自然每次不同。解决办法是计算 JA3N:剔除所有 GREASE 值,再把扩展列表和曲线列表排序后重新拼接取 MD5。生产环境建议同时存 JA3 和 JA3N,用 JA3N 做长期画像,用 JA3 做单次会话比对。

顺带一提,FoxIO 在 2023 年发布的 JA4 已经改成可读格式(形如 t13d1516h2_8daaf6152771_02713d6af862),把 TLS 版本、SNI 有无、扩展数、ALPN 都编码进去,不再依赖 MD5。新项目可以直接上 JA4,历史库继续用 JA3 兼容。

识别出可疑 JA3 之后该做什么?

结论:JA3 只能作为多特征联合判定中的一票,不能单独封禁。同一台机器上,TLS 指纹和 HTTP/2 指纹(Akamai fingerprint,取 SETTINGS 帧与窗口更新参数)、请求头顺序、TCP/IP 层指纹(p0f)一起看,误杀率才降得下来。单独看 JA3 的典型误判是把老旧 Android WebView 或某些企业内部工具当成爬虫。

另一侧也要清楚边界:curl_cffi 通过绑定 BoringSSL 复刻 Chrome 的 ClientHello,uTLS 可以手工指定扩展顺序,这两类工具能直接伪造出与 Chrome 完全一致的 JA3。所以 JA3 的真实价值是"用近乎零成本挡掉 90% 的裸 requests/裸 Go 采集脚本",而不是对抗有备而来的对手。

收个尾:先 tcpdump 抓 ClientHello,tshark 开 tls.generate_ja3 出哈希,或用 Zeek 的 ja3 脚本批量落地;比对时记得剔除 GREASE 用 JA3N 保持稳定;判定时把 JA3、HTTP/2 指纹、请求头顺序捆在一起看。这套流程跑通,默认配置的采集程序基本藏不住。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2239.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~