尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

CreepJS Canvas 2D 反检测实战:逐项触发与绕过

CreepJS Canvas 2D 反检测实战:逐项触发与绕过
📅 发布时间:2026/7/18 3:22:55

上一篇我们从源码角度梳理了 CreepJS Canvas 2D 模块的 6 道检测防线。本文进入实操:用一个浏览器扩展逐项触发检测,观察"亮灯"效果;再通过修改 Firefox 源码,在 C++ 层实现真正透明的指纹扰动,让所有检测静默通过。

前置文章:《Creepjs学习–Canvas 2D 指纹采集》


CreepJS Canvas 2D 的 6 道检测防线

先回顾一下 CreepJS 在 Canvas 2D 上设置了哪些检测。理解这张表是后续所有攻防的基础:

#检测名称原理被抓到会怎样
1lieProps(API 谎言预检)检查toString()是否返回[native code]卡片rejected/ hashlies
2getPixelMods(像素读写一致性)写→读→镜像→再读,对比两次结果xx% rgba noise+ 差异圆
3clearRect 后残像素清空画布后 getImageData 仍有非零像素pixel data modified+lied
4低熵已知像素库(2×2)固定绘制 → 像素串比对 Blink/Gecko/WebKit 白名单LowerEntropy.CANVAS/bold-fail
5TextMetrics 浮点噪声measureText('')包围盒出现小数metric noise detected+lied
6上下文不可用getContext('2d')返回 null整项undefined/ UI BLOCKED

这 6 项从上到下,覆盖了从"有没有人动过 API"到"像素数据对不对"再到"API 能不能用"的完整链条。


思路总览

为什么 JS 层 hook 必然被抓?

CreepJS 的第一道防线(检测1)会检查关键 API 的Function.prototype.toString()是否返回[native code]。只要你在 JS 层做了:

CanvasRenderingContext2D.prototype.getImageData=function(...){...}

无论你是否改了数据,toString()立刻暴露。这意味着——JS 层任何 hook 都会连带触发检测1。

浏览器层改动为什么能透明通过?

在 Firefox 的 C++ 源码中修改像素 buffer,JS 调用的仍然是真正的 native 函数:

  • toString()正常返回[native code]→ 检测1 通过
  • getImageData返回原始像素 → 检测2/3/4 通过
  • measureText没被动过 → 检测5 通过
  • getContext没被动过 → 检测6 通过

唯一变化的是toDataURL()输出的图片编码(指纹来源),而 CreepJS 的一致性校验全走getImageData——两条路径互不干扰。

核心设计原则

噪声只加在不可逆路径(图片编码),保持可逆路径(像素读回)干净。

实验环境

组件说明
Firefox 源码F:\mc(Gecko 主干)
触发扩展creapjs_plug/inject.js,注入页面主世界
检测页面CreepJS 官方演示
构建命令mach build或mach build dom/canvas(增量编译 canvas 模块)

触发扩展通过在页面加载前注入 JS,hook Canvas 相关 API 来模拟各种篡改场景。每次只开启一项,观察 CreepJS 对应的检测反应。


第1项:lieProps(API 谎言预检)

触发

扩展注入以下代码,对getImageData和toDataURL做了一层透明包装——数据完全没改,只是替换了函数引用:

constctxProto=CanvasRenderingContext2D.prototypeconstorigGetImageData=ctxProto.getImageDataconstorigToDataURL=HTMLCanvasElement.prototype.toDataURL// 纯 pass-through,不改任何数据ctxProto.getImageData=functionpatchedGetImageData(...args){returnorigGetImageData.apply(this,args)}HTMLCanvasElement.prototype.toDataURL=functionpatchedToDataURL(...args){returnorigToDataURL.apply(this,args)}

就这么简单。没改一个像素,没动一个返回值。

但 CreepJS 立刻报警了——因为lieProps检查的不是数据对不对,而是函数本身有没有被替换:

// CreepJS 内部的检测逻辑(简化)CanvasRenderingContext2D.prototype.getImageData.toString()// 原生函数返回: "function getImageData() { [native code] }"// 被 hook 后返回: "function patchedGetImageData(...args) { ... }"

预期效果:卡片显示rejected/ hashlies。

效果如图,卡片变为粉色背景(rejected),hashb413eb9e:

绕过

不需要任何 Firefox 代码改动。卸载扩展(或关闭注入),刷新页面即可——没有人动过 prototype,toString()自然返回[native code]。

