0x00 前言
注:本文不会涉及到反hook与反反hook。
0x01 Hook方法
在之前发的那篇Js Hook文章中,我是用console.log写了一个demo以便读者理解js hook技术,并且当时我是明确指出了console是一个内置对象,所以我们可以直接对其方法进行重写,但是如果要重写的是构造函数
里的方法,那么我们该如何重写?这里我先给大家推荐一个网站,就是在我文章中频繁出现的MDN(https://developer.mozilla.org/zh-CN/
):
通过MDN我们可以查询到一些构造函数的方法怎么样才能被我们调用,下面我拿Date举例:
Date是一个构造函数,所以我们需要实例化才能使用到它的一些方法(当然还有另一种情况,也就是我们可以不进行实例化也能调用的方法,那就是静态方法,后文还会提到),比如我这里拿getDate方法举例:
可见getDate方法是直接写在了Date的原型上,所以Date的实例对象都可以使用到getDate方法,那么还是老套路我们直接重写一波:
var test = Date.prototype.getDate;
Date.prototype.getDate = function(){
console.log(1);
return test.call(this,...arguments);
}
const birthday = new Date('August 19, 1975 23:15:30');
const date1 = birthday.getDate();
console.log(date1);
看下效果:
成功重写。
代码思路和我那篇Hook文章基本一致,我需要重点讲的就是下面这段代码:
Date.prototype.getDate = function(){
console.log(1);
return test.call(this,...arguments);
}
大家可能已经发现了这串代码和我之前重写console.log时的代码多了些东西,也就是方法内部做的return操作:
return test.call(this,...arguments);
代码中我将getDate方法赋给了test,然后用call方法去调用它,里面传了this
和...arguments
,接下来我一步一步来讲这串代码实际上干了什么。
首先是call方法,call方法是写在了function的原型上的,所以说所有函数都可以使用这个方法,MDN介绍:
它这里说的可能的比较抽象,我这里写了个demo简单演示一下这个方法的效果:
var test = {
name : "test",
method : function(){
console.log(test.name);
}
}
test.method.call();
执行结果:
显而易见call方法具有调用函数的能力,接下来我修改一下代码:
var test1 = {
method : function(){
console.log(this.name);
}
}
var test2 = {
name : "test2"
}
test1.method.call(test2);
执行结果:
通过这段代码大家应该就能很好的理解call方法的作用了,我们可以清楚的看到test1对象里是没有name属性的,而test2是有的,所以说第一个参数传的谁,方法中的this就会指向谁,这就是call方法的第一个参数的作用,这个参数将会改变this指向。验证一下method方法是否指向的是test2:
符合以上说明。
这个参数后面跟的就是函数的参数(可选项):
简单来说就是我们本来是要给方法传什么就往后面写就行,所以我在上文重写的那段getDate方法中,给call方法传的this后面写的是...arguments
,在之前的反反调试一文中我已经讲过这个...语法
,这里不作过多阐述。
通过上文我们也可以知道,call方法返回值其实就是函数调用的结果:
讲到这儿可能有朋友不知道为什么非得要用call方法去调用上文提到的test方法,也就是getDate方法,其实就是因为它不是一个静态方法,我们需要实例后才能用这个方法。但是如果它是一个静态方法的话,那么就很好说了,例如我这里拿Date的now方法举例:
它是一个静态方法,直接调用即可,所以我们重写它还是老套路就行:
var test = Date.now;
Date.now = function() {
console.log(1);
return test()
}
console.log(Date.now());
0x02 Hook属性
什么时候我们需要hook属性?我希望读者思考一下这个问题,比较经典的就是网站可能会通过动态cookie来进行反爬,这就涉及到了document.cookie
这个属性,所以下文中我会通过这个属性来演示在js中如何hook属性。
一般情况下我们hook属性,比如document.cookie,其实就是想看看是哪里设置了这个属性值,看看这段字符串是怎么生成出来的,如果加密了还要看看它是在哪里加密的,所以输出堆栈信息也是必要的,至少对我来说是比较重要的。而我们如果想hook属性,那就离不开Object.defineProperty
方法了:
当然还有一个和它效果几乎一致的方法:
大家自行对比一下这个方法名和上面的那个方法名,两个方法的区别就不言而喻了。下面我就拿Object.defineProperty
方法举例。
简单来说就是这个方法可以修改其对象现有的属性或定义一个新属性,然后我们可以给这个属性添加访问器描述符
(其他描述符在这里不进行介绍,读者可自行查阅MDN):
访问器描述符具有以下两个键:get和set,MDN里写的很清楚,如果设置了get键,则访问该属性时将将不带参地调用此函数
;如果设置了set键,则当该属性被赋值时,将调用此函数
。
hook之前我需要先给大家讲解一下document.cookie
这个属性该怎么用:
设置cookie的基本语法:
document.cookie = "cookieName=cookieValue; expires=expirationTime; path=/; domain=example.com; secure";
•cookieName:Cookie的名称。•cookieValue:Cookie的值。•expires:Cookie的过期时间,格式为GMT时间字符串。如果不设置expires属性,则Cookie是会话Cookie,关闭浏览器时会被删除。•path:可选,指定Cookie的路径,默认为当前页面路径。•domain:可选,指定Cookie的域名。•secure:可选,布尔值,指定是否只在HTTPS连接中传输Cookie。
例如我这里设置一个key为test,value为1的cookie:
document.cookie = "test=1";
再比如以下这段示例cookie:
document.cookie="username=John Doe; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";
如果想获取存储的cookie,直接访问document.cookie即可:
这里需要注意的有四点:
1.如果有多个cookie,则每个cookie以;
分隔,注意分号后有一个空格。2.我们可以发现,我在设置username这个cookie时,是给这个cookie设置了一堆属性值的,但是很明显我在访问document.cookie时,只会返回其cookie的key和value的,并不会返回它的属性值的。3.一次只能设置一个键值对,例如:
可见并没有设置bbb这个cookie。4.如果想要修改cookie值,直接覆盖即可,例如我上文设置了一个key为test,value为1的cookie,现在我将它的value修改为2:
修改成功。
参考资料:
https://developer.aliyun.com/article/1556357
https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie
https://www.runoob.com/js/js-cookies.html
下面我正式放出一段我写的hook cookie脚本:
先给大家看一下其效果:
当js设置了cookie,就会输出设置了什么cookie,并且也会打印出堆栈信息。
直接定位到是哪里设置了该cookie,在效果图下方的那段cookie就是上面那段cookie加密后的,网站将原始cookie进行了加密并设置了该cookie,我就不演示如何找到加密cookie的位置,这不是本文重点。
代码很长,下面我逐步讲一下一些关键代码。
var temp_cookie = document.cookie;
Object.defineProperty(document, "cookie", {
get: function () {
return temp_cookie;
},
set: function (cookie) {...}
});
开头我先将网站的cookie赋给了一个临时变量,这个临时变量的主要作用就是为了防止网站js先去获取了cookie,然后get键就会返回这个临时变量给js。这样做是因为一个很重要的原因:修改document.cookie属性后,其原本属性功能将全部失效。
下面我给大家简单演示一下:
Object.defineProperty(document, "cookie", {
get: function () {
return this.temp_cookie;
},
set: function (cookie) {
this.temp_cookie = cookie;
}
});
虽然没有报任何错,并且看着好像是设置成功了,但其实根本就没有设置到cookie里,所以我们需要在get和set键中还原一下document.cookie的作用。这就是为什么我在代码中先将cookie存到了一个临时变量,然后再去给cookie属性设置了get和set键,这样就能防止网站如果首先去获取了cookie,那么我们就可以通过这种方式返回目前存储的cookie。
不过我在这里需要提一个属性的数据描述符value
:
如果设置了value键,那么访问属性就会返回这个值,我这里写了一个demo演示一下其效果:
var test = {
name : "jack"
}
Object.defineProperty(test, "name", {
value : "mike"
});
console.log(test.name);
那么既然有这个值,为什么我写get和set键时不能多写一个value键呢?我给大家放个图:
所以说value或writable键
和get或set键
不能同时存在,这就是为什么我只写了get和set键的原因。
另外可能有朋友会疑惑,既然可以通过document.cookie设置网站的cookie,那么它本身应该也是有一个get和set键的,我们能不能获取这个值呢?我这里先给大家一个案例:
const object1 = {
property1: 42,
};
Object.defineProperty(object1, "property1", {
get: function () {
return this.temp_cookie;
},
set: function (cookie) {
this.temp_cookie = cookie;
}
});
const descriptor1 = Object.getOwnPropertyDescriptor(object1, 'property1');
console.log(descriptor1.set);
通过Object.getOwnPropertyDescriptor方法可以获取到其属性上的描述符:
但是我这里强调一下,这个方法不能获取document.cookie属性的描述符:
报的全是这样的Error:Uncaught TypeError: Cannot read properties of undefined (reading 'set')
,我们继续看一下这个方法返回的是什么:
很明显,在chrome中我们并不能通过Object.getOwnPropertyDescriptor获取到cookie属性的描述符,所以我们没办法通过这种方式还原其原本的功能,只能通过自己写的代码去尽可能地还原它原本的功能,这就是为什么我的这段代码写了这么一长串的原因,主要就是干这件事。
我在上文中放过一张hook后的效果图,一个功能是在控制台打印出设置了什么cookie,另一个功能就是输出了堆栈信息。做到这两个功能只需要用到set键,这是因为如果设置了set键,则当该属性被赋值时,将调用此函数
:
set: function (cookie) {
console.log(cookie);
console.log(new Error().stack);
}
如果有朋友不知道new Error().stack
是什么,可以去看一下我之前写的Js Hook一文。
最后我需要讲一下的就是关于设置cookie时加了好几个属性的问题,我在代码中分了两种情况,一种情况是设置cookie时只写了key和value的,并没有设置其他属性;另一种情况就是设置了一堆属性的cookie。我在上文中提到过,因为重写document.cookie后,其原本功能将全部失效,所以如果遇到设置了一堆属性的这种,我只将它的key和value拿到即可,因为通过document.cookie获取存储的cookie只会返回键值对,不会返回这些cookie的属性:
其他的代码就是基本操作了,主要就是还原一下document.cookie的原本功能,我个人认为是不需要讲什么了,如果读者有不懂的可以留言或私信我。
另外这段代码如果有朋友在使用过程中遇到什么报错或其他问题,可以联系我。
最后我需要提一下这段hook脚本的一个注意事项,由于hook之后document.cookie属性的原本功能失效,所以网站想设置cookie就实现不了,这就导致如果某些网站设置了类似waf的防火墙,在进入网站前可能会校验一下其cookie是否是有效的(读者可以把这个cookie理解成sign),比如我就拿下面这个案例举例:
进入网站时由于cookie不是有效的,所以访问后js会自动设置一个有效cookie,也就是图中的那段cookie,关键就在于如果我此时拿我的那段脚本hook,那么就不会设置成功,就导致这个有效cookie一直设置不成功,所以我请求这个网站时就会一直卡在这里,因为cookie不是有效的,所以读者需要注意这一点。当然了还是要看实际情况,如果读者就是为了了解这段cookie是怎么来的,那么还是开着这个脚本就行,另外建议搭配上我上期文章中提到的CC11001100师傅写的那个页面跳转JS代码定位通杀脚本,我这个案例由于cookie不是有效的,就会一直重新访问,所以建议搭配一下这个脚本。
0x03 location
在之前的两篇反调试与反反调试文章中,我都无一例外提到过我无法hook掉location的属性或方法,下面我来演示一下通过Object.defineProperty
方法修改location.href
属性会发生什么:
Object.defineProperty(location, "href", {
get: function () {
},
set: function (href) {
console.log(href);
}
});
可见通过Object.defineProperty修改href属性会直接报错,异常信息为无法重新定义href属性,这就不得不提到属性的一个数据描述符configurable
:
也就是说这个描述符只要设置为false时,那么这个属性的类型就不能在数据描述符和访问器描述符之间更改(上文我并未提到,这里补充一下,对象中存在的属性描述符只能是这两种类型之一:数据描述符和访问器描述符
),我写了个demo方便读者理解:
var test = {
a : 1
}
Object.defineProperty(test, "a", {
value : 2,
configurable : false
});
Object.defineProperty(test, "a", {
set: function (val) {
console.log(val);
}
});
console.log(test.a);
代码中我先修改了test对象的a属性,将a属性的value值改为了2,configurable设置为了false,然后我又修改了a属性,直接将a属性的数据描述符删掉了,换成了访问器描述符set键。浏览器执行一下看看效果:
可见会直接报错,异常信息就是上面重写location.href时的信息,而当我将configurable值改为true时就能修改了:
现在大家应该就能理解为什么我不能hook掉location的属性和方法了,我也可以进一步通过上文提到的那个Object.getOwnPropertyDescriptor
方法获取一下href属性的configurable值,验证一下是否为false:
const descriptor1 = Object.getOwnPropertyDescriptor(location, 'href');
console.log(descriptor1.configurable);
不出所料为false。
0x04 code
代码我依然是同步更新在了github上,地址:https://github.com/0xsdeo/Hook_JS
值得一提的是,我在这篇文章写完后抽空写了一个hook xhr的脚本,由于是临时起意所以我没有放到上文中,先给大家看一下效果:
两个hook脚本的运行时期皆为document-start
,不过我在这里还是要提一嘴,hook xhr只是我临时起意写的这么一个脚本,实际功能在图中大家也可以看到,如果读者有更好的改善提议可以联系我。
另外还需要注意的一点是,当请求内容输出为[object Blob]
,则表示请求内容为二进制流,例如:
Hook_xhr:
// ==UserScript==
// @name Hook_xhr
// @namespace http://tampermonkey.net/
// @version 2024-11-30
// @description set RequestHeader -> log stack and RequestHeader info && send Request -> log stack and request info
// @attention 当打印的request内容为[object Blob]时,则表示请求内容为二进制流。
// @author 0xsdeo
// @match http://*/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant none
// ==/UserScript==
(function () {
;
var hook_open = XMLHttpRequest.prototype.open;
var hook_setRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
var hook_send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function () {
this.method = arguments[0];
this.url = arguments[1];
return hook_open.call(this, ...arguments);
}
XMLHttpRequest.prototype.setRequestHeader = function () {
console.log(
"请求 " + this.url + " 时请求头被设置n" +
"请求头:" + arguments[0] + ": " + arguments[1]
)
console.log(new Error().stack);
return hook_setRequestHeader.call(this, ...arguments);
}
XMLHttpRequest.prototype.send = function () {
this.data = arguments[0];
if (this.data != null) {
console.log(
"请求方式:" + this.method + "n" +
"请求url:" + this.url + "n" +
"请求内容:" + this.data + "n"
);
} else {
console.log(
"请求方式:" + this.method + "n" +
"请求url:" + this.url + "n"
);
}
console.log(new Error().stack);
return hook_send.call(this, ...arguments);
}
})();
Hook_cookie:
// ==UserScript==
// @name Hook_cookie
// @namespace http://tampermonkey.net/
// @version 2024-11-28
// @description set cookie -> log stack and cookie
// @author 0xsdeo
// @match http://*/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant none
// ==/UserScript==
(function () {
;
var temp_cookie = document.cookie;
Object.defineProperty(document, "cookie", {
get: function () {
return temp_cookie;
},
set: function (cookie) {
console.log(cookie);
console.log(new Error().stack);
var key_arr = [];
var val_arr = [];
if (cookie.split('=').length > 2) {
var temp_k = cookie.split('=')[0];
var temp_val = cookie.substring(cookie.indexOf("=") + 1, cookie.indexOf(";"));
key_arr.push(temp_k);
val_arr.push(temp_val);
}
if (!(temp_cookie === '')) {
temp_cookie.split('; ').forEach(function (key) {
var k = key.split('=')[0];
var val = key.split('=')[1];
if (typeof temp_k != 'undefined' && temp_k === k) {
} else {
key_arr.push(k);
val_arr.push(val);
}
})
temp_cookie = '';
}
if (!(cookie.split('=').length > 2)) {
if (key_arr.length === 0) {
key_arr.push(cookie.split('=')[0]);
val_arr.push(cookie.split('=')[1]);
} else {
for (var i = 0; i < key_arr.length; i++) {
if (key_arr[i] === cookie.split('=')[0]) {
val_arr[i] = cookie.split('=')[1];
break;
} else if (i === key_arr.length - 1) {
key_arr.push(cookie.split('=')[0]);
val_arr.push(cookie.split('=')[1]);
break;
}
}
}
}
for (var i = 0; i < key_arr.length; i++) {
if (i === key_arr.length - 1) {
temp_cookie = temp_cookie + key_arr[i] + '=' + val_arr[i];
} else {
temp_cookie = temp_cookie + key_arr[i] + '=' + val_arr[i] + '; ';
}
}
}
});
})();
原文始发于微信公众号(Spade sec):JS逆向系列12-深入Js Hook
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论