浏览器硬件参数欺骗:CPU核心数、内存大小、设备像素比的精准伪造

在指纹浏览器的对抗中,当我们解决了 Canvas、WebGL、Audio 等高维度的渲染指纹后,往往会栽在几个最基础的硬件参数上:navigator.hardwareConcurrency(CPU 核心数)、navigator.deviceMemory(设备内存)和 window.devicePixelRatio(设备像素比)。

风控系统对这三个参数的检测逻辑极其简单粗暴:逻辑一致性校验 。如果你声称自己是顶配的 MacBook Pro,但 hardwareConcurrency 却返回 2deviceMemory 返回 2,风控不需要懂任何底层渲染原理,一秒钟就能用规则引擎将你封杀。更致命的是,这三个参数不仅暴露了硬件本身,它们还是多维度指纹关联的锚点。一个真实的用户,其 ClientRects 的渲染精度必然受到 DPR 的制约,其 WebGL 的渲染耗时必然与 CPU 核心数相关。

劣质指纹浏览器往往在 JS 层覆写这几个属性,却不知现代风控通过 iframe 隔离、Worker 线程探测,能轻易撕开 JS Hook 的伪装。

本文将摒弃水话,直插 Chromium 的 C++ 内核,在 Blink 引擎的数据源头,实现这三个硬件参数的精准、无痕、全上下文伪造。

一、 核心痛点:为什么 JS 层覆写必死?

1. Web Worker 的幽灵

navigator.hardwareConcurrency 最常见的探测手段不是在主线程读取,而是在 Web Worker 中读取。

主线程的 JS 环境你可以随意 Hook,但 Worker 运行在独立的线程,拥有独立的 V8 上下文。你无法通过主线程的代码去污染 Worker 内部的 NavigatorPrototype。风控只需一行代码:

javascript 复制代码
let worker = new Worker('data:text/javascript,self.postMessage(navigator.hardwareConcurrency)');

你的伪装瞬间土崩瓦解。

2. 设备像素比的渲染脱节

如果你在 JS 层拦截 window.devicePixelRatio 返回 2.0,但底层渲染引擎(Blink)依然认为当前设备的 DPR 是 1.0,会导致灾难性的后果:

  • 媒体查询穿透window.matchMedia('(min-resolution: 2dppx)') 返回 false
  • Canvas 模糊异常:Canvas 按照 1x 渲染,你却告诉 JS 它是 2x,风控通过比对 Canvas 的物理像素和逻辑像素,直接判定环境伪造。

3. 内存探测的时序陷阱

deviceMemory 是一个只读属性,且风控会测量其读取耗时。JS 层的 Object.defineProperty 会引入微秒级的延迟,而真实的底层 C++ 属性读取是纳秒级的。

唯一的解法:斩断 JS 层,深入 Blink 引擎的 C++ 实现,从源头上替换操作系统返回的真实数据。

二、 斩断 CPU 核心数:全上下文物理级伪造

navigator.hardwareConcurrency 反映的是操作系统的逻辑 CPU 核心数。Chromium 获取这个值的逻辑是:在主进程调用系统 API(如 Windows 的 GetSystemInfo,Mac 的 sysctlbyname),然后通过 Mojo IPC 传递给渲染进程缓存。

我们要做的,是在渲染进程(Blink)收到这个缓存值之前,将其替换。

精准坐标third_party/blink/renderer/core/frame/navigator.cc

找到 hardwareConcurrency 的 C++ 实现:

cpp 复制代码
unsigned Navigator::hardwareConcurrency() const {
  // 原始逻辑:返回从 Browser 进程 IPC 传来的真实核心数
  // return GetFrame()->Loader().GetServiceWorkerContainerHost()->hardware_concurrency();
  
  // 【指纹浏览器拦截点】
  const auto& fp_config = FingerprintConfig::GetInstance();
  if (fp_config->HasOverride("hardwareConcurrency")) {
    return fp_config->GetUnsignedInt("hardwareConcurrency"); // 返回预设的核心数
  }
  return base::SysInfo::NumberOfProcessors(); // 兜底
}

2. Web Worker 上下文拦截(致命一击)

这是区分工业级指纹浏览器和玩具的分水岭。Worker 中也有 Navigator 对象,但它不是 blink::Navigator 类,而是 blink::WorkerNavigator 类。

精准坐标third_party/blink/renderer/core/workers/worker_navigator.cc

cpp 复制代码
unsigned WorkerNavigator::hardwareConcurrency() const {
  // 必须在这里做同样的拦截!
  const auto& fp_config = FingerprintConfig::GetInstance();
  if (fp_config->HasOverride("hardwareConcurrency")) {
    return fp_config->GetUnsignedInt("hardwareConcurrency");
  }
  return base::SysInfo::NumberOfProcessors();
}

