【爬虫教程】第2章:TLS指纹识别与绕过实战

第2章:TLS指纹识别与绕过实战

目录

  • [2.1 TLS握手流程深度解析](#2.1 TLS握手流程深度解析)
    • [2.1.1 TLS握手完整流程](#2.1.1 TLS握手完整流程)
    • [2.1.2 TLS 1.3的新特性](#2.1.2 TLS 1.3的新特性)
    • [2.1.3 为什么TLS握手会被用于检测?](#2.1.3 为什么TLS握手会被用于检测?)
  • [2.2 JA3/JA3S指纹算法详解](#2.2 JA3/JA3S指纹算法详解)
    • [2.2.1 JA3指纹的构成](#2.2.1 JA3指纹的构成)
    • [2.2.2 JA3指纹的计算过程](#2.2.2 JA3指纹的计算过程)
    • [2.2.3 JA3S指纹(服务器端)](#2.2.3 JA3S指纹(服务器端))
    • [2.2.4 为什么JA3指纹能识别爬虫?](#2.2.4 为什么JA3指纹能识别爬虫?)
  • [2.3 TLS ClientHello报文结构详解](#2.3 TLS ClientHello报文结构详解)
    • [2.3.1 ClientHello报文格式](#2.3.1 ClientHello报文格式)
    • [2.3.2 密码套件列表详解](#2.3.2 密码套件列表详解)
    • [2.3.3 扩展列表详解](#2.3.3 扩展列表详解)
    • [2.3.4 椭圆曲线和点格式](#2.3.4 椭圆曲线和点格式)
  • [2.4 工具链:TLS指纹分析与检测](#2.4 工具链:TLS指纹分析与检测)
    • [2.4.1 使用Wireshark抓包分析TLS ClientHello](#2.4.1 使用Wireshark抓包分析TLS ClientHello)
    • [2.4.2 使用ja3er.com在线检测](#2.4.2 使用ja3er.com在线检测)
    • [2.4.3 使用Python检测JA3指纹](#2.4.3 使用Python检测JA3指纹)
    • [2.4.4 使用ja3-fingerprinting库](#2.4.4 使用ja3-fingerprinting库)
  • [2.5 TLS指纹绕过技术](#2.5 TLS指纹绕过技术)
    • [2.5.1 使用curl_cffi模拟浏览器指纹](#2.5.1 使用curl_cffi模拟浏览器指纹)
    • [2.5.2 使用tls-client库指定JA3指纹](#2.5.2 使用tls-client库指定JA3指纹)
    • [2.5.3 使用Python ssl模块自定义SSLContext](#2.5.3 使用Python ssl模块自定义SSLContext)
    • [2.5.4 常见TLS库指纹特征对比](#2.5.4 常见TLS库指纹特征对比)
  • [2.6 实战演练:绕过Cloudflare TLS指纹检测](#2.6 实战演练:绕过Cloudflare TLS指纹检测)
    • [2.6.1 场景描述](#2.6.1 场景描述)
    • [2.6.2 步骤1:使用requests库测试(被拦截)](#2.6.2 步骤1:使用requests库测试(被拦截))
    • [2.6.3 步骤2:使用Wireshark抓取请求流量](#2.6.3 步骤2:使用Wireshark抓取请求流量)
    • [2.6.4 步骤3:使用ja3er.com检测当前JA3指纹](#2.6.4 步骤3:使用ja3er.com检测当前JA3指纹)
    • [2.6.5 步骤4:对比真实Chrome浏览器的JA3指纹](#2.6.5 步骤4:对比真实Chrome浏览器的JA3指纹)
    • [2.6.6 步骤5:使用curl_cffi模拟Chrome指纹](#2.6.6 步骤5:使用curl_cffi模拟Chrome指纹)
    • [2.6.7 步骤6:验证请求成功绕过检测](#2.6.7 步骤6:验证请求成功绕过检测)
    • [2.6.8 完整实战代码](#2.6.8 完整实战代码)
  • [2.7 常见坑点与排错](#2.7 常见坑点与排错)
    • [2.7.1 JA3指纹包含密码套件顺序](#2.7.1 JA3指纹包含密码套件顺序)
    • [2.7.2 使用代理时TLS指纹可能被改变](#2.7.2 使用代理时TLS指纹可能被改变)
    • [2.7.3 curl_cffi需要与目标浏览器版本匹配](#2.7.3 curl_cffi需要与目标浏览器版本匹配)
    • [2.7.4 TLS 1.3对指纹识别的影响](#2.7.4 TLS 1.3对指纹识别的影响)
  • [2.8 总结](#2.8 总结)

2.1 TLS握手流程深度解析

TLS(Transport Layer Security)协议是HTTPS通信的基础,其握手过程决定了客户端和服务器之间的加密通信参数。理解TLS握手流程是理解TLS指纹识别的基础。

2.1.1 TLS握手完整流程

TLS握手是一个复杂的协商过程,客户端和服务器通过多次消息交换来建立安全连接。

TLS 1.2握手流程

服务器 客户端 服务器 客户端 TLS 1.2 完整握手流程 TLS版本、密码套件列表、扩展列表、随机数 选择的TLS版本、密码套件、随机数 服务器证书 密钥交换参数 请求客户端证书 alt [需要客户端证书] [不需要客户端证书] 握手完成,开始加密通信 ClientHello ServerHello Certificate ServerKeyExchange (可选) CertificateRequest (可选) ServerHelloDone Certificate ClientKeyExchange CertificateVerify ClientKeyExchange ChangeCipherSpec Finished ChangeCipherSpec Finished

详细步骤解析:

1. ClientHello(客户端问候)

客户端向服务器发送ClientHello消息,这是TLS握手的第一个消息,也是JA3指纹的来源。

ClientHello包含的内容:

  • TLS版本:客户端支持的最高TLS版本(如TLS 1.2 = 0x0303)
  • 随机数(Client Random):32字节随机数,用于密钥生成
  • 会话ID(Session ID):用于会话恢复(0-32字节)
  • 密码套件列表(Cipher Suites):客户端支持的密码套件,按优先级排序
  • 压缩方法列表 :通常只有null(0x00)
  • 扩展列表(Extensions):各种扩展,如SNI、ALPN、椭圆曲线等

为什么ClientHello是重要的指纹来源?

不同TLS库在构造ClientHello时有不同的特征:

  • 密码套件的顺序不同
  • 扩展列表的顺序和内容不同
  • 椭圆曲线的选择不同

这些差异就像"指纹"一样,可以唯一标识客户端类型。

2. ServerHello(服务器问候)

服务器响应ServerHello消息,选择协商参数。

ServerHello包含的内容:

  • 选择的TLS版本:服务器选择的TLS版本(通常是客户端支持的最高版本)
  • 随机数(Server Random):32字节随机数
  • 会话ID:服务器分配的会话ID(用于会话恢复)
  • 选择的密码套件:从客户端列表中选择的密码套件
  • 选择的压缩方法 :通常是null
  • 扩展列表:服务器支持的扩展

3. Certificate(证书)

服务器发送其数字证书,客户端验证证书的有效性。

证书验证过程:

  1. 检查证书是否过期
  2. 检查证书是否被吊销(CRL/OCSP)
  3. 验证证书链(根CA -> 中间CA -> 服务器证书)
  4. 验证域名匹配(CN或SAN)

4. ServerKeyExchange(服务器密钥交换,可选)

某些密码套件(如DHE、ECDHE)需要服务器发送额外的密钥交换信息。

包含的内容:

  • Diffie-Hellman参数(p, g, Ys)
  • 或椭圆曲线参数(曲线类型、公钥)
  • 数字签名(用于验证)

5. CertificateRequest(证书请求,可选)

如果服务器需要客户端证书认证(双向认证),会发送此消息。

6. ServerHelloDone(服务器问候完成)

服务器表示握手消息发送完毕,等待客户端响应。

7. Certificate(客户端证书,可选)

如果服务器请求了客户端证书,客户端会发送。

8. ClientKeyExchange(客户端密钥交换)

客户端生成预主密钥(Pre-Master Secret),使用服务器公钥加密后发送。

密钥生成过程:

  1. 客户端生成48字节的预主密钥
  2. 使用服务器公钥(从证书中获取)加密预主密钥
  3. 发送加密后的预主密钥给服务器

9. CertificateVerify(证书验证,可选)

如果发送了客户端证书,客户端需要证明其拥有证书对应的私钥。

10. ChangeCipherSpec(更改密码规范)

客户端和服务器都发送此消息,表示后续通信将使用协商的加密参数。

11. Finished(完成)

双方发送Finished消息,包含所有握手消息的哈希值,用于验证握手完整性。

Finished消息的计算:

复制代码
verify_data = PRF(master_secret, "client finished", Hash(handshake_messages))

如果Finished消息验证失败,连接会被终止。

Python代码演示TLS握手
python 复制代码
import socket
import ssl
import struct

def analyze_tls_handshake(hostname, port=443):
    """分析TLS握手过程"""
    
    # 创建TCP连接
    sock = socket.create_connection((hostname, port))
    
    # 创建SSL上下文
    context = ssl.create_default_context()
    
    # 包装为SSL套接字
    ssl_sock = context.wrap_socket(sock, server_hostname=hostname)
    
    # 获取TLS信息
    print(f"TLS版本: {ssl_sock.version()}")
    print(f"选择的密码套件: {ssl_sock.cipher()}")
    print(f"服务器证书: {ssl_sock.getpeercert()}")
    
    ssl_sock.close()

# 使用示例
if __name__ == '__main__':
    analyze_tls_handshake('www.example.com')

2.1.2 TLS 1.3的新特性

TLS 1.3相比TLS 1.2有以下重要改进:

  1. 更快的握手:支持0-RTT和1-RTT握手,减少往返次数
  2. 更强的安全性:移除了不安全的密码套件和加密算法
  3. 更简化的握手流程:减少了握手消息数量
  4. 密钥交换优化:使用Diffie-Hellman密钥交换,提供前向安全性
TLS 1.3握手流程

服务器 客户端 服务器 客户端 TLS 1.3 1-RTT握手 包含密钥共享(Key Share) 包含密钥共享(Key Share) 握手完成,开始加密通信 ClientHello ServerHello Certificate CertificateVerify Finished Finished

TLS 1.3的关键变化:

  1. 密钥交换提前:在ClientHello中就包含密钥共享信息
  2. 减少往返次数:从2-RTT减少到1-RTT
  3. 0-RTT支持:如果之前建立过连接,可以0-RTT发送数据
  4. 移除不安全的密码套件:只保留AEAD密码套件
TLS 1.3对指纹识别的影响

变化:

  1. 密码套件简化:TLS 1.3只有5个标准密码套件
  2. 扩展变化:某些扩展被移除或修改
  3. 密钥共享:新增Key Share扩展

影响:

  • JA3指纹在TLS 1.3中仍然有效,但特征可能不同
  • 某些检测系统可能需要更新算法

2.1.3 为什么TLS握手会被用于检测?

传统检测方法的局限性:

  1. User-Agent:容易被伪造
  2. Cookie:可以被复制
  3. IP地址:可以使用代理

TLS指纹检测的优势:

  1. 难以伪造:TLS指纹是客户端库的固有特征,需要深入理解TLS协议才能修改
  2. 隐蔽性强:检测发生在TLS握手阶段,在应用层数据之前
  3. 准确率高:不同客户端库的TLS指纹差异明显

实际案例:

python 复制代码
import requests

# 即使伪造了所有请求头
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    # ... 更多头部
}

response = requests.get('https://www.example.com', headers=headers)
# 可能返回 403 Forbidden

为什么会被拦截?

  1. requests使用的TLS库(OpenSSL/urllib3)的JA3指纹与Chrome不同
  2. 服务器在TLS握手阶段就识别出了这是爬虫
  3. 即使后续的HTTP请求头完全正确,也会被拒绝

2.2 JA3/JA3S指纹算法详解

JA3(JA3 Fingerprinting)是一种TLS客户端指纹识别方法,通过分析ClientHello消息的特征来识别客户端类型。

2.2.1 JA3指纹的构成

JA3指纹的计算基于ClientHello消息中的5个字段:

  1. TLS版本(TLSVersion)
  2. 密码套件(CipherSuites):按顺序排列,用"-"连接
  3. 扩展列表(Extensions):按顺序排列,用"-"连接
  4. 椭圆曲线(EllipticCurves):按顺序排列,用"-"连接
  5. 椭圆曲线点格式(EllipticCurvePointFormats):按顺序排列,用"-"连接

JA3字符串格式:

复制代码
TLSVersion,CipherSuites,Extensions,EllipticCurves,EllipticCurvePointFormats

示例JA3字符串:

复制代码
771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0

字段解析:

  • 771:TLS版本(0x0303 = TLS 1.2)
  • 4865-4866-4867...:密码套件列表(按顺序)
  • 0-23-65281...:扩展列表(按顺序)
  • 29-23-24:椭圆曲线列表
  • 0:椭圆曲线点格式

2.2.2 JA3指纹的计算过程

步骤1:提取ClientHello字段

从ClientHello消息中提取5个字段:

python 复制代码
def extract_ja3_fields(client_hello):
    """
    从ClientHello消息中提取JA3字段
    
    为什么需要提取这些字段?
    这些字段的组合在不同客户端库中有显著差异
    """
    # 1. TLS版本(2字节)
    tls_version = client_hello[0:2]
    
    # 2. 密码套件列表
    cipher_suites_length = struct.unpack('!H', client_hello[38:40])[0]
    cipher_suites = client_hello[40:40+cipher_suites_length]
    
    # 3. 扩展列表
    extensions_start = 40 + cipher_suites_length + 1  # +1 for compression methods
    extensions_length = struct.unpack('!H', client_hello[extensions_start:extensions_start+2])[0]
    extensions = client_hello[extensions_start+2:extensions_start+2+extensions_length]
    
    # 4. 从扩展中提取椭圆曲线和点格式
    elliptic_curves = []
    point_formats = []
    
    # 解析扩展列表
    i = 0
    while i < len(extensions):
        ext_type = struct.unpack('!H', extensions[i:i+2])[0]
        ext_length = struct.unpack('!H', extensions[i+2:i+4])[0]
        ext_data = extensions[i+4:i+4+ext_length]
        
        # 扩展10: supported_groups (椭圆曲线)
        if ext_type == 10:
            curves_length = struct.unpack('!H', ext_data[0:2])[0]
            for j in range(2, 2 + curves_length, 2):
                curve = struct.unpack('!H', ext_data[j:j+2])[0]
                elliptic_curves.append(str(curve))
        
        # 扩展11: ec_point_formats
        elif ext_type == 11:
            formats_length = ext_data[0]
            for j in range(1, 1 + formats_length):
                point_formats.append(str(ext_data[j]))
        
        i += 4 + ext_length
    
    return {
        'tls_version': tls_version,
        'cipher_suites': cipher_suites,
        'extensions': extensions,
        'elliptic_curves': elliptic_curves,
        'point_formats': point_formats
    }

步骤2:构造JA3字符串

将提取的字段转换为JA3字符串格式:

python 复制代码
def build_ja3_string(fields):
    """构造JA3字符串"""
    
    # 1. TLS版本(转换为十进制)
    tls_version = struct.unpack('!H', fields['tls_version'])[0]
    
    # 2. 密码套件列表(每2字节一个,转换为十进制,用"-"连接)
    cipher_suites = []
    for i in range(0, len(fields['cipher_suites']), 2):
        cipher = struct.unpack('!H', fields['cipher_suites'][i:i+2])[0]
        cipher_suites.append(str(cipher))
    cipher_suites_str = '-'.join(cipher_suites)
    
    # 3. 扩展列表(每4字节一个扩展:2字节类型+2字节长度)
    extensions = []
    ext_data = fields['extensions']
    i = 0
    while i < len(ext_data):
        ext_type = struct.unpack('!H', ext_data[i:i+2])[0]
        ext_length = struct.unpack('!H', ext_data[i+2:i+4])[0]
        extensions.append(str(ext_type))
        i += 4 + ext_length
    extensions_str = '-'.join(extensions)
    
    # 4. 椭圆曲线列表
    elliptic_curves_str = '-'.join(fields['elliptic_curves']) if fields['elliptic_curves'] else ''
    
    # 5. 椭圆曲线点格式
    point_formats_str = '-'.join(fields['point_formats']) if fields['point_formats'] else ''
    
    # 组合JA3字符串
    ja3_string = f"{tls_version},{cipher_suites_str},{extensions_str},{elliptic_curves_str},{point_formats_str}"
    
    return ja3_string

步骤3:计算MD5哈希

将JA3字符串计算MD5哈希,得到32位十六进制字符串:

python 复制代码
import hashlib

def calculate_ja3_hash(ja3_string):
    """计算JA3哈希值"""
    ja3_hash = hashlib.md5(ja3_string.encode()).hexdigest()
    return ja3_hash

# 示例
ja3_string = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0"
ja3_hash = calculate_ja3_hash(ja3_string)
print(f"JA3字符串: {ja3_string}")
print(f"JA3哈希: {ja3_hash}")

为什么使用MD5?

  1. 固定长度:MD5哈希值固定为32个十六进制字符,便于存储和比较
  2. 快速计算:MD5计算速度快,适合实时检测
  3. 唯一性:不同的JA3字符串会产生不同的哈希值

注意:MD5不是加密安全的,但对于指纹识别来说已经足够。

2.2.3 JA3S指纹(服务器端)

JA3S是服务器端的指纹,基于ServerHello消息的特征计算。

JA3S的构成:

  1. TLS版本
  2. 选择的密码套件
  3. 扩展列表

JA3S字符串格式:

复制代码
TLSVersion,CipherSuite,Extensions

JA3S的计算:

python 复制代码
def calculate_ja3s(server_hello):
    """计算JA3S指纹"""
    # 提取TLS版本
    tls_version = struct.unpack('!H', server_hello[0:2])[0]
    
    # 提取选择的密码套件
    cipher_suite = struct.unpack('!H', server_hello[34:36])[0]
    
    # 提取扩展列表
    # ... (类似JA3的扩展提取)
    
    ja3s_string = f"{tls_version},{cipher_suite},{extensions_str}"
    ja3s_hash = hashlib.md5(ja3s_string.encode()).hexdigest()
    
    return ja3s_hash

2.2.4 为什么JA3指纹能识别爬虫?

不同TLS库的指纹特征差异:

TLS库/浏览器 密码套件数量 扩展数量 典型JA3哈希前缀 特征
Python requests/urllib 10-15 5-8 常见但易识别 密码套件少,扩展简单
Python httpx 10-15 5-8 类似requests 类似requests
curl (系统) 30-40 15-20 较难识别 密码套件多,扩展完整
curl_cffi (Chrome) 30-40 20-25 与Chrome一致 完美模拟Chrome
Chrome浏览器 30-40 20-25 真实浏览器 真实浏览器指纹
Firefox浏览器 30-40 20-25 与Firefox一致 真实浏览器指纹

关键差异点:

  1. 密码套件顺序

    • Chrome:TLS_AES_256_GCM_SHA384在前
    • Python requests:TLS_RSA_WITH_AES_256_CBC_SHA在前
    • 顺序不同,JA3指纹就不同!
  2. 扩展列表

    • Chrome:包含大量扩展(SNI、ALPN、签名算法等)
    • Python requests:扩展较少
  3. 椭圆曲线

    • Chrome:支持多种椭圆曲线(X25519、P-256、P-384等)
    • Python requests:支持的曲线较少

检测原理:

服务器可以通过以下方式检测:

  1. JA3哈希匹配:将客户端的JA3哈希与已知的爬虫库哈希对比
  2. 特征分析:分析密码套件顺序、扩展列表等特征
  3. 机器学习:使用机器学习模型识别异常指纹

2.3 TLS ClientHello报文结构详解

理解ClientHello报文结构是理解JA3指纹的基础。

2.3.1 ClientHello报文格式

ClientHello消息的二进制结构如下:

text 复制代码
+-------+-------+-------+-------+-------+-------+-------+-------+
|  Content Type  |   Version    |         Length                |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                    Handshake Type                             |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                    Handshake Length                           |
+-------+-------+-------+-------+-------+-------+-------+-------+
|   Version      |      Random (32 bytes)                       |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                    Session ID Length                          |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                    Session ID (0-32 bytes)                   |
+-------+-------+-------+-------+-------+-------+-------+-------+
|   Cipher Suites Length       |   Cipher Suites               |
+-------+-------+-------+-------+-------+-------+-------+-------+
| Compression Methods Length   | Compression Methods           |
+-------+-------+-------+-------+-------+-------+-------+-------+
|   Extensions Length          |   Extensions                  |
+-------+-------+-------+-------+-------+-------+-------+-------+

字段详解:

  1. Content Type (1字节)

    • 0x16 = Handshake
  2. Version (2字节)

    • 0x0303 = TLS 1.2
    • 0x0304 = TLS 1.3
  3. Length (2字节)

    • Handshake消息的长度
  4. Handshake Type (1字节)

    • 0x01 = ClientHello
  5. Handshake Length (3字节)

    • ClientHello消息的长度
  6. Version (2字节)

    • TLS版本(与外层Version相同)
  7. Random (32字节)

    • Client Random,用于密钥生成
  8. Session ID Length (1字节)

    • Session ID的长度(0-32)
  9. Session ID (0-32字节)

    • 会话ID(用于会话恢复)
  10. Cipher Suites Length (2字节)

    • 密码套件列表的长度
  11. Cipher Suites (可变长度)

    • 密码套件列表(每2字节一个)
  12. Compression Methods Length (1字节)

    • 压缩方法列表的长度
  13. Compression Methods (可变长度)

    • 压缩方法列表(通常只有0x00 = null)
  14. Extensions Length (2字节)

    • 扩展列表的长度
  15. Extensions (可变长度)

    • 扩展列表(每个扩展4字节头+数据)
十六进制示例

完整的ClientHello报文(十六进制):

hex 复制代码
16 03 01 00 8a 01 00 00 86 03 03 5f 7e 1a 3b 4a
a1 2c 3d 4e 5f 6a 7b 8c 9d 0e 1f 2a 3b 4c 5d 6e
7f 8a 9b 0c 1d 2e 3f 4a 5b 6c 7d 8e 9f 0a 1b 2c
00 00 1a c0 2f c0 2b c0 30 c0 2c c0 31 c0 2d c0
32 c0 33 c0 34 c0 35 c0 28 c0 29 c0 14 c0 0a c0
09 c0 13 00 9c 00 9d 00 2f 00 35 01 00 00 3d 00
0a 00 34 00 32 00 17 00 1d 00 18 00 19 00 0b 00
0c 00 09 00 0a 00 16 00 13 00 10 00 0d 00 0e 00
0b 00 0c 00 09 00 0a 00 23 00 00 00 0d 00 20 00
1e 06 01 06 02 06 03 05 01 05 02 05 03 04 01 04
02 04 03 03 01 03 02 03 03 02 01 02 02 02 03 00
0f 00 01 01

逐字节解析:

复制代码
16              → Content Type: Handshake (0x16)
03 01           → Version: TLS 1.0 (0x0301,用于兼容性)
00 8a           → Length: 138字节

01              → Handshake Type: ClientHello (0x01)
00 00 86        → Handshake Length: 134字节

03 03           → Version: TLS 1.2 (0x0303)
5f 7e 1a 3b...  → Random: 32字节随机数
00              → Session ID Length: 0(新会话)

00 1a           → Cipher Suites Length: 26字节(13个密码套件)
c0 2f c0 2b...  → Cipher Suites: 13个密码套件(每2字节一个)

01              → Compression Methods Length: 1
00              → Compression Method: null (0x00)

00 3d           → Extensions Length: 61字节
00 0a 00 34...  → Extensions: 扩展列表

2.3.2 密码套件列表详解

密码套件定义了TLS连接使用的加密算法组合。

密码套件格式:

  • 每个密码套件占2字节
  • 格式:0xTLS_密钥交换_认证_加密_MAC

常见密码套件:

十六进制 名称 说明
0x002F TLS_RSA_WITH_AES_128_CBC_SHA RSA密钥交换,AES-128-CBC加密
0x0035 TLS_RSA_WITH_AES_256_CBC_SHA RSA密钥交换,AES-256-CBC加密
0xC02F TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ECDHE密钥交换,AES-128-GCM加密
0xC02B TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ECDHE密钥交换,ECDSA认证
0x1301 TLS_AES_256_GCM_SHA384 TLS 1.3密码套件

为什么密码套件顺序重要?

JA3指纹包含密码套件的顺序,而不仅仅是内容。即使两个客户端支持相同的密码套件,如果顺序不同,JA3指纹也会不同。

示例:

复制代码
客户端A: TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, ...
客户端B: TLS_CHACHA20_POLY1305_SHA256, TLS_AES_256_GCM_SHA384, ...

这两个客户端的JA3指纹会完全不同!

2.3.3 扩展列表详解

扩展(Extensions)是TLS协议的可选功能,用于协商额外参数。

扩展格式:

复制代码
+-------+-------+
|  Type (2)     |
+-------+-------+
|  Length (2)   |
+-------+-------+
|  Data (可变)   |
+-------+-------+

重要扩展:

  1. Server Name Indication (SNI, 0x0000)

    • 用于指定要连接的服务器名称
    • 允许一个IP地址托管多个SSL证书
  2. Application-Layer Protocol Negotiation (ALPN, 0x0010)

    • 用于协商应用层协议(如HTTP/2)
  3. Supported Groups (0x000A)

    • 支持的椭圆曲线列表(用于ECDHE)
  4. EC Point Formats (0x000B)

    • 椭圆曲线点格式
  5. Signature Algorithms (0x000D)

    • 支持的签名算法
  6. Extended Master Secret (0x0017)

    • 扩展主密钥

扩展顺序的重要性:

JA3指纹包含扩展的顺序,不同客户端库的扩展顺序可能不同。

2.3.4 椭圆曲线和点格式

椭圆曲线(Elliptic Curves):

TLS使用椭圆曲线密码学(ECC)进行密钥交换。

常见椭圆曲线:

曲线ID 名称 说明
0x001D X25519 现代、快速
0x0017 secp256r1 (P-256) 常用
0x0018 secp384r1 (P-384) 更安全
0x0019 secp521r1 (P-521) 最安全

椭圆曲线点格式(EC Point Formats):

格式ID 名称 说明
0x00 uncompressed 未压缩格式
0x01 ansiX962_compressed_prime 压缩格式(素数曲线)
0x02 ansiX962_compressed_char2 压缩格式(二进制曲线)

2.4 工具链:TLS指纹分析与检测

2.4.1 使用Wireshark抓包分析TLS ClientHello

步骤1:配置SSLKEYLOGFILE

在macOS/Linux中:

bash 复制代码
export SSLKEYLOGFILE=~/ssl-keys.log
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome

在Windows PowerShell中:

powershell 复制代码
$env:SSLKEYLOGFILE="C:\Users\YourName\ssl-keys.log"
Start-Process "C:\Program Files\Google\Chrome\Application\chrome.exe"

步骤2:Wireshark配置

  1. Preferences -> Protocols -> TLS
  2. (Pre)-Master-Secret log filename 指向 ~/ssl-keys.log

步骤3:抓包和分析

  1. 在Wireshark中选择网络接口
  2. 开始抓包
  3. 在浏览器中访问目标网站
  4. 停止抓包

步骤4:分析ClientHello

  1. 过滤:tls.handshake.type == 1(ClientHello)
  2. 展开 Transport Layer Security -> TLSv1.2 Record Layer -> Handshake Protocol: Client Hello
  3. 查看关键字段:
    • Version: TLS版本
    • Cipher Suites: 密码套件列表
    • Extensions: 扩展列表
    • Supported Groups: 椭圆曲线
    • EC Point Formats: 椭圆曲线点格式

2.4.2 使用ja3er.com在线检测

访问 https://ja3er.com/ 可以查看当前浏览器的JA3指纹。

使用方法:

  1. 在浏览器中访问 https://ja3er.com/
  2. 页面会显示当前浏览器的JA3指纹
  3. 可以搜索特定JA3哈希,查看对应的客户端类型

2.4.3 使用Python检测JA3指纹

python 复制代码
import requests
from curl_cffi import requests as curl_requests

def detect_ja3_fingerprint():
    """检测当前请求的JA3指纹"""
    
    # 使用tls.browserleaks.com检测
    url = 'https://tls.browserleaks.com/json'
    
    try:
        # 使用curl_cffi(可以模拟浏览器)
        response = curl_requests.get(url, impersonate="chrome120")
        data = response.json()
        
        print("=== TLS指纹信息 ===")
        print(f"JA3字符串: {data.get('ja3')}")
        print(f"JA3哈希: {data.get('ja3_hash')}")
        print(f"TLS版本: {data.get('tls_version')}")
        print(f"密码套件数量: {len(data.get('cipher_suites', []))}")
        print(f"扩展数量: {len(data.get('extensions', []))}")
        
        return data
    except Exception as e:
        print(f"❌ 检测失败: {e}")
        return None

if __name__ == '__main__':
    detect_ja3_fingerprint()

2.4.4 使用ja3-fingerprinting库

python 复制代码
# 安装: pip install ja3-fingerprinting

from ja3_fingerprinting import JA3Fingerprint

# 创建指纹对象
fingerprint = JA3Fingerprint()

# 从pcap文件提取JA3
ja3_data = fingerprint.extract_from_pcap('capture.pcap')

# 或者从ClientHello消息提取
ja3_data = fingerprint.extract_from_client_hello(client_hello_bytes)

print(f"JA3字符串: {ja3_data['ja3_string']}")
print(f"JA3哈希: {ja3_data['ja3_hash']}")

2.5 TLS指纹绕过技术

2.5.1 使用curl_cffi模拟浏览器指纹

curl_cffi是基于curl-impersonate的Python库,可以精确模拟浏览器的TLS指纹。

安装:

bash 复制代码
pip install curl_cffi

基础使用:

python 复制代码
from curl_cffi import requests

# 模拟Chrome 120
response = requests.get(
    'https://www.example.com',
    impersonate="chrome120"
)

# 模拟Firefox 120
response = requests.get(
    'https://www.example.com',
    impersonate="firefox120"
)

# 支持的浏览器版本
# chrome120, chrome119, chrome118, chrome117, chrome116
# firefox120, firefox119, firefox118, firefox117
# safari17, safari16, safari15
# edge120, edge119, edge118

完整示例:

python 复制代码
from curl_cffi import requests
import json

def test_tls_fingerprint():
    """测试TLS指纹"""
    url = 'https://tls.browserleaks.com/json'
    
    # 使用curl_cffi模拟Chrome
    response = requests.get(url, impersonate="chrome120")
    data = response.json()
    
    print("=== Chrome 120 TLS指纹 ===")
    print(f"JA3: {data.get('ja3')}")
    print(f"JA3 Hash: {data.get('ja3_hash')}")
    print(f"TLS Version: {data.get('tls_version')}")
    print(f"Cipher Suites: {data.get('cipher_suites')}")

if __name__ == '__main__':
    test_tls_fingerprint()

2.5.2 使用tls-client库指定JA3指纹

tls-client库允许直接指定JA3指纹字符串。

安装:

bash 复制代码
pip install tls-client

使用示例:

python 复制代码
import tls_client

# 创建会话,指定客户端标识符
session = tls_client.Session(
    client_identifier="chrome_120",
    random_tls_extension_order=True
)

# 或者使用自定义JA3指纹
session = tls_client.Session(
    ja3_string="771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0"
)

response = session.get('https://www.example.com')
print(response.text)

2.5.3 使用Python ssl模块自定义SSLContext

虽然Python的ssl模块不能完全控制ClientHello的所有细节,但可以修改部分参数:

python 复制代码
import ssl
import socket
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context

class CustomHTTPAdapter(HTTPAdapter):
    """自定义HTTP适配器,修改TLS参数"""
    
    def init_poolmanager(self, *args, **kwargs):
        ctx = create_urllib3_context()
        # 设置TLS版本
        ctx.minimum_version = ssl.TLSVersion.TLSv1_2
        ctx.maximum_version = ssl.TLSVersion.TLSv1_3
        
        # 设置密码套件(注意:顺序可能无法完全控制)
        ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS')
        
        kwargs['ssl_context'] = ctx
        return super().init_poolmanager(*args, **kwargs)

# 使用自定义适配器
session = requests.Session()
session.mount('https://', CustomHTTPAdapter())

response = session.get('https://www.example.com')

注意 :Python的ssl模块对ClientHello的控制有限,无法精确控制密码套件顺序和扩展列表,因此绕过效果不如curl_cffi

2.5.4 常见TLS库指纹特征对比

库/工具 JA3特征 绕过难度 推荐度
Python requests 简单密码套件列表 ⭐⭐⭐⭐⭐
Python httpx 类似requests ⭐⭐⭐⭐⭐
curl (系统) 完整密码套件 ⭐⭐⭐ ⚠️
curl_cffi 可模拟浏览器 ✅ 推荐
tls-client 可指定JA3 ✅ 推荐
Chrome浏览器 特定指纹 ✅ 真实浏览器
Firefox浏览器 特定指纹 ✅ 真实浏览器

2.6 实战演练:绕过Cloudflare TLS指纹检测

2.6.1 场景描述

目标网站使用Cloudflare的TLS指纹检测,使用普通的requests库请求会被拦截,返回403错误或要求验证码。

2.6.2 步骤1:使用requests库测试(被拦截)

python 复制代码
import requests

url = 'https://nowsecure.nl/'  # Cloudflare测试网站

try:
    response = requests.get(url, timeout=10)
    print(f"状态码: {response.status_code}")
    print(f"响应长度: {len(response.text)}")
    
    # 检查是否被拦截
    if response.status_code == 403:
        print("❌ 请求被拦截(可能是TLS指纹检测)")
    elif "challenge" in response.text.lower() or "cloudflare" in response.text.lower():
        print("❌ 触发了Cloudflare验证")
    else:
        print("✅ 请求成功")
except Exception as e:
    print(f"❌ 请求失败: {e}")

预期结果:请求被拦截,返回403或触发验证。

2.6.3 步骤2:使用Wireshark抓取请求流量

详细步骤:

  1. 启动Wireshark,选择网络接口
  2. 设置过滤器tls.handshake.type == 1(只显示ClientHello)
  3. 运行Python脚本发送请求
  4. 分析ClientHello报文
    • 查看TLS版本
    • 查看密码套件列表和顺序
    • 查看扩展列表
    • 记录JA3相关字段

在Wireshark中查看:

  1. 找到TLS握手包
  2. 展开"Transport Layer Security" -> "TLSv1.2 Record Layer" -> "Handshake Protocol: Client Hello"
  3. 记录以下信息:
    • Version
    • Cipher Suites
    • Extensions
    • Supported Groups
    • EC Point Formats

2.6.4 步骤3:使用ja3er.com检测当前JA3指纹

python 复制代码
from curl_cffi import requests

# 检测requests库的指纹
def check_fingerprint():
    url = 'https://tls.browserleaks.com/json'
    
    try:
        # 使用requests(会被检测)
        import requests as std_requests
        response = std_requests.get(url, timeout=10)
        print("=== requests库指纹 ===")
        print(response.json())
    except:
        pass
    
    # 使用curl_cffi检测
    response = requests.get(url, impersonate="chrome120")
    print("\n=== Chrome 120指纹 ===")
    data = response.json()
    print(json.dumps(data, indent=2))

check_fingerprint()

2.6.5 步骤4:对比真实Chrome浏览器的JA3指纹

在真实Chrome浏览器中访问 https://tls.browserleaks.com/json,记录JA3指纹。

Chrome 120典型JA3指纹:

复制代码
JA3字符串: 771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0
JA3哈希: b32309a26951912be7dba376398abc3b

对比分析:

特征 requests Chrome 120 差异
密码套件数量 10-15 30-40 2-3倍
扩展数量 5-8 20-25 3-4倍
椭圆曲线 较少 多种 明显差异

2.6.6 步骤5:使用curl_cffi模拟Chrome指纹

python 复制代码
from curl_cffi import requests
import json

def bypass_cloudflare():
    """使用curl_cffi绕过Cloudflare TLS指纹检测"""
    url = 'https://nowsecure.nl/'
    
    # 使用curl_cffi模拟Chrome 120
    response = requests.get(
        url,
        impersonate="chrome120",
        headers={
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language': 'en-US,en;q=0.5',
            'Accept-Encoding': 'gzip, deflate, br',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
        },
        timeout=30
    )
    
    print(f"状态码: {response.status_code}")
    print(f"响应长度: {len(response.text)}")
    
    # 检查是否成功
    if response.status_code == 200 and "challenge" not in response.text.lower():
        print("✅ 成功绕过Cloudflare TLS指纹检测!")
        return True
    else:
        print("❌ 仍然被拦截")
        return False

if __name__ == '__main__':
    bypass_cloudflare()

2.6.7 步骤6:验证请求成功绕过检测

python 复制代码
from curl_cffi import requests
import re

def verify_bypass():
    """验证绕过是否成功"""
    url = 'https://nowsecure.nl/'
    
    response = requests.get(url, impersonate="chrome120")
    
    # 检查响应内容
    if response.status_code == 200:
        # 查找页面中的特定内容
        if "Congratulations" in response.text or "success" in response.text.lower():
            print("✅ 验证成功:页面正常加载")
        else:
            print("⚠️ 页面加载,但内容可能异常")
            # 打印部分响应内容
            print(response.text[:500])
    else:
        print(f"❌ 请求失败,状态码: {response.status_code}")

verify_bypass()

2.6.8 完整实战代码

python 复制代码
"""
Cloudflare TLS指纹绕过实战
"""
from curl_cffi import requests
import json
import time

class CloudflareBypass:
    """Cloudflare TLS指纹绕过类"""
    
    def __init__(self, browser="chrome120"):
        """
        初始化
        
        Args:
            browser: 模拟的浏览器版本
                    chrome120, chrome119, firefox120等
        """
        self.browser = browser
        self.session = None
    
    def get(self, url, **kwargs):
        """发送GET请求"""
        headers = kwargs.get('headers', {})
        
        # 设置默认请求头
        default_headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language': 'en-US,en;q=0.9',
            'Accept-Encoding': 'gzip, deflate, br',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
            'Sec-Fetch-Dest': 'document',
            'Sec-Fetch-Mode': 'navigate',
            'Sec-Fetch-Site': 'none',
            'Cache-Control': 'max-age=0',
        }
        
        default_headers.update(headers)
        kwargs['headers'] = default_headers
        
        # 使用curl_cffi发送请求,自动模拟浏览器TLS指纹
        response = requests.get(
            url,
            impersonate=self.browser,
            **kwargs
        )
        
        return response
    
    def test_fingerprint(self):
        """测试当前TLS指纹"""
        url = 'https://tls.browserleaks.com/json'
        
        try:
            response = self.get(url)
            data = response.json()
            
            print("=== TLS指纹信息 ===")
            print(f"JA3: {data.get('ja3')}")
            print(f"JA3 Hash: {data.get('ja3_hash')}")
            print(f"TLS Version: {data.get('tls_version')}")
            print(f"支持的密码套件数量: {len(data.get('cipher_suites', []))}")
            
            return data
        except Exception as e:
            print(f"❌ 指纹检测失败: {e}")
            return None
    
    def bypass_test(self, test_url='https://nowsecure.nl/'):
        """测试绕过效果"""
        print(f"\n=== 测试绕过: {test_url} ===")
        
        try:
            response = self.get(test_url, timeout=30)
            
            print(f"状态码: {response.status_code}")
            print(f"响应长度: {len(response.text)}")
            
            # 检查是否被拦截
            if response.status_code == 200:
                if "challenge" not in response.text.lower() and "cloudflare" not in response.text.lower():
                    print("✅ 成功绕过检测!")
                    return True
                else:
                    print("⚠️ 触发了验证页面")
                    return False
            elif response.status_code == 403:
                print("❌ 请求被拦截(403)")
                return False
            else:
                print(f"⚠️ 异常状态码: {response.status_code}")
                return False
                
        except Exception as e:
            print(f"❌ 请求失败: {e}")
            return False

def main():
    """主函数"""
    # 创建绕过实例
    bypass = CloudflareBypass(browser="chrome120")
    
    # 测试TLS指纹
    print("1. 检测TLS指纹")
    bypass.test_fingerprint()
    
    # 测试绕过
    print("\n2. 测试绕过效果")
    bypass.bypass_test()
    
    # 可以测试其他网站
    print("\n3. 测试其他网站")
    test_urls = [
        'https://nowsecure.nl/',
        # 添加其他需要测试的URL
    ]
    
    for url in test_urls:
        bypass.bypass_test(url)
        time.sleep(2)  # 避免请求过快

if __name__ == '__main__':
    main()

2.7 常见坑点与排错

2.7.1 JA3指纹包含密码套件顺序

问题:

很多新手认为只要支持相同的密码套件就能绕过,但实际上JA3指纹包含密码套件的顺序

错误理解:

python 复制代码
# 错误:认为只要密码套件内容相同就行
cipher_suites = ['TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256']
# 顺序不同,JA3指纹就不同!

正确理解:

python 复制代码
# 正确:密码套件的顺序必须与目标浏览器一致
# Chrome 120的密码套件顺序:
# TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, ...
# 不能改变顺序!

解决方案:

  • 使用curl_cffi自动模拟浏览器的密码套件顺序
  • 或者手动使用tls-client指定完整的JA3字符串

Tips/坑点:

  • ⚠️ JA3指纹包含密码套件顺序而不只是内容,顺序不同指纹就不同

2.7.2 使用代理时TLS指纹可能被改变

问题:

在使用代理时,TLS握手可能发生在客户端和代理之间,而不是客户端和服务器之间。

场景:

复制代码
客户端 -> 代理服务器 -> 目标服务器

问题分析:

  1. 客户端与代理建立TLS连接(使用客户端的JA3指纹)
  2. 代理与目标服务器建立TLS连接(使用代理的JA3指纹)
  3. 目标服务器看到的是代理的JA3指纹,而不是客户端的

解决方案:

  1. 使用透明代理:代理不修改TLS握手
  2. 使用CONNECT方法:HTTP代理使用CONNECT方法建立隧道
  3. 直接连接:不使用代理(如果可能)

Tips/坑点:

  • ⚠️ 使用代理时TLS指纹可能被代理服务器改变,需要确保代理是透明的

2.7.3 curl_cffi需要与目标浏览器版本匹配

问题:

使用curl_cffi时,需要选择与目标网站检测逻辑匹配的浏览器版本。

错误示例:

python 复制代码
# 错误:使用chrome120,但目标网站检测chrome119
response = requests.get(url, impersonate="chrome120")
# 可能仍然被拦截

正确做法:

python 复制代码
# 正确:根据目标网站的检测逻辑选择合适的版本
# 可以通过测试不同版本来确定
for version in ["chrome120", "chrome119", "chrome118"]:
    try:
        response = requests.get(url, impersonate=version)
        if response.status_code == 200:
            print(f"✅ {version} 成功")
            break
    except:
        continue

Tips/坑点:

  • ⚠️ curl_cffi需要与目标浏览器版本匹配,不同版本可能有不同的JA3指纹

2.7.4 TLS 1.3对指纹识别的影响

问题:

TLS 1.3简化了密码套件和握手流程,可能影响JA3指纹的计算。

TLS 1.3的变化:

  1. 密码套件简化:只有5个标准密码套件
  2. 扩展变化:某些扩展被移除或修改
  3. 密钥共享:新增Key Share扩展

影响:

  • JA3指纹在TLS 1.3中仍然有效
  • 但特征可能与TLS 1.2不同
  • 某些检测系统可能需要更新算法

解决方案:

  • 确保curl_cffi支持TLS 1.3
  • 测试TLS 1.3和TLS 1.2的兼容性

2.8 总结

本章深入讲解了TLS指纹识别的原理和绕过方法:

  1. TLS握手流程:理解了ClientHello消息的构成和JA3指纹的计算方法
  2. 指纹识别原理:不同TLS库生成的ClientHello特征不同,可用于识别爬虫
  3. 绕过技术
    • curl_cffi:最推荐的方案,可精确模拟浏览器
    • tls-client:可自定义JA3指纹
    • Python ssl模块:控制有限,不推荐用于绕过
  4. 实战案例:完整演示了绕过Cloudflare TLS指纹检测的流程

关键要点:

  • ⚠️ JA3指纹基于密码套件顺序,不只是内容
  • ⚠️ 使用代理时TLS指纹可能被改变
  • ⚠️ 需要综合模拟浏览器的所有特征,不只是TLS指纹
  • ⚠️ 定期验证和更新绕过方案

在下一章中,我们将学习异步编程模型,进一步提升爬虫的性能和效率。

相关推荐
郝学胜-神的一滴5 小时前
Linux Socket编程核心:深入解析sockaddr数据结构族
linux·服务器·c语言·网络·数据结构·c++·架构
啊吧怪不啊吧5 小时前
极致性能的服务器Redis之String类型及相关指令介绍
网络·数据库·redis·分布式·mybatis
Whisper_Sy12 小时前
Flutter for OpenHarmony移动数据使用监管助手App实战 - 网络状态实现
android·java·开发语言·javascript·网络·flutter·php
Black蜡笔小新12 小时前
视频汇聚平台EasyCVR打造校园消防智能监管新防线
网络·人工智能·音视频
珠海西格电力科技12 小时前
双碳目标下,微电网为何成为能源转型核心载体?
网络·人工智能·物联网·云计算·智慧城市·能源
wifi chicken14 小时前
Linux Wlan L3~L2封包逻辑详解
linux·网络·ping·封包
jllllyuz15 小时前
基于MATLAB的D2D通信模式选择仿真
开发语言·网络·matlab
G311354227316 小时前
域名与IP:无限绑定的技术奥秘
网络·网络协议·tcp/ip
我不是程序员yy16 小时前
计算机网络七层模型,每层功能 + 经典协议详解
网络
byzh_rc18 小时前
[数学建模从入门到入土] 评价模型
网络·人工智能·深度学习·数学建模·回归·ar