- // ==UserScript==
- // @name ConsoleHook
- // @namespace http://tampermonkey.net/
- // @description utils of hook javascript function and value changes for js reverse engineering
- // @author @Esonhugh
- // @match http://*
- // @match https://*
- // @include http://*
- // @include https://*
- // @exclude http://127.0.0.1:*/*
- // @exclude http://localhost:*/*
- // @icon https://blog.eson.ninja/img/reol.png
- // @grant none
- // @license MIT
- // @run-at document-start
- // @version 2025-03-10
- // ==/UserScript==
-
- (function () {
- console.hooks = {
- // settings
- settings: {
- // trigger debugger if hook is caught
- autoDebug: false,
- // don't let page jump to other place
- blockPageJump: false,
- // log prefix
- prefix: "[EHOOKS] ", // u can filter all this things with this tag
- // init with eventListener added
- checkEventListnerAdded: false,
- // init with cookie change listener
- checkCookieChange: false,
- // init with localstorage get set
- checkLocalStorageGetSet: false,
- // anti dead loop debugger in script
- antiDeadLoopDebugger: true,
- // Run main in init
- runMain: false,
- // hidden too many default debug logs if you don't need it
- hiddenlog: false,
- },
-
- // init function to apply settings
- init: function () {
- if (this.utils) {
- this.utils.init();
- }
- if (this.settings.blockPageJump) {
- window.onbeforeunload = function () {
- return "ANTI LEAVE";
- };
- }
- if (this.settings.checkEventListnerAdded) {
- this.hookEvents();
- }
- if (this.settings.checkCookieChange) {
- this.hookCookie();
- }
- if (this.settings.checkLocalStorageGetSet) {
- this.hookLocalStorage();
- }
- if (this.settings.antiDeadLoopDebugger) {
- this.antiDebuggerLoops();
- }
- if (this.settings.runMain) {
- this.main();
- }
- },
-
- // hook data change
- main: function () {
-
- this.hookfunc(window, "eval");
- // this.hookfunc(window, "Function");
- this.hookfunc(window, "atob");
- this.hookfunc(window, "btoa");
- this.hookfunc(window, "fetch");
- this.hookfunc(window, "encodeURI");
- this.hookfunc(window, "decodeURI");
- this.hookfunc(window, "encodeURIComponent");
- this.hookfunc(window, "decodeURIComponent");
-
- this.hookfunc(JSON, "parse");
- this.hookfunc(JSON, "stringify");
-
- this.hookfunc(console, "log");
- // this.hookfunc(console, "warn")
- // this.hookfunc(console, "error")
- // this.hookfunc(console, "info")
- // this.hookfunc(console, "debug")
- // this.hookfunc(console, "table")
- // this.hookfunc(console, "trace")
- this.hookfunc(console, "clear");
- },
-
- // rawlogger for console hooks and it can be disabled by settings.hiddenlog
- rawlog: function (...data) {
- if (this.settings.hiddenlog) {
- return; // don't print
- }
- return console.debug(...data);
- },
-
- // log for console hooks, using console.warn for debug
- log: console.warn,
-
- // pasue if any trap triggered
- debugger: function () {
- // traped in debug
- if (this.settings.autoDebug) {
- // dump the real stack for u
- this.dumpstack();
- debugger;
- }
- },
-
- // It will store raw things all your hooked
- hooked: {},
-
- // dump stack and delete the userscript.html
- dumpstack(print = true) {
- var err = new Error();
- var stack = err.stack.split("\n");
- var ret = [`${this.settings.prefix}DUMP STACK: `];
- for (var i of stack) {
- if (!i.includes("userscript.html") && i !== "Error") {
- ret = ret.concat(i);
- }
- }
- ret = ret.join("\n");
- if (print) {
- this.log(ret);
- }
- return ret;
- },
-
- // dump raw data you hooked
- dumpHooked() {
- for (var i in this.hooked) {
- if (this.hooked[i].toString) {
- this.log(`${i}: ${this.hooked[i].toString()}`);
- } else {
- this.log(`${i}: ${this.hooked[i]}`);
- }
- }
- },
-
- // hookfunc will hooks functions when it called
- // e.g.
- // 1. basic use
- // hookfunc(window,"Function") ==> window.Function("return xxx")
- //
- // 2. if you need get things when it returns
- // hookfunc(window, "Function", (res)=>{
- // let [returnValue,originalFunction,realargs,this,] = res
- // })
- //
- // 3. if you need change what when it calls
- // hookfunc(window, "Function", ()=>{} ,(res)=>{
- // let [originalFunction,realargs,this,] = res
- // args = realargs
- // return args
- // })
- //
- // 4. if make this hooks sliently
- // hookfunc(window, "Function", ()=>{} ,(res)=>{
- // let [originalFunction,realargs,this,] = res
- // args = realargs
- // return args
- // }, true)
- directhookfunc: function (
- originalFn,
- posthook = () => {},
- prehook = () => {},
- slience = false
- ) {
- let hookedfunction = () => {}
- (function (originalFunction) {
- hookedfunction = function () {
- // hook logic
- // 1. Allow Check
- var args = prehook([originalFunction, arguments, this]);
- var realargs = arguments;
- if (args) {
- realargs = args;
- } else {
- realargs = arguments;
- }
- // 2. Execute old function
- var returnValue = originalFunction.apply(this, realargs);
- if (!slience) {
- // not slience
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook function trap-> func[${originalFunction.toString()}]`,
- "args->",
- realargs,
- "ret->",
- returnValue
- );
- console.hooks.debugger();
- }
- // 3. Post hook change values
- var newReturn = posthook([
- returnValue,
- originalFunction,
- realargs,
- this,
- ]);
- if (newReturn) {
- return newReturn;
- }
- return returnValue;
- };
- hookedfunction.toString = function () {
- console.hooks.log(
- `${console.hooks.settings.prefix}Found hook ${originalFunction.toString()}.toString check!`,
- );
- console.hooks.debugger();
- return originalFunction.toString();
- };
- })(originalFn);
- this.log(
- `${console.hooks.settings.prefix}Hook function`,
- originalFn,
- "success!"
- );
- return hookedfunction
- },
- // hookfunc will hooks functions when it called
- // e.g.
- // 1. basic use
- // hookfunc(window,"Function") ==> window.Function("return xxx")
- //
- // 2. if you need get things when it returns
- // hookfunc(window, "Function", (res)=>{
- // let [returnValue,originalFunction,realargs,this,] = res
- // })
- //
- // 3. if you need change what when it calls
- // hookfunc(window, "Function", ()=>{} ,(res)=>{
- // let [originalFunction,realargs,this,] = res
- // args = realargs
- // return args
- // })
- //
- // 4. if make this hooks sliently
- // hookfunc(window, "Function", ()=>{} ,(res)=>{
- // let [originalFunction,realargs,this,] = res
- // args = realargs
- // return args
- // }, true)
- hookfunc: function (
- object,
- functionName,
- posthook = () => {},
- prehook = () => {},
- slience = false
- ) {
- (function (originalFunction) {
- object[functionName] = function () {
- // hook logic
- // 1. Allow Check
- var args = prehook([originalFunction, arguments, this]);
- var realargs = arguments;
- if (args) {
- realargs = args;
- } else {
- realargs = arguments;
- }
- // 2. Execute old function
- var returnValue = originalFunction.apply(this, realargs);
- if (!slience) {
- // not slience
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook function trap-> func[${functionName}]`,
- "args->",
- realargs,
- "ret->",
- returnValue
- );
- console.hooks.debugger();
- }
- // 3. Post hook change values
- var newReturn = posthook([
- returnValue,
- originalFunction,
- realargs,
- this,
- ]);
- if (newReturn) {
- return newReturn;
- }
- return returnValue;
- };
- object[functionName].toString = function () {
- console.hooks.log(
- `${console.hooks.settings.prefix}Found hook ${object}.${functionName}.toString check! and origin function is `,
- originalFunction
- );
- console.hooks.debugger();
- return originalFunction.toString();
- };
- console.hooks.hooked[functionName] = originalFunction;
- })(object[functionName]);
- this.log(
- `${console.hooks.settings.prefix}Hook function`,
- functionName,
- "success!"
- );
- },
-
- unhookfunc: function (object, functionName) {
- object[functionName] = console.hooks.hooked[functionName];
- this.rawlog(
- `${console.hooks.settings.prefix}unHook function`,
- functionName,
- "success!"
- );
- },
-
- hookCookie: function () {
- try {
- var cookieDesc =
- Object.getOwnPropertyDescriptor(Document.prototype, "cookie") ||
- Object.getOwnPropertyDescriptor(HTMLDocument.prototype, "cookie");
- if (cookieDesc && cookieDesc.configurable) {
- this.hooked["Cookie"] = document.cookie;
- Object.defineProperty(document, "cookie", {
- set: function (val) {
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook捕获到cookie设置->`,
- val
- );
- console.hooks.debugger();
- console.hooks.hooked["Cookie"] = val;
- return val;
- },
- get: function () {
- return (console.hooks.hooked["Cookie"] = "");
- },
- configurable: true,
- });
- } else {
- var org = document.__lookupSetter__("cookie");
- document.__defineSetter__("cookie", function (cookie) {
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Cookie Set as`,
- cookie
- );
- console.hooks.debugger();
- org = cookie;
- });
- document.__defineGetter__("cookie", function () {
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Cookie Got`,
- org
- );
- console.hooks.debugger();
- return org;
- });
- }
- } catch (e) {
- this.rawlog(`${console.hooks.settings.prefix}Cookie hook failed!`);
- }
- },
-
- hookLocalStorage: function () {
- this.hookfunc(localStorage, "getItem");
- this.hookfunc(localStorage, "setItem");
- this.hookfunc(localStorage, "removeItem");
- this.hookfunc(localStorage, "clear");
- this.rawlog(`${console.hooks.settings.prefix}LocalStorage hooked!`);
- },
-
- hookValueViaGetSet: function (name, obj, key) {
- if (obj[key]) {
- this.hooked[key] = obj[key];
- }
- var obj_name = `OBJ_${name}.${key}`;
- var org = obj.__lookupSetter__(key);
- obj.__defineSetter__(key, function (val) {
- org = console.hooks.hooked[key];
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook value set `,
- obj_name,
- "value->",
- org,
- "newvalue->",
- val
- );
- console.hooks.debugger();
- console.hooks.hooked[key] = val;
- });
- obj.__defineGetter__(key, function () {
- org = console.hooks.hooked[key];
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook value get `,
- obj_name,
- "value->",
- org
- );
- console.hooks.debugger();
- return org;
- });
- },
-
- // return default getsetter obj
- GetSetter(obj_name, key) {
- return {
- get: function (target, property, receiver) {
- var ret = target[property];
- if (key === "default_all") {
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook Proxy value get`,
- `${obj_name}.${property}`,
- "value->",
- ret
- );
- console.hooks.debugger();
- }
- if (property == key && key != "default_all") {
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook Proxy value get`,
- `${obj_name}.${property}`,
- "value->",
- ret
- );
- console.hooks.debugger();
- }
- return target[property];
- },
- set: function (target, property, newValue, receiver) {
- var ret = target[property];
- if (key === "default_all") {
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook Proxy value set`,
- `${obj_name}.${property}`,
- "value->",
- ret,
- "newvalue->",
- newValue
- );
- console.hooks.debugger();
- }
- if (property == key && key != "default_all") {
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook Proxy value get`,
- `${obj_name}.${property}`,
- "value->",
- ret,
- "newvalue->",
- newValue
- );
- console.hooks.debugger();
- }
- target[property] = newValue;
- return true;
- },
- };
- },
-
- // hooks value using proxy
- // usage: obj = hookValueViaProxy("name", obj)
- hookValueViaProxy: function (name, obj, key = "default_all") {
- var obj_name = "OBJ_" + name;
- return this.utils.createProxy(obj, this.GetSetter(obj_name, key));
- },
-
- hookValueViaObject: function (name, obj, key) {
- var obj_desc = Object.getOwnPropertyDescriptor(obj, key);
- if (!obj_desc || !obj_desc.configurable || obj[key] === undefined) {
- return Error("No Priv to set Property or No such keys!");
- }
- var obj_name = "OBJ_" + name;
- this.hooked[obj_name] = obj[key];
- Object.defineProperty(obj, key, {
- configurable: true,
- get() {
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook Object value get`,
- `${obj_name}.${key}`,
- "value->",
- console.hooks.hooked[obj_name]
- );
- console.hooks.debugger();
- return console.hooks.hooked[obj_name];
- },
- set(v) {
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook Proxy value get`,
- `${obj_name}.${key}`,
- "value->",
- console.hooks.hooked[obj_name],
- "newvalue->",
- v
- );
- console.hooks.hooked[obj_name] = v;
- },
- });
- },
-
- hookEvents: function (params) {
- var placeToReplace;
- if (window.EventTarget && EventTarget.prototype.addEventListener) {
- placeToReplace = EventTarget;
- } else {
- placeToReplace = Element;
- }
- this.hookfunc(
- placeToReplace.prototype,
- "addEventListener",
- function (res) {
- let [ret, originalFunction, arguments] = res;
- console.hooks.rawlog(
- `${console.hooks.settings.prefix}Hook event listener added!`,
- arguments
- );
- }
- );
- },
-
- antiDebuggerLoops: function () {
- processDebugger = (type, res) => {
- let [originalFunction, arguments, t] = res;
- var handler = arguments[0];
- console.hooks.debugger();
- if (handler.toString().includes("debugger")) {
- console.hooks.log(
- `${console.hooks.settings.prefix}found debug loop in ${type}`
- );
- console.hooks.debugger();
- let func = handler.toString().replaceAll("debugger", `console.error(1332)`);
- arguments[0] = new Function("return " + func)();
- return arguments;
- } else {
- return arguments;
- }
- };
-
- this.hookfunc(
- window,
- "setInterval",
- () => {},
- (res) => {
- return processDebugger("setInterval", res);
- },
- true
- );
-
- this.hookfunc(
- window,
- "setTimeout",
- () => {},
- (res) => {
- return processDebugger("setTimeout", res);
- },
- true
- );
-
- this.hookfunc(
- window,
- "eval",
- () => {},
- (res) => {
- return processDebugger("eval", res);
- },
- true
- );
-
- this.hookfunc(
- Function.prototype,
- "constructor",
- (res) => {
- let [ret, originalFunction, arguments, env] = res;
- if (ret.toString().includes("debugger")) {
- console.hooks.log(
- `${console.hooks.settings.prefix}found debug loop in Function constructor`
- );
- console.hooks.debugger();
- let func = handler.toString().replaceAll("debugger", "console.error(1331)");
- return new Function("return " + func)();
- }
- return ret;
- },
- () => {},
- true
- );
- },
-
- vueinfo: {
- findVueRoot(root) {
- const queue = [root];
- while (queue.length > 0) {
- const currentNode = queue.shift();
-
- if (
- currentNode.__vue__ ||
- currentNode.__vue_app__ ||
- currentNode._vnode
- ) {
- console.hooks.log("vue detected on root element:", currentNode);
- return currentNode;
- }
-
- for (let i = 0; i < currentNode.childNodes.length; i++) {
- queue.push(currentNode.childNodes[i]);
- }
- }
-
- return null;
- },
- findVueRouter(vueRoot) {
- let router;
-
- try {
- if (vueRoot.__vue_app__) {
- router =
- vueRoot.__vue_app__.config.globalProperties.$router.options.routes;
- console.hooks.log("find router in Vue object", vueRoot.__vue_app__);
- } else if (vueRoot.__vue__) {
- router = vueRoot.__vue__.$root.$options.router.options.routes;
- console.hooks.log("find router in Vue object", vueRoot.__vue__);
- }
- } catch (e) {}
-
- try {
- if (vueRoot.__vue__ && !router) {
- router = vueRoot.__vue__._router.options.routes;
- console.hooks.log("find router in Vue object", vueRoot.__vue__);
- }
- } catch (e) {}
-
- return router;
- },
- walkRouter(rootNode, callback) {
- const stack = [{ node: rootNode, path: "" }];
-
- while (stack.length) {
- const { node, path } = stack.pop();
-
- if (node && typeof node === "object") {
- if (Array.isArray(node)) {
- for (const key in node) {
- stack.push({
- node: node[key],
- path: this.mergePath(path, node[key].path),
- });
- }
- } else if (node.hasOwnProperty("children")) {
- stack.push({ node: node.children, path: path });
- }
- }
-
- callback(path, node);
- }
- },
- mergePath(parent, path) {
- if (path.indexOf(parent) === 0) {
- return path;
- }
-
- return (parent ? parent + "/" : "") + path;
- },
- dump() {
- const vueRoot = this.findVueRoot(document.body);
- if (!vueRoot) {
- console.error("This website is not developed by Vue");
- return;
- }
-
- let vueVersion;
- if (vueRoot.__vue__) {
- vueVersion = vueRoot.__vue__.$options._base.version;
- } else {
- vueVersion = vueRoot.__vue_app__.version;
- }
-
- console.hooks.log("Vue version is ", vueVersion);
- const routers = [];
-
- const vueRouter = this.findVueRouter(vueRoot);
- if (!vueRouter) {
- console.error("No Vue-Router detected");
- return;
- }
-
- console.hooks.log(vueRouter);
- this.walkRouter(vueRouter, function (path, node) {
- if (node.path) {
- routers.push({ name: node.name, path });
- }
- });
- console.table(routers);
- return routers;
- }
- },
- };
-
- // Console Hooks utils for
- {
- console.hooks.utils = {};
-
- console.hooks.utils.init = () => {
- console.hooks.utils.preloadCache();
- };
-
- /**
- * Wraps a JS Proxy Handler and strips it's presence from error stacks, in case the traps throw.
- *
- * The presence of a JS Proxy can be revealed as it shows up in error stack traces.
- *
- * @param {object} handler - The JS Proxy handler to wrap
- */
- console.hooks.utils.stripProxyFromErrors = (handler = {}) => {
- const newHandler = {
- setPrototypeOf: function (target, proto) {
- if (proto === null)
- throw new TypeError("Cannot convert object to primitive value");
- if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {
- throw new TypeError("Cyclic __proto__ value");
- }
- return Reflect.setPrototypeOf(target, proto);
- },
- };
- // We wrap each trap in the handler in a try/catch and modify the error stack if they throw
- const traps = Object.getOwnPropertyNames(handler);
- traps.forEach((trap) => {
- newHandler[trap] = function () {
- try {
- // Forward the call to the defined proxy handler
- return handler[trap].apply(this, arguments || []);
- } catch (err) {
- // Stack traces differ per browser, we only support chromium based ones currently
- if (!err || !err.stack || !err.stack.includes(`at `)) {
- throw err;
- }
-
- // When something throws within one of our traps the Proxy will show up in error stacks
- // An earlier implementation of this code would simply strip lines with a blacklist,
- // but it makes sense to be more surgical here and only remove lines related to our Proxy.
- // We try to use a known "anchor" line for that and strip it with everything above it.
- // If the anchor line cannot be found for some reason we fall back to our blacklist approach.
-
- const stripWithBlacklist = (stack, stripFirstLine = true) => {
- const blacklist = [
- `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply
- `at Object.${trap} `, // e.g. Object.get or Object.apply
- `at Object.newHandler.<computed> [as ${trap}] `, // caused by this very wrapper :-)
- ];
- return (
- err.stack
- .split("\n")
- // Always remove the first (file) line in the stack (guaranteed to be our proxy)
- .filter((line, index) => !(index === 1 && stripFirstLine))
- // Check if the line starts with one of our blacklisted strings
- .filter(
- (line) =>
- !blacklist.some((bl) => line.trim().startsWith(bl))
- )
- .join("\n")
- );
- };
-
- const stripWithAnchor = (stack, anchor) => {
- const stackArr = stack.split("\n");
- anchor =
- anchor || `at Object.newHandler.<computed> [as ${trap}] `; // Known first Proxy line in chromium
- const anchorIndex = stackArr.findIndex((line) =>
- line.trim().startsWith(anchor)
- );
- if (anchorIndex === -1) {
- return false; // 404, anchor not found
- }
- // Strip everything from the top until we reach the anchor line
- // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)
- stackArr.splice(1, anchorIndex);
- return stackArr.join("\n");
- };
-
- // Special cases due to our nested toString proxies
- err.stack = err.stack.replace(
- "at Object.toString (",
- "at Function.toString ("
- );
- if ((err.stack || "").includes("at Function.toString (")) {
- err.stack = stripWithBlacklist(err.stack, false);
- throw err;
- }
-
- // Try using the anchor method, fallback to blacklist if necessary
- err.stack =
- stripWithAnchor(err.stack) || stripWithBlacklist(err.stack);
-
- throw err; // Re-throw our now sanitized error
- }
- };
- });
- return newHandler;
- };
-
- /**
- * Strip error lines from stack traces until (and including) a known line the stack.
- *
- * @param {object} err - The error to sanitize
- * @param {string} anchor - The string the anchor line starts with
- */
- console.hooks.utils.stripErrorWithAnchor = (err, anchor) => {
- const stackArr = err.stack.split("\n");
- const anchorIndex = stackArr.findIndex((line) =>
- line.trim().startsWith(anchor)
- );
- if (anchorIndex === -1) {
- return err; // 404, anchor not found
- }
- // Strip everything from the top until we reach the anchor line (remove anchor line as well)
- // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)
- stackArr.splice(1, anchorIndex);
- err.stack = stackArr.join("\n");
- return err;
- };
-
- /**
- * Replace the property of an object in a stealthy way.
- *
- * Note: You also want to work on the prototype of an object most often,
- * as you'd otherwise leave traces (e.g. showing up in Object.getOwnPropertyNames(obj)).
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
- *
- * @example
- * replaceProperty(WebGLRenderingContext.prototype, 'getParameter', { value: "alice" })
- * // or
- * replaceProperty(Object.getPrototypeOf(navigator), 'languages', { get: () => ['en-US', 'en'] })
- *
- * @param {object} obj - The object which has the property to replace
- * @param {string} propName - The property name to replace
- * @param {object} descriptorOverrides - e.g. { value: "alice" }
- */
- console.hooks.utils.replaceProperty = (
- obj,
- propName,
- descriptorOverrides = {}
- ) => {
- return Object.defineProperty(obj, propName, {
- // Copy over the existing descriptors (writable, enumerable, configurable, etc)
- ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),
- // Add our overrides (e.g. value, get())
- ...descriptorOverrides,
- });
- };
-
- /**
- * Preload a cache of function copies and data.
- *
- * For a determined enough observer it would be possible to overwrite and sniff usage of functions
- * we use in our internal Proxies, to combat that we use a cached copy of those functions.
- *
- * Note: Whenever we add a `Function.prototype.toString` proxy we should preload the cache before,
- * by executing `console.hooks.utils.preloadCache()` before the proxy is applied (so we don't cause recursive lookups).
- *
- * This is evaluated once per execution context (e.g. window)
- */
- console.hooks.utils.preloadCache = () => {
- if (console.hooks.utils.cache) {
- return;
- }
- console.hooks.utils.cache = {
- // Used in our proxies
- Reflect: {
- get: Reflect.get.bind(Reflect),
- apply: Reflect.apply.bind(Reflect),
- },
- // Used in `makeNativeString`
- nativeToStringStr: Function.toString + "", // => `function toString() { [native code] }`
- };
- };
-
- /**
- * Utility function to generate a cross-browser `toString` result representing native code.
- *
- * There's small differences: Chromium uses a single line, whereas FF & Webkit uses multiline strings.
- * To future-proof this we use an existing native toString result as the basis.
- *
- * The only advantage we have over the other team is that our JS runs first, hence we cache the result
- * of the native toString result once, so they cannot spoof it afterwards and reveal that we're using it.
- *
- * @example
- * makeNativeString('foobar') // => `function foobar() { [native code] }`
- *
- * @param {string} [name] - Optional function name
- */
- console.hooks.utils.makeNativeString = (name = "") => {
- return console.hooks.utils.cache.nativeToStringStr.replace(
- "toString",
- name || ""
- );
- };
-
- /**
- * Helper function to modify the `toString()` result of the provided object.
- *
- * Note: Use `console.hooks.utils.redirectToString` instead when possible.
- *
- * There's a quirk in JS Proxies that will cause the `toString()` result to differ from the vanilla Object.
- * If no string is provided we will generate a `[native code]` thing based on the name of the property object.
- *
- * @example
- * patchToString(WebGLRenderingContext.prototype.getParameter, 'function getParameter() { [native code] }')
- *
- * @param {object} obj - The object for which to modify the `toString()` representation
- * @param {string} str - Optional string used as a return value
- */
- console.hooks.utils.patchToString = (obj, str = "") => {
- const handler = {
- apply: function (target, ctx) {
- // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""`
- if (ctx === Function.prototype.toString) {
- return console.hooks.utils.makeNativeString("toString");
- }
- // `toString` targeted at our proxied Object detected
- if (ctx === obj) {
- // We either return the optional string verbatim or derive the most desired result automatically
- return str || console.hooks.utils.makeNativeString(obj.name);
- }
- // Check if the toString protype of the context is the same as the global prototype,
- // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case
- const hasSameProto = Object.getPrototypeOf(
- Function.prototype.toString
- ).isPrototypeOf(ctx.toString); // eslint-disable-line no-prototype-builtins
- if (!hasSameProto) {
- // Pass the call on to the local Function.prototype.toString instead
- return ctx.toString();
- }
- return target.call(ctx);
- },
- };
-
- const toStringProxy = new Proxy(
- Function.prototype.toString,
- console.hooks.utils.stripProxyFromErrors(handler)
- );
- console.hooks.utils.replaceProperty(Function.prototype, "toString", {
- value: toStringProxy,
- });
- };
-
- /**
- * Make all nested functions of an object native.
- *
- * @param {object} obj
- */
- console.hooks.utils.patchToStringNested = (obj = {}) => {
- return console.hooks.utils.execRecursively(
- obj,
- ["function"],
- utils.patchToString
- );
- };
-
- /**
- * Redirect toString requests from one object to another.
- *
- * @param {object} proxyObj - The object that toString will be called on
- * @param {object} originalObj - The object which toString result we wan to return
- */
- console.hooks.utils.redirectToString = (proxyObj, originalObj) => {
- const handler = {
- apply: function (target, ctx) {
- // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""`
- if (ctx === Function.prototype.toString) {
- return console.hooks.utils.makeNativeString("toString");
- }
-
- // `toString` targeted at our proxied Object detected
- if (ctx === proxyObj) {
- const fallback = () =>
- originalObj && originalObj.name
- ? console.hooks.utils.makeNativeString(originalObj.name)
- : console.hooks.utils.makeNativeString(proxyObj.name);
-
- // Return the toString representation of our original object if possible
- return originalObj + "" || fallback();
- }
-
- if (typeof ctx === "undefined" || ctx === null) {
- return target.call(ctx);
- }
-
- // Check if the toString protype of the context is the same as the global prototype,
- // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case
- const hasSameProto = Object.getPrototypeOf(
- Function.prototype.toString
- ).isPrototypeOf(ctx.toString); // eslint-disable-line no-prototype-builtins
- if (!hasSameProto) {
- // Pass the call on to the local Function.prototype.toString instead
- return ctx.toString();
- }
-
- return target.call(ctx);
- },
- };
-
- const toStringProxy = new Proxy(
- Function.prototype.toString,
- console.hooks.utils.stripProxyFromErrors(handler)
- );
- console.hooks.utils.replaceProperty(Function.prototype, "toString", {
- value: toStringProxy,
- });
- };
-
- /**
- * All-in-one method to replace a property with a JS Proxy using the provided Proxy handler with traps.
- *
- * Will stealthify these aspects (strip error stack traces, redirect toString, etc).
- * Note: This is meant to modify native Browser APIs and works best with prototype objects.
- *
- * @example
- * replaceWithProxy(WebGLRenderingContext.prototype, 'getParameter', proxyHandler)
- *
- * @param {object} obj - The object which has the property to replace
- * @param {string} propName - The name of the property to replace
- * @param {object} handler - The JS Proxy handler to use
- */
- console.hooks.utils.replaceWithProxy = (obj, propName, handler) => {
- const originalObj = obj[propName];
- const proxyObj = new Proxy(
- obj[propName],
- console.hooks.utils.stripProxyFromErrors(handler)
- );
-
- console.hooks.utils.replaceProperty(obj, propName, { value: proxyObj });
- console.hooks.utils.redirectToString(proxyObj, originalObj);
-
- return true;
- };
- /**
- * All-in-one method to replace a getter with a JS Proxy using the provided Proxy handler with traps.
- *
- * @example
- * replaceGetterWithProxy(Object.getPrototypeOf(navigator), 'vendor', proxyHandler)
- *
- * @param {object} obj - The object which has the property to replace
- * @param {string} propName - The name of the property to replace
- * @param {object} handler - The JS Proxy handler to use
- */
- console.hooks.utils.replaceGetterWithProxy = (obj, propName, handler) => {
- const fn = Object.getOwnPropertyDescriptor(obj, propName).get;
- const fnStr = fn.toString(); // special getter function string
- const proxyObj = new Proxy(
- fn,
- console.hooks.utils.stripProxyFromErrors(handler)
- );
-
- console.hooks.utils.replaceProperty(obj, propName, { get: proxyObj });
- console.hooks.utils.patchToString(proxyObj, fnStr);
-
- return true;
- };
-
- /**
- * All-in-one method to replace a getter and/or setter. Functions get and set
- * of handler have one more argument that contains the native function.
- *
- * @example
- * replaceGetterSetter(HTMLIFrameElement.prototype, 'contentWindow', handler)
- *
- * @param {object} obj - The object which has the property to replace
- * @param {string} propName - The name of the property to replace
- * @param {object} handlerGetterSetter - The handler with get and/or set
- * functions
- * @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description
- */
- console.hooks.utils.replaceGetterSetter = (
- obj,
- propName,
- handlerGetterSetter
- ) => {
- const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(
- obj,
- propName
- );
- const handler = { ...ownPropertyDescriptor };
-
- if (handlerGetterSetter.get !== undefined) {
- const nativeFn = ownPropertyDescriptor.get;
- handler.get = function () {
- return handlerGetterSetter.get.call(this, nativeFn.bind(this));
- };
- console.hooks.utils.redirectToString(handler.get, nativeFn);
- }
-
- if (handlerGetterSetter.set !== undefined) {
- const nativeFn = ownPropertyDescriptor.set;
- handler.set = function (newValue) {
- handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this));
- };
- console.hooks.utils.redirectToString(handler.set, nativeFn);
- }
-
- Object.defineProperty(obj, propName, handler);
- };
-
- /**
- * All-in-one method to mock a non-existing property with a JS Proxy using the provided Proxy handler with traps.
- *
- * Will stealthify these aspects (strip error stack traces, redirect toString, etc).
- *
- * @example
- * mockWithProxy(chrome.runtime, 'sendMessage', function sendMessage() {}, proxyHandler)
- *
- * @param {object} obj - The object which has the property to replace
- * @param {string} propName - The name of the property to replace or create
- * @param {object} pseudoTarget - The JS Proxy target to use as a basis
- * @param {object} handler - The JS Proxy handler to use
- */
- console.hooks.utils.mockWithProxy = (
- obj,
- propName,
- pseudoTarget,
- handler
- ) => {
- const proxyObj = new Proxy(
- pseudoTarget,
- console.hooks.utils.stripProxyFromErrors(handler)
- );
-
- console.hooks.utils.replaceProperty(obj, propName, { value: proxyObj });
- console.hooks.utils.patchToString(proxyObj);
-
- return true;
- };
-
- /**
- * All-in-one method to create a new JS Proxy with stealth tweaks.
- *
- * This is meant to be used whenever we need a JS Proxy but don't want to replace or mock an existing known property.
- *
- * Will stealthify certain aspects of the Proxy (strip error stack traces, redirect toString, etc).
- *
- * @example
- * createProxy(navigator.mimeTypes.__proto__.namedItem, proxyHandler) // => Proxy
- *
- * @param {object} pseudoTarget - The JS Proxy target to use as a basis
- * @param {object} handler - The JS Proxy handler to use
- */
- console.hooks.utils.createProxy = (pseudoTarget, handler) => {
- const proxyObj = new Proxy(
- pseudoTarget,
- console.hooks.utils.stripProxyFromErrors(handler)
- );
- console.hooks.utils.patchToString(proxyObj);
-
- return proxyObj;
- };
-
- /**
- * Helper function to split a full path to an Object into the first part and property.
- *
- * @example
- * splitObjPath(`HTMLMediaElement.prototype.canPlayType`)
- * // => {objName: "HTMLMediaElement.prototype", propName: "canPlayType"}
- *
- * @param {string} objPath - The full path to an object as dot notation string
- */
- console.hooks.utils.splitObjPath = (objPath) => ({
- // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`
- objName: objPath.split(".").slice(0, -1).join("."),
- // Extract last dot entry ==> `canPlayType`
- propName: objPath.split(".").slice(-1)[0],
- });
-
- /**
- * Convenience method to replace a property with a JS Proxy using the provided objPath.
- *
- * Supports a full path (dot notation) to the object as string here, in case that makes it easier.
- *
- * @example
- * replaceObjPathWithProxy('WebGLRenderingContext.prototype.getParameter', proxyHandler)
- *
- * @param {string} objPath - The full path to an object (dot notation string) to replace
- * @param {object} handler - The JS Proxy handler to use
- */
- console.hooks.utils.replaceObjPathWithProxy = (objPath, handler) => {
- const { objName, propName } = console.hooks.utils.splitObjPath(objPath);
- const obj = eval(objName); // eslint-disable-line no-eval
- return console.hooks.utils.replaceWithProxy(obj, propName, handler);
- };
-
- /**
- * Traverse nested properties of an object recursively and apply the given function on a whitelist of value types.
- *
- * @param {object} obj
- * @param {array} typeFilter - e.g. `['function']`
- * @param {Function} fn - e.g. `console.hooks.utils.patchToString`
- */
- console.hooks.utils.execRecursively = (obj = {}, typeFilter = [], fn) => {
- function recurse(obj) {
- for (const key in obj) {
- if (obj[key] === undefined) {
- continue;
- }
- if (obj[key] && typeof obj[key] === "object") {
- recurse(obj[key]);
- } else {
- if (obj[key] && typeFilter.includes(typeof obj[key])) {
- fn.call(this, obj[key]);
- }
- }
- }
- }
- recurse(obj);
- return obj;
- };
-
- /**
- * Everything we run through e.g. `page.evaluate` runs in the browser context, not the NodeJS one.
- * That means we cannot just use reference variables and functions from outside code, we need to pass everything as a parameter.
- *
- * Unfortunately the data we can pass is only allowed to be of primitive types, regular functions don't survive the built-in serialization process.
- * This utility function will take an object with functions and stringify them, so we can pass them down unharmed as strings.
- *
- * We use this to pass down our utility functions as well as any other functions (to be able to split up code better).
- *
- * @see console.hooks.utils.materializeFns
- *
- * @param {object} fnObj - An object containing functions as properties
- */
- console.hooks.utils.stringifyFns = (fnObj = { hello: () => "world" }) => {
- // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine
- // https://github.com/feross/fromentries
- function fromEntries(iterable) {
- return [...iterable].reduce((obj, [key, val]) => {
- obj[key] = val;
- return obj;
- }, {});
- }
- return (Object.fromEntries || fromEntries)(
- Object.entries(fnObj)
- .filter(([key, value]) => typeof value === "function")
- .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval
- );
- };
-
- /**
- * Utility function to reverse the process of `console.hooks.utils.stringifyFns`.
- * Will materialize an object with stringified functions (supports classic and fat arrow functions).
- *
- * @param {object} fnStrObj - An object containing stringified functions as properties
- */
- console.hooks.utils.materializeFns = (
- fnStrObj = { hello: "() => 'world'" }
- ) => {
- return Object.fromEntries(
- Object.entries(fnStrObj).map(([key, value]) => {
- if (value.startsWith("function")) {
- // some trickery is needed to make oldschool functions work :-)
- return [key, eval(`() => ${value}`)()]; // eslint-disable-line no-eval
- } else {
- // arrow functions just work
- return [key, eval(value)]; // eslint-disable-line no-eval
- }
- })
- );
- };
-
- // Proxy handler templates for re-usability
- console.hooks.utils.makeHandler = () => ({
- // Used by simple `navigator` getter evasions
- getterValue: (value) => ({
- apply(target, ctx, args) {
- // Let's fetch the value first, to trigger and escalate potential errors
- // Illegal invocations like `navigator.__proto__.vendor` will throw here
- console.hooks.utils.cache.Reflect.apply(...arguments);
- return value;
- },
- }),
- });
-
- /**
- * Compare two arrays.
- *
- * @param {array} array1 - First array
- * @param {array} array2 - Second array
- */
- console.hooks.utils.arrayEquals = (array1, array2) => {
- if (array1.length !== array2.length) {
- return false;
- }
- for (let i = 0; i < array1.length; ++i) {
- if (array1[i] !== array2[i]) {
- return false;
- }
- }
- return true;
- };
-
- /**
- * Cache the method return according to its arguments.
- *
- * @param {Function} fn - A function that will be cached
- */
- console.hooks.utils.memoize = (fn) => {
- const cache = [];
- return function (...args) {
- if (!cache.some((c) => console.hooks.utils.arrayEquals(c.key, args))) {
- cache.push({ key: args, value: fn.apply(this, args) });
- }
- return cache.find((c) => console.hooks.utils.arrayEquals(c.key, args))
- .value;
- };
- };
- }
- // auto run init
- console.hooks.init();
- })();