魔改chromium源码——canvas指纹修改 第二节

在进行以下操作之前,请确保已完成之前文章中提到的 源码拉取及编译 部分。

如果已顺利完成相关配置,即可继续执行后续操作。


在之前的文章中,我们分别探讨了以下两个主题:

  1. 魔改Chromium源码------Canvas指纹修改 第一节

    这篇文章则讲解了一种简单的随机化方法,用于修改Canvas指纹。然而,这种方法存在一个问题:由于每次生成的随机值不同,可能会被检测机制较为严格的站点识别。

  2. 魔改Chromium源码------新增自定义变量到Window属性

    在这篇文章中,我们介绍了如何将自定义变量注入到 window 全局对象中,从而实现全局变量的存储与访问。

基于上述两篇文章的原理,本章将它们结合起来,提出一种更优的解决方案:将随机值存储到 window 全局变量中,并在Canvas操作时从全局变量中取出使用。通过这种方式,可以避免每次生成的随机值不一致的问题,从而提升隐蔽性。


阅读前提

为了更好地理解本章内容,可以先阅读并掌握了以下两篇文章的实现细节:


实现步骤

步骤 1:定义全局变量

在 src/base 模块中定义全局变量 myCode,以便在整个 Chromium 项目中复用

在src/base目录下,创建文件:my_globals.h

文件内容:

cpp 复制代码
// base/my_globals.h
#ifndef BASE_MY_GLOBALS_H_
#define BASE_MY_GLOBALS_H_

#include <string>

namespace base {
const char* GetRandomColorCode();
}

#endif

在src/base目录下,创建文件:my_globals.cc

文件内容:

cpp 复制代码
#include "base/my_globals.h"
#include <array>
#include <random>
#include "base/time/time.h"  // 用于动态种子

namespace base {

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'};
    // 静态字符数组,用于存储颜色代码('#' + 6个字符 + '\0')
    static thread_local std::array<char, 8> colorCode;
    // 初始化伪随机数生成器
    // static thread_local std::mt19937 gen(42); // 使用固定种子 42
    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 (size_t i = 1; i < 7; ++i) {
        colorCode[i] = hexChars[static_cast<size_t>(dis(gen))]; // 显式转换为 size_t
    }
    colorCode[7] = '\0'; // 添加字符串终止符
    return colorCode.data(); // 返回指向静态数组的指针
}
}

修改 base/BUILD.gn

文件路径: src/base/BUILD.gn 操作: 在 component("base") 的 sources 列表中添加新文件的文件名

步骤 2:创建 JavaScript 绑定

在 src/content/renderer 目录中,创建文件:my_code_binding.h

文件内容:

cpp 复制代码
#ifndef CONTENT_RENDERER_MY_CODE_BINDING_H_
#define CONTENT_RENDERER_MY_CODE_BINDING_H_

#include "v8/include/v8.h"

namespace content {

class MyCodeBinding {
 public:
  static void Install(v8::Local<v8::Context> context);
};
 
}

#endif

在 src/content/renderer 目录中,创建文件:my_code_binding.cc

文件内容:

cpp 复制代码
#include "content/renderer/my_code_binding.h"

#include "base/my_globals.h"
#include "v8/include/v8.h"

namespace content {

void MyCodeBinding::Install(v8::Local<v8::Context> context) {
  v8::Isolate* isolate = context->GetIsolate();
  v8::HandleScope handle_scope(isolate);

  v8::Local<v8::Object> global = context->Global();

  // 每次页面加载时生成新的随机颜色代码
  const char* random_color = base::GetRandomColorCode();

  global->Set(
      context,
      v8::String::NewFromUtf8(isolate, "myCode").ToLocalChecked(),
      v8::String::NewFromUtf8(isolate, random_color).ToLocalChecked()).Check();
}

}

修改 content/renderer/BUILD.gn

文件路径: src/content/renderer/BUILD.gn 操作: 在 target(link_target_type, "renderer") 的 sources 列表中添加新文件

步骤 3:绑定到 RenderFrameImpl

在 RenderFrameImpl 中调用绑定逻辑,将 myCode 属性安装到脚本上下文中

修改文件路径:src/content/renderer/render_frame_impl.cc

在文件顶部添加 my_code_binding.h 头文件,可以按文件头字母顺序添加

