本文基于一个真实的 WPF 自动下单客户端(基于 CefSharp / CEF)的项目实现整理而成。 
本篇主要讲如何实现静态指纹伪装,让页面认为"这就是一台正常 Chrome"——CEF 初始化、渲染进程注入、webdriver / WebGL / deviceMemory 伪装、账号级环境隔离。

目录

引言:自动下单为什么先要"过检测"?

一、先搞清楚:CEF 到底暴露了哪些指纹

二、CEF 全局初始化:把"壳"伪装成 Chrome

2.1 UserAgent 与语言环境

2.2 GPU 与 WebGL 软渲染的矛盾处理

三、渲染进程注入:在页面 JS 跑起来之前抹掉特征

3.1 抹除 window.CefSharp

3.2 伪装 navigator.webdriver(反直觉细节)

3.3 deviceMemory:不伪装

3.4 WebGL 指纹伪装

四、账号级环境隔离:一个账号一个"人"

4.1 账号级 RequestContext

4.2 资源请求过滤:减少暴露面

4.3 弹窗接管

五、验证:伪装到底成没成?

5.1 环境指纹探测页

5.2 F12 开发者工具

5.3 第三方辅助检测工具

小结

附:本篇踩坑点速查


引言:自动下单为什么先要"过检测"?

自动下单客户端的运行形态一般是:宿主程序(WPF/WinForms)嵌入 CEF 浏览器,加载页面,接管登录、下单、支付流程。但电商平台的风控体系会在页面里部署大量检测,只要判定"这不是一台正常浏览器",轻则滑块验证、重则直接封号。

平台能识别的暴露点分三类:

类别 检测手段 例子
结构特征 检测嵌入浏览器注入的全局对象 window.CefSharpCefSharp.BindObjectAsync
环境特征 检测 JS 读取到的硬件/浏览器指纹 navigator.webdriver、WebGL 渲染器、deviceMemory、UA

本篇(一)解决结构特征 + 环境特征,也就是"静态指纹伪装"——让页面里跑的任何 JS,读到的一切都与正常 Chrome 完全一致。

一、先搞清楚:CEF 到底暴露了哪些指纹

动手伪装之前,先把暴露点列全,逐项对照:

  1. window.CefSharp / window.cefSharp:CEF 为宿主与渲染进程通信注入的全局对象,正常浏览器绝不会有。检测方式是 'CefSharp' in window
  2. navigator.webdriver:自动化浏览器的标志性属性。注意正常 Chrome 的原型上也有这个属性,值是 false——所以"删除"反而会暴露('webdriver' in navigator 变成 false)。
  3. WebGL 渲染器字符串:无物理 GPU 时 CEF 回退 SwiftShader 软渲染,UNMASKED_RENDERER_WEBGL 会返回 ANGLE (Google, SwiftShader...),这是自动化环境的强信号。
  4. navigator.deviceMemory:CEF 默认返回 8。但见 3.3 节,这恰好在真实 Chrome 的合法取值内,不伪装
  5. UserAgent:CEF 默认 UA 带 CefSharp/xxx 等后缀,直接暴露身份。
  6. Canvas / Audio / 字体 / 时区等常规指纹:这些主要由渲染内核差异造成,CEF 与 Chrome 同内核,一般问题不大,但需要逐一实测确认(下文有验证方法)。

小技巧:本项目保留了一个 env-probe.html(环境指纹探测页),用一份脚本把 UA、webdriver、插件、WebGL、Canvas、字体、音频、时区等几十项指纹全量输出成 JSON。把它分别在 CEF 和正常浏览器里打开,两份结果一对比,就知道哪些指纹有偏差、伪装到底成没成。


二、CEF 全局初始化:把"壳"伪装成 Chrome

指纹伪装的第一层是 CEF 启动配置。这一步能改底层的,绝不留到 JS 层去补。

2.1 UserAgent 与语言环境

var setting = new CefSettings
{
    Locale = "zh-CN",
    AcceptLanguageList = "zh-CN,zh;q=0.9,en;q=0.8",
    RootCachePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Cache"),
    CachePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Cache", "global"),
    PersistSessionCookies = true,
    BrowserSubprocessPath = Environment.ProcessPath,
    UserAgent = HttpConstants.User_Agent
};

UA 常量必须是完整、真实存在的 Chrome UA:

