JavaScript常用的Hook脚本

本文Hook脚本 来自 包子

页面最早加载代码Hook时机

  1. 在source里 用dom事件断点的script断点
  2. 然后刷新网页,就会断在第一个js标签,这时候就可以注入代码进行hook

监听 键盘 与 鼠标 事件

  1. // 判断是否按下F12 onkeydown事件
  2. /*
  3. 提示: 与 onkeydown 事件相关联的事件触发次序:
  4. onkeydown
  5. onkeypress
  6. onkeyup
  7. */
  8. // F12的键码为 123,可以直接全局搜索 keyCode == 123, == 123 ,keyCode
  9. document.onkeydown = function() {
  10. if (window.event && window.event.keyCode == 123) {
  11. // 改变键码
  12. event.keyCode = 0;
  13. event.returnValue = false;
  14. // 监听到F12被按下直接关闭窗口
  15. window.close();
  16. window.location = "about:blank";
  17. }
  18. }
  19. ;
  20. // 监听鼠标右键是否被按下方法 1, oncontextmenu事件
  21. document.oncontextmenu = function () { return false; };
  22. // 监听鼠标右键是否被按下方法 2,onmousedown事件
  23. document.onmousedown = function(evt){
  24. // button属性是2 就代表是鼠标右键
  25. if(evt.button == 2){
  26. alert('监听到鼠标右键被按下')
  27. evt.preventDefault() // 该方法将通知 Web 浏览器不要执行与事件关联的默认动作
  28. return false;
  29. }
  30. }
  31. // 监听用户工具栏调起开发者工具,判断浏览器的可视高度和宽度是否有改变,有改变则处理,
  32. // 判断是否开了开发者工具不太合理。
  33. var h = window.innerHeight, w = window.innerWidth;
  34. window.onresize = function(){
  35. alert('改变了窗口高度')
  36. }
  37. // hook代码
  38. (function() {
  39. //严谨模式 检查所有错误
  40. 'use strict';
  41. // hook 鼠标选择
  42. Object.defineProperty(document, 'onselectstart', {
  43. set: function(val) {
  44. console.log('Hook捕获到选中设置->', val);
  45. return val;
  46. }
  47. });
  48. // hook 鼠标右键
  49. Object.defineProperty(document,'oncontextmenu',{
  50. set:function(evt){
  51. console.log("检测到右键点击");
  52. return evt
  53. }
  54. });
  55. })();

webpack hook 半自动扣

  1. //在加载器后面下断点 执行下面代码
  2. // 这里的f 替换成需要导出的函数名
  3. window.zhiyuan = f;
  4. window.wbpk_ = "";
  5. window.isz = false;
  6. f = function(r){
  7. if(window.isz)
  8. {
  9. // e[r]里的e 是加载器里的call那里
  10. window.wbpk_ = window.wbpk_ + r.toString()+":"+(e[r]+"")+ ",";
  11. }
  12. return window.zhiyuan(r);
  13. }
  14. //在你要的方法加载前下断点 执行 window.isz=true
  15. //在你要的方法运行后代码处下断点 执行 window.wbpk_ 拿到所有代码 注意后面有个逗号
  16. function o(t) {
  17. if (n[t])
  18. return n[t].exports;
  19. var i = n[t] = {
  20. i: t,
  21. l: !1,
  22. exports: {}
  23. };
  24. console.log("被调用的 >>> ", e[t].toString());
  25. // 这里进行拼接,bb变量需要在全局定义一下
  26. // t 是模块名, e[t] 是模块对应的函数, 也就是key:value形式
  27. bb += `"${t}":${e[t].toString()},`
  28. return e[t].call(i.exports, i, i.exports, o),
  29. i.l = !0,
  30. i.exports
  31. }
  32. bz = o;

如果只是调用模块,不用模块里面的方法, 那么直接获取调用模块的时候所有加载过的模块,进行拼接

document下的createElement()方法的hook,查看创建了什么元素

  1. (function() {
  2. 'use strict'
  3. var _createElement = document.createElement.bind(document);
  4. document.createElement = function(elm){
  5. // 这里做判断 是否创建了script这个元素
  6. if(elm == 'body'){
  7. debugger;
  8. }
  9. return _createElement(elm);
  10. }
  11. })();