cpp 复制代码
#include "content/renderer/my_code_binding.h"

在RenderFrameImpl::DidCreateScriptContext中添加如下代码

cpp 复制代码
void RenderFrameImpl::DidCreateScriptContext(v8::Local<v8::Context> context,
                                             int world_id) {
   // 新增代码
  if (world_id == 0) {
    MyCodeBinding::Install(context);
  }
  // 新增代码

  TRACE_EVENT_WITH_FLOW0("navigation",
                         "RenderFrameImpl::DidCreateScriptContext",
                         TRACE_ID_LOCAL(this),
                         TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT);
  v8::MicrotasksScope microtasks(GetAgentGroupScheduler().Isolate(),
                                 context->GetMicrotaskQueue(),
                                 v8::MicrotasksScope::kDoNotRunMicrotasks);
  if (((enabled_bindings_.Has(BindingsPolicyValue::kMojoWebUi)) ||
       enable_mojo_js_bindings_) &&
      IsMainFrame() && world_id == ISOLATED_WORLD_ID_GLOBAL) {
    // We only allow these bindings to be installed when creating the main
    // world context of the main frame.
    blink::WebV8Features::EnableMojoJS(context, true);

    if (mojo_js_features_) {
      if (mojo_js_features_->file_system_access)
        blink::WebV8Features::EnableMojoJSFileSystemAccessHelper(context, true);
    }
  }

  if (world_id == ISOLATED_WORLD_ID_GLOBAL &&
      mojo_js_interface_broker_.is_valid()) {
    // MojoJS interface broker can be enabled on subframes, and will limit the
    // interfaces JavaScript can request to those provided in the broker.
    blink::WebV8Features::EnableMojoJSAndUseBroker(
        context, std::move(mojo_js_interface_broker_));
  }

  for (auto& observer : observers_)
    observer.DidCreateScriptContext(context, world_id);
}

在src目录下,执行 gn gen out/Default ,重新生成构建文件

构建成功之后运行一下命令进行编译

bash 复制代码
autoninja -C out/Default chrome

到这里我们实现了全局变量的随机化,每次只有当刷新页面才会更新这个值

步骤 4:在canvas中获取 window.myCode 随机值

在原来的 魔改Chromium源码------Canvas指纹修改 第一节 代码中,我们做一些修改

修改文件路径:src\third_party\blink\renderer\modules\canvas\canvas2d\canvas_2d_recorder_context.cc

新增头文件

cpp 复制代码
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/platform/bindings/v8_binding.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"

在void Canvas2DRecorderContext::setFillStyle中,新增以下标注新增的内容:

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.

  // 新增开始:获取 window.myCode 并准备用于填充样式
  const char* my_code_str = nullptr;
  v8::Local<v8::String> my_code_v8_string;
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context = isolate->GetCurrentContext();
  if (!context.IsEmpty()) {
    v8::Local<v8::Object> global = context->Global();
    v8::Local<v8::Value> my_code_value;
    if (global->Get(context, v8::String::NewFromUtf8(isolate, "myCode").ToLocalChecked()).ToLocal(&my_code_value)) {
      v8::String::Utf8Value utf8_value(isolate, my_code_value);
      my_code_str = *utf8_value ? *utf8_value : "undefined";
      // 将 my_code_str 转换回 V8 字符串以供后续使用
      my_code_v8_string = v8::String::NewFromUtf8(isolate, my_code_str).ToLocalChecked();
    }
  }
  // 新增结束


  if (value->IsString()) {
    v8::Local<v8::String> v8_string = value.As<v8::String>();

    // 新增开始
    if (!my_code_v8_string.IsEmpty()) {
      v8_string = my_code_v8_string;
    }
    // 新增结束

    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();
}

在src目录下,执行 gn gen out/Default ,重新生成构建文件

构建成功之后运行一下命令进行编译

bash 复制代码
autoninja -C out/Default chrome

以下是验证结果的代码:

