魔改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 逻辑代码多次执行,并对比每次生成的结果。如果所有结果完全一致,则说明浏览器环境未被篡改,处于正常状态;反之,如果结果存在不一致,则表明相关函数内容或者浏览器环境已被篡改。

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

相关推荐
Boilermaker19929 分钟前
【Java EE】SpringIoC
前端·数据库·spring
中微子21 分钟前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上102435 分钟前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁1 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry1 小时前
Fetch 笔记
前端·javascript
拾光拾趣录1 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟1 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan1 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson1 小时前
青苔漫染待客迟
前端·设计模式·架构