Recently, I have been studying captcha and Canvas-related things in web scraping. Here is a simple code snippet:
{
const getImageData = CanvasRenderingContext2D.prototype.getImageData;
//
const noisify = function (canvas, context) {
if (context) {
const shift = {
'r': Math.floor(Math.random() * 10) - 5,
'g': Math.floor(Math.random() * 10) - 5,
'b': Math.floor(Math.random() * 10) - 5,
'a': Math.floor(Math.random() * 10) - 5
};
//
const width = canvas.width;
const height = canvas.height;
//
if (width && height) {
const imageData = getImageData.apply(context, [0, 0, width, height]);
//
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
const n = ((i * (width * 4)) + (j * 4));
imageData.data[n + 0] = imageData.data[n + 0] + shift.r;
imageData.data[n + 1] = imageData.data[n + 1] + shift.g;
imageData.data[n + 2] = imageData.data[n + 2] + shift.b;
imageData.data[n + 3] = imageData.data[n + 3] + shift.a;
}
}
//
window.top.postMessage("canvas-defender-alert", '*');
context.putImageData(imageData, 0, 0);
}
}
};
//
HTMLCanvasElement.prototype.toBlob = new Proxy(HTMLCanvasElement.prototype.toBlob, {
apply(target, self, args) {
noisify(self, self.getContext("2d"));
//
return Reflect.apply(target, self, args);
}
});
//
HTMLCanvasElement.prototype.toDataURL = new Proxy(HTMLCanvasElement.prototype.toDataURL, {
apply(target, self, args) {
noisify(self, self.getContext("2d"));
//
return Reflect.apply(target, self, args);
}
});
//
CanvasRenderingContext2D.prototype.getImageData = new Proxy(CanvasRenderingContext2D.prototype.getImageData, {
apply(target, self, args) {
noisify(self.canvas, self);
//
return Reflect.apply(target, self, args);
}
});
}
{
const mkey = "canvas-defender-sandboxed-frame";
document.documentElement.setAttribute(mkey, '');
//
window.addEventListener("message", function (e) {
if (e.data && e.data === mkey) {
e.preventDefault();
e.stopPropagation();
//
if (e.source) {
if (e.source.CanvasRenderingContext2D) {
e.source.CanvasRenderingContext2D.prototype.getImageData = CanvasRenderingContext2D.prototype.getImageData;
}
//
if (e.source.HTMLCanvasElement) {
e.source.HTMLCanvasElement.prototype.toBlob = HTMLCanvasElement.prototype.toBlob;
e.source.HTMLCanvasElement.prototype.toDataURL = HTMLCanvasElement.prototype.toDataURL;
}
}
}
}, false);
}
This JavaScript code is used to manipulate Canvas elements. The code can be explained as follows:
Part 1:
const getImageData = CanvasRenderingContext2D.prototype.getImageData;
This line of code creates a variable named getImageData
and assigns it the reference of CanvasRenderingContext2D.prototype.getImageData
. This means that in the subsequent code, the getImageData
variable can be used to call the getImageData
method on the prototype of CanvasRenderingContext2D
.
Next is a function named noisify
, which is used to add a noise effect to the Canvas. The following code defines and implements the noisify
function.
Inside the noisify
function, the following logic exists:
- First, check the
canvas
andcontext
parameters. - Then, generate a
shift
object containing random numbers to control the color offset of the noise. - Next, get the width and height of the passed-in Canvas.
- If both width and height exist, use the
getImageData
method to retrieve the pixel data from the Canvas and store it in theimageData
variable. - Then, use nested loops to iterate through each pixel and apply the color offset based on the random numbers in the
shift
object. - After processing all the pixels, send a message to the top-level window to trigger the "canvas-defender-alert" event.
- Finally, use
context.putImageData(imageData, 0, 0)
to redraw the processed pixel data onto the Canvas.
Part 2:
HTMLCanvasElement.prototype.toBlob = new Proxy(HTMLCanvasElement.prototype.toBlob, {
apply(target, self, args) {
noisify(self, self.getContext("2d"));
return Reflect.apply(target, self, args);
}
});
HTMLCanvasElement.prototype.toDataURL = new Proxy(HTMLCanvasElement.prototype.toDataURL, {
apply(target, self, args) {
noisify(self, self.getContext("2d"));
return Reflect.apply(target, self, args);
}
});
CanvasRenderingContext2D.prototype.getImageData = new Proxy(CanvasRenderingContext2D.prototype.getImageData, {
apply(target, self, args) {
noisify(self.canvas, self);
return Reflect.apply(target, self, args);
}
});
This part of the code redefines three methods: toBlob
, toDataURL
, and getImageData
, and uses Proxy
to handle the proxying. By proxying these methods, the noisify
function will be automatically called when these methods are invoked, adding the noise effect to the Canvas.
Part 3:
const mkey = "canvas-defender-sandboxed-frame";
document.documentElement.setAttribute(mkey, '');
window.addEventListener("message", function (e) {
if (e.data && e.data === mkey) {
e.preventDefault();
e.stopPropagation();
if (e.source) {
if (e.source.CanvasRenderingContext2D) {
e.source.CanvasRenderingContext2D.prototype.getImageData = CanvasRenderingContext2D.prototype.getImageData;
}
if (e.source.HTMLCanvasElement) {
e.source.HTMLCanvasElement.prototype.toBlob = HTMLCanvasElement.prototype.toBlob;
e.source.HTMLCanvasElement.prototype.toDataURL = HTMLCanvasElement.prototype.toDataURL;
}
}
}
}, false);
This part of the code defines an event listener to listen for messages. When a specific message mkey
is received, a series of operations are performed. Specifically:
- First, add a custom attribute
mkey
to the root element of the page (document.documentElement
). - Then, register a listener for the "message" event and handle the received message based on a condition.
- If the message is equal to
mkey
, prevent the default behavior and event propagation. - Next, access the window object of the message source through
e.source
and check if it has theCanvasRenderingContext2D
andHTMLCanvasElement
methods on their prototypes. - If so, redefine the corresponding methods as the previously proxied methods to ensure that calls to these methods go through the
noisify
function.
In summary, this JavaScript code adds a noise effect to Canvas elements and redefines and proxies certain Canvas-related methods under specific conditions.