魔改chromium源码——canvas指纹修改 第一节
在进行以下操作之前,请确保已完成之前文章中提到的 源码拉取及编译 部分。
如果已顺利完成相关配置,即可继续执行后续操作。
Canvas 指纹技术简介
Canvas 指纹 是一种基于浏览器的隐式用户标识技术,利用 HTML5 的 元素及其绘图功能生成设备或浏览器的唯一标识符。其核心原理是:不同设备、浏览器或操作系统在渲染相同的图形内容时,可能会产生细微的差异。这些差异可以被提取并转换为一个唯一的“指纹”,从而用于识别用户的设备或浏览器。
Canvas 指纹的实现过程
以字节某音产品中的 Canvas 指纹生成代码为例,分析其实现过程:
▼javascript复制代码function canvasFingerprint(e = 3735928559) { var urlStr; // 创建 Canvas 元素并设置尺寸 var canvasEl = document.createElement('canvas'); canvasEl.width = 100; canvasEl.height = 100; // 获取绘图上下文并绘制内容 var canvas = canvasEl.getContext('2d'); canvas.fillStyle = '#0078d4'; canvas.fillRect(0, 0, 100, 100); canvas.font = '14px serif'; // 设置字体样式 canvas.fillText('龘ฑภ경', 2, 12); // 绘制多语言字符 canvas.shadowBlur = 2; // 添加阴影效果 canvas.showOffsetX = 1; // 自定义属性(非标准) canvas.showColor = 'lime'; // 自定义属性(非标准) canvas.arc(8, 8, 8, 0, 2); // 绘制圆弧 canvas.stroke(); // 描边 // 将 Canvas 内容转换为 Base64 数据 URL urlStr = canvasEl.toDataURL(); // 基于数据 URL 计算哈希值 for (var i = 0; i < 32; i++) { e = (65599 * e + urlStr.charCodeAt(e % urlStr.length)) >>> 0; } return e; // 返回最终的指纹值 } // 调用函数生成指纹 canvasFingerprint();
实现步骤解析
-
创建 Canvas 元素: 使用 document.createElement('canvas') 创建一个 元素,并设置其宽度为 100 像素,高度为 100 像素。 这些尺寸经过精心设计,既能保证足够的复杂性,又能避免过高的计算开销。
-
绘制图形内容: 设置字体样式为 14px serif,并在画布上绘制包含多种语言字符的文本 '龘ฑภ경'。这种多语言字符组合增加了渲染结果的多样性。 添加阴影效果(shadowBlur)和绘制一个绿色圆弧(arc),进一步丰富图形内容。
-
提取渲染结果: 使用 canvasEl.toDataURL() 方法将画布内容转换为 Base64 编码的数据 URL。这个 URL 包含了当前设备对图形内容的渲染结果。
-
计算哈希值: 遍历数据 URL 的字符序列,通过一个自定义的哈希算法(基于乘法因子 65599 和字符 ASCII 值)计算出一个 32 位整数作为最终的指纹值。 这个哈希值具有高区分度,能够唯一标识当前设备或浏览器。
在src\third_party\blink\renderer\modules\canvas\canvas2d\canvas_2d_recorder_context.cc所在目录中,新建一个random_hex_color.h文件。文件内容如下
▼cpp复制代码#ifndef RANDOM_COLOR_H #define RANDOM_COLOR_H #include <array> #include <random> #include <string> #include "base/time/time.h" // 用于动态种子 namespace content { // 声明并定义 getRandomColorCode 函数 inline std::string getRandomColorCode() { // 十六进制字符集合 const std::array<char, 17> hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', '\0'}; // 静态字符数组,用于存储颜色代码('#' + 6个字符 + '\0') static thread_local std::array<char, 8> colorCode; // 初始化伪随机数生成器 static thread_local std::mt19937 gen(static_cast<unsigned>(base::Time::Now().ToTimeT())); static std::uniform_int_distribution<> dis(0, 15); // 生成 [0, 15] 范围内的随机数 // 构造颜色代码字符串 colorCode[0] = '#'; // 设置第c一个字符为 '#' for (int i = 1; i < 7; ++i) { colorCode[i] = hexChars[dis(gen)]; // 随机选择一个十六进制字符 } colorCode[7] = '\0'; // 添加字符串终止符 return colorCode.data(); // 返回指向静态数组的指针 } } #endif // RANDOM_COLOR_H
在src\third_party\blink\renderer\modules\canvas\canvas2d\canvas_2d_recorder_context.cc目录中,找到底下这段函数的源码。
我们以修改 canvas 的 fillStyle 值为例进行说明。对于其他属性的随机化处理,可以参考同样的思路:找到对应的代码位置,并按照类似的方式调整逻辑。这里不再逐一演示,但方法思路是通用的。
如果在这个文件找不到这个代码,全局搜索一下canvas2d文件夹,不同版本的 chromium 源码结构会有些许差别,但功能是一致的
▼cpp复制代码#include "third_party/blink/renderer/modules/canvas/canvas2d/random_color.h" // 引入头文件 void Canvas2DRecorderContext::setFillStyle(v8::Isolate* isolate, v8::Local<v8::Value> value, ExceptionState& exception_state) { ValidateStateStack(); CanvasRenderingContext2DState& state = GetState(); // This block is similar to that in setStrokeStyle(), see comments there for // details on this. if (value->IsString()) { // v8::Local<v8::String> v8_string = value.As<v8::String>(); 这行注释 // 新增的 std::string color_code_str = content::getRandomColorCode(); const char* color_code = color_code_str.c_str(); // 新增的 v8::Local<v8::String> v8_string = v8::String::NewFromUtf8(isolate, color_code, v8::NewStringType::kNormal).ToLocalChecked(); UpdateIdentifiabilityStudyBeforeSettingStrokeOrFill( v8_string, CanvasOps::kSetFillStyle); if (state.IsUnparsedFillColor(v8_string)) { return; } Color parsed_color = Color::kTransparent; if (!ExtractColorFromV8StringAndUpdateCache( isolate, v8_string, exception_state, parsed_color)) { return; } if (state.FillStyle().IsEquivalentColor(parsed_color)) { state.SetUnparsedFillColor(isolate, v8_string); return; } state.SetFillColor(parsed_color); state.ClearUnparsedFillColor(); state.ClearResolvedFilter(); return; } V8CanvasStyle v8_style; if (!ExtractV8CanvasStyle(isolate, value, v8_style, exception_state)) { return; } UpdateIdentifiabilityStudyBeforeSettingStrokeOrFill(v8_style, CanvasOps::kSetFillStyle); switch (v8_style.type) { case V8CanvasStyleType::kCSSColorValue: state.SetFillColor(v8_style.css_color_value); break; case V8CanvasStyleType::kGradient: state.SetFillGradient(v8_style.gradient); break; case V8CanvasStyleType::kPattern: if (!origin_tainted_by_content_ && !v8_style.pattern->OriginClean()) { SetOriginTaintedByContent(); } state.SetFillPattern(v8_style.pattern); break; case V8CanvasStyleType::kString: { Color parsed_color = Color::kTransparent; if (ParseColorOrCurrentColor(v8_style.string, parsed_color) == ColorParseResult::kParseFailed) { return; } if (!state.FillStyle().IsEquivalentColor(parsed_color)) { state.SetFillColor(parsed_color); } break; } } state.ClearUnparsedFillColor(); state.ClearResolvedFilter(); }
新增头文件
先将v8::Localv8::String v8_string = value.Asv8::String();这行注释掉,然后新增底下两行代码。
修改完了之后保存,然后执行以下命令编译
▼sh复制代码autoninja -C out/Default chrome
创建一个index.html文件,文件内容如下:
▼html复制代码<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"> <title>canvas</title> </head> <body> <canvas class="" id="myCanvas1" width="100" height="100"></canvas> <canvas class="" id="myCanvas2" width="100" height="100"></canvas> <script> function canvasFingerprint (urlStr) { var e = 3735928559 // 基于数据 URL 计算哈希值 for (var i = 0; i < 32; i++) { e = (65599 * e + urlStr.charCodeAt(e % urlStr.length)) >>> 0; } return e; // 返回最终的指纹值 } function cavasCreate (el) { // 同一段逻辑的canvas做多次渲染 const canvas = document.getElementById(el); const ctx = canvas.getContext('2d'); ctx.fillStyle = '#0078d4'; ctx.fillRect(0, 0, 100, 100); ctx.font = '14px Arial, sans-serif'; ctx.fillStyle = 'black'; ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'; ctx.shadowBlur = 2; ctx.fillText('龘ฑภ경', 10, 50); const urlStr = canvas.toDataURL(); return urlStr } const urlStr1 = cavasCreate('myCanvas1') const urlStr2 = cavasCreate('myCanvas2') // 调用函数生成指纹 console.log(canvasFingerprint(urlStr1)); console.log(canvasFingerprint(urlStr2)); </script> </body> </html>

特别说明
通过对上述随机化逻辑的分析和实现,可以发现一种简单而有效的检测方法: 对同一段 Canvas 逻辑代码多次执行,并对比每次生成的结果。如果所有结果完全一致,则说明浏览器环境未被篡改,处于正常状态;反之,如果结果存在不一致,则表明相关函数内容或者浏览器环境已被篡改。
关于这一问题的解决方案,我们将在下一节详细探讨。
