// ==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 v1.0.0
// ==/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();
})();