一、前言
1.1 hook是什么
在js逆向中,重新定义函数,使得调用位置走我们重新定义的函数,改变执行流程, 通常我们叫它钩子技术。
1.2 hook的作用
- 输出分析日志
- 定位关键点
- 改变执行结果
二、以hookbtoa
为例做一次对抗演习
2.1 第一回合
hook方:
let _btoa = btoa;
btoa = function (str) {
console.log('待编码的str', str)
result = _btoa(str)
console.log('编码后的结果', result)
return result
}
反hook方
if (btoa.toString() !== 'function btoa() { [native code] }') {
console.log('你方已被识别出来')
}
可以看出调用btoa
的toString
方法得到函数字符串形式,用于和真实函数字符串比对,可以检测出函数被hook。
2.2 第二回合
hook方:
btoa.toString = function () {
return 'function btoa() { [native code] }'
}
hook方直接修改btoa.toString
方法调用的返回结果,伪装成功。
反hook方:
if (Function.prototype.toString.call(btoa) !== 'function btoa() { [native code] }') {
console.log('你方已被识别出来')
}
反hook方不直接调用btoa的toString方法,而改为从Function原型链上的toString调用,并用call方法改变了对象指向为btoa,成功检测出hook痕迹。
2.3 第三回合
hook方:
Function.prototype.toString = function () {
return `function ${this.name} () { [native code] }`
}
hook方修改Function.prototype.toString
方法,并且其还特别”贴心”的使用模版字符串,让返回字符串的函数名指向调用方,伪装成功。
反hook方:
if (Function.prototype.toString.toString() !== 'function () { [native code] }') {
console.log('你方已被识别出来')
}
反hook方检测Function.prototype.toString.toString
方法有无被修改,成功检测出hook痕迹。
2.4 第四回合
hook方:
Function.prototype.toString.toString = function () {
return `function () { [native code] }`
}
hook方法重写了Function.prototype.toString.toString
方法,伪装成功~
反hook方:
//能不能别搞我了?有那时间干点正事不行吗?
至此,hook方暂时取得本次对抗的胜利。
总结一下双方的对抗过程:
- hook方修改指定函数的方法体,在其中加了一些自己的代码。反hook方则利用函数(函数也是对象)自身的toString方法查看函数有无被修改。
- hook方修改函数的toString调用返回值,反hook方法用Function原型链上的toString检查。
- hook方修改Function原型链上的toString,反hook方用Function原型链上的toString的toString检查。
- 最后hook方修改Function原型链上的toString的toString方法绕过检查。
对于hook方来说,每次为了hook一个方法,做这么多伪装多有些方便,那么可否封装一下,在需要hook某个方法的时候,直接调用封装好的反反hook方法绕过toString检测?这是合理且可行的要求。
三、反反hook封装成方法
!function () {
const $toString = Function.prototype.toString;
const symbol = Symbol()
const myToString = function () {
return typeof this === 'function' && this[symbol] || $toString.call(this);
}
function set_native(func, key, value) {
Object.defineProperty(func, key, {
enumerable: false,
configurable: true,
writable: true,
value: value
})
}
delete Function.prototype.toString;
set_native(Function.prototype, 'toString', myToString);
set_native(Function.prototype.toString, symbol, "function toString() { [native code] }");
globalThis.setNative = function (func, funcname) {
set_native(func, symbol, `function ${funcname || func.name || ''}() { [native code] }`);
}
}()
- 为什么要用自执行函数包裹起来?
- 为了防止内部变量污染外部变量,比如
set_native
,如果外部也有这么一个同步函数,那么产生覆盖效果。
- 为什么要用
symbol
作为属性名?
- symbol一但用Symbol创造出来,就具有独一无二的特性,能够有效的避免与对象中其它属性成员的命名冲突。
globalThis
是什么意思?
- 在nodejs中,等同于
global
,在window中,等同于window
,这里的作用是将setNative
这个对象指向的函数导出来外部作用域中。
- 能简单说一下这段代码的逻辑吗?
- 首先,为传入的函数定义一个叫symbol的属性,这个属性的值是
function ${funcname || func.name || ''}() { [native code] }
,其中funcname是传进来的参数,如果不传,会自动调用func. name
获取;其次重写Function.prototype
的toString属性,让其返回调用对象的symbol属性值,如此,每次调用函数toString方法都会返回事先定义好的symbol属性值;最后,手动为Function.prototype. toString
定义一个symbol属性,返回"function toString() { [native code] }"
,如此,即使用toSting检测Function.prototype. toString
有无被重写,也只会得到指定内容,至此toString型的反反hook检测封装完成。
四、测试
未启用setNative
时:
启用setNative
后:
五、总结
本文先介绍了hook的原理和作用,其次用btoa
为例列举了hook与反hook之间的对抗情形,最后将hook的反反检测手段封装成一个方法并进行测试,测试结果表明,该方法可以有效的对抗toString型的反hook检测。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,如有问题请邮件至2454612285@qq.com。