js逆向中hook与反hook之间的对抗

一、前言

1.1 hook是什么

在js逆向中,重新定义函数,使得调用位置走我们重新定义的函数,改变执行流程, 通常我们叫它钩子技术。

1.2 hook的作用

  1. 输出分析日志
  2. 定位关键点
  3. 改变执行结果

二、以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('你方已被识别出来')
}


可以看出调用btoatoString方法得到函数字符串形式,用于和真实函数字符串比对,可以检测出函数被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方暂时取得本次对抗的胜利。

总结一下双方的对抗过程:

  1. hook方修改指定函数的方法体,在其中加了一些自己的代码。反hook方则利用函数(函数也是对象)自身的toString方法查看函数有无被修改。
  2. hook方修改函数的toString调用返回值,反hook方法用Function原型链上的toString检查。
  3. hook方修改Function原型链上的toString,反hook方用Function原型链上的toString的toString检查。
  4. 最后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] }`);
    }
}()
  1. 为什么要用自执行函数包裹起来?
  • 为了防止内部变量污染外部变量,比如set_native,如果外部也有这么一个同步函数,那么产生覆盖效果。
  1. 为什么要用symbol作为属性名?
  • symbol一但用Symbol创造出来,就具有独一无二的特性,能够有效的避免与对象中其它属性成员的命名冲突。
  1. globalThis是什么意思?
  • 在nodejs中,等同于global,在window中,等同于window,这里的作用是将setNative这个对象指向的函数导出来外部作用域中。
  1. 能简单说一下这段代码的逻辑吗?
  • 首先,为传入的函数定义一个叫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。
跃迁主页