在指纹浏览器的开发历程中,从 JS Hook 转向 C++ 底层修改,是区分“玩具”与“工业级产品”的分水岭。
所有基于Object.defineProperty或Proxy的 JS 注入,本质上都是在应用层贴膏药。风控系统只需通过iframe隔离、toString()检验或Function.prototype原型链比对,就能瞬间让膏药脱落。
真正的隐身,必须从基因层面改造。在 Chromium 的体系中,这意味着你要深入Blink 渲染引擎(负责实现 Web API 的 C++ 类)和V8 绑定层(负责将 C++ 类桥接到 JS 环境的胶水代码),在数据返回给 JS 引擎的最后一道关卡进行拦截。
本文将摒弃水话,直接剖开 Chromium 的源码结构,手把手教你如何在 C++ 层面实现无痕的指纹伪装。
一、 核心认知:Blink 与 V8 的协作真相
在动手改代码前,必须清晰理解 JS 调用navigator.platform时,Chromium 内部发生了什么。
- V8 引擎:只认 ECMAScript 标准。它不知道
window或navigator为何物,只负责执行 JS 代码和管理堆内存。 - Blink 引擎:Web 标准的实现者。它用 C++ 写了
Navigator类,内部有Platform()方法。 - V8 Bindings (绑定层):桥梁。它把 Blink 的
Navigator类“包装”成 V8 认识的v8::Object,把Platform()方法映射为 JS 中的属性 getter。
传统的 JS Hook 逻辑:JS 调用navigator.platform-> V8 执行原有的 Getter -> 返回真值 ->JS 拦截层截获并返回假值。(留下了拦截层的痕迹)
C++ 底层拦截逻辑:JS 调用navigator.platform-> V8 执行绑定的 Getter ->Getter 内部调用的 Blink C++ 方法直接返回假值-> V8 将假值返回给 JS。(整个过程中,JS 环境中没有任何异常代码执行,环境纯净如初)
二、 第一道防线:基于 IDL 的“合法篡改”
Chromium 并没有让开发者手动编写那层复杂的 V8 绑定胶水代码,而是使用了一种名为Web IDL (Interface Definition Language)的接口定义语言。
在third_party/blink/renderer/core/frame/navigator.idl文件中,你可以看到类似这样的定义:
[ Exposed=Window ] interface Navigator : ScriptWrappable { readonly attribute DOMString platform; readonly attribute DOMString userAgent; readonly attribute unsigned long hardwareConcurrency; };编译 Chromium 时,构建系统会根据这个 IDL 文件,自动生成v8_navigator.cc和navigator.h等 V8 绑定代码。
实战:彻底抹除navigator.webdriver
这是最经典、最必须的 C++ 层修改。我们要让webdriver属性不仅仅返回false,而是从 JS 的原型链上彻底消失。
- 打开文件
third_party/blink/renderer/core/frame/navigator.idl。 - 找到
readonly attribute boolean webdriver;这一行。 - 直接删除这一行,或者注释掉。
- 重新编译。
底层逻辑剖析:因为 IDL 中没有了这个定义,自动生成的 V8 绑定代码就不会在Navigator的v8::ObjectTemplate上挂载webdriver的访问器。当风控 JS 尝试读取时,只能得到undefined,且Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver')返回undefined。没有任何 Hook 痕迹,这是最高级别的隐身。
三、 深入敌后:Blink 平台层的 C++ 实现
删除属性很简单,但大多数指纹参数不能删,必须返回合理的伪装值。这时候,我们需要深入 Blink 的 C++ 实现层。
精准定位目录:third_party/blink/renderer/core/frame/
在这个目录下,你会找到navigator.cc和navigator.h。这是 Blink 引擎对Navigator对象的具体实现。
实战 1:伪装navigator.platform和navigator.userAgent
打开navigator.cc,寻找对应的方法实现(不同 Chrome 版本可能略有差异,可能在navigator.cc或单独的文件中):
StringNavigator::platform()const{// 原始代码可能类似这样,返回系统的宏定义// return String(PLATFORM);// 【指纹浏览器修改点】// 我们需要在这里返回配置文件中的预设值// 假设我们有一个全局的指纹配置管理器 FingerprintConfigif(FingerprintConfig::GetInstance()->HasOverride("platform")){returnFingerprintConfig::GetInstance()->GetOverrideString("platform");}returnString(PLATFORM);// 兜底返回真实值}对于userAgent,同理修改:
StringNavigator::userAgent()const{// 原始代码会调用 GetFrame()->Loader().UserAgent();// 我们直接拦截if(FingerprintConfig::GetInstance()->HasOverride("userAgent")){returnFingerprintConfig::GetInstance()->GetOverrideString("userAgent");}returnGetFrame()->Loader().UserAgent();}关键问题:配置从哪里来?
前面说过,Renderer 进程处于沙箱中,无法读取本地文件。FingerprintConfig的数据必须由 Browser 主进程在启动时通过 Mojo IPC 传递进来。你需要实现一个自定义的 Mojo 接口,在 Renderer 进程初始化时接收指纹配置并缓存在内存中。这部分属于 IPC 架构设计,后续专栏会详述,在此只需理解拦截的注入点。
实战 2:伪装navigator.hardwareConcurrency(CPU 核心数)
风控非常看重硬件一致性。你改了 UA 为高端机,但核心数只有 2,必定被秒杀。
unsignedNavigator::hardwareConcurrency()const{// 原始代码调用系统 API 获取真实核心数// base::SysInfo::NumberOfProcessors();// 【指纹浏览器修改点】if(FingerprintConfig::GetInstance()->HasOverride("hardwareConcurrency")){returnFingerprintConfig::GetInstance()->GetOverrideInt("hardwareConcurrency");}returnbase::SysInfo::NumberOfProcessors();}实战 3:伪装navigator.deviceMemory(设备内存)
内存获取涉及系统的特权调用,Blink 原本通过 Mojo 向 Browser 进程查询。
doubleNavigatorDeviceMemory::deviceMemory(Navigator&navigator){// 原始逻辑:// return navigator.GetFrame()->GetBrowserInterfaceBroker()->GetDeviceMemory();// 【指纹浏览器修改点】if(FingerprintConfig::GetInstance()->HasOverride("deviceMemory")){returnFingerprintConfig::GetInstance()->GetOverrideDouble("deviceMemory");}// 兜底逻辑...}为什么这种修改无懈可击?
因为当风控 JS 执行Navigator.prototype.hasOwnProperty('platform')时,V8 绑定代码会根据 Blink 的 C++ 类结构返回true;当读取属性描述符时,V8 绑定层生成的 Getter 是纯正的 C++ 函数指针,其toString()输出为function get platform() { [native code] }。风控找不到任何伪造的破绽。
四、 更深层的绞杀:V8 DOM Wrapper 的硬拦截
修改 Blink 的 C++ 实现是主流做法,但在某些极端对抗场景下,风控会使用更变态的检测手段。
比如,风控会通过 V8 的底层 API 遍历对象的内部字段,或者通过内存布局比对来检测对象是否被替换。在某些情况下,我们甚至需要绕过 Blink,直接在 V8 的 Wrapper 层面做文章。
当 Blink 的 C++Navigator对象被暴露给 V8 时,它会被包装成一个v8::Object。这个包装过程由v8_navigator.cc(自动生成)处理。
高阶思路:替换 V8 的 Accessor
如果你不想修改 Blink 源码(为了减少合并 Chromium 新版本时的冲突),你可以直接修改 V8 绑定生成的代码(虽然不推荐,但作为一种终极武器需要了解)。
在自动生成的v8_navigator.cc中,会有类似设置属性访问器的代码:
// 伪代码,展示自动生成的绑定逻辑voidV8NavigatorPlatformAttributeGetter(v8::Local<v8::String>name,constv8::PropertyCallbackInfo<v8::Value>&info){// 获取 C++ 对象Navigator*impl=V8Navigator::ToImpl(info.Holder());// 调用 Blink 方法V8SetReturnValueString(info,impl->platform(),info.GetIsolate());}// 安装到原型链上instance_template->SetAccessor(v8::String::NewFromUtf8(isolate,"platform"),V8NavigatorPlatformAttributeGetter,...);终极拦截:你可以重新写一个 Getter 函数,并在初始化时替换掉 V8 原本绑定的 Accessor。
// 自定义的无痕 GettervoidCustomFingerprintGetter(v8::Local<v8::String>name,constv8::PropertyCallbackInfo<v8::Value>&info){// 直接从内存读取预设的指纹值,甚至不经过 Blink 的 Navigator 对象v8::Isolate*isolate=info.GetIsolate();std::string fake_value=FingerprintConfig::GetInstance()->Get("platform");info.GetReturnValue().Set(v8::String::NewFromUtf8(isolate,fake_value.c_str()).ToLocalChecked());}// 在 Renderer 进程初始化的早期,执行挂钩voidInstallCustomFingerprintHooks(v8::Local<v8::Context>context){v8::Isolate*isolate=context->GetIsolate();v8::Local<v8::Object>global=context->Global();v8::Local<v8::Object>navigator=global->Get(context,v8::String::NewFromUtf8(isolate,"navigator")).ToLocalChecked().As<v8::Object>();// 强行覆写 V8 对象的属性访问器navigator->SetAccessor(context,v8::String::NewFromUtf8(isolate,"platform").ToLocalChecked(),CustomFingerprintGetter).Check();}避坑警告:这种直接操作 V8 API 的方式极度危险,很容易引发内存泄漏或类型错误崩溃。除非遇到特别变态的风控检测(如检测 Blink C++ 对象的内存地址变化),否则强烈建议使用前文所述的修改 Blink C++ 实现的方式。修改 Blink 实现是顺水推舟,修改 V8 Wrapper 是逆天而行。
五、 隐患与性能:C++ 层拦截的暗礁
在 C++ 层修改并非银弹,如果实现不当,依然会留下蛛丝马迹。
1. 执行时序的漏洞
风控 JS 有可能在页面最早期(如document.createElement阶段)就读取指纹。如果你的 Mojo IPC 配置下发慢于 JS 的执行速度,你的FingerprintConfig::GetInstance()->HasOverride()可能会返回 false,导致浏览器吐出了真实的硬件信息。
解决方案:必须保证在 Renderer 进程创建v8::Context之前,完成 IPC 握手和配置注入。在 Browser 进程启动 Renderer 时,将加密的指纹配置作为命令行参数传入,Renderer 进程启动时直接解析本地命令行参数,彻底斩断网络延迟的隐患。
2. 函数耗时异常
风控会测量navigator.hardwareConcurrency的执行时间。原本调用系统 API 可能需要几毫秒,你改成了纯内存读取,耗时变成了微秒级。这种时间数量级的差异也会成为判定依据。
解决方案:在 C++ 的拦截函数中,加入人工延时,模拟系统调用的耗时特征。
3. 线程安全的噩梦
Blink 的代码执行在主线程上。如果你的FingerprintConfig是异步更新的,在 C++ 拦截层读取配置时,必须使用锁或原子操作。一旦在 Blink 的关键路径上引发数据竞争,浏览器会瞬间崩溃。
六、 结语:无形之刃,最为致命
在 JS 层面的伪装,无论代码写得多么天花乱坠,终究是在风控的规则下玩捉迷藏。而深入 Blink 和 V8 的 C++ 底层修改,则是直接修改了游戏规则的底层物理法则。
当你的navigator.platform是由你亲手改写的 C++ 逻辑返回,当它拥有完美的原生属性描述符、纯正的[native code]标识、合理的执行耗时,风控的 JS 探针将彻底变成瞎子。
掌握了 C++ 层拦截的艺术,你的指纹浏览器才真正拥有了对抗顶级风控的底气。但这仅仅是基础参数的伪装,面对 Canvas、WebGL 这种依赖硬件算力的复杂指纹,我们需要深入更底层的渲染管线,在像素生成的瞬间动刀。