Greasy Fork 还支持 简体中文。

ConsoleHook

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

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