Win7 指纹浏览器开发教程(二)Canvas 指纹伪造实战
引言
在上一篇教程中,我们完成了 Win7 环境下指纹浏览器开发环境的搭建,并了解了浏览器指纹的基本原理。本篇将聚焦于最核心的指纹类型之一 —— Canvas 指纹,深入讲解其伪造方法和实战技巧。
Canvas 指纹之所以成为指纹识别的主流手段,是因为它具有以下几个显著优势:首先,采集过程完全依赖标准 JavaScript API,无需额外插件;其次,不同设备之间的差异足够稳定,能够在多次采集后保持一致;最后,Canvas 指纹与其他指纹维度组合后,识别准确率可达 95% 以上。
理解 Canvas 指纹的伪造原理,对于开发指纹浏览器至关重要。市面上主流的 easybr指纹浏览器 等产品,其核心能力之一就是高效、隐蔽的 Canvas 指纹伪造。下面我们将从原理剖析到代码实现,完整讲解 Canvas 指纹伪造的各个环节。
Canvas 指纹原理深度剖析
指纹生成机制
Canvas 指纹利用不同设备在渲染相同 Canvas 内容时产生的细微差异来生成唯一标识。这些差异主要来源于以下几个方面:
- GPU 硬件差异:不同型号的显卡在像素处理和渲染管线上存在差异
- 显卡驱动版本:同一厂商不同版本的驱动可能产生不同的渲染结果
- 操作系统渲染引擎:Windows、macOS、Linux 的图形子系统实现不同
- 抗锯齿算法实现:不同浏览器的抗锯齿算法存在细微区别
- 字体渲染差异:系统字体的微小差异影响文本渲染结果
攻击者通常通过以下步骤生成 Canvas 指纹:
- 创建一个隐藏的 Canvas 元素
- 在 Canvas 上绘制特定的文本和图形组合
- 获取 Canvas 的像素数据或 Base64 编码
- 对数据进行哈希处理,生成固定长度的指纹字符串
典型采集代码分析
以下是指纹采集网站常用的 Canvas 指纹采集代码:
functiongetCanvasFingerprint(){constcanvas=document.createElement('canvas');constctx=canvas.getContext('2d');canvas.width=280;canvas.height=50;// 设置文本样式ctx.textBaseline='top';ctx.font='14px Arial';ctx.fillStyle='#f60';ctx.fillRect(125,1,62,20);ctx.fillStyle='#069';ctx.fillText('Browser Fingerprint 测试 123',2,15);ctx.fillStyle='rgba(102, 204, 0, 0.7)';ctx.fillText('Browser Fingerprint 测试 123',4,17);// 获取 Base64 编码returncanvas.toDataURL();}这段代码之所以有效,是因为即使是完全相同的文本和颜色,在不同设备上渲染出的像素也会有细微差异。这些差异肉眼无法察觉,但通过哈希算法可以稳定地提取出来。
Canvas 指纹伪造方案
方案一:像素扰动法(推荐)
最经典的伪造方法是给 Canvas 像素数据添加微小的随机扰动。这种方法人眼完全无法察觉,但足以改变指纹哈希值。
(function(){constoriginalGetImageData=CanvasRenderingContext2D.prototype.getImageData;constoriginalToDataURL=HTMLCanvasElement.prototype.toDataURL;constoriginalToBlob=HTMLCanvasElement.prototype.toBlob;// 生成随机噪声值functiongenerateNoise(){return(Math.random()*2-1)*0.02;}// 伪造 getImageDataCanvasRenderingContext2D.prototype.getImageData=function(x,y,w,h){constimageData=originalGetImageData.call(this,x,y,w,h);constdata=imageData.data;// 随机选择少量像素点进行扰动constnumPixelsToPerturb=3+Math.floor(Math.random()*3);for(leti=0;i<numPixelsToPerturb;i++){constpos=Math.floor(Math.random()*(data.length/4))*4;data[pos]=Math.max(0,Math.min(255,data[pos]+generateNoise()*255));data[pos+1]=Math.max(0,Math.min(255,data[pos+1]+generateNoise()*255));data[pos+2]=Math.max(0,Math.min(255,data[pos+2]+generateNoise()*255));}returnimageData;};// 伪造 toDataURLHTMLCanvasElement.prototype.toDataURL=function(type,...args){constctx=this.getContext('2d');if(ctx){constw=this.width;consth=this.height;ctx.getImageData(0,0,w,h);}returnoriginalToDataURL.call(this,type,...args);};// 伪造 toBlobHTMLCanvasElement.prototype.toBlob=function(callback,type,...args){constctx=this.getContext('2d');if(ctx){constw=this.width;consth=this.height;ctx.getImageData(0,0,w,h);}returnoriginalToBlob.call(this,callback,type,...args);};})();方案优势:
- 实现简单,代码量少
- 每次调用产生不同的指纹
- 扰动值可控,不影响正常使用
方案缺点:
- 如果网站连续多次采集指纹,可能发现指纹变化规律
- 需要配合 Profile 管理保持指纹一致性
方案二:固定噪声注入
针对上述缺点,我们可以使用固定噪声注入方案。为每个 Profile 生成固定的随机种子,确保同一 Profile 的指纹保持一致。
(function(){// 为当前 Profile 生成固定种子constprofileSeed=getProfileSeed();// 使用种子生成确定性随机数functionseededRandom(seed){letx=Math.sin(seed)*10000;returnx-Math.floor(x);}constoriginalGetImageData=CanvasRenderingContext2D.prototype.getImageData;CanvasRenderingContext2D.prototype.getImageData=function(x,y,w,h){constimageData=originalGetImageData.call(this,x,y,w,h);constdata=imageData.data;// 使用固定种子进行扰动for(leti=0;i<5;i++){constpos=Math.floor(seededRandom(profileSeed+i)*(data.length/4))*4;constnoise=(seededRandom(profileSeed+i+100)-0.5)*0.03*255;data[pos]=Math.max(0,Math.min(255,data[pos]+noise));data[pos+1]=Math.max(0,Math.min(255,data[pos+1]+noise*0.8));data[pos+2]=Math.max(0,Math.min(255,data[pos+2]+noise*0.6));}returnimageData;};})();方案三:OffscreenCanvas 替换法
利用 OffscreenCanvas 创建副本,在副本上绘制后再导出。这种方式更加隐蔽,不容易被检测。
HTMLCanvasElement.prototype.toDataURL=function(type){constoffscreen=newOffscreenCanvas(this.width,this.height);constoffCtx=offscreen.getContext('2d');// 复制原 Canvas 内容offCtx.drawImage(this,0,0);// 在副本上添加微小扰动constimageData=offCtx.getImageData(0,0,this.width,this.height);constdata=imageData.data;// 添加噪声for(leti=0;i<data.length;i+=400){data[i]+=Math.random()>0.5?1:-1;data[i+1]+=Math.random()>0.5?1:-1;data[i+2]+=Math.random()>0.5?1:-1;}offCtx.putImageData(imageData,0,0);returnoffscreen.toDataURL(type);};指纹检测与反检测
常见检测手段
一些高级指纹检测网站会监控 JavaScript 执行环境,检测是否被 Hook。常见检测手段包括:
- 原型链检测:检查 API 方法是否被重新定义
- toString 检测:检查函数的 toString 返回值是否包含
[native code] - 行为一致性检测:连续多次采集指纹,检测是否变化
反检测实现
针对上述检测手段,我们可以采取以下对策:
// 使用 Proxy 替代直接原型链修改functioncreateStealthHook(originalFn,hookFn){returnnewProxy(originalFn,{apply(target,thisArg,args){returnhookFn(target,thisArg,args);}});}// 让被 Hook 的函数看起来像原生functionmakeNative(fn){returnnewProxy(fn,{get(target,key){if(key==='toString'){return()=>`function${target.name}() { [native code] }`;}returnReflect.get(target,key);}});}// 应用反检测 HookHTMLCanvasElement.prototype.toDataURL=makeNative(createStealthHook(HTMLCanvasElement.prototype.toDataURL,function(original,thisArg,args){constresult=Reflect.apply(original,thisArg,args);returnperturbCanvasData(result);}));Electron 中的注入方式
在 Electron 环境中,Canvas 指纹伪造代码需要在合适的时机注入。推荐的做法是在 preload 脚本中注入:
// preload.jsconst{contextBridge}=require('electron');// 在 DOMContentLoaded 后注入 Canvas 伪造代码window.addEventListener('DOMContentLoaded',()=>{injectCanvasForgery();});functioninjectCanvasForgery(){// 注入上述伪造代码// ...}// 暴露安全 API 到渲染进程contextBridge.exposeInMainWorld('fingerprintAPI',{updateProfile:(profile)=>{window.currentProfile=profile;}});验证方法
完成 Canvas 指纹伪造后,建议使用以下工具验证效果:
- FingerprintJS:https://fingerprintjs.com/
- EFF Cover Your Tracks:https://coveryourtracks.eff.org/
- Creepy.js:https://abrahamjuliot.github.io/creepy/
对比伪造前后的指纹变化,确认指纹已成功改变且保持一致性。
注意事项
- 扰动值不宜过大,否则可能影响正常 Canvas 使用
- 建议为每个 Profile 使用固定的随机种子
- 需要与 WebGL、AudioContext 等其他指纹保持逻辑一致性
- 定期更新伪造策略以应对新的检测手段
下一步
下一篇将讲解 WebGL 指纹和 User Agent 指纹的伪造方法,包括 GPU 信息伪装和 UA 字符串修改等进阶技术。