卸载扩展后,卡片恢复白色背景,hash 变为c0421881(正常状态):

小结

这项检测的存在意义:它是所有后续检测的"前哨"。如果一个反指纹方案连 lieProps 都过不了,后面的检测压根不需要看。这也是为什么浏览器层改动是正道——从根上消除了 JS 可观测的痕迹。


第2项:像素读写一致性(getPixelMods)

检测原理

CreepJS 的getPixelMods()做了一个巧妙的一致性测试:

8×8 画布:随机填充像素 ↓ getImageData 读出 镜像画布:用读出的颜色再画一遍 ↓ getImageData 再读 对比两次 → 不一致则判定有噪声注入

如果有人 hook 了getImageData并注入随机噪声,两次读取同一 buffer 会返回不同结果——暴露无遗。

触发

扩展注入以下代码,hookgetImageData,在每次读取时对非零像素注入噪声:

constctxProto=CanvasRenderingContext2D.prototypeconstorigGetImageData=ctxProto.getImageData ctxProto.getImageData=functionpatchedGetImageData(...args){constimageData=origGetImageData.apply(this,args)constdata=imageData&&imageData.dataif(data&&data.length>=4){// 检查是否有非零像素(避免对空画布误注入)lethasNonZero=falsefor(leti=0;i<data.length;i++){if(data[i]!==0){hasNonZero=true;break}}if(hasNonZero){// 每隔4个像素,对第一个字节(B通道) +1for(leti=0;i<data.length;i+=16){data[i]=(data[i]+1)%256}}}returnimageData}

CreepJS 的getPixelMods先随机写入像素,然后读出来。因为我们的 hook 在每次读取时都注入不同偏移,"写→读→镜像→再读"两次结果必然不一致。

预期效果:卡片显示rendering: 16% rgba noise+ 差异圆可视化图。同时 lieProps(检测1)连带亮灯——因为 hook 了原型方法。

效果如图,卡片粉色背景,显示rendering: 16% rgba noise ●,hash 变为8fc3119a,data 变为23ea1c0c:

为什么 要修改FireFox源码?

你可能想到"用确定性噪声"——每次对同一 buffer 加同样的偏移,这样两次读结果一致。但问题在于:

  1. CreepJS 先写入随机像素,然后用读出的结果再画一遍。如果你的 hook 改了第一次读的值,第二次画的内容就和原始不同了。
  2. 无论怎么设计,只要 hook 了getImageData,lieProps 都会先亮灯。
  3. CreepJS 还有 clearRect 后的二次确认(检测3)交叉验证。

绕过:修改 Firefox 源码

核心洞察:CreepJS 的指纹 hash 来自toDataURL(),而检测2 的一致性校验走getImageData()。这是两条完全独立的代码路径:

toDataURL() → GetImageBuffer() → ImageEncoder → base64 字符串 getImageData() → GetImageDataArray() → Uint8ClampedArray

只要噪声加在GetImageBuffer()(toDataURL 专用路径),getImageData完全不受影响。

修改文件:dom/canvas/CanvasRenderingContext2D.cpp

修改位置:CanvasRenderingContext2D::GetImageBuffer()函数,在像素 buffer 返回之前

完整改动:

// 在 RandomizePixels 之后、return ret 之前插入:// --- Canvas fingerprint noise (toDataURL only) ---// Deterministic per-session noise: XOR a few pixels based on a hash seed// derived from buffer content. getImageData is not affected.{uint32_tbufLen=out_imageSize->width*out_imageSize->height*4;uint8_t*buf=ret.get();// Simple hash seed from first 64 bytes of pixel datauint32_tseed=0x5A3C6B2D;uint32_thashLen=std::min(bufLen,(uint32_t)64);for(uint32_ti=0;i<hashLen;i++){seed^=(uint32_t)buf[i]<<((i%4)*8);seed=(seed<<13)|(seed>>19);seed*=0x85EBCA6B;}// Perturb a small number of pixels (every 4th pixel's B channel, ±1)for(uint32_ti=0;i<bufLen;i+=16){uint32_tnoise=seed^(seed>>17);seed=seed*0xACAB3251+0x12345;buf[i]=(uint8_t)(buf[i]+(noise&1?1:-1));}}

改动要点:

设计决策原因
确定性种子(从buffer内容派生)同一画布内容多次 toDataURL 产出相同 hash,不会被调用对比检出
不同画布内容 → 不同 seedPicasso 图、emoji、字母 A 各自产生不同指纹
步长 16(每 4 像素改 1 字节)改动量约 6%,肉眼不可见但 hash 完全不同
只在 GetImageBuffer 中操作仅影响 toDataURL 路径,getImageData 完全不经过这里

编译:

cdF:\mc mach build dom/canvas

验证

卸载触发扩展,用编译后的 Firefox 打开 CreepJS 检测页面。

预期结果:

  • ✅ Canvas hash 与原版 Firefox 不同(指纹确实被扰动了)
  • ✅ 无rgba noise提示(getImageData 读写一致)
  • ✅ 无lied/rejected(lieProps 全绿)

效果如图,卡片白色背景(通过),hash 为932421c6(不同于未修改 Firefox 的c0421881,说明噪声生效),data 为5e874131,无任何 noise 提示:

小结

检测2 的本质是对getImageData做一致性测试。绕过的关键不是"怎么让 getImageData 的噪声更隐蔽",而是根本不动 getImageData——把噪声放在下游的编码路径。这个设计同时免费搞定了检测3 和检测4(它们也走 getImageData 校验),后文会展示。


为什么一处改动能同时绕过检测 2/3/4?

三项检测的共同点:校验逻辑都走 getImageData。

检测2:getImageData 读两次,比对一致性 检测3:clearRect 后 getImageData 读,检查是否全零 检测4:固定绘制后 getImageData 读 2×2,比对白名单

我们的 C++ 改动只影响GetImageBuffer()(toDataURL 路径),GetImageDataArray()(getImageData 路径)原封不动。所以:

  • 检测2:两次 getImageData 返回相同值 → 通过
  • 检测3:clearRect 后 getImageData 返回全零 → 通过
  • 检测4:2×2 像素串是原生渲染结果 → 命中 Gecko 白名单 → 通过

这正是"只动编码路径"策略的优雅之处——一处改动,三项检测免费通过。


后续预告

后续将继续演示:

  • 第3项(clearRect 残像素)触发效果与验证
  • 第4项(2×2 低熵白名单)触发效果与验证
  • 第5项(TextMetrics 浮点噪声)触发效果与验证
  • 第6项(getContext 硬失败)触发效果与验证

相关代码

文件用途
F:\mc\dom\canvas\CanvasRenderingContext2D.cppFirefox 源码改动位置
F:\mc\dom\html\HTMLCanvasElement.cpptoDataURL 入口(调用 GetImageBuffer)
F:\mc\dom\base\ImageEncoder.cpp图片编码流程

参考

  • 前置文章:《Creepjs学习–Canvas 2D 指纹采集》
  • CreepJS 源码:github.com/AbrahamJuliot/creepjs
  • Firefox 源码(Gecko):searchfox.org,版本:Firefox Nightly 151.0a1

相关新闻

  • Windows内存压缩技术解析与性能优化指南
  • Python并行计算实战:突破GIL限制的性能优化
  • 2026年7月最新大连天梭官方售后服务热线与网点地址查询 - 天梭服务中心

最新新闻

  • 2026年AI大模型API聚合平台复盘:从手动运维到智能调度的效能跃迁之路
  • 卡地亚中国官方售后服务中心|官方热线及全部网点地址权威信息声明(2026年7月更新) - 卡地亚服务中心
  • ai-agent框架spring ai/alibaba 原理源码分析(五)graph III 图执行
  • Qwen3开源引爆MCP协议:大模型互操作新标准
  • 动漫日语敬语语气词翻译实测:くん様等称谓怎么处理最地道
  • Next.js 14核心特性与性能优化实战指南

日新闻

  • 宝珀中国官方售后服务中心|官方热线和维修地址权威信息声明(2026年7月更新) - 宝珀官方售后服务中心
  • # 2026年北京知识产权律师推荐怎么选?看这五点关键不踩雷 - 本地品牌推荐
  • 2026实测教程:生成的拼豆图纸不满意怎么修改才省事 - 省事研究所

周新闻

  • IX9104 PCIe5.0 高速交换芯片@ACP#完整规格 + 应用场景总结
  • Unity游戏集成Coze智能体:实现NPC智能对话与知识库联动
  • SAP EPIC 建行回单查询:从标准类CL_EPIC_EXAMPLE_CN_CCB_GHTD到Z类的5处关键修改

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号