之前我不知道我用的是 var _createElement = document.createElement 导致一直报错 Uncaught TypeError: Illegal invocation

原来是需要绑定一下对象 var _createElement = document.createElement.bind(document);

headers hook 当header中包含Authorization时,则插入断点


  1. var code = function(){
  2. var org = window.XMLHttpRequest.prototype.setRequestHeader;
  3. window.XMLHttpRequest.prototype.setRequestHeader = function(key,value){
  4. if(key=='Authorization'){
  5. debugger;
  6. }
  7. return org.apply(this,arguments);
  8. }
  9. }
  10. var script = document.createElement('script');
  11. script.textContent = '(' + code + ')()';
  12. (document.head||document.documentElement).appendChild(script);
  13. script.parentNode.removeChild(script);

请求hook 当请求的url里包含MmEwMD时,则插入断点


  1. var code = function(){
  2. var open = window.XMLHttpRequest.prototype.open;
  3. window.XMLHttpRequest.prototype.open = function (method, url, async){
  4. if (url.indexOf("MmEwMD")>-1){
  5. debugger;
  6. }
  7. return open.apply(this, arguments);
  8. };
  9. }
  10. var script = document.createElement('script');
  11. script.textContent = '(' + code + ')()';
  12. (document.head||document.documentElement).appendChild(script);
  13. script.parentNode.removeChild(script);

docuemnt.getElementById 以及value属性的hook

  1. // docuemnt.getElementById 以及value属性的hook,可以参考完成innerHTML的hook
  2. document.getElementById = function(id) {
  3.     var value = document.querySelector('#' + id).value;
  4.     console.log('DOM操作 id: ', id)
  5.     try {
  6.         Object.defineProperty(document.querySelector('#'+ id), 'value', {
  7.             get: function() {
  8.                 console.log('getting -', id, 'value -', value);
  9.                 return value;
  10.             },
  11.             set: function(val) {
  12.                 console.log('setting -', id, 'value -', val)
  13.                 value = val;
  14.             }
  15.         })
  16.     } catch (e) {
  17.         console.log('---------华丽的分割线--------')
  18.     }
  19.     return document.querySelector('#' + id);
  20. }

过debugger 阿布牛逼


  1. function Closure(injectFunction) {
  2. return function() {
  3. if (!arguments.length)
  4. return injectFunction.apply(this, arguments)
  5. arguments[arguments.length - 1] = arguments[arguments.length - 1].replace(/debugger/g, "");
  6. return injectFunction.apply(this, arguments)
  7. }
  8. }
  9. var oldFunctionConstructor = window.Function.prototype.constructor;
  10. window.Function.prototype.constructor = Closure(oldFunctionConstructor)
  11. //fix native function
  12. window.Function.prototype.constructor.toString = oldFunctionConstructor.toString.bind(oldFunctionConstructor);
  13. var oldFunction = Function;
  14. window.Function = Closure(oldFunction)
  15. //fix native function
  16. window.Function.toString = oldFunction.toString.bind(oldFunction);
  17. var oldEval = eval;
  18. window.eval = Closure(oldEval)
  19. //fix native function
  20. window.eval.toString = oldEval.toString.bind(oldEval);
  21. // hook GeneratorFunction
  22. var oldGeneratorFunctionConstructor = Object.getPrototypeOf(function*() {}).constructor
  23. var newGeneratorFunctionConstructor = Closure(oldGeneratorFunctionConstructor)
  24. newGeneratorFunctionConstructor.toString = oldGeneratorFunctionConstructor.toString.bind(oldGeneratorFunctionConstructor);
  25. Object.defineProperty(oldGeneratorFunctionConstructor.prototype, "constructor", {
  26. value: newGeneratorFunctionConstructor,
  27. writable: false,
  28. configurable: true
  29. })
  30. // hook Async Function
  31. var oldAsyncFunctionConstructor = Object.getPrototypeOf(async function() {}).constructor
  32. var newAsyncFunctionConstructor = Closure(oldAsyncFunctionConstructor)
  33. newAsyncFunctionConstructor.toString = oldAsyncFunctionConstructor.toString.bind(oldAsyncFunctionConstructor);
  34. Object.defineProperty(oldAsyncFunctionConstructor.prototype, "constructor", {
  35. value: newAsyncFunctionConstructor,
  36. writable: false,
  37. configurable: true
  38. })
  39. // hook dom
  40. var oldSetAttribute = window.Element.prototype.setAttribute;
  41. window.Element.prototype.setAttribute = function(name, value) {
  42. if (typeof value == "string")
  43. value = value.replace(/debugger/g, "")
  44. // 向上调用
  45. oldSetAttribute.call(this, name, value)
  46. }
  47. ;
  48. var oldContentWindow = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, "contentWindow").get
  49. Object.defineProperty(window.HTMLIFrameElement.prototype, "contentWindow", {
  50. get() {
  51. var newV = oldContentWindow.call(this)
  52. if (!newV.inject) {
  53. newV.inject = true;
  54. core.call(newV, globalConfig, newV);
  55. }
  56. return newV
  57. }
  58. })

