# 反爬检测数的是你说了几次谎:真实浏览器为什么天生没破绽

做浏览器自动化的人迟早会掉进一个坑:想尽办法把一个自动化浏览器伪装成真人,被网站一眼识破,再加一层伪装,再被识破。我自己绕明白这件事,是从想通"检测方到底在查什么"开始的。

先说个反直觉的结论:像 CreepJS 这种检测,它压根不直接判断"你是不是机器人"。它做的是另一件事,把你浏览器暴露出来的几十项信号收齐,然后互相对账,看哪儿对不上。对不上一处,记一条"谎"。谎越多,分越低。它内部就管这个叫 lies。

视角一换,很多事就通了。

![左:假浏览器/自动化被 CreepJS 检测查出 webdriver=true、headless、指纹对不上、Runtime.enable 四面红旗而失败;右:真实 Chrome 四项全绿通过](https://img.leeguoo.com/media/f8b08367-08c2-45c4-bf08-8cb52da8abce/creepjs-hero.webp)

## 一台真实浏览器长什么样

我让 chrome-use 连上这台 Mac 上正开着的 Chrome,读了几项最常被查的信号:

```json
{
  "webdriver": false,
  "gpu": "ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version)",
  "plugins": 5,
  "languages": "en,en-US,ja",
  "timeZone": "Asia/Tokyo"
}
```

看着平平无奇,但每一项都是检测方盯着的点:

`navigator.webdriver` 是 false。W3C 的 WebDriver 规范规定,浏览器被自动化控制时这个 getter 返回 true;Selenium、Puppeteer、Playwright 默认都是 true。我这里是 false,因为确实没有框架在驱动,是我本人在用。

GPU 那串 `Apple M3`,是通过 WebGL 的 `WEBGL_debug_renderer_info` 扩展拿到的真实显卡名。无头 Chrome 没有真实 GPU,这一项通常是 `SwiftShader` 或 `llvmpipe`,也就是软件渲染。软件渲染本身不违法,可它跟一个"自称是普通 Mac"的浏览器摆在一起,就成了矛盾。

plugins 有 5 个、时区 Asia/Tokyo、语言列表里有 ja,这些琐碎信号单看没意义,价值在于它们**能不能对上**。

## 检测的杀招是交叉核对

一个自动化浏览器要伪装,改的通常是表层:改 UA 谎称自己是 Mac,改 `navigator.webdriver`,注脚本盖掉 headless 的痕迹。但你改得了 UA,改不了那一大串跟着联动的东西。

UA 说 macOS,WebGL 却报 `llvmpipe`(Linux 软件渲染的典型),对不上。声称时区在东京,出口 IP 却在法兰克福,对不上。手动改了屏幕分辨率,`devicePixelRatio` 和 Canvas 实际渲染出的尺寸又对不上。CreepJS 把这些信号两两一验,每个矛盾记一条谎。

所以伪装真正的难点从来不是"盖住某一项",是盖住这一项之后,它跟其余几十项还得自洽。改一个,漏一串。这是伪装天然的劣势。

## 最难盖的那条痕迹,不在 JS 层

上面这些还都是 JS 能读到的信号,理论上都能 hook 掉。但有一类痕迹在更底下。

用 Chrome DevTools Protocol 驱动浏览器时,一旦发了 `Runtime.enable`(不少 CDP 库默认就发),Chrome 会开始把执行上下文和 console 调用通过协议往外报。[rebrowser 的检测](https://bot-detector.rebrowser.net/)利用的正是这点:它构造一个带 getter 的对象,`console.debug` 一下。只要 Runtime.enable 开着,CDP 就要把这个对象序列化传出去,序列化会触发那个 getter;getter 一响,页面当场就知道"有个开了 Runtime.enable 的调试器挂在我身上"。

这条难受在于,它不是你 JS 里的某个属性,你 hook 不掉,它是你选择的驱动方式本身留下的。rebrowser 面板里专门有 `runtimeEnableLeak` 这一项,不少号称隐身的方案就栽在这。

## 真实浏览器为什么省心

回头看开头那几项:真实 Chrome 的 webdriver 本来就是 false,GPU 本来就是 Apple M3,时区、语言、Canvas 本来就出自同一台真机、处处自洽,没有谎可数。chrome-use 连它走的是浏览器扩展加 native messaging,不开 `--remote-debugging-port`,中继路径默认不开 `Runtime.enable`,连协议层那条痕迹都没有。

这不是"伪装得好",是根本没有要伪装的东西。

还有一层容易被忽略:打补丁本身也留痕。你 hook `navigator.webdriver` 的那段代码,覆盖属性时会改掉它的 property descriptor、改掉 `toString` 的返回值,检测方反过来查"这个属性是不是被人动过手脚"就行。你补得越多,能被指认动过手脚的地方越多。真实浏览器没这个负担,因为它没打补丁。

## 自己拉一遍对比

别信我说的,两个浏览器各跑一遍最直接:

- [CreepJS](https://abrahamjuliot.github.io/creepjs/):看它给的 trust score,以及它列出来的 lies 具体是哪几条。
- [rebrowser-bot-detector](https://bot-detector.rebrowser.net/):重点看 `runtimeEnableLeak` 和 `navigatorWebdriver`。

拿一个普通无头自动化跑一遍,再拿你自己天天用的浏览器跑一遍,把两边的 lies 列表并排一看,差在哪儿不用我解释。真要做那些"必须是真人才过得去"的活,登录态、风控敏感的操作,用一个没有谎要圆的浏览器,比用一个靠不停打补丁硬撑的假浏览器,省的事不是一星半点。
