检测网络爬虫流量并对其进行指纹识别的最隐秘和最不为人知的方法之一是传输层安全性 (TLS) 分析。每个 HTTPS 连接都必须建立安全握手,而这种握手的执行方式可能会导致指纹识别和网络抓取阻塞。 在本文中,我们将了解 TLS 如何泄露连接客户端是网络抓取工具的事实,以及它如何用于建立指纹以在网络上跟踪客户端。
什么是 TLS?
传输安全层为所有 HTTPS 连接提供支持。它允许客户端和服务器之间的端到端加密通信。 在网络抓取的上下文中,我们很少关心网站是使用 HTTP 还是 HTTPS 连接,因为这不会影响我们的数据收集逻辑。然而,一种新兴的指纹识别技术将此连接步骤作为目标,不仅可以对用户进行指纹识别以进行跟踪,还可以阻止网络抓取工具。
TLS 指纹识别
TLS 是一个相当复杂的协议,我们不需要理解所有的协议来确定我们的问题,尽管一些基础知识会有所帮助。让我们快速概览一下 TLS,以便了解它如何用于指纹识别。 在每个 HTTPS 连接开始时,客户端和服务器需要互相问候并协商连接的安全方式。这称为“Client Hello”握手。从数据上看,它看起来像这样:
这里有很多数据,这就是找不同游戏开始的地方:这次握手的哪些值在不同的 HTTP 客户端(如 Web 浏览器或编程库)中可能会有所不同? 首先要注意的是,有多个TLS 版本:通常是 1.2 或 1.3(最新版本)。 此版本确定握手中使用的其余数据。TLS 1.3 提供了额外的优化和更少的数据,因此更容易强化,但无论是 1.2 还是 1.3,我们的目标都是一样的——让它看起来像一个真正的网络浏览器。实际上,我们必须加强多个版本,因为某些网站尚不支持 TLS 1.3。 此外,我们还有最重要的字段:Cipher Suites。 该字段是谈判方支持的加密算法的列表。该列表按优先级排序,双方确定第一个匹配值。 因此,我们必须确保我们的 HTTP 客户端列表与普通 Web 浏览器的列表相匹配,包括顺序。 与密码套件列表类似,我们有Enabled Extensions列表。 这些扩展表示客户端支持的功能,以及一些元数据,如服务器域名。就像使用 Cipher Suites 一样,我们需要确保这些值及其顺序与普通 Web 浏览器的顺序相匹配。
JA3指纹
正如我们所见,有几个值可能因客户而异。为此,经常使用 JA3 指纹技术,它本质上是一串不同的值:
TLSVersion, Ciphers, Extensions, support_groups(previously EllipticCurves), EllipticCurvePointFormats,
每个值由 a 分隔,
,数组值由 a 分隔-
。 因此,例如 Linux 上 Chrome 网络浏览器的配置文件:
Handshake Type: Client Hello (1) Length: 508 Version: TLS 1.2 (0x0303) #1 (note that 0x0303 is hex for 771) Cipher Suites Length: 32 Cipher Suites (16 suites) #2.1 Cipher Suite: Reserved (GREASE) (0x1a1a) #2.2 Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301) #2.3 Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302) #2.4 Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303) #2.5 Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b) #2.6 Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f) #2.7 Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c) #2.8 Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030) #2.9 Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9) #2.10 Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8) #2.11 Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013) #2.12 Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014) #2.13 Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c) #2.14 Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d) #2.15 Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f) #2.16 Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035) Extensions Length: 403 Extension: Reserved (GREASE) (len=0) #3.1 Type: Reserved (GREASE) (56026) Extension: server_name (len=16) #3.2 Type: server_name (0) Extension: extended_master_secret (len=0) #3.3 Type: extended_master_secret (23) Extension: renegotiation_info (len=1) #3.4 Type: renegotiation_info (65281) Extension: supported_groups (len=10) #3.5 Type: supported_groups (10) Supported Groups (4 groups) #4.1 Supported Group: Reserved (GREASE) (0x7a7a) #4.2 Supported Group: x25519 (0x001d) #4.3 Supported Group: secp256r1 (0x0017) #4.4 Supported Group: secp384r1 (0x0018) Extension: ec_point_formats (len=2) #3.6 Type: ec_point_formats (11) Elliptic curves point formats (1) #5 EC point format: uncompressed (0) Extension: session_ticket (len=0) #3.7 Type: session_ticket (35) Extension: application_layer_protocol_negotiation (len=14) #3.8 Type: application_layer_protocol_negotiation (16) Extension: status_request (len=5) #3.9 Type: status_request (5) Extension: signature_algorithms (len=18) #3.10 Type: signature_algorithms (13) Extension: signed_certificate_timestamp (len=0) #3.11 Type: signed_certificate_timestamp (18) Extension: key_share (len=43) #3.12 Type: key_share (51) Extension: psk_key_exchange_modes (len=2) #3.13 Type: psk_key_exchange_modes (45) Extension: supported_versions (len=7) #3.14 Type: supported_versions (43) Extension: compress_certificate (len=3) #3.15 Type: compress_certificate (27) Extension: application_settings (len=5) #3.16 Type: application_settings (17513) Extension: Reserved (GREASE) (len=1) #3.17 Type: Reserved (GREASE) (27242) Extension: padding (len=44) #3.18 Type: padding (21) Extension: pre_shared_key (len=156) #3.19 Type: pre_shared_key (41)
会产生一个指纹:
771,6682-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,56026-0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-27242-21-41,31354-29-23-24,0
JA3 指纹通常会进一步进行 md5 哈希处理以减少指纹长度:
dbe0907495f5e986a232e2405a67bed1
如何读取 TLS 数据?
观察 TLS 握手的最常见方法是使用Wireshark数据包分析器:
tls
当我们在网络浏览器或网络爬虫脚本中提交请求时,使用过滤器我们可以很容易地观察到 TLS 握手。查找“Client Hello”消息,这是握手过程的第一步。 Wireshark 甚至可以为您计算 JA3 指纹:
为了测试 JA3 指纹,我们以ScrapFly JA3 工具为例,来测试 HTTP 客户端指纹。 例如,这是 Python 中请求库的结果:
import requests import json print(json.dumps(requests.get("https://tools.scrapfly.io/api/fp/ja3?extended=1").json())
{ "digest": "8d9f7747675e24454cd9b7ed35c58707", "ja3": "771,4866-4867-4865-49196-49200-49195-49199-52393-52392-159-158-52394-49327-49325-49326-49324-49188-49192-49187-49191-49162-49172-49161-49171-49315-49311-49314-49310-107-103-57-51-157-156-49313-49309-49312-49308-61-60-53-47-255,0-11-10-16-22-23-49-13-43-45-51-21,29-23-30-25-24,0-1-2", "tls": { "version": "0x303 - TLS 1.2", "ciphers": [ "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256", "TLS_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "..." ], "curves": [ "X25519 (29)", "secp256r1 (23)", "X448 (30)", "secp521r1 (25)", "secp384r1 (24)" ], "extensions": [ "server_name (0) (IANA)", "ec_point_formats (11) (IANA)", "supported_groups (10) (IANA)", "application_layer_protocol_negotiation (16) (IANA)", "encrypt_then_mac (22) (IANA)", "extended_master_secret (23) (IANA)", "post_handshake_auth (49) (IANA)", "signature_algorithms (13) (IANA)", "supported_versions (43) (IANA)", "psk_key_exchange_modes (45) (IANA)", "key_share (51) (IANA)", "padding (21) (IANA)" ], "points": [ "0", "1", "2" ], "protocols": [ "http/1.1" ], "versions": [ "0x303 - TLS 1.2", "0x302 - TLS 1.1", "0x301 - TLS 1.0", "0x300 - SSL 3.0" ] } }
这看起来不太像 Chrome 或 Firefox——这意味着这些 Python 网络抓取工具很容易识别!让我们来看看如何解决这个问题。
TLS指纹如何导致阻塞?
当涉及到阻止网络抓取工具时,主要目标是发现差异——这个客户端与一般的网络浏览器不同吗? 我们可以看到 JA3 指纹算法考虑的变量很少,这意味着唯一指纹的可能性相对较少,这使得创建白名单和黑名单数据库变得容易。
反网页抓取服务收集海量JA3指纹数据库,用于将类浏览器列入白名单,将常见网页抓取列入黑名单。意思是为了避免阻塞,我们必须确保我们的 JA3 指纹被列入白名单(匹配常见的网络浏览器)或足够独特。
如何伪造 TLS 指纹?
不幸的是,配置 TLS 欺骗非常复杂,在许多情况下都不容易实现。不过,让我们来看看一些常见的情况。
Python 中的 TLS 强化
在 Python 中,我们只能配置“密码套件”和“TLS 版本”变量,这意味着每个 Python HTTP 客户端都容易受到 TLS 扩展指纹识别的攻击。我们无法将指纹列入白名单,但通过欺骗这两个变量,我们至少可以避免黑名单: 在 requests 中更改密码套件和TLS版本(以ScrapFly为例)
import ssl import requests from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager from urllib3.util.ssl_ import create_urllib3_context # see "openssl ciphers" command for cipher names CIPHERS = "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384" class TlsAdapter(HTTPAdapter): def __init__(self, ssl_options=0, **kwargs): self.ssl_options = ssl_options super(TlsAdapter, self).__init__(**kwargs) def init_poolmanager(self, *pool_args, **pool_kwargs): ctx = create_urllib3_context(ciphers=CIPHERS, cert_reqs=ssl.CERT_REQUIRED, options=self.ssl_options) self.poolmanager = PoolManager(*pool_args, ssl_context=ctx, **pool_kwargs) adapter = TlsAdapter(ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1) # prioritize TLS 1.2 default = requests.get("https://tools.scrapfly.io/api/fp/ja3?extended=1").json() session = requests.session() session.mount("https://", adapter) fixed = session.get("https://tools.scrapfly.io/api/fp/ja3?extended=1").json() print('Default:') print(default['tls']['ciphers']) print(default['ja3']) print('Patched:') print(fixed['tls']['ciphers']) print(fixed['ja3'])
在httpx中更改密码套件和 TLS 版本
import httpx, ssl ssl_ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2) # prefer TLS 1.2 ssl_ctx.set_alpn_protocols(["h2", "http/1.1"]) # see "openssl ciphers" command for cipher names CIPHERS = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384" ssl_ctx.set_ciphers(CIPHERS) default = httpx.get("https://tools.scrapfly.io/api/fp/ja3?extended=1").json() fixed = httpx.get("https://tools.scrapfly.io/api/fp/ja3?extended=1", verify=ssl_ctx).json() print('Default:') print(default['tls']['ciphers']) print(default['ja3']) print('Patched:') print(fixed['tls']['ciphers']) print(fixed['ja3'])
Go 中的 TLS 强化
Go 语言是少数支持通过Refraction Networking 的 utls、CycleTLS或ja3transport库进行可访问的 TLS 欺骗的语言之一。
基于 LibCurl 的 HTTP 客户端
基于 libcurl 的 HTTP 客户端可以更新为使用curl-impersonate – libcurl 库的修改版本,它修补了 TLS 指纹识别,使其类似于常见的 Web 浏览器。 以下是一些使用 libcurl 并可以通过这种方式修补的库:
基于无头浏览器的爬虫
当我们使用 Playwright、Puppeteer 或 Selenium 进行抓取时,我们使用的是真正的浏览器,因此我们获得了真正的 TLS 指纹,这太棒了!话虽这么说,当使用不同的浏览器/操作系统版本集合进行大规模抓取时,可以帮助通过多个指纹而不是一个指纹来扩展连接。 无头浏览器有不同的指纹吗? 不会。通常,以无头模式运行的浏览器(无论是 Selenium、Playwright 还是 Puppeteer)不应更改 TLS 指纹。这意味着 JA3 和其他 TLS 指纹识别技术无法识别连接的浏览器是否是无头的。