CEF浏览器过机器人检测(一)
本文基于一个真实的 WPF 自动下单客户端(基于 CefSharp / CEF)的项目实现整理而成。
本篇主要讲如何实现静态指纹伪装,让页面认为"这就是一台正常 Chrome"——CEF 初始化、渲染进程注入、webdriver / WebGL / deviceMemory 伪装、账号级环境隔离。
目录
引言:自动下单为什么先要"过检测"?
自动下单客户端的运行形态一般是:宿主程序(WPF/WinForms)嵌入 CEF 浏览器,加载页面,接管登录、下单、支付流程。但电商平台的风控体系会在页面里部署大量检测,只要判定"这不是一台正常浏览器",轻则滑块验证、重则直接封号。
平台能识别的暴露点分三类:
| 类别 | 检测手段 | 例子 |
|---|---|---|
| 结构特征 | 检测嵌入浏览器注入的全局对象 | window.CefSharp、CefSharp.BindObjectAsync |
| 环境特征 | 检测 JS 读取到的硬件/浏览器指纹 | navigator.webdriver、WebGL 渲染器、deviceMemory、UA |
本篇(一)解决结构特征 + 环境特征,也就是"静态指纹伪装"——让页面里跑的任何 JS,读到的一切都与正常 Chrome 完全一致。
一、先搞清楚:CEF 到底暴露了哪些指纹
动手伪装之前,先把暴露点列全,逐项对照:
window.CefSharp/window.cefSharp:CEF 为宿主与渲染进程通信注入的全局对象,正常浏览器绝不会有。检测方式是'CefSharp' in window。navigator.webdriver:自动化浏览器的标志性属性。注意正常 Chrome 的原型上也有这个属性,值是false——所以"删除"反而会暴露('webdriver' in navigator变成false)。- WebGL 渲染器字符串:无物理 GPU 时 CEF 回退 SwiftShader 软渲染,
UNMASKED_RENDERER_WEBGL会返回ANGLE (Google, SwiftShader...),这是自动化环境的强信号。 navigator.deviceMemory:CEF 默认返回8。但见 3.3 节,这恰好在真实 Chrome 的合法取值内,不伪装。- UserAgent:CEF 默认 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;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 拆成
Sua(Windows NT 10.0; Win64; x64)和Av(Chrome/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 源码后决定不动它,原因有三:
- 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...) 是强信号,必须替换。方案: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);
};
实现要点(都是实测过的坑):
- hook
HTMLCanvasElement.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.html:
- CEF 里打开 → 点"复制结果"得到 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 第三方辅助检测工具
自建探测页只覆盖我们关心的字段,难免有盲区。实战里还需要第三方检测站点作为交叉验证——它们往往集成了几十项浏览器自动化检测指标,并提供清晰的"通过/不通过"判定,能在十几秒内告诉我们哪里还漏了。
这类工具里比较有代表性的是 BrowserScan(browserscan.net)。它会从四个维度输出机器人检测结果:
- Webdriver:
navigator.webdriver原型/值、'webdriver' in navigator、CDP(Chrome DevTools Protocol)是否暴露。 - User-Agent:UA 字符串是否符合主流浏览器规范、是否带有 CefSharp / HeadlessChrome 等暴露后缀。
- CDP:检测
window.cdc_*、document.__selenium_*等注入痕迹。 - Navigator:
navigator.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 invocation:WebGL 方法不能 bind 到原型 → 必须在真实 context 实例上包闭包。- iframe 漏注入:
OnContextCreated对每个 frame 都触发 → 伪装脚本必须覆盖 iframe。 - 指纹共用风险:WebGL 字符串必须取自真实机器,否则多账号共用一套假指纹会被关联封号。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)