关注微信公众号:K 哥爬虫,QQ 交换群:808574309,继续分享爬虫进阶、JS/ 安卓逆向等技术干货!
什么是 Hook?
Hook 中文译为钩子,Hook 实际上是 Windows 中提供的一种用以替换 DOS 下“中断”的零碎机制,Hook 的概念在 Windows 桌面软件开发很常见,特地是各种事件触发的机制,在对特定的零碎事件进行 Hook 后,一旦产生已 Hook 事件,对该事件进行 Hook 的程序就会收到零碎的告诉,这时程序就能在第一工夫对该事件做出响应。在程序中将其了解为“劫持”可能会更好了解,咱们能够通过 Hook 技术来劫持某个对象,把某个对象的程序拉进去替换成咱们本人改写的代码片段,批改参数或替换返回值,从而管制它与其余对象的交互。
艰深来讲,Hook 其实就是拦路打劫,马邦德带着老婆,出了城,吃着火锅,还唱着歌,忽然就被麻匪劫了,张麻子劫下县长马邦德的火车,摇身一变化身县长,带着手下赶赴鹅城上任。Hook 的过程,就是张麻子顶替马邦德的过程。
JS 逆向中的 Hook
在 JavaScript 逆向中,替换原函数的过程都能够被称为 Hook,以下先用一段简略的代码了解 Hook 的过程:
function a() {console.log("I'm a.");
}
a = function b() {console.log("I'm b.");
};
a() // I'm b.
间接笼罩原函数是最简略的做法,以上代码将 a 函数进行了重写,再次调用 a 函数将会输入 I'm b.
,如果还想执行原来 a
函数的内容,能够应用两头变量进行贮存:
function a() {console.log("I'm a.");
}
var c = a;
a = function b() {console.log("I'm b.");
};
a() // I'm b.
c() // I'm a.
此时,调用 a 函数会输入 I'm b.
,调用 c 函数会输入 I'm a.
。
这种原函数间接笼罩的办法通常只用来进行长期调试,实用性不大,然而它可能帮忙咱们了解 Hook 的过程,在理论 JS 逆向过程中,咱们会用到更加高级一点的办法,比方 Object.defineProperty()
。
Object.defineProperty()
根本语法:Object.defineProperty(obj, prop, descriptor)
,它的作用就是间接在一个对象上定义一个新属性,或者批改一个对象的现有属性,接管的三个参数含意如下:
obj
:须要定义属性的以后对象;
prop
:以后须要定义的属性名;
descriptor
:属性描述符,能够取以下值:
属性名 | 默认值 | 含意 |
---|---|---|
get | undefined | 存取描述符,指标属性获取值的办法 |
set | undefined | 存取描述符,指标属性设置值的办法 |
value | undefined | 数据描述符,设置属性的值 |
writable | false | 数据描述符,指标属性的值是否能够被重写 |
enumerable | false | 指标属性是否能够被枚举 |
configurable | false | 指标属性是否能够被删除或是否能够再次批改个性 |
通常状况下,对象的定义与赋值是这样的:
var people = {}
people.name = "Bob"
people["age"] = "18"
console.log(people)
// {name: 'Bob', age: '18'}
应用 Object.defineProperty()
办法:
var people = {}
Object.defineProperty(people, 'name', {
value: 'Bob',
writable: true // 是否能够被重写
})
console.log(people.name) // 'Bob'
people.name = "Tom"
console.log(people.name) // 'Tom'
在 Hook 中,应用最多的是存取描述符,即 get 和 set。
get:属性的 getter 函数,如果没有 getter,则为 undefined,当拜访该属性时,会调用此函数,执行时不传入任何参数,然而会传入 this 对象(因为继承关系,这里的 this 并不一定是定义该属性的对象),该函数的返回值会被用作属性的值。
set:属性的 setter 函数,如果没有 setter,则为 undefined,当属性值被批改时,会调用此函数,该办法承受一个参数,也就是被赋予的新值,会传入赋值时的 this 对象。
用一个例子来演示:
var people = {name: 'Bob',};
var count = 18;
// 定义一个 age 获取值时返回定义好的变量 count
Object.defineProperty(people, 'age', {get: function () {console.log('获取值!');
return count;
},
set: function (val) {console.log('设置值!');
count = val + 1;
},
});
console.log(people.age);
people.age = 20;
console.log(people.age);
输入:
获取值!18
设置值!获取值!21
通过这样的办法,咱们就能够在设置某个值的时候,增加一些代码,比方 debugger;
,让其断下,而后利用调用栈进行调试,找到参数加密、或者参数生成的中央,须要留神的是,网站加载时首先要运行咱们的 Hook 代码,再运行网站本人的代码,才可能胜利断下,这个过程咱们能够称之为 Hook 代码的注入,以下将介绍几种支流的注入办法。
Hook 注入的几种办法
以下以某奇艺 cookie 中的 __dfp
值为例,来演示具体如何注入 Hook。
1、Fiddler 插件注入
来到某奇艺首页,能够看到其 cookie 外面有个 __dfp
值:
如果间接搜寻是搜不到的,咱们想通过 Hook 的形式,让在生成 __dfp
值的中央断下,就能够编写如下自执行函数:
(function () {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {set: function (val) {if (val.indexOf('__dfp') != -1) {debugger;}
console.log('Hook 捕捉到 cookie 设置 ->', val);
cookieTemp = val;
return val;
},
get: function () {return cookieTemp;},
});
})();
if (val.indexOf('__dfp') != -1) {debugger;}
的意思是检索 __dfp
在字符串中首次呈现的地位,等于 -1 示意这个字符串值没有呈现,反之则呈现。如果呈现了,那么就 debugger 断下,这里要留神的是不能写成 if (val == '__dfp') {debugger}
,因为 val 传过来的值相似于 __dfp=xxxxxxxxxx
,这样写是无奈断下的。
有了代码该如何应用呢?也就是怎么注入 Hook 代码呢?这里举荐 Fiddler 抓包工具搭配编程猫的插件应用,插件能够在公众号输出关键字【Fiddler 插件 】获取,其原理能够了解为拦挡 —> 加工 —> 放行的一个过程,利用 Fiddler 替换响应,在 Fiddler 拦挡到数据后,在源码第一行插入 Hook 代码,因为 Hook 代码是一个自执行函数,那么网页一旦加载,就必然会先运行 Hook 代码。装置实现后如下图所示,关上抓包,点击开启注入 Hook:
浏览器革除 cookie 后从新进入某奇艺的页面,能够看到胜利断下,在 console 控制台能够看到捕捉的一些 cookie 值,此时的 val
就是 __dfp
的值,接下来在右侧的 Call Stack 调用栈里就能够看到一些函数的调用过程,顺次向上跟进就可能找到最开始 __dfp
生成的中央。
2、TamperMonkey 注入
TamperMonkey 俗称油猴插件,是一款收费的浏览器扩大和最为风行的用户脚本管理器,反对很多支流的浏览器,包含 Chrome、Microsoft Edge、Safari、Opera、Firefox、UC 浏览器、360 浏览器、QQ 浏览器等等,基本上实现了脚本的一次编写,所有平台都能运行,能够说是基于浏览器的利用算是真正的跨平台了。用户能够在 GreasyFork、OpenUserJS 等平台间接获取他人公布的脚本,性能泛滥且弱小,比方视频解析、去广告等。
咱们仍旧以某奇艺的 cookie 为例来演示如何编写 TamperMonkey 脚本,首先去利用商店装置 TamperMonkey,装置过程不再赘述,而后点击图标,增加新脚本,或者点击治理面板,再点击加号新建脚本,写入以下代码:
// ==UserScript==
// @name Cookie Hook
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Cookie Hook 脚本示例
// @author K 哥爬虫
// @match *
// @icon https://www.kuaidaili.com/img/favicon.ico
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {set: function (val) {if (val.indexOf('__dfp') != -1) {debugger;}
console.log('Hook 捕捉到 cookie 设置 ->', val);
cookieTemp = val;
return val;
},
get: function () {return cookieTemp;},
});
})();
主体的 JavaScript 自执行函数和后面都是一样的,这里须要留神的是最后面的正文,每个选项都是有意义的,所有的选项参考 TamperMonkey 官网文档,以下列出了比拟罕用、比拟重要的局部选项(其中须要特地留神 @match
、@include
和 @run-at
选项):
选项 | 含意 |
---|---|
@name | 脚本的名称 |
@namespace | 命名空间,用来辨别雷同名称的脚本,个别写作者名字或者网址就能够 |
@version | 脚本版本,油猴脚本的更新会读取这个版本号 |
@description | 形容这个脚本是干什么用的 |
@author | 编写这个脚本的作者的名字 |
@match |
从字符串的起始地位匹配正则表达式,只有匹配的网址才会执行对应的脚本,例如 * 匹配所有,https://www.baidu.com/* 匹配百度等,能够参考 Python re 模块外面的 re.match() 办法,容许多个实例 |
@include |
和 @match 相似,只有匹配的网址才会执行对应的脚本,然而 @include 不会从字符串起始地位匹配,例如 *://*baidu.com/* 匹配百度,具体区别能够参考 TamperMonkey 官网文档 |
@icon | 脚本的 icon 图标 |
@grant | 指定脚本运行所需权限,如果脚本领有相应的权限,就能够调用油猴扩大提供的 API 与浏览器进行交互。如果设置为 none 的话,则不应用沙箱环境,脚本会间接运行在网页的环境中,这时候无奈应用大部分油猴扩大的 API。如果不指定的话,油猴会默认增加几个最罕用的 API |
@require | 如果脚本依赖其余 JS 库的话,能够应用 require 指令导入,在运行脚本之前先加载其它库 |
@run-at |
脚本注入机会,该选项是能不能 hook 到的要害,有五个值可选:document-start :网页开始时;document-body :body 呈现时;document-end :载入时或者之后执行;document-idle :载入实现后执行,默认选项;context-menu :在浏览器上下文菜单中单击该脚本时,个别将其设置为 document-start |
革除 cookie,开启 TamperMonkey 插件,再次来到某奇艺首页,能够看到也胜利被断下,同样的也能够跟进调用栈来进一步剖析 __dfp
值的起源。
3、浏览器插件注入
浏览器插件官网叫法应该是浏览器扩大(Extension),浏览器插件可能加强浏览器性能,同样也可能帮忙咱们 Hook,浏览器插件的编写并不简单,以 Chrome 插件为例,只须要保障我的项目下有一个 manifest.json 文件即可,它用来设置所有和插件相干的配置,必须放在根目录。其中 manifest_version
、name
、version
3 个参数是必不可少的,如果想要深刻学习,能够参考小茗同学的博客和 Google 官网文档。须要留神的是,火狐浏览器插件不肯定能在其余浏览器上运行,而 Chrome 插件除了能运行在 Chrome 浏览器之外,还能够运行在所有 webkit 内核的国产浏览器,比方 360 极速浏览器、360 平安浏览器、搜狗浏览器、QQ 浏览器等等。咱们还是以某奇艺的 cookie 来演示如何编写一个 Chrome 浏览器 Hook 插件。
新建 manifest.json 文件:
{
"name": "Cookie Hook", // 插件名称
"version": "1.0", // 插件版本
"description": "Cookie Hook", // 插件形容
"manifest_version": 2, // 清单版本,必须是 2 或者 3
"content_scripts": [{"matches": ["<all_urls>"], // 匹配所有地址
"js": ["cookie_hook.js"], // 注入的代码文件名和门路,如果有多个,则顺次注入
"all_frames": true, // 容许将内容脚本嵌入页面的所有框架中
"permissions": ["tabs"], // 权限申请,tabs 示意标签
"run_at": "document_start" // 代码注入的工夫
}]
}
新建 cookie_hook.js 文件:
var hook = function() {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {set: function(val) {if (val.indexOf('__dfp') != -1) {debugger;}
console.log('Hook 捕捉到 cookie 设置 ->', val);
cookieTemp = val;
return val;
},
get: function() {return cookieTemp;},
});
}
var script = document.createElement('script');
script.textContent = '(' + hook + ')()';
(document.head || document.documentElement).appendChild(script);
script.parentNode.removeChild(script);
将这两个文件放到同一个文件夹,关上 chrome 的扩大程序, 关上开发者模式,加载已解压的扩大程序,抉择创立的文件夹即可:
来到某奇艺页面,革除 cookie 后从新进入,能够看到同样也胜利断下,跟踪调用栈就能够找到其值生成的中央:
罕用 Hook 代码总汇
除了应用上述的 Object.defineProperty()
办法,还能够间接捕捉相干接口,而后重写这个接口,以下列出了常见的 Hook 代码。留神:以下只是要害的 Hook 代码,具体注入的形式不同,要进行相干的批改。
Hook Cookie
Cookie Hook 用于定位 Cookie 中要害参数生成地位,以下代码演示了当 Cookie 中匹配到了 __dfp
关键字,则插入断点:
(function () {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {set: function (val) {if (val.indexOf('__dfp') != -1) {debugger;}
console.log('Hook 捕捉到 cookie 设置 ->', val);
cookieTemp = val;
return val;
},
get: function () {return cookieTemp;},
});
})();
(function () {
'use strict';
var org = document.cookie.__lookupSetter__('cookie');
document.__defineSetter__('cookie', function (cookie) {if (cookie.indexOf('__dfp') != -1) {debugger;}
org = cookie;
});
document.__defineGetter__('cookie', function () {return org;});
})();
Hook Header
Header Hook 用于定位 Header 中要害参数生成地位,以下代码演示了当 Header 中蕴含 Authorization
关键字时,则插入断点:
(function () {
var org = window.XMLHttpRequest.prototype.setRequestHeader;
window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {if (key == 'Authorization') {debugger;}
return org.apply(this, arguments);
};
})();
Hook URL
URL Hook 用于定位申请 URL 中要害参数生成地位,以下代码演示了当申请的 URL 里蕴含 login
关键字时,则插入断点:
(function () {
var open = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function (method, url, async) {if (url.indexOf("login") != 1) {debugger;}
return open.apply(this, arguments);
};
})();
Hook JSON.stringify
JSON.stringify()
办法用于将 JavaScript 值转换为 JSON 字符串,在某些站点的加密过程中可能会遇到,以下代码演示了遇到 JSON.stringify()
时,则插入断点:
(function() {
var stringify = JSON.stringify;
JSON.stringify = function(params) {console.log("Hook JSON.stringify ——>", params);
debugger;
return stringify(params);
}
})();
Hook JSON.parse
JSON.parse()
办法用于将一个 JSON 字符串转换为对象,在某些站点的加密过程中可能会遇到,以下代码演示了遇到 JSON.parse()
时,则插入断点:
(function() {
var parse = JSON.parse;
JSON.parse = function(params) {console.log("Hook JSON.parse ——>", params);
debugger;
return parse(params);
}
})();
Hook eval
JavaScript eval()
函数的作用是计算 JavaScript 字符串,并把它作为脚本代码来执行。如果参数是一个表达式,eval()
函数将执行表达式。如果参数是 Javascript 语句,eval()
将执行 Javascript 语句,常常被用来动静执行 JS。以下代码执行后,之后所有的 eval()
操作都会在控制台打印输出将要执行的 JS 源码:
(function() {
// 保留原始办法
window.__cr_eval = window.eval;
// 重写 eval
var myeval = function(src) {console.log(src);
console.log("=============== eval end ===============");
debugger;
return window.__cr_eval(src);
}
// 屏蔽 JS 中对原生函数 native 属性的检测
var _myeval = myeval.bind(null);
_myeval.toString = window.__cr_eval.toString;
Object.defineProperty(window, 'eval', {value: _myeval});
})();
Hook Function
以下代码执行后,所有的函数操作都会在控制台打印输出将要执行的 JS 源码:
(function() {
// 保留原始办法
window.__cr_fun = window.Function;
// 重写 function
var myfun = function() {var args = Array.prototype.slice.call(arguments, 0, -1).join(","),
src = arguments[arguments.length - 1];
console.log(src);
console.log("=============== Function end ===============");
debugger;
return window.__cr_fun.apply(this, arguments);
}
// 屏蔽 js 中对原生函数 native 属性的检测
myfun.toString = function() {return window.__cr_fun + ""}
Object.defineProperty(window, 'Function', {value: myfun});
})();