public const string User_Agent =
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";

要点:

  • 在 CefSettings 层面全局覆盖 UA,比页面里改 navigator.userAgent 更彻底——页面用 getOwnPropertyDescriptor 也探测不出篡改痕迹。
  • AcceptLanguageList 必须与 UA 的语言环境一致,否则浏览器本身语言不一致就穿帮。
  • 把 UA 拆成 SuaWindows NT 10.0; Win64; x64)和 AvChrome/132.0.0.0)两段常量,供网络请求头和 JS 签名拼接复用,保证"HTTP 请求的 UA"与"页面 JS 读到的 UA"完全一致。

2.2 GPU 与 WebGL 软渲染的矛盾处理

这是本项目踩过的坑,值得单列:

在远程桌面 / 虚拟显卡环境下,CEF 的 GPU 进程启动即崩溃(STATUS_BREAKPOINT),而 SwiftShader 又运行在 GPU 进程内,导致 WebGL supported=false。 解法:彻底禁用 GPU 进程,让 WebGL 回退 SwiftShader 软渲染;但 Chromium 132 起默认拒绝软件 WebGL 回退,必须显式放行。

setting.CefCommandLineArgs.Add("disable-gpu", "1");
setting.CefCommandLineArgs.Add("disable-gpu-compositing", "1");
setting.CefCommandLineArgs.Add("enable-unsafe-swiftshader", "1");

// 顺手提高渲染进程 JS 堆上限,避免大页面 OOM
setting.CefCommandLineArgs.Add("js-flags", "--max_old_space_size=2048");
setting.CefCommandLineArgs.Add("enable-media-stream", "1");

注意 disable-gpu 是"双刃剑":它解决了 WebGL 不可用的问题,但软渲染的 GPU 字符串本身就是暴露信号。必须配合后文 3.4 节的 WebGL 指纹伪装,把 ANGLE (Google, SwiftShader...) 替换成真实物理显卡。


三、渲染进程注入:在页面 JS 跑起来之前抹掉特征

CEF 提供一个关键接口:IRenderProcessMessageHandler。它的 OnContextCreated 会在每个 frame(含 iframe)的 V8 上下文创建时回调——这个时机早于页面上任何业务 JS 和风控 JS 的执行。

public class HideCefSharpRenderHandler : IRenderProcessMessageHandler
{
    public void OnContextCreated(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame)
    {
        frame.ExecuteJavaScriptAsync(HideScript);
        frame.ExecuteJavaScriptAsync(WebglSpoofScript);
    }
}

3.1 抹除 window.CefSharp

try { delete window.CefSharp; } catch (e) {}
try { delete window.cefSharp; } catch (e) {}

为什么每个 frame 都要执行?风控组件会在每一个 frame 里检查 window.CefSharp

3.2 伪装 navigator.webdriver(反直觉细节)

这是本项目注释里明确强调过的一个坑:

正常浏览器中 Navigator.prototype 上存在 webdriver 属性且值为 false。若从原型摘除,'webdriver' in navigator 会返回 false,反而成为自动化环境的异常信号。

正确做法是在原型上定义并返回 false

Object.defineProperty(Navigator.prototype, 'webdriver', {
    get: function () { return false; },
    configurable: true
});

这样三个探测角度全部一致:

  • navigator.webdriver → false ✓
  • 'webdriver' in navigator → true(正常浏览器原型上本就有)✓
  • Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver') → 存在 getter(与真实 Chrome 一致)✓

3.3 deviceMemory:不伪装

navigator.deviceMemory 一开始也打算伪装成真实内存,但研究 Chromium 源码后决定不动它,原因有三:

  1. CEF 默认返回的 8 本身就是合法值。在低版本的Chromium 中 ApproximatedDeviceMemory 的算法是:取物理内存 MB 数 → 取整到最近的 2 的幂(基于最高位)→ 除以 1024 转 GB → 超过 8 一律封顶为 8。也就是说,8GB 以上的机器在真实 Chrome 里也只报告 8。CEF 默认返回 8,恰好落在合法取值内。
  2. 伪装需要精确复刻 Chromium 算法:取整到"最近"的 2 的幂(注意是最近而非向下取整,例如 6GB 会取 4 或 8 中更近的一个),再封顶。用 C# 读 GlobalMemoryStatusEx 拿到真实内存后,如果换算逻辑和 Chromium 不一致(比如直接硬编码 32),反而制造出真实 Chrome 不可能出现的值,成为更强的暴露信号。
  3. 封顶值本身有反识别价值:源码里 ReduceDeviceMemory 特性开启时甚至直接硬编码返回 8.08 是最常见的取值,绝大多数正常浏览器都在这个桶里,不伪装反而更"隐身"。

