在进行以下操作之前,请确保已完成之前文章中提到的 源码拉取及编译 部分。
如果已顺利完成相关配置,即可继续执行后续操作。
在之前的文章中,我们分别探讨了以下两个主题:
-
魔改Chromium源码------Canvas指纹修改 第一节
这篇文章则讲解了一种简单的随机化方法,用于修改Canvas指纹。然而,这种方法存在一个问题:由于每次生成的随机值不同,可能会被检测机制较为严格的站点识别。
-
魔改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>
