之前看过某些文章,很好地论证了 JA3 指纹不能作为设备指纹的特征。也让我对 JA3 指纹的具体的细节和在其他方面的应用产生了兴趣,这里我们站在攻击者的视角,以代码的角度记录下 JA3 的计算和生成自定义 JA3 指纹。
关于 JA3 指纹生成
struct {
ProtocolVersion client_version;
Random random;
SessionID session_id;
CipherSuite cipher_suites<2..2^16-2>;
CompressionMethod compression_methods<1..2^8-1>;
select (extensions_present) {
case false:
struct {};
case true:
Extension extensions<0..2^16-1>;
};
} ClientHello;
-
client_version 为本次会话中的 TLS 版本 -
random 为 client 产生的随机数结构体,主要用来避免重放攻击
-
session_id 本次连接中所使用的会话 ID,这个字段主要用在会话恢复中
-
cipher_suites 为所支持的密码套件列表,具体值可以通过 openssl 查看列表
$ openssl ciphers -V | column -t -s " "
0x13,0x02 - TLS_AES_256_GCM_SHA384 TLSv1.3 Kx=any Au=any Enc=AESGCM(256) Mac=AEAD
0x13,0x03 - TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any Au=any Enc=CHACHA20/POLY1305(256) Mac=AEAD
0x13,0x01 - TLS_AES_128_GCM_SHA256 TLSv1.3 Kx=any Au=any Enc=AESGCM(128) Mac=AEAD
0xC0,0x2C - ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(256) Mac=AEAD
0xC0,0x30 - ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD
0x00,0x9F - DHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(256) Mac=AEAD
0xCC,0xA9 - ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=ECDSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
0xCC,0xA8 - ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=RSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
……
-
compression_methods 为 client 所支持的压缩算法列表 -
extensions 为 TLS 扩展,用于告知服务端一些额外信息。如 SNI (Server Name Indication) 扩展包含了所要链接的明文域名信息、supported curves 扩展为支持的椭圆曲线,supported point formats 扩展为支持的曲线格式
JA3 指纹的生成就是基于 ClientHello 数据包中五个字段,client_version
、cipher_suites
、extensions
和椭圆曲线supported curves与椭圆曲线格式 supported point formats,将这些值串联在一起,然后使用 , 分隔这 5 个字段,使用 – 分隔字段中的值。最终得到了 JA3 完整字符,再经过 md5 就是指纹。
$ curl https://www.immomo.com/
cURL 产生的 JA3 Hash Origin 为 771,4866-4867-4865-49196-49200-159-52393-52392-52394-49195-49199-158-49188-49192-107-49187-49191-103-49162-49172-57-49161-49171-51-157-156-61-60-53-47-255,0-11-10-13172-16-22-23-49-13-43-45-51-21,29-23-30-25-24,0-1-2,MD5 后值 f436b9416f37d134cadd04886327d3e8
即为 JA3 Hash
func main() {
_, err := http.DefaultClient.Get("https://immomo.com/")
if err != nil {
panic(err)
}
}
可以看到,Chrome 和 Chromium 的 JA3 Hash Origin 只有一个字符的微小差异,而使用 Go 发送 HTTP 请求时,哪怕是不同 Go 版本在不同操作系统上的 JA3 Hash 是完全相同的。基于此特性,TLS 指纹也就有了不同的应用。
TLS 指纹的应用
反爬虫
既然同一版本的库发起 TLS 连接的指纹是相同的,那么可以通过建立正常浏览器指纹和不同编程语言指纹库,就可以区分开 BOT 和正常用户,CluoudFare 和 Akamai 就是这么做的,JA3 指纹作为了识别爬虫的一个重要指标
流量检测
C2 恶意流量检测
360Quake 和 360netlab 利用 TLS 指纹来追踪僵尸网络和识别互联网上的 C2 服务
Vmess 流量识别
2020 年,有安全研究员提出 v2ray 服务的 TLS 指纹可被精准识别
空间测绘
网络空间引擎如 Shadon、ZoomEye 也添加了对 SSL 指纹搜索的支持
指纹计算和修改
计算 JA3 指纹
为了能为不同的 TLS 链接生成不同的指纹,我们先自己实现下指纹计算,Wireshark 保存刚刚访问immomo.com的数据包为immomo.pcap,使用 gopacket 解析 PCAP 并设置过滤条件拿到 ClientHello 数据包。再计算出 JA3Hash
计算 JA3Hash 过程
最终结果与 Wireshark 抓包得到的一致
生成可变的 TLS 指纹
JA3 指纹是通过 ClientHello 中的值计算得到的,是不是通过是修改 ClientHello 中的值就能让指纹变成我们想要的值。
type Transport struct {
// ...
DialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)
DialTLS func(network, addr string) (net.Conn, error)
TLSClientConfig *tls.Config
// ...
}
使用 utls 的 DialTLS 替换原始 transport,再发送请求到 www.immomo.com
发现 JA3 的值已经变为 83 版本 Chrome 的 JA3 值,再修改 UA 为 Chrome,就能绕过常见 TLS 指纹校验了。
小节
这篇主要以代码的角度记录了下 JA3 指纹的生成和修改,可以在遇到反爬或需要藏匿流量场景时通过修改 tranport 的 TLS 配置来隐匿自己。
About us
陌陌安全
致力于以务实的工作保障陌陌旗下所有产品及亿万用户的信息安全
以开放的心态拥抱信息安全机构、团队与个人之间的共赢协作
/ 往 期 分 享 /
App合规实践3000问
原文始发于微信公众号(陌陌安全):如何绕过 JA3 指纹校验?