魔改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();

实现步骤解析

  1. 创建 Canvas 元素: 使用 document.createElement('canvas') 创建一个 元素,并设置其宽度为 100 像素,高度为 100 像素。 这些尺寸经过精心设计,既能保证足够的复杂性,又能避免过高的计算开销。

  2. 绘制图形内容: 设置字体样式为 14px serif,并在画布上绘制包含多种语言字符的文本 '龘ฑภ경'。这种多语言字符组合增加了渲染结果的多样性。 添加阴影效果(shadowBlur)和绘制一个绿色圆弧(arc),进一步丰富图形内容。

  3. 提取渲染结果: 使用 canvasEl.toDataURL() 方法将画布内容转换为 Base64 编码的数据 URL。这个 URL 包含了当前设备对图形内容的渲染结果。

  4. 计算哈希值: 遍历数据 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>

inline const char* getRandomColorCode() {
    const std::array<char, 17> hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', '\0'};
    static std::array<char, 8> colorCode;
    static std::mt19937 gen(42);
    static std::uniform_int_distribution<> dis(0, 15);
    colorCode[0] = '#';
    for (int i = 1; i < 7; ++i) {
        colorCode[i] = hexChars[dis(gen)];
    }
    colorCode[7] = '\0';
    return colorCode.data();
}

#endif

在src\third_party\blink\renderer\modules\canvas\canvas2d\canvas_2d_recorder_context.cc目录中,找到底下这段函数的源码。

我们以修改 canvas 的 fillStyle 值为例进行说明。对于其他属性的随机化处理,可以参考同样的思路:找到对应的代码位置,并按照类似的方式调整逻辑。这里不再逐一演示,但方法思路是通用的。

如果在这个文件找不到这个代码,全局搜索一下canvas2d文件夹,不同版本的 chromium 源码结构会有些许差别,但功能是一致的

cpp 复制代码
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>(); 这行注释
    // 新增的
    const char* color_code = getRandomColorCode(); 
    // 新增的
    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; // 返回最终的指纹值
        }

        const canvas = document.getElementById('myCanvas1');
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = 'blue';
        ctx.fillRect(0, 0, 100, 100);

        const canvas2 = document.getElementById('myCanvas2');
        const ctx2 = canvas2.getContext('2d');
        ctx2.fillStyle = 'blue';
        ctx2.fillRect(0, 0, 100, 100);
        const urlStr1 = canvas.toDataURL();
        const urlStr2 = canvas2.toDataURL();
        // 调用函数生成指纹
        console.log(canvasFingerprint(urlStr1));
        console.log(canvasFingerprint(urlStr2));
    </script>
</body>
</html>

在编译好的chromium中打开,就可以看到,每次刷新,他的颜色都不一样,已经被我们修改的源码给hook掉了。


特别说明

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

关于这一问题的解决方案,我们将在下一节详细探讨。

相关推荐
兰德里的折磨55012 分钟前
基于若依和elementui实现文件上传(导入Excel表)
前端·elementui·excel
喝拿铁写前端15 分钟前
一个列表页面,初级中级高级前端之间的鸿沟就显出来了
前端·架构·代码规范
magic 2451 小时前
ES6变量声明:let、var、const全面解析
前端·javascript·ecmascript·es6
M_chen_M1 小时前
es6学习02-let命令和const命令
前端·学习·es6
好_快1 小时前
Lodash源码阅读-dropWhile
前端·javascript·源码阅读
M_chen_M1 小时前
JS6(ES6)学习01-babel转码器
前端·学习·es6
好_快1 小时前
Lodash源码阅读-dropRightWhile
前端·javascript·源码阅读
二川bro2 小时前
Vue 项目中 package.json 文件的深度解析
前端·vue.js·json
寰宇视讯2 小时前
铼赛智能Edge mini斩获2025法国设计大奖 | 重新定义数字化齿科美学
前端·数据库·edge
excel2 小时前
webpack 模块 第 三 节
前端