过debugger—1 constructor 构造器构造出来的

  1. var _constructor = constructor;
  2. Function.prototype.constructor = function(s) {
  3. if (s == "debugger") {
  4. console.log(s);
  5. return null;
  6. }
  7. return _constructor(s);
  8. }

过debugger—2 eval的

  1. (function() {
  2. 'use strict';
  3. var eval_ = window.eval;
  4. window.eval = function(x) {
  5. eval_(x.replace("debugger;", " ; "));
  6. }
  7. ;
  8. window.eval.toString = eval_.toString;
  9. }
  10. )();

JSON HOOK

  1. var my_stringify = JSON.stringify;
  2. JSON.stringify = function (params) {
  3. //这里可以添加其他逻辑比如 debugger
  4. console.log("json_stringify params:",params);
  5. return my_stringify(params);
  6. };
  7. var my_parse = JSON.parse;
  8. JSON.parse = function (params) {
  9. //这里可以添加其他逻辑比如 debugger
  10. console.log("json_parse params:",params);
  11. return my_parse(params);
  12. };

对象属性hook 属性自定义


  1. (function(){
  2. // 严格模式,检查所有错误
  3. 'use strict'
  4. // document 为要hook的对象 ,属性是cookie
  5. Object.defineProperty(document,'cookie',{
  6. // hook set方法也就是赋值的方法,get就是获取的方法
  7. set: function(val){
  8. // 这样就可以快速给下面这个代码行下断点,从而快速定位设置cookie的代码
  9. debugger; // 在此处自动断下
  10. console.log('Hook捕获到set-cookie ->',val);
  11. return val;
  12. }
  13. })
  14. })();

cookies - 1 (不是万能的 有些时候hook不到 自己插入debugger)

  1. var cookie_cache = document.cookie;
  2. Object.defineProperty(document, 'cookie', {
  3. get: function() {
  4. console.log('Getting cookie');
  5. return cookie_cache;
  6. },
  7. set: function(val) {
  8. console.log("Seting cookie",val);
  9. var cookie = val.split(";")[0];
  10. var ncookie = cookie.split("=");
  11. var flag = false;
  12. var cache = cookie_cache.split("; ");
  13. cache = cache.map(function(a){
  14. if (a.split("=")[0] === ncookie[0]){
  15. flag = true;
  16. return cookie;
  17. }
  18. return a;
  19. })
  20. }
  21. })

cookies - 2


  1. var code = function(){
  2. var org = document.cookie.__lookupSetter__('cookie');
  3. document.__defineSetter__("cookie",function(cookie){
  4. if(cookie.indexOf('TSdc75a61a')>-1){
  5. debugger;
  6. }
  7. org = cookie;
  8. });
  9. document.__defineGetter__("cookie",function(){return org;});
  10. }
  11. var script = document.createElement('script');
  12. script.textContent = '(' + code + ')()';
  13. (document.head||document.documentElement).appendChild(script);
  14. script.parentNode.removeChild(script);
  15. // 当cookie中匹配到了 TSdc75a61a, 则插入断点。

