Greasy Fork 还支持 简体中文。

ConsoleHook

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

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

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