一、背景介绍
在网络安全监控、流量分析、威胁检测以及精细化业务运营等领域,准确识别和标记网络流量是一项至关重要的基础能力。然而,当前目前现有的流量标记方案普遍存在以下痛点:
1.1、缺乏统一的流量标记系统
- 许多现有基础设施或分析管道中,尚未构建系统化、标准化的流量标识体系。流量往往被视为“匿名字节流”,缺乏标识其来源、类型、客户端设备或应用特征的固有标签。
- 这使得区分不同类型流量(如:合法用户访问 vs. 爬虫扫描 vs. 恶意攻击)、识别特定客户端群体或基于客户端的细粒度的安全策略实施变得极其困难,需要依赖昂贵且复杂的后期处理分析。
1.2、过度依赖孤立的应用层参数进行标记
部分流量标记方案主要依赖应用层协议(如 HTTP )中携带的显式参数进行粗略打标,例如:
- User-Agent 字符串:用于标识浏览器或客户端类型。
- HTTP Headers (`X-Forwarded-For`, `Referer` etc.)。
- URL 参数或 Cookies。
但是这种流量标记的方式局限性极其显著:
- 易篡改与伪造: User-Agent 等头部信息可由客户端或中间代理轻易修改、伪装或移除,可信度低,攻击者常利用此特性规避检测。
- 加密流量的盲区: 随着 TLS/SSL 协议的普及,应用层内容(包括 HTTP Headers)已被加密。在 TLS/SSL 握手完成后,无法对协议底层的技术信息进行读取。仅依赖应用层参数无法对流量进行有效识别和标记。这是现有方案面临的最大挑战之一。
- 粒度粗糙且不稳定: 基于 User-Agent 的识别粒度较粗(通常仅到浏览器大类),且易受版本更新和客户端配置影响,难以精准识别具体的客户端库、自动化工具甚至恶意软件变种。
- 缺乏上下文关联性: 孤立的参数难以提供客户端在建立连接时的行为特征和意图线索。
这些局限性共同指向了一个核心问题:我们需要一种更底层、更稳定、更难篡改的流量标记机制。答案就隐藏在每一个 HTTPS 连接建立的最初阶段—— TLS/SSL 握手过程中。客户端在发起加密请求时,首先会发送一个 Client Hello 消息,这个消息中包含了客户端支持的 TLS 版本、加密套件、椭圆曲线、扩展列表等一系列“技术参数”。这些参数的组合方式,就像是客户端在技术层面的“自我介绍”。
JA3指纹正是基于这一原理诞生的,它为我们提供了一种全新的、高效的流量标记识别方案,下面将详细进行介绍。
二、JA3 指纹介绍
JA3 指纹通过采集 TLS 握手过程中 Client Hello 数据包中以下字段的十进制值:
- TLS Version (TLS 版本)
- Accepted Ciphers(加密套件列表)
- List of Extensions(扩展列表)
- Elliptic Curves(椭圆曲线列表)
- Elliptic Curve Formats(椭圆曲线点格式)
然后,它将这些值按顺序连接在一起,使用“,”分隔每个字段,使用“-”分隔每个字段中的每个值。
字段顺序如下:
TLS 版本、加密套件列表、扩展列表、椭圆曲线列表、椭圆曲线点格式
例如,JA3 采集到 Client Hello 数据包的特征参数内容如下:
771,49195-49199-49196-49200-49171-49172-156-157-47-53-49192-49188-49162-165-163-161-159-107-106-105-104-57-56-55-54-49202-49198-49194-49190-49167-49157-61-49191-49187-49161-164-162-160-158-103-64-63-62-51-50-49-48-49201-49197-49193-49189-49166-49156-60-154-153-152-151-150-10-255,0-11-10-35-13-15-21,23-25-28-27-24-26-22-14-13-11-12-9-10,0-1-2
最后对这些字符串进行 MD5 哈希处理,生成一个易于使用且可共享的 32 个字符的指纹:
82F7B24AB8C716674E9E78D4B856D388
这就是 TLS 客户端的 JA3 指纹。
2.1、JA3 指纹的优势
1、JA3 指纹特征深度嵌入 TLS 协议层,不随网络客户端的 IP 地址、User-Agent 变化而改变。
例如:
网络客户端 python-requests/2.31.0 的指纹为 bc29aa426fc99c0be1b9be941869f88a 。
模拟浏览器的 User-Agent 后,网络客户端 python-requests/2.31.0 的指纹依然不会发生变化。
配置了代理 IP 之后,网络客户端 python-requests/2.31.0 的指纹还是不会发生变化。
2、JA3 指纹能够准确识别和区分不同的客户端,不同网络客户端生成的指纹有显著差异。
例如:
- python-requests/2.31.0 的 JA3 指纹为 bc29aa426fc99c0be1b9be941869f88a 。
- curl/8.7.1 的 JA3 指纹为 2bab0327a296230f9f6427341e716ea0。
- Safari/605.1.15 的 JA3 指纹为 773906b0efdefa24a7f2b8eb6985bf37。
3、JA3 指纹相对稳定,改变原有网络客户端的 JA3 指纹或伪造其他客户端的 JA3 指纹门槛都比较高。
例如:
- 对于 Web 端应用来说,爬虫程序想要修改自身网络工具的 JA3 指纹往往需要修改网络工具底层源码中关于密码套件顺序、扩展列表等部分的逻辑,然后重新编译。对中、低端的爬虫程序,有较高的技术要求。
- 对于移动端应用(IOS & Android)来说,通常会使用操作系统提供的网络框架(如 IOS 的 URLSession )或内置的第三方网络库(如 OkHttp)进行网络通讯。这些框架和库的 TLS 实现是高度标准化的。移动客户端的 JA3 指纹依赖设备(真机 & 模拟器)环境,天然就难以伪造。
- 对于物联网端应用(Iot)来说,其 TLS 功能往往由一个轻量级的、高度固化的库(如 mbed TLS)提供。一旦固件被烧录,其网络行为和 JA3 指纹就几乎是永久固定的。
2.2、JA3 指纹的劣势
1、JA3 指纹在同一网络客户端的不同版本中可能会有不同,这要求 JA3 指纹库必须持续更新,否则会影响流量标记的准确性。
例如:
- curl/7.52.1 的 JA3 指纹为 c5deb9465d47232dd48772f9c4d14679
- curl/7.88.1 的 JA3 指纹为 0149f47eabf9a20d0893e2a44e5a6323
- curl/8.7.1 的 JA3 指纹为 2bab0327a296230f9f6427341e716ea0
2、JA3 指纹虽然相对稳定,但仍有开源工具可以模拟真实浏览器的 JA3 指纹。
例如:curl_cffi 工具可以模拟特定版本 Chrome、Safari、Firefox 浏览器指纹,能够应用在爬虫程序中,该工具目前只有 Python 实现。
浏览器 | curl_cffi 支持的浏览器版本 |
Chrome | chrome99, chrome100, chrome101, chrome104, chrome107, chrome110, chrome116, chrome119, chrome120, chrome123, chrome124, chrome131, chrome133a, chrome136 |
Chrome Android | chrome99_android, chrome131_android |
Safari | safari153 , safari155 , safari170 , safari180 , safari184 , safari260 |
Safari iOS | safari172_ios, safari180_ios, safari184_ios, safari260_ios |
Firefox | firefox133, firefox135 |
下图分别使用 scraper、requests、curl_cffi 多个网络工具请求 https://toppsta.com 网站,curl_cffi 由于模拟了真实浏览器的 JA3 指纹,能够正常返回数据,scraper、requests 均被拦截。
3、Chrome 浏览器为了防止 TLS 协议的僵化,同一版本的 Chrome 浏览器由于 GREASE 机制的存在,使得 JA3 指纹出现不稳定的表现。
GREASE 机制会向 Client Hello 数据包中的加密套件列表和扩展列表随机插入一个或多个 GREASE 值。导致列表的内容变了,进而导致最终计算出的 MD5 哈希值(即 JA3 指纹)也跟着改变 。
由于 GREASE 值有非常明确的模式( 0x?A?A ),所以很容易识别并移除。JA3 指纹在计算之前需要将已知的 GREASE 值从列表中过滤掉,并将加密套件列表和扩展列表的数值进行排序后,最终计算出的 JA3 指纹都会是稳定且唯一的 。
4、JA3 指纹作用于 Nginx 层,且对 Nginx 与 OpenSSL 的版本有一定要求,需要升级基础网络设施。
例如: OpenSSL 升级到 3.0 版本后,对安卓 4.4.2 以下的版本不兼容,这部分用户将无法使用云音乐产品。
不兼容的原因是客户端与服务端之间找不到共同的加密套件:低版本安卓客户端在 ClientHello 消息中提供了一系列它支持的加密套件,但服务器(OpenSSL 3.0)检查后发现,这个列表里的所有套件都因为不够安全而被自己的策略禁用了。因此,服务器无法选择一个双方都同意的加密套件,连接建立失败。
三、JA3 指纹集成方案
JA3 指纹作用于 Nginx 层,对用户与客户端是无感知的。在 Nginx 层计算 JA3 指纹,我们考虑使用开源的 nginx-ssl-fingerprint 模块。它是一个为 Nginx 设计的高性能模块,专门用于采集和分析客户端的 JA3 和 HTTP2 指纹。
nginx-ssl-fingerprint http 模块能够采集到的指纹数据有:
字段 | 默认值 | 说明 |
http_ssl_greased | 0 | 是否存在 GREASE 机制 |
http_ssl_ja3 | NULL | JA3 指纹数据 |
http_ssl_ja3_hash | NULL | JA3 指纹 MD5 值 |
http2_fingerprint | NULL | HTTP2 的指纹数据 |
# 代码示例
http {
server {
listen 127.0.0.1:4433 ssl http2;
ssl_certificate cert.pem;
ssl_certificate_key priv.key;
error_log /dev/stderr debug;
return 200 "ja3: $http_ssl_ja3\nh2fp: $http2_fingerprint";
}
}
stream 模块能够采集到的指纹数据有:
字段 | 默认值 | 说明 |
http_ssl_greased | 0 | 是否存在 GREASE 机制 |
http_ssl_ja3 | NULL | JA3 指纹数据 |
http_ssl_ja3_hash | NULL | JA3 指纹 MD5 值 |
# 代码示例
stream {
server {
listen 127.0.0.1:4443 ssl;
ssl_certificate cert.pem;
ssl_certificate_key priv.key;
error_log /dev/stderr debug;
return "ja3: $stream_ssl_ja3\n";
}
}
nginx-ssl-fingerprint 模块具有较为广泛的兼容性,支持的 Nginx、OpenSSL 版本如下:
OpenSSL 3.0 | OpenSSL 3.1 | OpenSSL 3.2 | OpenSSL 3.4 | |
Nginx 1.20 | ✅ | ✅ | ✅ | |
Nginx 1.21 | ✅ | ✅ | ✅ | |
Nginx 1.22 | ✅ | ✅ | ✅ | |
Nginx 1.23 | ✅ | ✅ | ✅ | |
Nginx 1.24 | ✅ | ✅ | ✅ | |
Nginx 1.25 | ✅ | ✅ | ✅ | |
Nginx 1.26 | ✅ | ✅ | ✅ | |
Nginx 1.27 | ✅ |
且具有良好的性能表现,具体性能数据如下表格所示:
Nginx 基线性能 | Nginx 启用指纹识别后 | |
请求数 | 2,155,428 requests in 30.07s | 2,162,578 requests in 30.07s |
吞吐量 | 71,669.13 requests/sec | 71,909.53 requests/sec |
传输速率 | 78.81MB/sec | 75.44MB/sec |
平均延迟 | 19.54ms | 21.03ms |
最大延迟 | 626.85ms | 618.44ms |
总的来说, nginx-ssl-fingerprint 模块在性能方面表现优异,几乎不会对服务器性能造成负面影响,同时提供了 JA3 指纹采集能力。
结合上文对 nginx-ssl-fingerprint 模块的介绍,我更倾向使用 Nginx 1.26 + OpenSSL 3.0 的版本组合。其中 OpenSSL 3.0 最低可支持到安卓 4.4.2 版本,能够最大程度地降低受影响的用户规模。
方案示意图如下: