ConsoleHook

utils of hook javascript function and value changes for js reverse engineering

目前為 2025-01-22 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name ConsoleHook
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025-01-22
  5. // @description utils of hook javascript function and value changes for js reverse engineering
  6. // @author @Esonhugh
  7. // @match http://*
  8. // @match https://*
  9. // @include http://*
  10. // @include https://*
  11. // @icon https://blog.eson.ninja/img/reol.png
  12. // @grant none
  13. // @license MIT
  14. // @run-at document-start
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. console.hooks = {
  19. // settings
  20. settings: {
  21. // trigger debugger if hook is caught
  22. autoDebug: false,
  23. // don't let page jump to other place
  24. blockPageJump: false,
  25. // log prefix
  26. prefix: "[EHOOKS] ", // u can filter all this things with this tag
  27. // init with eventListener added
  28. checkEventListnerAdded: false,
  29. // init with cookie change listener
  30. checkCookieChange: false,
  31. // init with localstorage get set
  32. checkLocalStorageGetSet: false,
  33. // anti dead loop debugger in script
  34. antiDeadLoopDebugger: true,
  35.  
  36. // hidden too many default debug logs if you don't need it
  37. hiddenlog: false,
  38. },
  39.  
  40. rawlog: function (...data) {
  41. if (this.settings.hiddenlog) {
  42. return; // don't print
  43. }
  44. return console.debug(...data);
  45. },
  46.  
  47. log: console.warn,
  48.  
  49. debugger: function () {
  50. // traped in debug
  51. if (this.settings.autoDebug) {
  52. // dump the real stack for u
  53. this.dumpstack();
  54. debugger;
  55. }
  56. },
  57.  
  58. hooked: {},
  59.  
  60. dumpstack(print = true) {
  61. var err = new Error();
  62. var stack = err.stack.split("\n");
  63. var ret = [`${this.settings.prefix}DUMP STACK: `];
  64. for (var i of stack) {
  65. if (!i.includes("userscript.html") && i !== "Error") {
  66. ret = ret.concat(i);
  67. }
  68. }
  69. ret = ret.join("\n");
  70. if (print) {
  71. this.log(ret);
  72. }
  73. return ret;
  74. },
  75.  
  76. dumpHooked() {
  77. for (var i in this.hooked) {
  78. if (this.hooked[i].toString) {
  79. this.log(`${i}: ${this.hooked[i].toString()}`);
  80. } else {
  81. this.log(`${i}: ${this.hooked[i]}`);
  82. }
  83. }
  84. },
  85.  
  86. hookfunc: function (
  87. object,
  88. functionName,
  89. posthook = () => {},
  90. prehook = () => {},
  91. slience = false
  92. ) {
  93. (function (originalFunction) {
  94. object[functionName] = function () {
  95. // hook logic
  96. // 1. Allow Check
  97. var args = prehook([originalFunction, arguments, this]);
  98. var realargs = arguments;
  99. if (args) {
  100. realargs = args;
  101. } else {
  102. realargs = arguments;
  103. }
  104. // 2. Execute old function
  105. var returnValue = originalFunction.apply(this, realargs);
  106. if (!slience) { // not slience
  107. console.hooks.rawlog(
  108. `${console.hooks.settings.prefix}Hook function trap-> func[${functionName}]`,
  109. "args->",
  110. realargs,
  111. "ret->",
  112. returnValue
  113. );
  114. console.hooks.debugger();
  115. }
  116. // 3. Post hook change values
  117. var newReturn = posthook([
  118. returnValue,
  119. originalFunction,
  120. realargs,
  121. this,
  122. ]);
  123. if (newReturn) {
  124. return newReturn;
  125. }
  126. return returnValue;
  127. };
  128. object[functionName].toString = function () {
  129. console.hooks.log(
  130. `${console.hooks.settings.prefix}Found hook toString check!`,
  131. originalFunction
  132. );
  133. console.hooks.debugger();
  134. return originalFunction.toString();
  135. };
  136. console.hooks.hooked[functionName] = originalFunction;
  137. })(object[functionName]);
  138. this.log(
  139. `${console.hooks.settings.prefix}Hook function`,
  140. functionName,
  141. "success!"
  142. );
  143. },
  144.  
  145. unhookfunc: function (object, functionName) {
  146. object[functionName] = console.hooks.hooked[functionName];
  147. this.rawlog(
  148. `${console.hooks.settings.prefix}unHook function`,
  149. functionName,
  150. "success!"
  151. );
  152. },
  153.  
  154. hookCookie: function () {
  155. try {
  156. var cookieDesc =
  157. Object.getOwnPropertyDescriptor(Document.prototype, "cookie") ||
  158. Object.getOwnPropertyDescriptor(HTMLDocument.prototype, "cookie");
  159. if (cookieDesc && cookieDesc.configurable) {
  160. this.hooked["Cookie"] = document.cookie;
  161. Object.defineProperty(document, "cookie", {
  162. set: function (val) {
  163. console.hooks.rawlog(
  164. `${console.hooks.settings.prefix}Hook捕获到cookie设置->`,
  165. val
  166. );
  167. console.hooks.debugger();
  168. console.hooks.hooked["Cookie"] = val;
  169. return val;
  170. },
  171. get: function () {
  172. return (console.hooks.hooked["Cookie"] = "");
  173. },
  174. configurable: true,
  175. });
  176. } else {
  177. var org = document.__lookupSetter__("cookie");
  178. document.__defineSetter__("cookie", function (cookie) {
  179. console.hooks.rawlog(
  180. `${console.hooks.settings.prefix}Cookie Set as`,
  181. cookie
  182. );
  183. console.hooks.debugger();
  184. org = cookie;
  185. });
  186. document.__defineGetter__("cookie", function () {
  187. console.hooks.rawlog(
  188. `${console.hooks.settings.prefix}Cookie Got`,
  189. org
  190. );
  191. console.hooks.debugger();
  192. return org;
  193. });
  194. }
  195. } catch (e) {
  196. this.rawlog(`${console.hooks.settings.prefix}Cookie hook failed!`);
  197. }
  198. },
  199.  
  200. hookLocalStorage: function () {
  201. this.hookfunc(localStorage, "getItem");
  202. this.hookfunc(localStorage, "setItem");
  203. this.hookfunc(localStorage, "removeItem");
  204. this.hookfunc(localStorage, "clear");
  205. this.rawlog(`${console.hooks.settings.prefix}LocalStorage hooked!`);
  206. },
  207.  
  208. hookValueViaGetSet: function (name, obj, key) {
  209. if (obj[key]) {
  210. this.hooked[key] = obj[key];
  211. }
  212. var obj_name = `OBJ_${name}.${key}`;
  213. var org = obj.__lookupSetter__(key);
  214. obj.__defineSetter__(key, function (val) {
  215. org = console.hooks.hooked[key];
  216. console.hooks.rawlog(
  217. `${console.hooks.settings.prefix}Hook value set `,
  218. obj_name,
  219. "value->",
  220. org,
  221. "newvalue->",
  222. val
  223. );
  224. console.hooks.debugger();
  225. console.hooks.hooked[key] = val;
  226. });
  227. obj.__defineGetter__(key, function () {
  228. org = console.hooks.hooked[key];
  229. console.hooks.rawlog(
  230. `${console.hooks.settings.prefix}Hook value get `,
  231. obj_name,
  232. "value->",
  233. org
  234. );
  235. console.hooks.debugger();
  236. return org;
  237. });
  238. },
  239.  
  240. GetSetter(obj_name, key) {
  241. return {
  242. get: function (target, property, receiver) {
  243. var ret = target[property];
  244. if (key === "default_all") {
  245. console.hooks.rawlog(
  246. `${console.hooks.settings.prefix}Hook Proxy value get`,
  247. `${obj_name}.${property}`,
  248. "value->",
  249. ret
  250. );
  251. console.hooks.debugger();
  252. }
  253. if (property == key && key != "default_all") {
  254. console.hooks.rawlog(
  255. `${console.hooks.settings.prefix}Hook Proxy value get`,
  256. `${obj_name}.${property}`,
  257. "value->",
  258. ret
  259. );
  260. console.hooks.debugger();
  261. }
  262. return target[property];
  263. },
  264. set: function (target, property, newValue, receiver) {
  265. var ret = target[property];
  266. if (key === "default_all") {
  267. console.hooks.rawlog(
  268. `${console.hooks.settings.prefix}Hook Proxy value set`,
  269. `${obj_name}.${property}`,
  270. "value->",
  271. ret,
  272. "newvalue->",
  273. newValue
  274. );
  275. console.hooks.debugger();
  276. }
  277. if (property == key && key != "default_all") {
  278. console.hooks.rawlog(
  279. `${console.hooks.settings.prefix}Hook Proxy value get`,
  280. `${obj_name}.${property}`,
  281. "value->",
  282. ret,
  283. "newvalue->",
  284. newValue
  285. );
  286. console.hooks.debugger();
  287. }
  288. target[property] = newValue;
  289. return true;
  290. },
  291. };
  292. },
  293.  
  294. hookValueViaProxy: function (name, obj, key = "default_all") {
  295. var obj_name = "OBJ_" + name;
  296. return this.utils.createProxy(obj, this.GetSetter(obj_name, key));
  297. },
  298.  
  299. hookValueViaObject: function (name, obj, key) {
  300. var obj_desc = Object.getOwnPropertyDescriptor(obj, key);
  301. if (!obj_desc || !obj_desc.configurable || obj[key] === undefined) {
  302. return Error("No Priv to set Property or No such keys!");
  303. }
  304. var obj_name = "OBJ_" + name;
  305. this.hooked[obj_name] = obj[key];
  306. Object.defineProperty(obj, key, {
  307. configurable: true,
  308. get() {
  309. console.hooks.rawlog(
  310. `${console.hooks.settings.prefix}Hook Object value get`,
  311. `${obj_name}.${key}`,
  312. "value->",
  313. console.hooks.hooked[obj_name]
  314. );
  315. console.hooks.debugger();
  316. return console.hooks.hooked[obj_name];
  317. },
  318. set(v) {
  319. console.hooks.rawlog(
  320. `${console.hooks.settings.prefix}Hook Proxy value get`,
  321. `${obj_name}.${key}`,
  322. "value->",
  323. console.hooks.hooked[obj_name],
  324. "newvalue->",
  325. v
  326. );
  327. console.hooks.hooked[obj_name] = v;
  328. },
  329. });
  330. },
  331.  
  332. hookEvents: function (params) {
  333. var placeToReplace;
  334. if (window.EventTarget && EventTarget.prototype.addEventListener) {
  335. placeToReplace = EventTarget;
  336. } else {
  337. placeToReplace = Element;
  338. }
  339. this.hookfunc(
  340. placeToReplace.prototype,
  341. "addEventListener",
  342. function (res) {
  343. let [ret, originalFunction, arguments] = res;
  344. console.hooks.rawlog(
  345. `${console.hooks.settings.prefix}Hook event listener added!`,
  346. arguments
  347. );
  348. }
  349. );
  350. },
  351.  
  352. antiDebuggerLoops: function () {
  353. processDebugger = (type, res) => {
  354. let [originalFunction, arguments, t] = res;
  355. var handler = arguments[0];
  356. console.hooks.debugger();
  357. if (handler.toString().includes("debugger")) {
  358. console.hooks.log(
  359. `${console.hooks.settings.prefix}found debug loop in ${type}`
  360. );
  361. console.hooks.debugger();
  362. let func = handler.toString().replaceAll("debugger", "");
  363. arguments[0] = new Function("return " + func)();
  364. return arguments;
  365. } else {
  366. return arguments;
  367. }
  368. };
  369.  
  370. this.hookfunc(
  371. window,
  372. "setInterval",
  373. () => {},
  374. (res) => {
  375. return processDebugger("setInterval", res);
  376. }, true
  377. );
  378. this.hookfunc(
  379. window,
  380. "setTimeout",
  381. () => {},
  382. (res) => {
  383. return processDebugger("setTimeout", res);
  384. }, true
  385. );
  386.  
  387. this.hookfunc(Function.prototype, "constructor", (res) => {
  388. let [ret, originalFunction, arguments, env] = res;
  389. if (ret.toString().includes("debugger")) {
  390. console.hooks.log(
  391. `${console.hooks.settings.prefix}found debug loop in Function constructor`
  392. );
  393. console.hooks.debugger();
  394. let func = ret.toString().replaceAll("debugger", "");
  395. return new Function("return " + func)();
  396. }
  397. return ret;
  398. });
  399. },
  400.  
  401. init: function () {
  402. if (this.utils)
  403. if (this.settings.blockPageJump) {
  404. window.onbeforeunload = function () {
  405. return "ANTI LEAVE";
  406. };
  407. }
  408. if (this.settings.checkEventListnerAdded) {
  409. this.hookEvents();
  410. }
  411. if (this.settings.checkCookieChange) {
  412. this.hookCookie();
  413. }
  414. if (this.settings.checkLocalStorageGetSet) {
  415. this.hookLocalStorage();
  416. }
  417. if (this.settings.antiDeadLoopDebugger) {
  418. this.antiDebuggerLoops();
  419. }
  420. },
  421.  
  422. main: function () {
  423. if (!this.settings.antiDeadLoopDebugger) {
  424. this.hookfunc(window, "setInterval");
  425. this.hookfunc(window, "setTimeout");
  426. this.hookfunc(Function.prototype, "constructor");
  427. }
  428. this.hookfunc(window, "eval");
  429. this.hookfunc(window, "Function");
  430. this.hookfunc(window, "atob");
  431. this.hookfunc(window, "btoa");
  432. this.hookfunc(window, "fetch");
  433. this.hookfunc(window, "encodeURI");
  434. this.hookfunc(window, "encodeURIComponent");
  435.  
  436. this.hookfunc(JSON, "parse");
  437. this.hookfunc(JSON, "stringify");
  438.  
  439. this.hookfunc(console, "log");
  440. // this.hookfunc(console, "warn")
  441. // this.hookfunc(console, "error")
  442. // this.hookfunc(console, "info")
  443. // this.hookfunc(console, "debug")
  444. // this.hookfunc(console, "table")
  445. // this.hookfunc(console, "trace")
  446. this.hookfunc(console, "clear");
  447. },
  448. };
  449.  
  450. // Console Hooks utils for
  451. {
  452. console.hooks.utils = {};
  453.  
  454. console.hooks.utils.init = () => {
  455. console.hooks.utils.preloadCache();
  456. };
  457.  
  458. /**
  459. * Wraps a JS Proxy Handler and strips it's presence from error stacks, in case the traps throw.
  460. *
  461. * The presence of a JS Proxy can be revealed as it shows up in error stack traces.
  462. *
  463. * @param {object} handler - The JS Proxy handler to wrap
  464. */
  465. console.hooks.utils.stripProxyFromErrors = (handler = {}) => {
  466. const newHandler = {
  467. setPrototypeOf: function (target, proto) {
  468. if (proto === null)
  469. throw new TypeError("Cannot convert object to primitive value");
  470. if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {
  471. throw new TypeError("Cyclic __proto__ value");
  472. }
  473. return Reflect.setPrototypeOf(target, proto);
  474. },
  475. };
  476. // We wrap each trap in the handler in a try/catch and modify the error stack if they throw
  477. const traps = Object.getOwnPropertyNames(handler);
  478. traps.forEach((trap) => {
  479. newHandler[trap] = function () {
  480. try {
  481. // Forward the call to the defined proxy handler
  482. return handler[trap].apply(this, arguments || []);
  483. } catch (err) {
  484. // Stack traces differ per browser, we only support chromium based ones currently
  485. if (!err || !err.stack || !err.stack.includes(`at `)) {
  486. throw err;
  487. }
  488.  
  489. // When something throws within one of our traps the Proxy will show up in error stacks
  490. // An earlier implementation of this code would simply strip lines with a blacklist,
  491. // but it makes sense to be more surgical here and only remove lines related to our Proxy.
  492. // We try to use a known "anchor" line for that and strip it with everything above it.
  493. // If the anchor line cannot be found for some reason we fall back to our blacklist approach.
  494.  
  495. const stripWithBlacklist = (stack, stripFirstLine = true) => {
  496. const blacklist = [
  497. `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply
  498. `at Object.${trap} `, // e.g. Object.get or Object.apply
  499. `at Object.newHandler.<computed> [as ${trap}] `, // caused by this very wrapper :-)
  500. ];
  501. return (
  502. err.stack
  503. .split("\n")
  504. // Always remove the first (file) line in the stack (guaranteed to be our proxy)
  505. .filter((line, index) => !(index === 1 && stripFirstLine))
  506. // Check if the line starts with one of our blacklisted strings
  507. .filter(
  508. (line) =>
  509. !blacklist.some((bl) => line.trim().startsWith(bl))
  510. )
  511. .join("\n")
  512. );
  513. };
  514.  
  515. const stripWithAnchor = (stack, anchor) => {
  516. const stackArr = stack.split("\n");
  517. anchor =
  518. anchor || `at Object.newHandler.<computed> [as ${trap}] `; // Known first Proxy line in chromium
  519. const anchorIndex = stackArr.findIndex((line) =>
  520. line.trim().startsWith(anchor)
  521. );
  522. if (anchorIndex === -1) {
  523. return false; // 404, anchor not found
  524. }
  525. // Strip everything from the top until we reach the anchor line
  526. // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)
  527. stackArr.splice(1, anchorIndex);
  528. return stackArr.join("\n");
  529. };
  530.  
  531. // Special cases due to our nested toString proxies
  532. err.stack = err.stack.replace(
  533. "at Object.toString (",
  534. "at Function.toString ("
  535. );
  536. if ((err.stack || "").includes("at Function.toString (")) {
  537. err.stack = stripWithBlacklist(err.stack, false);
  538. throw err;
  539. }
  540.  
  541. // Try using the anchor method, fallback to blacklist if necessary
  542. err.stack =
  543. stripWithAnchor(err.stack) || stripWithBlacklist(err.stack);
  544.  
  545. throw err; // Re-throw our now sanitized error
  546. }
  547. };
  548. });
  549. return newHandler;
  550. };
  551.  
  552. /**
  553. * Strip error lines from stack traces until (and including) a known line the stack.
  554. *
  555. * @param {object} err - The error to sanitize
  556. * @param {string} anchor - The string the anchor line starts with
  557. */
  558. console.hooks.utils.stripErrorWithAnchor = (err, anchor) => {
  559. const stackArr = err.stack.split("\n");
  560. const anchorIndex = stackArr.findIndex((line) =>
  561. line.trim().startsWith(anchor)
  562. );
  563. if (anchorIndex === -1) {
  564. return err; // 404, anchor not found
  565. }
  566. // Strip everything from the top until we reach the anchor line (remove anchor line as well)
  567. // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)
  568. stackArr.splice(1, anchorIndex);
  569. err.stack = stackArr.join("\n");
  570. return err;
  571. };
  572.  
  573. /**
  574. * Replace the property of an object in a stealthy way.
  575. *
  576. * Note: You also want to work on the prototype of an object most often,
  577. * as you'd otherwise leave traces (e.g. showing up in Object.getOwnPropertyNames(obj)).
  578. *
  579. * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  580. *
  581. * @example
  582. * replaceProperty(WebGLRenderingContext.prototype, 'getParameter', { value: "alice" })
  583. * // or
  584. * replaceProperty(Object.getPrototypeOf(navigator), 'languages', { get: () => ['en-US', 'en'] })
  585. *
  586. * @param {object} obj - The object which has the property to replace
  587. * @param {string} propName - The property name to replace
  588. * @param {object} descriptorOverrides - e.g. { value: "alice" }
  589. */
  590. console.hooks.utils.replaceProperty = (
  591. obj,
  592. propName,
  593. descriptorOverrides = {}
  594. ) => {
  595. return Object.defineProperty(obj, propName, {
  596. // Copy over the existing descriptors (writable, enumerable, configurable, etc)
  597. ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),
  598. // Add our overrides (e.g. value, get())
  599. ...descriptorOverrides,
  600. });
  601. };
  602.  
  603. /**
  604. * Preload a cache of function copies and data.
  605. *
  606. * For a determined enough observer it would be possible to overwrite and sniff usage of functions
  607. * we use in our internal Proxies, to combat that we use a cached copy of those functions.
  608. *
  609. * Note: Whenever we add a `Function.prototype.toString` proxy we should preload the cache before,
  610. * by executing `console.hooks.utils.preloadCache()` before the proxy is applied (so we don't cause recursive lookups).
  611. *
  612. * This is evaluated once per execution context (e.g. window)
  613. */
  614. console.hooks.utils.preloadCache = () => {
  615. if (console.hooks.utils.cache) {
  616. return;
  617. }
  618. console.hooks.utils.cache = {
  619. // Used in our proxies
  620. Reflect: {
  621. get: Reflect.get.bind(Reflect),
  622. apply: Reflect.apply.bind(Reflect),
  623. },
  624. // Used in `makeNativeString`
  625. nativeToStringStr: Function.toString + "", // => `function toString() { [native code] }`
  626. };
  627. };
  628.  
  629. /**
  630. * Utility function to generate a cross-browser `toString` result representing native code.
  631. *
  632. * There's small differences: Chromium uses a single line, whereas FF & Webkit uses multiline strings.
  633. * To future-proof this we use an existing native toString result as the basis.
  634. *
  635. * The only advantage we have over the other team is that our JS runs first, hence we cache the result
  636. * of the native toString result once, so they cannot spoof it afterwards and reveal that we're using it.
  637. *
  638. * @example
  639. * makeNativeString('foobar') // => `function foobar() { [native code] }`
  640. *
  641. * @param {string} [name] - Optional function name
  642. */
  643. console.hooks.utils.makeNativeString = (name = "") => {
  644. return console.hooks.utils.cache.nativeToStringStr.replace(
  645. "toString",
  646. name || ""
  647. );
  648. };
  649.  
  650. /**
  651. * Helper function to modify the `toString()` result of the provided object.
  652. *
  653. * Note: Use `console.hooks.utils.redirectToString` instead when possible.
  654. *
  655. * There's a quirk in JS Proxies that will cause the `toString()` result to differ from the vanilla Object.
  656. * If no string is provided we will generate a `[native code]` thing based on the name of the property object.
  657. *
  658. * @example
  659. * patchToString(WebGLRenderingContext.prototype.getParameter, 'function getParameter() { [native code] }')
  660. *
  661. * @param {object} obj - The object for which to modify the `toString()` representation
  662. * @param {string} str - Optional string used as a return value
  663. */
  664. console.hooks.utils.patchToString = (obj, str = "") => {
  665. const handler = {
  666. apply: function (target, ctx) {
  667. // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""`
  668. if (ctx === Function.prototype.toString) {
  669. return console.hooks.utils.makeNativeString("toString");
  670. }
  671. // `toString` targeted at our proxied Object detected
  672. if (ctx === obj) {
  673. // We either return the optional string verbatim or derive the most desired result automatically
  674. return str || console.hooks.utils.makeNativeString(obj.name);
  675. }
  676. // Check if the toString protype of the context is the same as the global prototype,
  677. // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case
  678. const hasSameProto = Object.getPrototypeOf(
  679. Function.prototype.toString
  680. ).isPrototypeOf(ctx.toString); // eslint-disable-line no-prototype-builtins
  681. if (!hasSameProto) {
  682. // Pass the call on to the local Function.prototype.toString instead
  683. return ctx.toString();
  684. }
  685. return target.call(ctx);
  686. },
  687. };
  688.  
  689. const toStringProxy = new Proxy(
  690. Function.prototype.toString,
  691. console.hooks.utils.stripProxyFromErrors(handler)
  692. );
  693. console.hooks.utils.replaceProperty(Function.prototype, "toString", {
  694. value: toStringProxy,
  695. });
  696. };
  697.  
  698. /**
  699. * Make all nested functions of an object native.
  700. *
  701. * @param {object} obj
  702. */
  703. console.hooks.utils.patchToStringNested = (obj = {}) => {
  704. return console.hooks.utils.execRecursively(
  705. obj,
  706. ["function"],
  707. utils.patchToString
  708. );
  709. };
  710.  
  711. /**
  712. * Redirect toString requests from one object to another.
  713. *
  714. * @param {object} proxyObj - The object that toString will be called on
  715. * @param {object} originalObj - The object which toString result we wan to return
  716. */
  717. console.hooks.utils.redirectToString = (proxyObj, originalObj) => {
  718. const handler = {
  719. apply: function (target, ctx) {
  720. // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""`
  721. if (ctx === Function.prototype.toString) {
  722. return console.hooks.utils.makeNativeString("toString");
  723. }
  724.  
  725. // `toString` targeted at our proxied Object detected
  726. if (ctx === proxyObj) {
  727. const fallback = () =>
  728. originalObj && originalObj.name
  729. ? console.hooks.utils.makeNativeString(originalObj.name)
  730. : console.hooks.utils.makeNativeString(proxyObj.name);
  731.  
  732. // Return the toString representation of our original object if possible
  733. return originalObj + "" || fallback();
  734. }
  735.  
  736. if (typeof ctx === "undefined" || ctx === null) {
  737. return target.call(ctx);
  738. }
  739.  
  740. // Check if the toString protype of the context is the same as the global prototype,
  741. // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case
  742. const hasSameProto = Object.getPrototypeOf(
  743. Function.prototype.toString
  744. ).isPrototypeOf(ctx.toString); // eslint-disable-line no-prototype-builtins
  745. if (!hasSameProto) {
  746. // Pass the call on to the local Function.prototype.toString instead
  747. return ctx.toString();
  748. }
  749.  
  750. return target.call(ctx);
  751. },
  752. };
  753.  
  754. const toStringProxy = new Proxy(
  755. Function.prototype.toString,
  756. console.hooks.utils.stripProxyFromErrors(handler)
  757. );
  758. console.hooks.utils.replaceProperty(Function.prototype, "toString", {
  759. value: toStringProxy,
  760. });
  761. };
  762.  
  763. /**
  764. * All-in-one method to replace a property with a JS Proxy using the provided Proxy handler with traps.
  765. *
  766. * Will stealthify these aspects (strip error stack traces, redirect toString, etc).
  767. * Note: This is meant to modify native Browser APIs and works best with prototype objects.
  768. *
  769. * @example
  770. * replaceWithProxy(WebGLRenderingContext.prototype, 'getParameter', proxyHandler)
  771. *
  772. * @param {object} obj - The object which has the property to replace
  773. * @param {string} propName - The name of the property to replace
  774. * @param {object} handler - The JS Proxy handler to use
  775. */
  776. console.hooks.utils.replaceWithProxy = (obj, propName, handler) => {
  777. const originalObj = obj[propName];
  778. const proxyObj = new Proxy(
  779. obj[propName],
  780. console.hooks.utils.stripProxyFromErrors(handler)
  781. );
  782.  
  783. console.hooks.utils.replaceProperty(obj, propName, { value: proxyObj });
  784. console.hooks.utils.redirectToString(proxyObj, originalObj);
  785.  
  786. return true;
  787. };
  788. /**
  789. * All-in-one method to replace a getter with a JS Proxy using the provided Proxy handler with traps.
  790. *
  791. * @example
  792. * replaceGetterWithProxy(Object.getPrototypeOf(navigator), 'vendor', proxyHandler)
  793. *
  794. * @param {object} obj - The object which has the property to replace
  795. * @param {string} propName - The name of the property to replace
  796. * @param {object} handler - The JS Proxy handler to use
  797. */
  798. console.hooks.utils.replaceGetterWithProxy = (obj, propName, handler) => {
  799. const fn = Object.getOwnPropertyDescriptor(obj, propName).get;
  800. const fnStr = fn.toString(); // special getter function string
  801. const proxyObj = new Proxy(
  802. fn,
  803. console.hooks.utils.stripProxyFromErrors(handler)
  804. );
  805.  
  806. console.hooks.utils.replaceProperty(obj, propName, { get: proxyObj });
  807. console.hooks.utils.patchToString(proxyObj, fnStr);
  808.  
  809. return true;
  810. };
  811.  
  812. /**
  813. * All-in-one method to replace a getter and/or setter. Functions get and set
  814. * of handler have one more argument that contains the native function.
  815. *
  816. * @example
  817. * replaceGetterSetter(HTMLIFrameElement.prototype, 'contentWindow', handler)
  818. *
  819. * @param {object} obj - The object which has the property to replace
  820. * @param {string} propName - The name of the property to replace
  821. * @param {object} handlerGetterSetter - The handler with get and/or set
  822. * functions
  823. * @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description
  824. */
  825. console.hooks.utils.replaceGetterSetter = (
  826. obj,
  827. propName,
  828. handlerGetterSetter
  829. ) => {
  830. const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(
  831. obj,
  832. propName
  833. );
  834. const handler = { ...ownPropertyDescriptor };
  835.  
  836. if (handlerGetterSetter.get !== undefined) {
  837. const nativeFn = ownPropertyDescriptor.get;
  838. handler.get = function () {
  839. return handlerGetterSetter.get.call(this, nativeFn.bind(this));
  840. };
  841. console.hooks.utils.redirectToString(handler.get, nativeFn);
  842. }
  843.  
  844. if (handlerGetterSetter.set !== undefined) {
  845. const nativeFn = ownPropertyDescriptor.set;
  846. handler.set = function (newValue) {
  847. handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this));
  848. };
  849. console.hooks.utils.redirectToString(handler.set, nativeFn);
  850. }
  851.  
  852. Object.defineProperty(obj, propName, handler);
  853. };
  854.  
  855. /**
  856. * All-in-one method to mock a non-existing property with a JS Proxy using the provided Proxy handler with traps.
  857. *
  858. * Will stealthify these aspects (strip error stack traces, redirect toString, etc).
  859. *
  860. * @example
  861. * mockWithProxy(chrome.runtime, 'sendMessage', function sendMessage() {}, proxyHandler)
  862. *
  863. * @param {object} obj - The object which has the property to replace
  864. * @param {string} propName - The name of the property to replace or create
  865. * @param {object} pseudoTarget - The JS Proxy target to use as a basis
  866. * @param {object} handler - The JS Proxy handler to use
  867. */
  868. console.hooks.utils.mockWithProxy = (
  869. obj,
  870. propName,
  871. pseudoTarget,
  872. handler
  873. ) => {
  874. const proxyObj = new Proxy(
  875. pseudoTarget,
  876. console.hooks.utils.stripProxyFromErrors(handler)
  877. );
  878.  
  879. console.hooks.utils.replaceProperty(obj, propName, { value: proxyObj });
  880. console.hooks.utils.patchToString(proxyObj);
  881.  
  882. return true;
  883. };
  884.  
  885. /**
  886. * All-in-one method to create a new JS Proxy with stealth tweaks.
  887. *
  888. * This is meant to be used whenever we need a JS Proxy but don't want to replace or mock an existing known property.
  889. *
  890. * Will stealthify certain aspects of the Proxy (strip error stack traces, redirect toString, etc).
  891. *
  892. * @example
  893. * createProxy(navigator.mimeTypes.__proto__.namedItem, proxyHandler) // => Proxy
  894. *
  895. * @param {object} pseudoTarget - The JS Proxy target to use as a basis
  896. * @param {object} handler - The JS Proxy handler to use
  897. */
  898. console.hooks.utils.createProxy = (pseudoTarget, handler) => {
  899. const proxyObj = new Proxy(
  900. pseudoTarget,
  901. console.hooks.utils.stripProxyFromErrors(handler)
  902. );
  903. console.hooks.utils.patchToString(proxyObj);
  904.  
  905. return proxyObj;
  906. };
  907.  
  908. /**
  909. * Helper function to split a full path to an Object into the first part and property.
  910. *
  911. * @example
  912. * splitObjPath(`HTMLMediaElement.prototype.canPlayType`)
  913. * // => {objName: "HTMLMediaElement.prototype", propName: "canPlayType"}
  914. *
  915. * @param {string} objPath - The full path to an object as dot notation string
  916. */
  917. console.hooks.utils.splitObjPath = (objPath) => ({
  918. // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`
  919. objName: objPath.split(".").slice(0, -1).join("."),
  920. // Extract last dot entry ==> `canPlayType`
  921. propName: objPath.split(".").slice(-1)[0],
  922. });
  923.  
  924. /**
  925. * Convenience method to replace a property with a JS Proxy using the provided objPath.
  926. *
  927. * Supports a full path (dot notation) to the object as string here, in case that makes it easier.
  928. *
  929. * @example
  930. * replaceObjPathWithProxy('WebGLRenderingContext.prototype.getParameter', proxyHandler)
  931. *
  932. * @param {string} objPath - The full path to an object (dot notation string) to replace
  933. * @param {object} handler - The JS Proxy handler to use
  934. */
  935. console.hooks.utils.replaceObjPathWithProxy = (objPath, handler) => {
  936. const { objName, propName } = console.hooks.utils.splitObjPath(objPath);
  937. const obj = eval(objName); // eslint-disable-line no-eval
  938. return console.hooks.utils.replaceWithProxy(obj, propName, handler);
  939. };
  940.  
  941. /**
  942. * Traverse nested properties of an object recursively and apply the given function on a whitelist of value types.
  943. *
  944. * @param {object} obj
  945. * @param {array} typeFilter - e.g. `['function']`
  946. * @param {Function} fn - e.g. `console.hooks.utils.patchToString`
  947. */
  948. console.hooks.utils.execRecursively = (obj = {}, typeFilter = [], fn) => {
  949. function recurse(obj) {
  950. for (const key in obj) {
  951. if (obj[key] === undefined) {
  952. continue;
  953. }
  954. if (obj[key] && typeof obj[key] === "object") {
  955. recurse(obj[key]);
  956. } else {
  957. if (obj[key] && typeFilter.includes(typeof obj[key])) {
  958. fn.call(this, obj[key]);
  959. }
  960. }
  961. }
  962. }
  963. recurse(obj);
  964. return obj;
  965. };
  966.  
  967. /**
  968. * Everything we run through e.g. `page.evaluate` runs in the browser context, not the NodeJS one.
  969. * That means we cannot just use reference variables and functions from outside code, we need to pass everything as a parameter.
  970. *
  971. * Unfortunately the data we can pass is only allowed to be of primitive types, regular functions don't survive the built-in serialization process.
  972. * This utility function will take an object with functions and stringify them, so we can pass them down unharmed as strings.
  973. *
  974. * We use this to pass down our utility functions as well as any other functions (to be able to split up code better).
  975. *
  976. * @see console.hooks.utils.materializeFns
  977. *
  978. * @param {object} fnObj - An object containing functions as properties
  979. */
  980. console.hooks.utils.stringifyFns = (fnObj = { hello: () => "world" }) => {
  981. // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine
  982. // https://github.com/feross/fromentries
  983. function fromEntries(iterable) {
  984. return [...iterable].reduce((obj, [key, val]) => {
  985. obj[key] = val;
  986. return obj;
  987. }, {});
  988. }
  989. return (Object.fromEntries || fromEntries)(
  990. Object.entries(fnObj)
  991. .filter(([key, value]) => typeof value === "function")
  992. .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval
  993. );
  994. };
  995.  
  996. /**
  997. * Utility function to reverse the process of `console.hooks.utils.stringifyFns`.
  998. * Will materialize an object with stringified functions (supports classic and fat arrow functions).
  999. *
  1000. * @param {object} fnStrObj - An object containing stringified functions as properties
  1001. */
  1002. console.hooks.utils.materializeFns = (
  1003. fnStrObj = { hello: "() => 'world'" }
  1004. ) => {
  1005. return Object.fromEntries(
  1006. Object.entries(fnStrObj).map(([key, value]) => {
  1007. if (value.startsWith("function")) {
  1008. // some trickery is needed to make oldschool functions work :-)
  1009. return [key, eval(`() => ${value}`)()]; // eslint-disable-line no-eval
  1010. } else {
  1011. // arrow functions just work
  1012. return [key, eval(value)]; // eslint-disable-line no-eval
  1013. }
  1014. })
  1015. );
  1016. };
  1017.  
  1018. // Proxy handler templates for re-usability
  1019. console.hooks.utils.makeHandler = () => ({
  1020. // Used by simple `navigator` getter evasions
  1021. getterValue: (value) => ({
  1022. apply(target, ctx, args) {
  1023. // Let's fetch the value first, to trigger and escalate potential errors
  1024. // Illegal invocations like `navigator.__proto__.vendor` will throw here
  1025. console.hooks.utils.cache.Reflect.apply(...arguments);
  1026. return value;
  1027. },
  1028. }),
  1029. });
  1030.  
  1031. /**
  1032. * Compare two arrays.
  1033. *
  1034. * @param {array} array1 - First array
  1035. * @param {array} array2 - Second array
  1036. */
  1037. console.hooks.utils.arrayEquals = (array1, array2) => {
  1038. if (array1.length !== array2.length) {
  1039. return false;
  1040. }
  1041. for (let i = 0; i < array1.length; ++i) {
  1042. if (array1[i] !== array2[i]) {
  1043. return false;
  1044. }
  1045. }
  1046. return true;
  1047. };
  1048.  
  1049. /**
  1050. * Cache the method return according to its arguments.
  1051. *
  1052. * @param {Function} fn - A function that will be cached
  1053. */
  1054. console.hooks.utils.memoize = (fn) => {
  1055. const cache = [];
  1056. return function (...args) {
  1057. if (!cache.some((c) => console.hooks.utils.arrayEquals(c.key, args))) {
  1058. cache.push({ key: args, value: fn.apply(this, args) });
  1059. }
  1060. return cache.find((c) => console.hooks.utils.arrayEquals(c.key, args))
  1061. .value;
  1062. };
  1063. };
  1064. }
  1065. // auto run init
  1066. console.hooks.init();
  1067. })();