逆向工程实战手册 第 107 章

第 107 章 指纹伪造与人类行为模拟(反检测进阶)

第 106 章讲了 Bot Protection 的四路线;本章深入"反检测"的底层技术:浏览器指纹伪造(JS 层)与人类行为模拟(鼠标/键盘轨迹)。这是路线 B/D 的完整实战手册——让自动化"看起来像人"。

📍 知识点地图 | 主题:指纹伪造与行为模拟 | 前置:第106章 | 后续:— | 核心概念:Canvas/WebGL/TLS指纹、贝塞尔轨迹、CDP原生事件、一致性

107.1 指纹全景(先知道检测什么)

浏览器指纹 = 浏览器暴露给网站的所有"可区分特征":

硬件/系统层:
  UA / 平台 / 语言 / 时区 / 屏幕分辨率 / 色深 / 内存 / 核心数

渲染层(最难伪造):
  Canvas 指纹(canvas 绘制结果哈希)
  WebGL 指纹(GPU 渲染器/供应商/参数)
  字体指纹(可用字体列表)
  音频指纹(AudioContext 输出)

行为层(第 107.5):
  鼠标轨迹 / 键盘节奏 / 滚动模式 / 事件时序

网络层(第 107.4):
  TLS 指纹(JA3/JA4)/ HTTP/2 指纹 / IP 信誉

核心认知:
  □ 指纹是"组合拳"(单个特征不致命,组合一致性才关键)
  □ 伪造失败模式 = 内部不一致(如 UA 说 Windows 但字体是 Linux)
  □ 目标: 保持"内部一致的真实感",而非完美

107.2 UA / 基础属性伪造(最基础层)

# Playwright context 伪造基础属性
ctx = browser.new_context(
    user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
               "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
    viewport={"width": 1920, "height": 1080},
    screen={"width": 1920, "height": 1080},
    device_scale_factor=1,
    locale="zh-CN",
    timezone_id="Asia/Shanghai",
    color_scheme="light",
    extra_http_headers={
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    },
)

一致性检查清单(伪造后自查)

□ UA 平台 ↔ navigator.platform ↔ 字体列表 ↔ 插件列表
□ 时区 ↔ 语言 ↔ 日期格式
□ 屏幕尺寸 ↔ 视口 ↔ devicePixelRatio
□ OS 类型 ↔ 触摸支持(触屏 vs 非触屏)

107.3 JS 层指纹伪造(Canvas / WebGL / 字体)

Canvas 指纹(最经典)

原理: 网站画一段图形 → 浏览器渲染结果有细微差异(字体/抗锯齿/GPU)
  → 结果哈希 = 指纹(同一浏览器稳定,不同浏览器不同)

伪造思路:
  1. 让 canvas 调用"看起来正常但结果一致"(hook toDataURL/getImageData)
  2. 注入噪声(在每个像素加微小扰动 → 每次哈希不同 → 无稳定指纹)
     → 网站看到"每次都不一致" = 高度可疑(正常浏览器稳定!)
  3. 正确做法: hook 返回"固定但合理"的值(与真实浏览器一致)
// 注入脚本: 固定 canvas 指纹(在页面加载前注入)
// 注意: 要与"真实浏览器"一致,不要随机
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function(...args) {
    // 先正常绘制,再叠加一个固定偏移(与目标浏览器一致)
    // 简化示例: 返回固定字符串会太假,应做"像素级微调"
    return originalToDataURL.apply(this, args);
};

WebGL 指纹

检测内容: 渲染器名(ANGLE/GPU 厂商)、GL 版本、扩展列表、参数
伪造: 
  □ 最稳 = 用真实浏览器的真实 GPU(本机 Chrome 天然真实)
  □ 需要改时: hook WebGLRenderingContext.getParameter
    但 GPU 相关参数是"可信源" → 改错 = 更可疑
实战建议: WebGL 用"真实环境"(路线 A 本机浏览器),少伪造

字体指纹

原理: 网站检测"可用字体列表"(渲染测试字符对比宽度)
  → Linux 缺 Windows 字体 = 明显信号(无头/容器环境)

伪造: 
  □ 装目标平台的字体(Linux 装 MS 核心字体)
  □ 或 hook 字体检测(复杂,不建议)
实战建议: 环境装字体 > 代码伪造

自动化特征消除(webdriver 等)

// 注入脚本(页面加载前)—— 消除最明显的自动化信号
// navigator.webdriver 是最高频检测点
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });

// 其他常见特征:
//   window.chrome 对象完整性(真实 Chrome 有)
//   navigator.plugins 长度(真实 Chrome 有插件)
//   Permissions API 行为差异
// 用 playwright-stealth 自动处理大部分

107.4 TLS 指纹伪造(网络层,最容易被忽略)

为什么重要

curl/requests/Scrapy 的 TLS 握手特征与浏览器完全不同
  → 即使 JS 层完美,TLS 层直接暴露"非浏览器"

JA3/JA3S: TLS ClientHello 的指纹(cipher 顺序/扩展/椭圆曲线)
JA4: 更新的指纹标准(更细粒度)

伪造工具

# curl-impersonate(最成熟)—— 伪装成 Chrome/Firefox 的 TLS 指纹
curl-impersonate-chrome https://target.com
# 支持: chrome99-131 / firefox 等 profile

