在指纹浏览器的开发历程中,从 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 文件中,你可以看到类似这样的定义:
webidl
[
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 或单独的文件中):
cpp
String Navigator::platform() const {
// 原始代码可能类似这样,返回系统的宏定义
// return String(PLATFORM);
// 【指纹浏览器修改点】
// 我们需要在这里返回配置文件中的预设值
// 假设我们有一个全局的指纹配置管理器 FingerprintConfig
if (FingerprintConfig::GetInstance()->HasOverride("platform")) {
return FingerprintConfig::GetInstance()->GetOverrideString("platform");
}
return String(PLATFORM); // 兜底返回真实值
}
对于 userAgent,同理修改:
cpp
String Navigator::userAgent() const {
// 原始代码会调用 GetFrame()->Loader().UserAgent();
// 我们直接拦截
if (FingerprintConfig::GetInstance()->HasOverride("userAgent")) {
return FingerprintConfig::GetInstance()->GetOverrideString("userAgent");
}
return GetFrame()->Loader().UserAgent();
}
关键问题:配置从哪里来?
前面说过,Renderer 进程处于沙箱中,无法读取本地文件。FingerprintConfig 的数据必须由 Browser 主进程在启动时通过 Mojo IPC 传递进来。你需要实现一个自定义的 Mojo 接口,在 Renderer 进程初始化时接收指纹配置并缓存在内存中。这部分属于 IPC 架构设计,后续专栏会详述,在此只需理解拦截的注入点。
实战 2:伪装 navigator.hardwareConcurrency (CPU 核心数)
风控非常看重硬件一致性。你改了 UA 为高端机,但核心数只有 2,必定被秒杀。
cpp
unsigned Navigator::hardwareConcurrency() const {
// 原始代码调用系统 API 获取真实核心数
// base::SysInfo::NumberOfProcessors();
// 【指纹浏览器修改点】
if (FingerprintConfig::GetInstance()->HasOverride("hardwareConcurrency")) {
return FingerprintConfig::GetInstance()->GetOverrideInt("hardwareConcurrency");
}
return base::SysInfo::NumberOfProcessors();
}
实战 3:伪装 navigator.deviceMemory (设备内存)
内存获取涉及系统的特权调用,Blink 原本通过 Mojo 向 Browser 进程查询。
cpp
double NavigatorDeviceMemory::deviceMemory(Navigator& navigator) {
// 原始逻辑:
// return navigator.GetFrame()->GetBrowserInterfaceBroker()->GetDeviceMemory();
// 【指纹浏览器修改点】
if (FingerprintConfig::GetInstance()->HasOverride("deviceMemory")) {
return FingerprintConfig::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 中,会有类似设置属性访问器的代码:
cpp
// 伪代码,展示自动生成的绑定逻辑
void V8NavigatorPlatformAttributeGetter(v8::Local<v8::String> name, const v8::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。
cpp
// 自定义的无痕 Getter
void CustomFingerprintGetter(v8::Local<v8::String> name, const v8::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 进程初始化的早期,执行挂钩
void InstallCustomFingerprintHooks(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 这种依赖硬件算力的复杂指纹,我们需要深入更底层的渲染管线,在像素生成的瞬间动刀。