本文基于一个真实的 WPF 自动下单客户端基于 CefSharp / CEF的项目实现整理而成。本篇主要讲如何实现静态指纹伪装让页面认为这就是一台正常 Chrome——CEF 初始化、渲染进程注入、webdriver / WebGL / deviceMemory 伪装、账号级环境隔离。目录引言自动下单为什么先要过检测一、先搞清楚CEF 到底暴露了哪些指纹二、CEF 全局初始化把壳伪装成 Chrome2.1 UserAgent 与语言环境2.2 GPU 与 WebGL 软渲染的矛盾处理三、渲染进程注入在页面 JS 跑起来之前抹掉特征3.1 抹除 window.CefSharp3.2 伪装 navigator.webdriver反直觉细节3.3 deviceMemory不伪装3.4 WebGL 指纹伪装四、账号级环境隔离一个账号一个人4.1 账号级 RequestContext4.2 资源请求过滤减少暴露面4.3 弹窗接管五、验证伪装到底成没成5.1 环境指纹探测页5.2 F12 开发者工具5.3 第三方辅助检测工具小结附本篇踩坑点速查引言自动下单为什么先要过检测自动下单客户端的运行形态一般是宿主程序WPF/WinForms嵌入 CEF 浏览器加载页面接管登录、下单、支付流程。但电商平台的风控体系会在页面里部署大量检测只要判定这不是一台正常浏览器轻则滑块验证、重则直接封号。平台能识别的暴露点分三类类别检测手段例子结构特征检测嵌入浏览器注入的全局对象window.CefSharp、CefSharp.BindObjectAsync环境特征检测 JS 读取到的硬件/浏览器指纹navigator.webdriver、WebGL 渲染器、deviceMemory、UA本篇一解决结构特征 环境特征也就是静态指纹伪装——让页面里跑的任何 JS读到的一切都与正常 Chrome 完全一致。一、先搞清楚CEF 到底暴露了哪些指纹动手伪装之前先把暴露点列全逐项对照window.CefSharp/window.cefSharpCEF 为宿主与渲染进程通信注入的全局对象正常浏览器绝不会有。检测方式是CefSharp in window。navigator.webdriver自动化浏览器的标志性属性。注意正常 Chrome 的原型上也有这个属性值是false——所以删除反而会暴露webdriver in navigator变成false。WebGL 渲染器字符串无物理 GPU 时 CEF 回退 SwiftShader 软渲染UNMASKED_RENDERER_WEBGL会返回ANGLE (Google, SwiftShader...)这是自动化环境的强信号。navigator.deviceMemoryCEF 默认返回8。但见 3.3 节这恰好在真实 Chrome 的合法取值内不伪装。UserAgentCEF 默认 UA 带CefSharp/xxx等后缀直接暴露身份。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;q0.9,en;q0.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 UApublic 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 supportedfalse。 解法彻底禁用 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_size2048); 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.CefSharptry { 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反而成为自动化环境的异常信号。正确做法是在原型上定义并返回falseObject.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.3deviceMemory不伪装navigator.deviceMemory一开始也打算伪装成真实内存但研究 Chromium 源码后决定不动它原因有三CEF 默认返回的8本身就是合法值。在低版本的Chromium 中ApproximatedDeviceMemory的算法是取物理内存 MB 数 → 取整到最近的 2 的幂基于最高位→ 除以 1024 转 GB →超过 8 一律封顶为 8。也就是说8GB 以上的机器在真实 Chrome 里也只报告 8。CEF 默认返回 8恰好落在合法取值内。伪装需要精确复刻 Chromium 算法取整到最近的 2 的幂注意是最近而非向下取整例如 6GB 会取 4 或 8 中更近的一个再封顶。用 C# 读GlobalMemoryStatusEx拿到真实内存后如果换算逻辑和 Chromium 不一致比如直接硬编码 32反而制造出真实 Chrome 不可能出现的值成为更强的暴露信号。封顶值本身有反识别价值源码里ReduceDeviceMemory特性开启时甚至直接硬编码返回8.0。8是最常见的取值绝大多数正常浏览器都在这个桶里不伪装反而更隐身。反例参考安全研究Castle.io在真实流量中观察到大量deviceMemory 8的 Chrome 事件几乎全部来自自动化工具和伪装扩展——高值本身就是异常信号。3.4 WebGL 指纹伪装软渲染的ANGLE (Google, SwiftShader...)是强信号必须替换。方案hookgetParameter和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); };实现要点都是实测过的坑hookHTMLCanvasElement.prototype.getContext当请求webgl / experimental-webgl / webgl2时对返回的 context 做 patch。WebGL1 和 WebGL2 是两条原型链WebGLRenderingContext和WebGL2RenderingContext的getParameter必须分别覆盖缺一不可。getExtension(WEBGL_debug_renderer_info)也要 hook返回的扩展对象上再包一层getParameter。不能 bind 到原型上WebIDL 方法要求this是真实 context 实例bind 到原型会抛Illegal invocation——所以是在每个 context 实例上包一层闭包。轮询重试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.htmlCEF 里打开 → 点复制结果得到 JSON正常 Edge/Chrome 打开 → 再复制一份两份逐字段对比重点看webdriver、webdriver_in_proto、webgl.unmaskedVendor/Renderer、ua、languages、plugins。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 第三方辅助检测工具自建探测页只覆盖我们关心的字段难免有盲区。实战里还需要第三方检测站点作为交叉验证——它们往往集成了几十项浏览器自动化检测指标并提供清晰的通过/不通过判定能在十几秒内告诉我们哪里还漏了。这类工具里比较有代表性的是 BrowserScanbrowserscan.net。它会从四个维度输出机器人检测结果Webdrivernavigator.webdriver原型/值、webdriver in navigator、CDPChrome DevTools Protocol是否暴露。User-AgentUA 字符串是否符合主流浏览器规范、是否带有 CefSharp / HeadlessChrome 等暴露后缀。CDP检测window.cdc_*、document.__selenium_*等注入痕迹。Navigatornavigator.plugins、navigator.languages、navigator.platform、navigator.hardwareConcurrency、navigator.deviceMemory等硬件/平台指纹。使用方法很简单在伪装好的 CEF 里打开https://www.browserscan.net/zh/bot-detect。等几秒让它跑完所有检测脚本看四项是否都是正常。如果某项标红再回到对应章节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 invocationWebGL 方法不能 bind 到原型 → 必须在真实 context 实例上包闭包。iframe 漏注入OnContextCreated对每个 frame 都触发 → 伪装脚本必须覆盖 iframe。指纹共用风险WebGL 字符串必须取自真实机器否则多账号共用一套假指纹会被关联封号。