新建一个 .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>
    <canvas class="" id="myCanvas3" width="100" height="100"></canvas>
    <canvas class="" id="myCanvas4" 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')
        const urlStr3 = cavasCreate('myCanvas3')
        const urlStr4 = cavasCreate('myCanvas4')
        // 调用函数生成指纹
        console.log('cavas1 URL hash值:', canvasFingerprint(urlStr1));
        console.log('cavas2 URL hash值:', canvasFingerprint(urlStr2));
        console.log('cavas3 URL hash值:', canvasFingerprint(urlStr3));
        console.log('cavas4 URL hash值:', canvasFingerprint(urlStr4));
    </script>
</body>
</html>

可以看到,每个canvas输出的指纹都是一致的。

www.browserscan.net/canvas 的检测结果是通过的,他的检测原理就是绘制多个canvas来每个对比是否一致,进而判断环境是否被修改。

特别说明

通过对上述随机化逻辑的分析与实现,我们已经能够成功绕过大部分Canvas指纹检测机制。然而,经过进一步的观察和分析可以发现,如果检测机制不仅对比父窗口的Canvas指纹,还通过嵌套iframe的方式,将iframe内部渲染的Canvas指纹与父窗口的Canvas指纹进行对比,那么这种检测方式仍然可能识别出我们的修改。

因此,仅对父窗口的Canvas进行随机化处理并不足以应对更为复杂的检测场景。我们需要进一步优化方案,确保在嵌套iframe的情况下,父子窗口的Canvas指纹保持一致性。

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

以下是检测演示:

首先创建 canvasiframe.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>canvasiframe</title>
</head>
<body>
    <canvas class="" id="myCanvas1" width="100" height="100"></canvas>
    <canvas class="" id="myCanvas2" width="100" height="100"></canvas>
    <canvas class="" id="myCanvas3" width="100" height="100"></canvas>
    <canvas class="" id="myCanvas4" 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')
        const urlStr3 = cavasCreate('myCanvas3')
        const urlStr4 = cavasCreate('myCanvas4')
        // 调用函数生成指纹
        console.log('cavas1 URL hash值:', canvasFingerprint(urlStr1));
        console.log('cavas2 URL hash值:', canvasFingerprint(urlStr2));
        console.log('cavas3 URL hash值:', canvasFingerprint(urlStr3));
        console.log('cavas4 URL hash值:', canvasFingerprint(urlStr4));
    </script>
</body>
</html>

再创建 canvas.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>

    <style>
        canvas {
            margin: auto 30px;
        }

        iframe {
            border: 2px solid tomato;
        }
    </style>
</head>

<body>
    <canvas class="" id="myCanvas1" width="100" height="100"></canvas>
    <canvas class="" id="myCanvas2" width="100" height="100"></canvas>
    <canvas class="" id="myCanvas3" width="100" height="100"></canvas>

    <iframe src="/canvasiframe.html" width="350" height="200" frameborder="0"></iframe>
    <iframe src="/canvasiframe.html" width="350" height="200" frameborder="0"></iframe>

    <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')
        const urlStr3 = cavasCreate('myCanvas3')

        // 调用函数生成指纹
        console.log('cavas1 URL hash值:', canvasFingerprint(urlStr1));
        console.log('cavas2 URL hash值:', canvasFingerprint(urlStr2));
        console.log('cavas3 URL hash值:', canvasFingerprint(urlStr3));
    </script>
</body>

</html>
相关推荐
Azer.Chen3 小时前
Chrome 浏览器插件收录
前端·chrome
IT专家-大狗3 小时前
Google Chrome Canary版官方下载及安装教程【适用于开发者与进阶用户】
开发语言·javascript·chrome·ecmascript
Thenunaoer15 小时前
【Ubutun】 在Linux Yocto的基础上去适配4G模块
linux·运维·chrome
攻城狮7号1 天前
Python爬虫第13节-解析库pyquery 的使用
爬虫·python·python爬虫
Blood_J1 天前
python网络爬虫
开发语言·爬虫·python
Spider Cat 蜘蛛猫1 天前
chrome extension开发框架WXT之WXT Storage api解析【补充说明一】
前端·javascript·chrome
limit00751 天前
CesiumEarth能够本地浏览的三维倾斜模型切片(3DTiles)
chrome·低代码·arcgis·web3·旅游
q567315231 天前
使用Java的HttpClient实现文件下载器
java·开发语言·爬虫·scrapy
MinggeQingchun2 天前
Python - 爬虫-网页抓取数据-库requests
爬虫·python·requests