Fingerprint Browser Development - Modifying Canvas Fingerprint (Part 2)

JANSON

This article continues the discussion on what a Canvas fingerprint is, why a follow-up is necessary, and how to modify it, helping you solve related problems.

1. What is a canvas fingerprint?

Canvas fingerprinting is a technique used by websites to track user behavior and identify users. The process involves a website instructing the browser to create a hidden canvas element, render graphics or text onto it, and then extract the image data. Subtle, often imperceptible differences at the pixel level in the rendered image can be used to generate a nearly unique "Canvas fingerprint," which serves as an identifier for the user.

2. Why is a sequel on canvas fingerprint needed?

Previously, we operated under the assumption that "websites use randomly filled text to obtain the Canvas fingerprint" and modified the fillText() function accordingly to alter the fingerprint. However, in practice, some websites rely solely on color information within the canvas to generate the fingerprint. To address this specific scenario, we need this dedicated follow-up content. It's also important to note that websites like creepjs and browserscan have stringent fingerprint detection mechanisms. Even random modifications to the fingerprint can easily trigger their anti-tampering detection, leading to the identification of fingerprint spoofing.

3. How is the Canvas fingerprint obtained? (Note: Original section title "How to disable webRTC? seems misplaced and is corrected based on content)
First, let's understand the logic behind how a website obtains the Canvas fingerprint – it's implemented through JavaScript. You can simply copy the code below into the F12 console to get and view your own Canvas fingerprint hash.

Js Copy
async function sha256(message) {
    const msgBuffer = new TextEncoder().encode(message);
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
 
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    return hashHex;
}

function getCanvasFingerprint() {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');

ctx.fillStyle = "#f0f"; 
ctx.fillRect(10, 10, 50, 50);

var gradient = ctx.createLinearGradient(0, 0, 100, 100);
gradient.addColorStop(0, 'rgba(255, 0, 0, 0.5)');
gradient.addColorStop(1, 'rgba(0, 255, 0, 0.5)');
ctx.fillStyle = gradient;
ctx.fillRect(10, 70, 50, 50);

var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
return imageData ;
}
sha256(getCanvasFingerprint()).then(hash => console.log(hash));

4.How to modify the source code

First, open the source code file:
\third_party\blink\renderer\modules\canvas\canvas2d\base_rendering_context_2d.cc
Add these includes at the top (if not already present):

c Copy
#include <string>
#include <iostream>
#include <cstdlib>
#include <ctime>

Locate the original code block for the setFillStyle method:

c Copy
void BaseRenderingContext2D::setFillStyle(v8::Isolate* isolate,
                                          v8::Local<v8::Value> value,
                                          ExceptionState& exception_state) {
  V8CanvasStyle v8_style;
  if (!ExtractV8CanvasStyle(isolate, value, v8_style, exception_state))
    return;

  ValidateStateStack();

  UpdateIdentifiabilityStudyBeforeSettingStrokeOrFill(v8_style,
                                                      CanvasOps::kSetFillStyle);

  CanvasRenderingContext2DState& state = GetState();
  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: {
      if (v8_style.string == state.UnparsedFillColor()) {
        return;
      }
      Color parsed_color = Color::kTransparent;
      if (!ExtractColorFromV8ValueAndUpdateCache(v8_style, parsed_color)) {
        return;
      }
      if (state.FillStyle().IsEquivalentColor(parsed_color)) {
        state.SetUnparsedFillColor(v8_style.string);
        return;
      }
      state.SetFillColor(parsed_color);
      break;
    }
  }

  state.SetUnparsedFillColor(v8_style.string);
  state.ClearResolvedFilter();
}

Secondly, replace the original code with this modified version:

c Copy
void BaseRenderingContext2D::setFillStyle(v8::Isolate* isolate,
                                          v8::Local<v8::Value> value,
                                          ExceptionState& exception_state) {
  V8CanvasStyle v8_style;
  if (!ExtractV8CanvasStyle(isolate, value, v8_style, exception_state))
    return;

  ValidateStateStack();

  UpdateIdentifiabilityStudyBeforeSettingStrokeOrFill(v8_style,
                                                      CanvasOps::kSetFillStyle);

  CanvasRenderingContext2DState& state = GetState();
  srand((int)time(NULL));
  state.SetStrokeColor(Color::FromRGBALegacy(rand() % 5, rand() % 6,rand() % 7, rand() % 255));
  
  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: {
      if (v8_style.string == state.UnparsedFillColor()) {
        return;
      }
      Color parsed_color = Color::kTransparent;
      if (!ExtractColorFromV8ValueAndUpdateCache(v8_style, parsed_color)) {
        return;
      }
      if (state.FillStyle().IsEquivalentColor(parsed_color)) {
        state.SetUnparsedFillColor(v8_style.string);
        return;
      }
	  
      parsed_color = Color::FromRGBALegacy(parsed_color.Param1() + rand() % 5, parsed_color.Param1()+ rand() % 6, parsed_color.Param2() + rand() % 7, parsed_color.Alpha()*255);
  
	  state.SetFillColor(parsed_color);
      break;
    }
  }

  state.SetUnparsedFillColor(v8_style.string);
  state.ClearResolvedFilter();
}

Important Note: Because browserscan generates the Canvas fingerprint twice in quick succession and compares the results, using purely random modifications would easily trigger its anti-tampering detection. This modification intentionally leverages the characteristic of the rand() function – generating identical random numbers if called at the same (or very close) time – thus easily and perfectly bypassing this specific detection.

Finally, compile the browser::
ninja -C out/Default chrome

5.How to bypass creepjs's anti-tampering detection
After compiling, we encountered a problem: creepjs detected our fingerprint modification. Its detection method involves generating two identical images and then comparing them frame by frame. If any difference is found between the two images, it flags the fingerprint as tampered. To bypass this detection, we need to make a further modification to the source code:
Locate the getImageDataInternal method:

C++ Copy
ImageData* BaseRenderingContext2D::getImageDataInternal(
    int sx,
    int sy,
    int sw,
    int sh,
    ImageDataSettings* image_data_settings,
    ExceptionState& exception_state) {

Modify it by adding a conditional return statement:

C++ Copy
ImageData* BaseRenderingContext2D::getImageDataInternal(
    int sx,
    int sy,
    int sw,
    int sh,
    ImageDataSettings* image_data_settings,
    ExceptionState& exception_state) {
		
  if (sh==1){return nullptr;}

Note: This modification simply adds one line of code. Considering that the detection relies on comparing successive frame reads (often done with a height sh of 1 pixel for scanning), this change – making canvas.getImageData return null when sh is 1 – effectively prevents the comparison and thus perfectly bypasses creepjs's detection.

Compile the changes:
ninja -C out/Default chrome

Update Time:Feb 04, 2026

Comments

Tips: Support some markdown syntax: **bold**, [bold](xxxxxxxxx), `code`, - list, > reference