反例参考:安全研究(Castle.io)在真实流量中观察到大量 deviceMemory > 8 的 Chrome 事件,几乎全部来自自动化工具和伪装扩展——高值本身就是异常信号

3.4 WebGL 指纹伪装

软渲染的 ANGLE (Google, SwiftShader...) 是强信号,必须替换。方案:hook getParameter 和 getExtension,把 unmasked vendor/renderer 换成真实物理 GPU。(用前面提到的 env-probe.html获取到真实浏览器的进行替换)

var FAKE_VENDOR   = 'Google Inc. (AMD)';
var FAKE_RENDERER = 'ANGLE (AMD, AMD Radeon(TM) Graphics (0x00001638) Direct3D11 vs_5_0 ps_5_0, D3D11)';
var UNMASKED_VENDOR   = 0x9245;  // UNMASKED_VENDOR_WEBGL
var UNMASKED_RENDERER = 0x9246;  // UNMASKED_RENDERER_WEBGL

ctx.getParameter = function (pname) {
    if (pname === UNMASKED_VENDOR || pname === 0x9245) return FAKE_VENDOR;
    if (pname === UNMASKED_RENDERER || pname === 0x9246) return FAKE_RENDERER;
    return origGetParam.call(this, pname);
};

实现要点(都是实测过的坑):

  1. hook HTMLCanvasElement.prototype.getContext:当请求 webgl / experimental-webgl / webgl2 时,对返回的 context 做 patch。
  2. WebGL1 和 WebGL2 是两条原型链WebGLRenderingContext 和 WebGL2RenderingContext 的 getParameter 必须分别覆盖,缺一不可。
  3. getExtension('WEBGL_debug_renderer_info') 也要 hook:返回的扩展对象上再包一层 getParameter
  4. 不能 bind 到原型上:WebIDL 方法要求 this 是真实 context 实例,bind 到原型会抛 Illegal invocation——所以是在每个 context 实例上包一层闭包。
  5. 轮询重试:V8 上下文刚创建时 DOM 原型可能尚未就绪,用 setInterval 每 100ms 重试,最多 20 次(2 秒):
if (!install()) {
    var tries = 0;
    var timer = setInterval(function () {
        tries++;
        if (install() || tries > 20) clearInterval(timer);
    }, 100);
}

四、账号级环境隔离:一个账号一个"人"

静态指纹是"浏览器像不像 Chrome",而账号隔离解决的是"多个账号会不会被关联"。风控会通过 Cookie 关联、缓存关联、IP 归属 判断多个账号是不是同一个人在操作。

4.1 账号级 RequestContext

每个账号创建独立的 RequestContext 和缓存目录:

var cachePath = Path.Combine(currentDirectory, "Cache", $"TB-{account.AccountId}");
var requestContext = new RequestContext(new RequestContextSettings
{
    CachePath = cachePath,
    PersistSessionCookies = true
});

var browser = new ChromiumWebBrowser("https://www.taobao.com")
{
    RequestContext = requestContext,
    RenderProcessMessageHandler = new HideCefSharpRenderHandler()
};

这样每个账号拥有独立的 Cookie 存储、LocalStorage、IndexedDB,互不串号,也便于按账号单独清理缓存。

4.2 资源请求过滤:减少暴露面

通过 RequestHandler 拦截特定后缀资源,阻止部分风控/埋点脚本加载:

public class FilterResourceFilesRequestHandler : RequestHandler
{
    protected override IResourceRequestHandler GetResourceRequestHandler(...)
    {
        // 去掉 URL 参数后检查后缀
        if (_blockedExtensions.Any(ext => url.EndsWith(ext)))
            return new BlockResourceRequestHandler();
        return base.GetResourceRequestHandler(...);
    }
}

BlockResourceRequestHandler 在 OnBeforeResourceLoad 里直接返回 CefReturnValue.Cancel 取消请求。

4.3 弹窗接管