# Python:
#   tls-client(配置 JA3/cipher 顺序)
#   curl_cffi(requests 兼容 API + 浏览器 TLS)
import curl_cffi.requests as requests
resp = requests.get("https://target.com", impersonate="chrome131")
验证效果(先自查):
  tlsfingerprint.io / 各平台指纹检测页
  → 对比 curl vs curl-impersonate 的 JA3 差异
  → 目标是和真实浏览器一致

107.5 人类行为模拟(鼠标/键盘/滚动)

为什么行为检测难绕

人类行为特征:
  □ 鼠标轨迹: 加速-减速曲线、微抖动、非直线
  □ 移动时间: 与距离相关(短距离快、长距离有弧线)
  □ 键盘: 击键间隔不规则、错误后回退
  □ 滚动: 分段滚动、停顿阅读、速度变化
  □ 事件时序: 自然间隔(不是 0.5s 均匀)

机器特征(检测信号):
  □ 直线匀速移动(无加速/减速)
  □ 精确网格点(目标坐标无抖动)
  □ 恒定间隔事件(人为定时器)
  □ 轨迹无"过冲"(人类会过头再回来)

鼠标轨迹模拟(核心算法)

# 人类化鼠标移动: 贝塞尔曲线 + 随机抖动 + 加速减速
import random, math, time

def human_curve(start, end, steps=30):
    """生成类人轨迹点(带弧线和抖动)"""
    points = []
    # 随机控制点(制造弧线,不是直线)
    cx = (start[0] + end[0]) / 2 + random.uniform(-80, 80)
    cy = (start[1] + end[1]) / 2 + random.uniform(-80, 80)
    for i in range(steps + 1):
        t = i / steps
        # 二次贝塞尔: (1-t)²P0 + 2(1-t)t·C + t²P1
        x = (1-t)**2 * start[0] + 2*(1-t)*t * cx + t**2 * end[0]
        y = (1-t)**2 * start[1] + 2*(1-t)*t * cy + t**2 * end[1]
        # 抖动
        x += random.uniform(-1.5, 1.5)
        y += random.uniform(-1.5, 1.5)
        points.append((x, y))
    return points

def human_delay(distance):
    """距离相关延迟(短快长慢)+ 随机"""
    base = 0.1 + distance / 800  # 基础: 距离越大越久
    return base * random.uniform(0.8, 1.3)  # 随机 ±30%

CDP 原生鼠标事件(第 105 章案例的进阶)

# 用 CDP 发送原生鼠标事件(绕过 webdriver 检测)
# 配合 human_curve 轨迹逐点移动

async def human_drag(page, from_x, from_y, to_x, to_y):
    """类人拖拽(滑块场景)"""
    points = human_curve((from_x, from_y), (to_x, to_y))
    for i, (x, y) in enumerate(points):
        await page.mouse.move(x, y)
        # 非匀速: 中间快两头慢(加速-减速)
        await page.wait_for_timeout(int(human_delay(0) * 1000))
    await page.mouse.down()
    await page.mouse.up()

# 关键: 原生事件 = 绕过自动化检测(与 JS 模拟的 mouseenter 不同)

键盘模拟

类人输入: 逐字符输入 + 随机间隔 + 偶尔错误回退
  page.type 太均匀 → 自己写逐字符 + sleep(random)
  密码/邮箱输入: 更快的节奏(熟练用户)
  长文本: 分段停顿

107.6 行为指纹的完整解决方案(组合拳)

一个"看起来像人"的自动化会话:
  1. 真实浏览器(本机 Chrome)+ 真实指纹(路线 A)
  2. stealth 消除自动化特征(webdriver 等)
  3. 必要时 JS 层固定 canvas/字体(与真实环境一致)
  4. TLS 指纹用 curl-impersonate(网络层)
  5. 人类化交互: 贝塞尔轨迹 + 随机延迟 + 自然滚动
  6. 会话行为: 先浏览再操作(不是进去就点目标)
  7. 频率控制: 请求间隔随机化(不是固定 1s)

107.7 反检测自查清单(上线前验证)

□ navigator.webdriver 消失
□ UA/平台/时区/语言 内部一致
□ Canvas 指纹稳定(不每次变)
□ WebGL 渲染器正常(真实 GPU)
□ 字体列表正常(装过目标平台字体)
□ TLS 指纹与浏览器一致(tlsfingerprint.io 验证)
□ 鼠标轨迹有弧线和抖动
□ 事件间隔随机
□ 会话行为自然(浏览→操作)
□ 测试目标验证通过(成功率记录)

107.8 伦理边界

□ 指纹伪造与行为模拟用于: 授权测试/安全研究/合规抓取
□ 违反目标服务条款的操作有法律风险(先读规则)
□ 不用于: 绕过付费/滥用/未授权数据采集
□ 检测绕过本身不是漏洞(是防护对抗)——报告时应说明

动手练习

  1. 用 tlsfingerprint.io 对比 curl / curl-impersonate / 真实浏览器的 JA3,记录差异。
  2. 写一个"贝塞尔轨迹"函数,在测试页面(可记录鼠标事件的页面)观察轨迹与直线的区别。
  3. 用 playwright-stealth 启动一个页面,检查 navigator.webdriver 和常见特征是否消除。
  4. 组合练习:对一个有 Cloudflare 的测试目标,用"真实浏览器+人类轨迹"完成一次完整浏览。

深入阅读

  • 本教材:第 106 章(Bot Protection 四路线——本章是其底层技术)
  • 本教材:第 105 章(CDP 原生事件实战案例)
  • 本教材:第 13 章(JS 逆向:注入时机/补环境)
  • 外部: curl-impersonate、playwright-stealth、指纹检测原理研究