避坑实录 :如果你只改了主线程的 Navigator,没改 WorkerNavigator,风控只要对比主线程和 Worker 线程返回的核心数是否一致,就能立刻识破你的双轨制伪装。

三、 斩断设备像素比:渲染管线的深度联动

DPR(Device Pixel Ratio)是物理像素与逻辑像素的比值。它不仅是一个 JS 变量,更是整个 Chromium 渲染管线的基石。

如果你只改了 window.devicePixelRatio 的返回值,而没有改底层排版引擎的缩放因子,你的浏览器在高清屏上会呈现出一种诡异的"逻辑自洽但视觉错乱"的状态。

1. JS 属性与媒体查询的双重堵截

精准坐标third_party/blink/renderer/core/frame/screen.cc (或 window.cc 视 Chromium 版本而定)

cpp 复制代码
double Window::devicePixelRatio() const {
  // 原始逻辑:返回 Frame 的缩放因子
  // return GetFrame()->DevicePixelRatio();
  
  // 【指纹浏览器拦截点】
  const auto& fp_config = FingerprintConfig::GetInstance();
  if (fp_config->HasOverride("devicePixelRatio")) {
    return fp_config->GetDouble("devicePixelRatio");
  }
  return 1.0;
}

仅仅拦截 JS 属性是不够的,风控常用 CSS 媒体查询探测 DPR:

@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { ... }

媒体查询的求值不经过 JS,而是在 Blink 的 CSS 解析器中直接计算的。

精准坐标third_party/blink/renderer/core/css/media_query_evaluator.cc

在评估 device-pixel-ratio 特征时,Chromium 会调用 GetFrame()->DevicePixelRatio()。因此,最优雅的做法是直接在 Frame 层面修改 DevicePixelRatio 的源头

终极拦截点third_party/blink/renderer/core/frame/local_frame.cc

cpp 复制代码
float LocalFrame::DevicePixelRatio() const {
  // 【源头拦截】
  const auto& fp_config = FingerprintConfig::GetInstance();
  if (fp_config->HasOverride("devicePixelRatio")) {
    return static_cast<float>(fp_config->GetDouble("devicePixelRatio"));
  }
  // 兜底:真实计算
  return GetPage()->DeviceScaleFactorDeferred();
}

效果 :一旦在这里修改,从 JS 的 window.devicePixelRatio,到 CSS 媒体查询,再到 Blink 排版引擎的布局计算,所有模块获取的 DPR 都将强制对齐为你预设的值,实现了全局逻辑自洽。

2. Canvas 渲染分辨率的联动(防模糊关键)

如果你将 DPR 改为 2.0,Canvas 必须以 2 倍物理分辨率渲染,再缩放回逻辑尺寸,否则 Canvas 画面会模糊,风控通过像素比对即可发现物理分辨率与声明 DPR 不符。

这不需要我们在 C++ 层额外处理,因为 Blink 的 Canvas 绑定层在分配缓冲区时,会自动调用 DevicePixelRatio() 来乘以宽高。只要我们在 LocalFrame 层面成功伪造了 DPR,Canvas 的渲染分辨率也会自动适配,这是 Chromium 架构设计的精妙之处。

四、 斩断设备内存:跨进程的数据篡改

navigator.deviceMemory 是一个粗粒度的值(0.25, 0.5, 1, 2, 4, 8),它反映的是系统可用内存。

这个值的获取流程极度危险:它只能 在 Browser 进程中通过系统 API 获取,然后通过 Mojo 发送给 Renderer 进程。Renderer 进程处于沙箱中,根本无权查询系统内存。

如果在 JS 层 Hook,风控在 Worker 中读取就会穿帮。如果在 Blink 层拦截,我们需要处理繁琐的 Mojo 回调。

最暴力也最稳妥的解法:直接修改 Browser 进程的系统 API 返回值

我们不需要修改复杂的 IPC 逻辑,只需要在 Browser 进程获取真实内存的源头动刀。

精准坐标base/system/sys_info.cc

Chromium 获取系统内存的核心函数是 AmountOfPhysicalMemoryImpl

cpp 复制代码
// static
int64_t SysInfo::AmountOfPhysicalMemoryImpl() {
  // 【指纹浏览器拦截点 - Browser 进程】
  const auto& fp_config = FingerprintConfig::GetInstance();
  if (fp_config->HasOverride("deviceMemoryGB")) {
    // 将 GB 转换为 Bytes 返回
    return static_cast<int64_t>(fp_config->GetDouble("deviceMemoryGB")) * 1024 * 1024 * 1024;
  }
  
  // 兜底:调用真实 OS API
#if defined(OS_WIN)
  ...
#elif defined(OS_MACOSX)
  ...
#endif
}

