Appearance
Hook作用以及常用场景
随着 JS 的不断发展,开发者们正在寻找更加灵活和可扩展的方式来修改或扩展现有的代码,其中一种广为应用的技术是 Hook,它允许开发者拦截和修改现有函数或方法的行为,在逆向方向也发挥着巨大作用。
什么是 Hook?
Hook 并不单单仅限于 JS,在任何语言都可以实现 Hook,Hook 是一种能够拦截和修改函数或方法行为的技术,通过使用 Hook,开发者可以在现有的函数执行前、执行后插入一些新功能或直接替换原有函数的逻辑,这种灵活的能力使得开发者能够定制和扩展现有代码的行为,而无需修改原始代码。
在 Web 逆向中,学会掌握 Hook 技术,可以使我们逆向的效率极大的提升,往往会发生一些意想不到的惊喜。
Hook 小试牛刀
在一些网站的逆向过程中,有一部分代码是作为字符串作为参数传入了 eval
来执行,所以我们需要对 eval 进行 Hook,以下是一个简单的示例:
js
// 创建一个自执行函数用来hook eval
(function () {
// 保存原始的eval
let _eval = eval;
// 重写eval
eval = function (str) {
// 输出eval的参数
console.log(`eval函数调用, 参数:${str}`);
// 执行原始函数,并传入参数
let result = _eval.apply(this, arguments);
// 当eval执行完毕后输出
console.log(`eval函数执行完毕`);
return result;
}
})();
// 调用hook后的eval
eval("function add(a,b){return a+b};add(1,2)");
打开 Chrome 浏览器,打开 F12 控制台,粘贴代码,结果如上图所示
- 创建了一个自执行函数(为了防止污染变量名);
- 创建一个变量用来保存原始需要 Hook 的函数(后面需要用到);
- 创建一个名字与 Hook 函数名相同的函数,在原始函数执行前后加入自己的逻辑,一般用于输出参数、结果;
- 执行 Hook 后的函数;
常见的 Hook 方式
上述牛刀小试中的 Hook 方式可以理解为替换原函数,这种方便简单粗暴,但是只针对函数 Hook 效果有佳,如果我们要 Hook 对象的话,例如当我们想要在获取某个对象的属性的时候进行 Hook,我们就需要另外一种高级点的 Hook 方式 Object.defineProperty()
了。
Object.defineProperty()
静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。
js
// 创建一个object1对象
const object1 = {};
// 通过Object.defineProperty在object1上添加一个property1属性,值为42
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
// 输出object1.property1为42
console.log(object1.property1);
语法
js
Object.defineProperty(obj, prop, descriptor)
参数
obj
:要定义属性的对象 prop
:当前需要定义的属性名 discriptor
:属性描述符,可以取以下值
描述符名称 | 默认值 | 含义 |
---|---|---|
get | undefined | 存取描述符,目标属性获取值的方法 |
set | undefined | 存取描述符,目标属性获取值的方法 |
value | undefined | 数据描述符,设置属性的值 |
writable | false | 数据描述符,目标属性的值是否可以被重写 |
enumerable | false | 目标属性是否可以被枚举 |
configurable | false | 目标属性是否可以被删除或是否可以被再次修改 |
通常我们定义一个对象属性的代码如下: |
js
let person = {};
person.name = '张三';
person.age = 18;
console.log(person.name);
console.log(person.age);
使用 Object.defineProperty()
赋值的代码如下:
js
let person = {};
Object.defineProperty(person, 'name', {
value: '张三',
writable: true, // 属性的值可以被重写
});
Object.defineProperty(person, 'age', {
value: 18,
writable: true, // 属性的值可以被重写
});
console.log(person.name);
console.log(person.age);
在 Hook 中,使用最多的是存取描述符,即 get
和 set
。
get:属性的 getter 函数,如果没有 getter,则为 undefined,当访问该属性时,会调用此函数,执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象),该函数的返回值会被用作属性的值;
set:属性的 setter 函数,如果没有 setter,则为 undefined,当属性值被修改时,会调用此函数,该方法接收一个参数,也就是被赋予的新值,会传入赋值时的 this 对象;
以下是一个 get、set 的例子:
js
let person = {};
person.name = '张三';
age = 18;
Object.defineProperty(person, 'age', {
get: function () {
console.log('获取值');
return age;
},
set: function (val) {
console.log('设置值');
age = val + 1;
return age;
},
});
console.log(person.age);
person.age = 20;
console.log(person.age);
输出
获取值
18
设置值
获取值
21
通过这样的方式,我们可以在对某个对象进行 Hook,这样当获取这个对象某些属性的时候添加一些代码,例如 debugger
,让其在获取的时候断下,然后利用调用堆栈进行调试,找到参数加密或者参数生成的地方,不过需要注意的是,网站加载时首先要使我们的 Hook 代码先运行,然后在运行网站自己的代码,才能成功 Hook,这个 Hook 的时机非常重要,接下来介绍几种主流的 Hook 方式。
Hook 注入的几种方式
以下以某奇艺 cookie 中的 __dfp
值为例,来演示具体如何注入 Hook。
Fiddler 插件注入
来到某奇艺首页,可以看到其 cookie 里面有个 __dfp
值:
如果直接搜索是搜不到的,我们想通过 Hook 的方式,让在生成 __dfp
值的地方断下,就可以编写如下自执行函数:
js
(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
生成的地方。
TamperMonkey 注入
TamperMonkey 俗称油猴插件,是一款免费的浏览器扩展和最为流行的用户脚本管理器,支持很多主流的浏览器, 包括 Chrome、Microsoft Edge、Safari、Opera、Firefox、UC 浏览器、360 浏览器、QQ 浏览器等等,基本上实现了脚本的一次编写,所有平台都能运行,可以说是基于浏览器的应用算是真正的跨平台了。用户可以在 GreasyFork、OpenUserJS 等平台直接获取别人发布的脚本,功能众多且强大,比如视频解析、去广告等。
我们依旧以某奇艺的 cookie 为例来演示如何编写 TamperMonkey 脚本,首先去应用商店安装 TamperMonkey,安装过程不再赘述,然后点击图标,添加新脚本,或者点击管理面板,再点击加号新建脚本,写入以下代码:
js
// ==UserScript==
// @name Cookie Hook
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Cookie Hook 脚本示例
// @match *
// @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 值的来源。 |
浏览器插件注入
浏览器插件官方叫法应该是浏览器扩展(Extension),浏览器插件能够增强浏览器功能,同样也能够帮助我们 Hook,浏览器插件的编写并不复杂,以 Chrome 插件为例,只需要保证项目下有一个 manifest.json 文件即可,它用来设置所有和插件相关的配置,必须放在根目录。其中 manifest_version
、name
、version
3个参数是必不可少的,如果想要深入学习,可以参考小茗同学的博客和 Google 官方文档。需要注意的是,火狐浏览器插件不一定能在其他浏览器上运行,而 Chrome 插件除了能运行在 Chrome 浏览器之外,还可以运行在所有 webkit 内核的国产浏览器,比如 360 极速浏览器、360 安全浏览器、搜狗浏览器、QQ 浏览器等等。我们还是以某奇艺的 cookie 来演示如何编写一个 Chrome 浏览器 Hook 插件。
新建 manifest.json 文件:
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 文件:
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
关键字, 则插入断点:
js
(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;
},
});
})();
js
(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
关键字时,则插入断点:
js
(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
关键字时,则插入断点:
js
(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()
时,则插入断点:
js
(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()
时,则插入断点
js
(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 源码:
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
});
})();
总结
- Hook 就是可以在不修改原函数的基础之上,在原函数执行前、执行后添加一些额外功能;
- Hook 的方式:
- 直接替换原函数,这种方式针对只需要 Hook 函数的时候好用;
- 使用 Object.defineProperty() 重新定义属性,可以 Hook 属性的获取、设置等操作;
- Hook 生不生效取决于注入 Hook 代码的时机是否正确,需要在网页 JS 代码运行前注入,注入的方式有以下几种:
- 使用 Fiddler 插件,原理是拦截 Response 内容,在 JS 文件头部加入 Hook 代码;
- 使用 TamperMonkey 插件;
- 自己编写浏览器插件;
- 常见的 Hook 代码有很多,可以根据自身需要去修改代码;