ConsoleHook

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

当前为 2025-01-20 提交的版本,查看 最新版本

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