window attr


  1. // 定义hook属性
  2. var window_flag_1 = "_t";
  3. var window_flag_2 = "ccc";
  4. var key_value_map = {};
  5. var window_value = window[window_flag_1];
  6. // hook
  7. Object.defineProperty(window, window_flag_1, {
  8. get: function(){
  9. console.log("Getting",window,window_flag_1,"=",window_value);
  10. //debugger
  11. return window_value
  12. },
  13. set: function(val) {
  14. console.log("Setting",window, window_flag_1, "=",val);
  15. //debugger
  16. window_value = val;
  17. key_value_map[window[window_flag_1]] = window_flag_1;
  18. set_obj_attr(window[window_flag_1],window_flag_2);
  19. },
  20. });
  21. function set_obj_attr(obj,attr){
  22. var obj_attr_value = obj[attr];
  23. Object.defineProperty(obj,attr, {
  24. get: function() {
  25. console.log("Getting", key_value_map[obj],attr, "=", obj_attr_value);
  26. //debugger
  27. return obj_attr_value;
  28. },
  29. set: function(val){
  30. console.log("Setting", key_value_map[obj], attr, "=", val);
  31. //debugger
  32. obj_attr_value = val;
  33. },
  34. });
  35. }

eval/Function


  1. window.__cr_eval = window.eval;
  2. var myeval = function(src) {
  3. // src就是eval运行后 最终返回的值
  4. console.log(src);
  5. console.log("========= eval end ===========");
  6. return window.__cr_eval;
  7. }
  8. var _myeval = myeval.bind(null);
  9. _myeval.toString = window.__cr_eval.toString;
  10. Object.defineProperty(window, 'eval',{value: _myeval});
  11. window._cr_fun = window.Function
  12. var myfun = function(){
  13. var args = Array.prototype.slice.call(arguments, 0, -1).join(","), src = arguments[arguments.lenght -1];
  14. console.log(src);
  15. console.log("======== Function end =============");
  16. return window._cr_fun.apply(this, arguments)
  17. }
  18. myfun.toString = function() {return window._cr_fun + ""} //小花招,这里防止代码里检测原生函数
  19. Object.defineProperty(window, "Function",{value: myfun})

eval 取返回值

  1. _eval = eval;
  2. eval = (res)=>{
  3. res1 = res // 返回值
  4. return _eval(res)
  5. }
  6. eval(xxxxxxxxx)

eval proxy代理 https://segmentfault.com/a/1190000025154230

  1. // 代理eval
  2. eval = new Proxy(eval,{
  3. // 如果代理的是函数 查看调用 就用apply属性
  4. // 第二个参数是prop 这里用不上 因为是属性,eval只是个函数 所以prop为undefind 这里设置了下划线 ——
  5. apply: (target,_,arg)=>{
  6. // target 是被代理的函数或对象名称,当前是[Function: eval]
  7. // arg是传进来的参数,返回的是个列表
  8. console.log(arg[0])
  9. }
  10. })
  11. // eval执行的时候就会被代理拦截
  12. // 传入的如果是字符串 那么只会返回字符串,这里是匿名函数 直接执行 return了内容
  13. eval(
  14. (function(){return "我是包子 自己执行了"})()
  15. )
  16. // 结果 : 我是包子 自己执行了

websocket hook

  1. // 1、webcoket 一般都是json数据格式传输,那么发生之前需要JSON.stringify
  2. var my_stringify = JSON.stringify;
  3. JSON.stringify = function (params) {
  4. //这里可以添加其他逻辑比如 debugger
  5. console.log("json_stringify params:",params);
  6. return my_stringify(params);
  7. };
  8. var my_parse = JSON.parse;
  9. JSON.parse = function (params) {
  10. //这里可以添加其他逻辑比如 debugger
  11. console.log("json_parse params:",params);
  12. return my_parse(params);
  13. };
  14. // 2 webScoket 绑定在windows对象,上,根据浏览器的不同,websokcet名字可能不一样
  15. //chrome window.WebSocket firfox window.MozWebSocket;
  16. window._WebSocket = window.WebSocket;
  17. // hook send
  18. window._WebSocket.prototype.send = function (data) {
  19. console.info("Hook WebSocket", data);
  20. return this.send(data)
  21. }
  22. Object.defineProperty(window, "WebSocket",{value: WebSocket})

hook 正则 —— 1

  1. (function () {
  2.     var _RegExp = RegExp;
  3.     RegExp = function (pattern, modifiers) {
  4.         console.log("Some codes are setting regexp");
  5.         debugger;
  6.         if (modifiers) {
  7.             return _RegExp(pattern, modifiers);
  8.         } else {
  9.             return _RegExp(pattern);
  10.         }
  11.     };
  12.     RegExp.toString = function () {
  13.         return "function setInterval() { [native code] }"
  14.     };
  15. })();

