背景介绍
前段时间做了一个看似很简单的需求,即爬取百度搜索内容,例如通过百度搜索“JA3指纹”。在编写程序测试过程中,发现无法正常采集到搜索结果。如下代码通过python requests工具访问百度搜索请求,会被百度安全验证拦截下来。
import requests
res = requests.get("https://www.baidu.com/s?wd=ssl%E6%8C%87%E7%BA%B9",
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"
}
)
res.encoding = "utf-8"
print(res.text)
但通过以下 curl 语句能够正常访问百度搜索结果。
curl 'https://www.baidu.com/s?wd=ssl%E6%8C%87%E7%BA%B9' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36' \
--compressed
那么在目标网站、请求头、请求参数都相同的情况下,curl 与 requests 在网络请求过程中还有什么不同呢?由此引出本文介绍的对象“JA3指纹”(也称作SSL指纹、TLS指纹)。
什么是 JA3 指纹
JA3 指纹于2017年6月首次发布在 GitHub 上,是 Salesforce 研究人员 John Althouse、Jeff Atkinson和Josh Atkins 的作品。简单来说,JA3 是一种从 SSL/TLS Client Hello 数据包中提取字段并生成指纹用以识别特定客户端的技术,它于它不会随着客户端更换 IP 或者 User-Agent 而改变。JA3 指纹从一开始就说明客户端应用程序是否存在恶意。
JA3 是怎么生成的
回顾一下 https 建立连接的过程,能更直观的感受到 JA3 指纹是在什么位置生成的。
JA3 在 SSL/TLS 握手期间解析客户端发送的 Client Hello 数据包,收集以下字段的十进制字节值:TLS Version、Accepted Ciphers、List of Extensions、Elliptic Curves和Elliptic Curve Formats
。然后,它将这些值串联起来,使用“,”来分隔各个字段,同时使用“-”来分隔各个字段中的值。最后,计算这些字符串的md5哈希值,即得到易于使用和共享的长度为32字符的指纹。
我们可以通过浏览器请求 https://check.ja3.zone/ 来得到浏览器的 JA3 指纹,其中 hash
字段对应的值就是我们所说的 JA3 指纹,它是 fingerprint
字段的 md5 计算结果。
{
"hash": "82f7b24ab8c716674e9e78d4b856d388",
"fingerprint": "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",
"ciphers": "ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-ECDSA-AES256-SHA,UNKNOWN(00a5),DHE-DSS-AES256-GCM-SHA384,UNKNOWN(00a1),DHE-RSA-AES256-GCM-SHA384,DHE-RSA-AES256-SHA256,DHE-DSS-AES256-SHA256,UNKNOWN(0069),UNKNOWN(0068),DHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,UNKNOWN(0037),UNKNOWN(0036),UNKNOWN(c032),UNKNOWN(c02e),UNKNOWN(c02a),UNKNOWN(c026),UNKNOWN(c00f),UNKNOWN(c005),AES256-SHA256,ECDHE-RSA-AES128-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-ECDSA-AES128-SHA,UNKNOWN(00a4),DHE-DSS-AES128-GCM-SHA256,UNKNOWN(00a0),DHE-RSA-AES128-GCM-SHA256,DHE-RSA-AES128-SHA256,DHE-DSS-AES128-SHA256,UNKNOWN(003f),UNKNOWN(003e),DHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,UNKNOWN(0031),UNKNOWN(0030),UNKNOWN(c031),UNKNOWN(c02d),UNKNOWN(c029),UNKNOWN(c025),UNKNOWN(c00e),UNKNOWN(c004),AES128-SHA256,DHE-RSA-SEED-SHA,DHE-DSS-SEED-SHA,UNKNOWN(0098),UNKNOWN(0097),SEED-SHA,UNKNOWN(000a),TLS_EMPTY_RENEGOTIATION_INFO_SCSV",
"curves": "0017:0019:001C:001B:0018:001A:0016:000E:000D:000B:000C:0009:000A",
"protocol": "TLSv1.2",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"
}
为了更近一步描述清楚这些数据的来源,我们将fingerprint
与Client Hello数据包中的字段做了映射。
例如fingerprint内容如下: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
其中第三个字段来自各个扩展项的前2个字节的十进制数值。如下图所示
这里就能解释,为什么我用 python requests 工具访问百度搜索会被百度的安全验证拦截了,因为 requests 的 JA3 指纹在百度黑名单中。下面是我的 requests 工具的 JA3 指纹,多次请求 JA3 指纹不变。
JA3 误报率如何
官方解释,存在一定的误报风险,我们可以将 JA3 视为 User-Agent 字符串的 TLS 等价物。 仅仅因为一个软件或恶意软件具有特定的 JA3 指纹并不意味着它对于该软件来说总是唯一的。 其他软件可能拥有一样的 JA3指纹。 但是,没有理由不使用 JA3指纹来增强我们的分析和检测。 就像其他网络元数据一样,JA3 是用于丰富数据的额外信息。 JA3S 与 JA3 结合使用时,可以显着降低误报率。
JA3S 指纹是为 Server Hello 数据包中的以下字段收集字节的十进制值:版本、密码套件和扩展列表。 然后它将这些值按顺序连接在一起,使用“,”来分隔每个字段,使用“-”来分隔每个字段中的每个值。虽然服务器对不同客户端响应的 JA3S不同,但它对同一个客户端响应的 JA3S 总是相同的。
JA3 如何绕过
介绍几种可尝试的方案
本地代理中转请求
在本地启动代理服务器,比如 Charles,requests 发起 https 请求时配置 Charles 本地代理服务,让 Charles 来进行 TLS 握手,算是一种曲线救国的方法。
import requests
res = requests.get("https://www.baidu.com/s?wd=ssl%E6%8C%87%E7%BA%B9",
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"
},
proxies = {
"http": "http://127.0.0.1:8888",
"https": "http://127.0.0.1:8888",
}
)
res.encoding = "utf-8"
print(res.text)
这种方案需要找一个不会被拦截的客户端代理才可以。
更换网络客户端
比如 python requests 客户端,可以尝试使用其他客户端比如 scrapy、pycurl、aiohttp 进行网络连接,或者更换客户端的版本。
魔改客户端工具
以 python requests工具为例,requests 其实是对urllib3的一个封装,可以修改 urllib3 代码中的 TLS 握手特征,比如增加或删除部分加密套件。
// vim /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/urllib3/util/ssl_.py
83 DEFAULT_CIPHERS = ":".join(
84 [
85 "ECDHE+AESGCM",
86 "ECDHE+CHACHA20",
87 "DHE+AESGCM",
88 "DHE+CHACHA20",
89 "ECDH+AESGCM",
90 "DH+AESGCM",
91 "ECDH+AES",
92 "DH+AES",
93 "RSA+AESGCM",
94 "RSA+AES",
95 "!aNULL",
96 "!eNULL",
97 "!MD5",
98 "!DSS",
99 ]
100 )
如果需要修改scrapy的JA3指纹,则可以在contextfactory.py文件中进行修改。
# vim scrapy/core/downloader/contextfactory.py
def __init__(self, method=SSL.SSLv23_METHOD, tls_verbose_logging=False, tls_ciphers=None, *args, **kwargs):
super(ScrapyClientContextFactory, self).__init__(*args, **kwargs)
self._ssl_method = method
self.tls_verbose_logging = tls_verbose_logging
if tls_ciphers:
# 修改示例:打乱ciphers顺序,改变TLS指纹
ciphers = AcceptableCiphers.fromOpenSSLCipherString(tls_ciphers)._ciphers
random.shuffle(ciphers)
self.tls_ciphers = OpenSSLAcceptableCiphers(ciphers)
else:
self.tls_ciphers = DEFAULT_CIPHERS