底层逻辑剖析

  1. Browser 进程启动时,会调用 AmountOfPhysicalMemoryImpl() 获取真实内存。
  2. 我们在这里拦截,返回预设的内存大小(如 8GB 对应的字节数)。
  3. Browser 进程将这个假数据通过正常的 Mojo 通道发给 Renderer 进程。
  4. Renderer 进程的 Blink 引擎和 Worker 线程拿到这个假数据,按照标准算法截断(如 8GB 截断为 8),返回给 JS 的 navigator.deviceMemory
    这种做法的威力
    它不仅骗过了 JS 层,甚至骗过了 Chromium 自身的内存管理模块。浏览器内部的缓存分配策略、OOM 杀手机制,都会基于这个伪造的内存值进行计算,使得整个运行时的内存压力特征完全符合你声明的配置,彻底杜绝了行为特征与声明参数不一致的风险。

五、 避坑实录:硬件伪造的三个致命悖论

在实战中,单点伪造很容易,难的是不产生逻辑悖论

1. 核心数与并发能力的悖论

你声称 hardwareConcurrency 为 16,但风控 JS 启动了 16 个密集计算的 Web Worker,结果由于宿主机真实核心只有 4 个,导致执行时间比真实的 16 核机器慢了 3 倍。

破局 :指纹配置必须与宿主机性能相匹配。如果宿主机是 4 核,你可以伪装成 4 核或 8 核,绝不能伪装成 32 核。高级的风控会通过 Performance.now() 测量多线程并发任务的耗时,反推真实的硬件算力。

2. 内存大小与 OOM 崩溃的悖论

你声称 deviceMemory 为 8,但风控 JS 尝试分配 6GB 的 ArrayBuffer(真实机器只有 4GB 内存)。此时由于底层 OS 内存不足,浏览器会直接崩溃。

破局:不要过分夸大内存容量。将伪装值设定在宿主机真实内存的 50% - 100% 之间是最安全的策略。

3. DPR 与屏幕分辨率的悖论

你声称 devicePixelRatio 为 2,但 screen.width 返回 1920,window.innerWidth 也返回 1920。在真实的 2x 设备上,如果物理分辨率是 3840,逻辑宽度应该是 1920。如果你的逻辑宽度占满了物理宽度,说明你根本不是 2x 设备。

破局 :DPR、Screen 分辨率、Viewport 宽高必须作为一组强关联的配置同时下发。修改 DPR 时,必须同步调整 Screen 类的宽高返回值,确保 物理像素 = 逻辑像素 * DPR 的绝对数学等式成立。

六、 结语:微观世界的宏观法则

硬件参数的伪造,看似只是修改几个数字,实则是在重构浏览器运行时的物理法则。

从 CPU 的算力分配,到内存的边界,再到像素的密度,这些参数构成了浏览器存在的物质基础。当我们深入 Chromium 的 C++ 内核,在 LocalFrameSysInfoWorkerNavigator 这些底层枢纽中偷天换日时,我们创造的不只是一个骗过风控的脚本,而是一个逻辑自洽、行为一致的赛博硬件幽灵。

至此,浏览器本地的环境伪装已至化境。然而,浏览器从来不是一座孤岛,当它向服务器发出第一个请求时,网络层的较量才刚刚开始。TLS 指纹、HTTP/2 帧特征、QUIC 协议,这些更底层的网络协议级指纹,将是是更严格的风控。

相关推荐
数据知道2 小时前
字体与排版防线:ClientRects 与系统字体枚举的底层拦截与伪造
javascript·数据采集·指纹浏览器·风控·浏览器指纹
数据知道2 小时前
视觉伪装(上):Canvas 指纹生成原理与 Skia 图形库底层注入噪声
开发语言·javascript·ecmascript·数据采集·指纹浏览器
捷米特网关模块通讯3 小时前
老旧松下PLC组网难?串口转以太网模块实现ModbusTCP服务器无缝对接
数据采集·以太网模块·工业自动化·智能网关·总线协议·松下plc
如烟花的信页3 小时前
加速乐cookie逆向分析
javascript·爬虫·python·js逆向
捷米特网关模块通讯4 小时前
ProfiNet 转EtherNet/IP协议转换网关 实现工业模块称重设备联网改造
数据采集·工业自动化·profinet·网关模块·总线协议·称重仪表
xmtxz4 小时前
Burp Suite、爬虫、目录扫描工具实操深度总结
爬虫
yijianace5 小时前
Python爬虫实战:BooksToScrape 多线程爬取与图片下载
开发语言·爬虫·python
数据知道7 小时前
浏览器指纹开发:AudioContext 指纹的底层计算逻辑与偏移注入
数据采集·指纹浏览器
深蓝电商API7 小时前
Playwright 多浏览器并发:同时操控 100 个 Chrome 实例
爬虫·playwright