hook 正则 2 加在sojson头部过字符串格式化检测

  1. (function() {
  2. var _RegExp = RegExp;
  3. RegExp = function(pattern, modifiers) {
  4. if (pattern == decodeURIComponent("%5Cw%2B%20*%5C(%5C)%20*%7B%5Cw%2B%20*%5B'%7C%22%5D.%2B%5B'%7C%22%5D%3B%3F%20*%7D") || pattern == decodeURIComponent("function%20*%5C(%20*%5C)") || pattern == decodeURIComponent("%5C%2B%5C%2B%20*(%3F%3A_0x(%3F%3A%5Ba-f0-9%5D)%7B4%2C6%7D%7C(%3F%3A%5Cb%7C%5Cd)%5Ba-z0-9%5D%7B1%2C4%7D(%3F%3A%5Cb%7C%5Cd))") || pattern == decodeURIComponent("(%5C%5C%5Bx%7Cu%5D(%5Cw)%7B2%2C4%7D)%2B")) {
  5. pattern = '.*?';
  6. console.log("发现sojson检测特征,已帮您处理。")
  7. }
  8. if (modifiers) {
  9. console.log("疑似最后一个检测...已帮您处理。")
  10. console.log("已通过全部检测,请手动处理debugger后尽情调试吧!")
  11. return _RegExp(pattern, modifiers);
  12. } else {
  13. return _RegExp(pattern);
  14. }
  15. }
  16. ;
  17. RegExp.toString = function() {
  18. return _RegExp.toString();
  19. }
  20. ;
  21. }
  22. )();

hook canvas (定位图片生成的地方)

  1. (function() {
  2. 'use strict';
  3. let create_element = document.createElement.bind(doument);
  4. document.createElement = function (_element) {
  5. console.log("create_element:",_element);
  6. if (_element === "canvas") {
  7. debugger;
  8. }
  9. return create_element(_element);
  10. }
  11. })();

setInterval 定时器

  1. (function() {
  2. setInterval_ = setInterval;
  3. console.log("原函数已被重命名为setInterval_")
  4. setInterval = function() {}
  5. ;
  6. setInterval.toString = function() {
  7. console.log("有函数正在检测setInterval是否被hook");
  8. return setInterval_.toString();
  9. }
  10. ;
  11. }
  12. )();

setInterval 循环清除定时器

  1. for(var i = 0; i < 9999999; i++) window.clearInterval(i)

console.log 检测例子 (不让你输出调试)

  1. var oldConsole = ["debug", "error", "info", "log", "warn", "dir", "dirxml", "table", "trace", "group", "groupCollapsed", "groupEnd", "clear", "count", "countReset", "assert", "profile", "profileEnd", "time", "timeLog", "timeEnd", "timeStamp", "context", "memory"].map(key=>{
  2. var old = console[key];
  3. console[key] = function() {}
  4. ;
  5. console[key].toString = old.toString.bind(old)
  6. return old;
  7. }
  8. )

检测函数是否被hook例子

  1. if (window.eval == 'native code') {
  2. console.log('发现eval函数被hook了 开始死循环');
  3. }