风控或登录跳转常会弹新窗口。用 LifeSpanHandler 把 OnBeforePopup 的弹窗全部导向当前浏览器加载,保持会话连续性:

protected override bool OnBeforePopup(...)
{
    newBrowser = null;
    chromiumWebBrowser.Load(targetUrl);
    return true;
}

五、验证:伪装到底成没成?

所有伪装做完,必须用数据说话。两个手段:

5.1 环境指纹探测页

用前面提到的 env-probe.html

  1. CEF 里打开 → 点"复制结果"得到 JSON;
  2. 正常 Edge/Chrome 打开 → 再复制一份;
  3. 两份逐字段对比,重点看:webdriverwebdriver_in_protowebgl.unmaskedVendor/Rendererualanguagesplugins

5.2 F12 开发者工具

在 KeyboardHandler 里注册 F12 打开 DevTools,方便在目标页面里手动执行检测脚本复验:

protected override bool OnKeyEvent(...)
{
    var key = (Keys)windowsKeyCode;
    if (key == Keys.F12) chromiumWebBrowser.ShowDevTools();
    else if (key == Keys.F5) browser.Reload(modifiers == CefEventFlags.ControlDown);
    return base.OnKeyEvent(...);
}

5.3 第三方辅助检测工具

自建探测页只覆盖我们关心的字段,难免有盲区。实战里还需要第三方检测站点作为交叉验证——它们往往集成了几十项浏览器自动化检测指标,并提供清晰的"通过/不通过"判定,能在十几秒内告诉我们哪里还漏了。

这类工具里比较有代表性的是 BrowserScan(browserscan.net)。它会从四个维度输出机器人检测结果:

  • Webdrivernavigator.webdriver 原型/值、'webdriver' in navigator、CDP(Chrome DevTools Protocol)是否暴露。
  • User-Agent:UA 字符串是否符合主流浏览器规范、是否带有 CefSharp / HeadlessChrome 等暴露后缀。
  • CDP:检测 window.cdc_*document.__selenium_* 等注入痕迹。
  • Navigatornavigator.pluginsnavigator.languagesnavigator.platformnavigator.hardwareConcurrencynavigator.deviceMemory 等硬件/平台指纹。

使用方法很简单:

  1. 在伪装好的 CEF 里打开 https://www.browserscan.net/zh/bot-detect
  2. 等几秒让它跑完所有检测脚本,看四项是否都是"正常"。
  3. 如果某项标红,再回到对应章节(webdriver → 3.2、UA → 2.1、Navigator → 3.x)检查。

⚠️ 使用注意

  • BrowserScan 本身是基于公开 JS API 探测,不代表真实风控的检测逻辑。通过 BrowserScan 只能说明"静态指纹伪装合格",不能保证实际风控不拦截
  • 它对 WebGL / Canvas / Audio 指纹 只做"能不能取到值"的检测,不判断是否异常。WebGL 的 SwiftShader 暴露点(3.4 节)需要在它专门的 WebGL 检测页里看。

小结

本篇完成了"静态指纹伪装"的全部地基:

  • 启动配置层:全局 UA、语言、禁用 GPU + 放行 SwiftShader;
  • 渲染进程注入层:在页面 JS 之前,删 CefSharp、伪装 webdriver、WebGL;
  • 网络隔离层:账号级 RequestContext、资源过滤、弹窗接管;
  • 验证手段:指纹探测页 + DevTools。

但到这里,页面的风控 JS 已经"看不出这辆车的牌子不对"了——可它还能看出"开车的人是不是机器"。事件是不是系统级真实输入、鼠标轨迹像不像人手、滑块验证过不过得去,就是下一篇的内容。


附:本篇踩坑点速查

  • WebGL 一启动就崩disable-gpu 后 SwiftShader 也被禁了 → 必须加 enable-unsafe-swiftshader
  • 删 webdriver 反而暴露:正常浏览器原型上有该属性 → 应 defineProperty 返回 false,而非 delete。
  • Illegal invocation:WebGL 方法不能 bind 到原型 → 必须在真实 context 实例上包闭包。
  • iframe 漏注入OnContextCreated 对每个 frame 都触发 → 伪装脚本必须覆盖 iframe。
  • 指纹共用风险:WebGL 字符串必须取自真实机器,否则多账号共用一套假指纹会被关联封号。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