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
canvasandcontextparameters. - Then, generate a
shiftobject 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
getImageDatamethod to retrieve the pixel data from the Canvas and store it in theimageDatavariable. - Then, use nested loops to iterate through each pixel and apply the color offset based on the random numbers in the
shiftobject. - 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
mkeyto 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.sourceand check if it has theCanvasRenderingContext2DandHTMLCanvasElementmethods on their prototypes. - If so, redefine the corresponding methods as the previously proxied methods to ensure that calls to these methods go through the
noisifyfunction.
In summary, this JavaScript code adds a noise effect to Canvas elements and redefines and proxies certain Canvas-related methods under specific conditions.