JavaScript常用的Hook脚本的更多相关文章

  1. JavaScript常用表单验证正则表达式(身份证、电话号码、邮编、日期、IP等)

    身份证正则表达式 //身份证正则表达式(15位)isIDCard1=/^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$/;//身份证正则表达式 ...

  2. JavaScript常用内置对象(window、document、form对象)

    由于刚开始学习B/S编程,下面对各种脚本语言有一个宏观的简单认识. 脚本语言(JavaScript,Vbscript,JScript等)介于HTML和C,C++,Java,C#等编程语言之间.它的优势 ...

  3. javascript常用知识点集

    javascript常用知识点集 目录结构 一.jquery源码中常见知识点 二.javascript中原型链常见的知识点 三.常用的方法集知识点 一.jquery源码中常见的知识点 1.string ...

  4. 【javascript】javascript常用函数大全

    javascript函数一共可分为五类:   •常规函数   •数组函数   •日期函数   •数学函数   •字符串函数   1.常规函数   javascript常规函数包括以下9个函数:   ( ...

  5. 【前端】javaScript 常用技巧总结

    javaScript 常用技巧总结 1.  彻底屏蔽鼠标右键  oncontextmenu="window.event.returnValue=false" <table b ...

  6. javascript常用知识汇总

    javascript这个语言庞大而复杂,我用了三年多了,还是皮毛都不会.从刚开始的jquery,到后来的es6,每天都在学习,每天都在忘记. 1.禁止手机虚拟键盘弹出 在开发适配手机的页面时,出现了这 ...

  7. JavaScript常用API合集汇总(一)

    今天这篇文章跟大家分享一些JavaScript常用的API代码,有DOM操作.CSS操作.对象(Object对象.Array对象.Number对象.String对象.Math对象.JSON对象和Con ...

  8. Oracle手边常用70则脚本知识汇总

    Oracle手边常用70则脚本知识汇总 作者:白宁超 时间:2016年3月4日13:58:36 摘要: 日常使用oracle数据库过程中,常用脚本命令莫不是用户和密码.表空间.多表联合.执行语句等常规 ...

  9. JavaScript 常用功能总结

    小编吐血整理加上翻译,太辛苦了~求赞! 本文主要总结了JavaScript 常用功能总结,如一些常用的JS 对象,基本数据结构,功能函数等,还有一些常用的设计模式. 目录: 众所周知,JavaScri ...

随机推荐

  1. SSE图像算法优化系列三十一:Base64编码和解码算法的指令集优化。

        一.基础原理 Base64是一种用64个Ascii字符来表示任意二进制数据的方法.主要用于将不可打印的字符转换成可打印字符,或者简单的说是将二进制数据编码成Ascii字符.Base64也是网络 ...

  2. Linux nginx 负载的几种方式

    2021-08-191. 轮询 (这是默认的方式)就是在 nginx 映射的几个服务器按请求的时间顺序逐一分配,几率是随机的.如果后端服务器 down 掉,能自动忽略不用.这种情况一般是每台服务器配置 ...

  3. 图解最长回文子串「Manacher 算法」,基础思路感性上的解析

    问题描述: 给你一个字符串 s,找到 s 中最长的回文子串. 链接:https://leetcode-cn.com/problems/longest-palindromic-substring 「Ma ...

  4. 一、Git分布式版本控制系统

    1.引入 在开发一个软件项目时,本地只有几十行代码或几百行代码时还可以维护,但当代码达到一定的数量后或两三个人共同开发一个项目时,就很容易会出现代码混乱.冲突.排错难等问题.一旦开发完工以后发现整个项 ...

  5. Django+Ansible构建任务中心思路

    Ansible作为老牌的自动化运维工具,由Python开发,应用广泛,但其默认只提供了命令行下的使用方式,好在提供有完善的API支持二次开发,可以很方便的集成到我们的自动化运维系统中 最近一个朋友跳槽 ...

  6. 【死磕NIO】— NIO基础详解

    Netty 是基于Java NIO 封装的网络通讯框架,只有充分理解了 Java NIO 才能理解好Netty的底层设计.Java NIO 由三个核心组件组件: Buffer Channel Sele ...

  7. webgl 图像处理 加速计算

    webgl 图像处理 webgl 不仅仅可以用来进行图形可视化, 它还能进行图像处理 图像处理1---数据传输 webgl 进行图形处理的第一步: 传输数据到 GPU 下图为传输点数据到 GPU 并进 ...

  8. Linux 文本相关命令(1)

    Linux 文本相关命令(1) 前言 最近线上环境(Windows Server)出现了一些问题,需要分析一下日志.感觉 Windows 下缺少了一些 Linux 系统中的小工具,像在这波操作中用到的 ...

  9. 一文彻底搞懂Hive的数据存储与压缩

    目录 行存储与列存储 行存储的特点 列存储的特点 常见的数据格式 TextFile SequenceFile RCfile ORCfile 格式 数据访问 Parquet 测试 准备测试数据 存储空间 ...

  10. SourceTree使用详解-摘录收藏

    前言: 非原创,好文收录,原创作者:追逐时光者 俗话说的好工欲善其事必先利其器,Git分布式版本控制系统是我们日常开发中不可或缺的.目前市面上比较流行的Git可视化管理工具有SourceTree.Gi ...