jQuery Hook

用于快速定位使用jQuery绑定到DOM元素上的事件的代码的真实位置,辅助逆向分析。

当前为 2024-08-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name jQuery Hook
  3. // @namespace https://github.com/JSREI/jQuery-hook
  4. // @version 0.4
  5. // @description 用于快速定位使用jQuery绑定到DOM元素上的事件的代码的真实位置,辅助逆向分析。
  6. // @document https://github.com/JSREI/jQuery-hook
  7. // @author CC11001100
  8. // @match *://*/*
  9. // @run-at document-start
  10. // @grant none
  11. // ==/UserScript==
  12. (() => {
  13.  
  14. // 可自定义的一个变量前缀,尽量唯一有区分度即可,可以替换为为自己的ID
  15. const globalUniqPrefix = "cc11001100";
  16.  
  17. // 用于控制打印在控制台的消息的大小
  18. const consoleLogFontSize = 12;
  19.  
  20. // ----------------------------------------------- -----------------------------------------------------------------
  21.  
  22. /**
  23. * 用于统一构建待颜色的日志输出,采用构建者模式
  24. *
  25. * from: https://github.com/JSREI/js-color-log
  26. */
  27. class ColorLogBuilder {
  28.  
  29. /**
  30. * 创建一条日志,调用show()方法将其打印到控制台
  31. *
  32. * 因为认为字体颜色是没有区分度的,所以这里就不支持指定字体的颜色,字体恒定为黑色
  33. *
  34. * @param normalTextBackgroundColor {string} 此条日志中普通文本的背景色
  35. * @param highlightTextBackgroundColor {string} 此条日志中要高亮的文本的背景色
  36. * @param _consoleLogFontSize {string} 日志的大小
  37. */
  38. constructor(normalTextBackgroundColor = "#FFFFFF", highlightTextBackgroundColor = "#FFFFFF", _consoleLogFontSize = consoleLogFontSize) {
  39. this.normalTextBackgroundColor = normalTextBackgroundColor;
  40. this.highlightTextBackgroundColor = highlightTextBackgroundColor;
  41. this.consoleLogFontSize = _consoleLogFontSize;
  42. this.messageArray = [];
  43.  
  44. // 每天日志都使用统一的前缀,在创建的时候就设置好
  45. // 先是一个日期,然后是插件的名字,以便与其它工具的输出相区分
  46. // 此处的统一前缀自行修改,因为使用的时候都是拷贝过去的
  47. this.append(`[${this.nowTimeString()}] `).append("jQuery Hook: ");
  48. }
  49.  
  50. /**
  51. * 往日志中追加普通类型的信息
  52. *
  53. * @param msg {string}
  54. * @return {ColorLogBuilder}
  55. */
  56. append(msg) {
  57. this.appendNormal(msg);
  58. return this;
  59. }
  60.  
  61. /**
  62. * 往日志中追加普通类型的信息
  63. *
  64. * @param msg {string}
  65. * @return {ColorLogBuilder}
  66. */
  67. appendNormal(msg) {
  68. this.messageArray.push(`color: black; background: ${this.normalTextBackgroundColor}; font-size: ${this.consoleLogFontSize}px;`);
  69. this.messageArray.push(msg);
  70. return this;
  71. }
  72.  
  73. /**
  74. * 往日志中追加高亮的内容
  75. *
  76. * @param msg {string}
  77. */
  78. appendHighlight(msg) {
  79. this.messageArray.push(`color: black; background: ${this.highlightTextBackgroundColor}; font-size: ${this.consoleLogFontSize}px; font-weight: bold;`);
  80. this.messageArray.push(msg);
  81. return this;
  82. }
  83.  
  84. /**
  85. * 把当前这条日志打印出来
  86. */
  87. show() {
  88. console.log(this.genFormatArray(this.messageArray), ...this.messageArray);
  89. }
  90.  
  91. nowTimeString(fmt = "yyyy-MM-dd HH:mm:ss") {
  92. const now = new Date();
  93. let o = {
  94. "M+": now.getMonth() + 1, "d+": now.getDate(), //日
  95. "H+": now.getHours(), //小时
  96. "m+": now.getMinutes(), //分
  97. "s+": now.getSeconds(), //秒
  98. "q+": Math.floor((now.getMonth() + 3) / 3), //季度
  99. "S": now.getMilliseconds() //毫秒
  100. };
  101. if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (now.getFullYear() + "").substr(4 - RegExp.$1.length));
  102. for (let k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
  103. return fmt;
  104. }
  105.  
  106. genFormatArray(messageAndStyleArray) {
  107. const formatArray = [];
  108. for (let i = 0, end = messageAndStyleArray.length / 2; i < end; i++) {
  109. formatArray.push("%c%s");
  110. }
  111. return formatArray.join("");
  112. }
  113.  
  114. }
  115.  
  116. // ----------------------------------------------- -----------------------------------------------------------------
  117.  
  118. // 在第一次初始化jQuery的时候添加Hook,jQuery初始化的时候会添加一个名为$的全局变量,在添加这个变量的时候对其动一些手脚
  119. Object.defineProperty(window, "$", {
  120. set: $ => {
  121.  
  122. // 为jquery的各种方法添加Hook
  123. try {
  124. addHook($);
  125. } catch (e) {
  126. new ColorLogBuilder("#FF6766", "#E50000")
  127. .append("add hook error, msg = ")
  128. .appendHighlight(e)
  129. .show();
  130. }
  131. // 删除set描述符拦截,恢复正常赋值,假装啥都没发生过,但实际上已经狸猫换太子了...
  132. delete window["$"];
  133. window["$"] = $;
  134. }, configurable: true
  135. });
  136.  
  137. /**
  138. * 为jquery添加一些hook,等会儿使用jquery为dom元素绑定事件的话就会被捕获到
  139. * @param $
  140. */
  141. function addHook($) {
  142.  
  143. addEventHook($);
  144.  
  145. addAjaxHook($);
  146.  
  147. new ColorLogBuilder("#65CC66", "#669934")
  148. .append("在当前页面上检测到jQuery的加载,添加jQuery Hook完成")
  149. .show();
  150. }
  151.  
  152. /**
  153. * 增加Ajax Hook
  154. *
  155. * @param $
  156. */
  157. function addAjaxHook($) {
  158. if (!$["ajaxSetup"]) {
  159. new ColorLogBuilder("#FF6766", "#E50000")
  160. .appendHighlight("$不是jQuery对象,没有 ajaxSetup 属性,因此不添加Ajax Hook")
  161. .show();
  162. return;
  163. }
  164. const oldAjaxSetUp = $.ajaxSetup;
  165. $.ajaxSetup = function () {
  166. try {
  167. if (arguments.length === 1) {
  168. const {formatEventName, eventFuncGlobalName} = storeToWindow("ajaxSetup", arguments[0]);
  169. new ColorLogBuilder("#65CC66", "#669934")
  170. .append("检测到ajaxSetup全局拦截器设置请求参数,已经挂载到全局变量:")
  171. .appendHighlight(eventFuncGlobalName)
  172. .show();
  173. }
  174. } catch (e) {
  175. console.error(e);
  176. }
  177. return oldAjaxSetUp.apply(this, arguments);
  178. }
  179. }
  180.  
  181. /**
  182. * 增加事件Hook
  183. *
  184. * @param $
  185. */
  186. function addEventHook($) {
  187. if (!$["fn"]) {
  188. new ColorLogBuilder("#FF6766", "#E50000")
  189. .appendHighlight("$不是jQuery对象,没有 fn 属性,因此不添加 Event Hook")
  190. .show();
  191. return;
  192. }
  193.  
  194. // 一些比较通用的事件的拦截
  195. const eventNameList = ["click", "dblclick", "blur", "change", "contextmenu", "error", "focus", "focusin", "focusout", "hover", "holdReady", "proxy", "ready", "keydown", "keypress", "keyup", "live", "load", "mousedown", "mouseenter", "mouseleave", "mousemove", "mouseout", "mouseover", "mouseup"];
  196. for (let eventName of eventNameList) {
  197. const old = $.fn[eventName];
  198. $.fn[eventName] = function () {
  199. try {
  200. setEventFunctionNameToDomObjectAttribute(this, eventName, arguments[0]);
  201. } catch (e) {
  202. new ColorLogBuilder("#FF6766", "#E50000")
  203. .appendHighlight(`为jQuery添加${eventName}类型的事件的Hook时发生错误: ${e}`)
  204. .show();
  205. }
  206. return old.apply(this, arguments);
  207. }
  208. }
  209.  
  210. // on,不仅是内置事件类型,还有可能有一些自定义的事件类型
  211. // https://api.jquery.com/on/
  212. const fnOnHolder = $.fn.on;
  213. $.fn.on = function () {
  214. try {
  215. const eventName = arguments[0];
  216. let eventFunction = undefined;
  217. for (let x of arguments) {
  218. if (x instanceof Function) {
  219. eventFunction = x;
  220. break;
  221. }
  222. }
  223. if (eventFunction instanceof Function) {
  224. setEventFunctionNameToDomObjectAttribute(this, eventName, eventFunction);
  225. }
  226. } catch (e) {
  227. new ColorLogBuilder("#FF6766", "#E50000")
  228. .appendHighlight(`为jQuery添加on方法的Hook时发生错误: ${e}`)
  229. .show();
  230. }
  231. return fnOnHolder.apply(this, arguments);
  232. }
  233.  
  234. // TODO 还有delegate之类的比较隐晦的绑定事件的方式
  235.  
  236. }
  237.  
  238.  
  239. /**
  240. * 为绑定了jquery事件的dom元素添加元素,提示所绑定的事件与对应的函数代码的全局变量的名称,只需要复制粘贴跟进去即可
  241. * 注意,有可能会为同一个元素重复绑定相同的事件
  242. *
  243. * @param domObject
  244. * @param eventName
  245. * @param eventFunction
  246. */
  247. function setEventFunctionNameToDomObjectAttribute(domObject, eventName, eventFunction) {
  248. eventName.split(' ').map((eventName) => {
  249. const {formatEventName, eventFuncGlobalName} = storeToWindow(eventName, eventFunction);
  250. const attrName = `${globalUniqPrefix}-jQuery-${formatEventName}-event-function`;
  251. if (domObject.attr(attrName)) {
  252. domObject.attr(attrName + "-" + new Date().getTime(), eventFuncGlobalName);
  253. } else {
  254. domObject.attr(attrName, eventFuncGlobalName);
  255. }
  256. })
  257. }
  258.  
  259. // ----------------------------------------------- -----------------------------------------------------------------
  260.  
  261. // 用于缓存事件函数到全局变量的映射关系
  262. // <事件函数, 全局变量>
  263. const eventFuncCacheMap = new Map();
  264.  
  265. /**
  266. * 为事件的函数绑定一个全局变量,如果之前已经绑定过了则返回之前的
  267. *
  268. * @param eventName {string}
  269. * @param eventFunc {Function}
  270. * @return {{string, string}} 事件名和其对应的函数绑定到的全局变量
  271. */
  272. function storeToWindow(eventName, eventFunc) {
  273. if (eventFunc in eventFuncCacheMap) {
  274. return eventFuncCacheMap[eventFunc];
  275. }
  276. // 注意,事件名可能会包含一些非法的字符,所以需要转义
  277. // cc11001100-jquery-$destroy-event-function
  278. const formatEventName = safeSymbol(eventName);
  279. const eventFuncGlobalName = globalUnique(formatEventName);
  280. window[eventFuncGlobalName] = eventFunc;
  281. eventFuncCacheMap[eventFunc] = eventFuncGlobalName;
  282. return {
  283. formatEventName, eventFuncGlobalName,
  284. };
  285. }
  286.  
  287. /***
  288. * 将事件名称转为合法的变量名称
  289. *
  290. * @param name
  291. */
  292. function safeSymbol(name) {
  293. const replaceMap = {
  294. ".": "_dot_",
  295. "$": "_dollar_",
  296. "-": "_dash_",
  297. " ": "_whitespace_"
  298. };
  299. let newName = "";
  300. for (let c of name) {
  301. if (c in replaceMap) {
  302. newName += replaceMap[c];
  303. } else if (isOkVarChar(c)) {
  304. newName += c;
  305. }
  306. }
  307. return newName;
  308. }
  309.  
  310. /**
  311. * 判断字符是否是合法的变量名字符
  312. *
  313. * @param c {string}
  314. * @returns {boolean}
  315. */
  316. function isOkVarChar(c) {
  317. return (/^[a-zA-Z0-9]$/).test(c);
  318. }
  319.  
  320. // ----------------------------------------------- -----------------------------------------------------------------
  321.  
  322. // 每个事件一个独立的自增id
  323. const addressIdGeneratorMap = {};
  324.  
  325. /**
  326. * 为给定的事件生成一个全局唯一的标识,这个标识中会带上事件类型以方便区分不同事件
  327. *
  328. * @param eventName {string}
  329. */
  330. function globalUnique(eventName) {
  331. const id = (addressIdGeneratorMap[eventName] || 0) + 1;
  332. addressIdGeneratorMap[eventName] = id;
  333. return `${globalUniqPrefix}__${eventName}__${id}`;
  334. }
  335.  
  336. // ----------------------------------------------- -----------------------------------------------------------------
  337.  
  338. /**
  339. * 解析当前代码的位置,以便能够直接定位到事件触发的代码位置
  340. *
  341. * @param keyword {string}
  342. * @returns {string}
  343. */
  344. function getCodeLocation(keyword = "cc11001100") {
  345. const callstack = new Error().stack.split("\n");
  346. while (callstack.length && callstack[0].indexOf(keyword) === -1) {
  347. callstack.shift();
  348. }
  349. callstack.shift();
  350. // callstack.shift();
  351.  
  352. return callstack[0].trim();
  353. }
  354.  
  355. })();