Skip to content
On this page

检测点屏蔽toString和this等

在补浏览器环境的过程中,会有很多地方有纰漏,比如常见的 toString,网站如果检测的话很容易检测到,这里给一个简单的例子

拿系统函数 setTimeout 来说,如果我们想要 Hook 这个函数,会有以下代码

js
setTimeout = function setTimeout() {  
	console.log('setTimeout');  
};

我们调用 setTimeout.toString() 来看下输出结果,内容如下

js
function setTimeout() {
    console.log('setTimeout');
}

可以看到,把我们重写的函数内容以字符串的形式打印了出来,接下来看下在浏览器中调用 setTimeout.toString() 看下输入结果,内容如下

js
function setTimeout() { [native code] }

可以看出两者结果不一样,所以很多网站通过判断 xxx.toString() === xxxx 来检测。

native 一般是内置函数,函数的详细代码无法看到

处理 toString()

通过上面的例子可以知道,当我们修改系统函数的时候,会改变 系统函数.toString() 的结果,那么可以通过以下代码解决这个问题

js
!function () {
    // 创建 _GetOwnPropertySymbols 变量来保存 Object.getOwnPropertySymbols 原始函数
    const _GetOwnPropertySymbols = Object.getOwnPropertySymbols;
    // 对 Object.getOwnPropertySymbols 进行重写,主要删除我们自己添加的 symbol 属性
    Object.getOwnPropertySymbols = function getOwnPropertySymbols() {
        const result = _GetOwnPropertySymbols.apply(this, arguments);
        // 通过遍历,删除我们自己添加的 symbol 属性
        for (let i = 0; i < result.length; i++) {
            if (result[i].toString().indexOf("toStringFlag") !== -1) {
                result.splice(i, 1);
                break;
            }
        }
        return result;
    };

    // 创建 _toString 变量来保存 Function.prototype.toString 原始函数
    const _toString = Function.toString;
    // 这里采用随机数来生成一个唯一的 Symbol,防止被检测到,添加 toString 关键字方便后面处理关键特征
    const _toStringSymbol = Symbol('toStringFlag('.concat('', ')_', (Math.random()) + '').toString(36))
    // 创建一个自定义的 toString 函数
    const customToString = function () {
        return typeof this === 'function' && this[_toStringSymbol] || _toString.call(this)
    }
    // 创建一个函数名为 setNative,核心就是修改函数的 symbol 属性的内容,因为后续读取也是通过 symbol 属性来读取的
    function setNative(func, key, value) {
        Object.defineProperty(func, key, {
            enumerable: false,
            configurable: true,
            writable: true,
            value: value
        })
    }
    // 删除所有函数的 toString 方法
    delete Function.prototype.toString
    // 重定义一个所有函数公用的 toString 方法,也就是我们自己定义的 toString,并且保护起来
    setNative(Function.prototype, "toString", customToString);
    // 对我们自己写对 toString 进行保护
    setNative(Function.prototype.toString, _toStringSymbol, "function toString() { [native code] }")
    // 对 Object.getOwnPropertySymbols 进行保护,因为我们要对其进行重写
    setNative(Object.getOwnPropertySymbols, _toStringSymbol, `function getOwnPropertySymbols() { [native code] }`)
    // 两个对外暴露的方法,一个是对函数进行保护,一个是对函数进行保护并且修改函数名
    funcSetNative = (func) => {
        setNative(func, _toStringSymbol, `function ${func.name || ''}() { [native code] }`)
    }
    funcSetNativeDiv = (func, name) => {
        setNative(func, _toStringSymbol, `function ${name || ''}() { [native code] }`)
    }
}.bind(this)();

测试代码

js
// 保护函数前
setTimeout = function setTimeout() {
    console.log('setTimeout');
};
console.log(setTimeout.toString());

// 保护函数后
funcSetNative(setTimeout);
console.log(setTimeout.toString());

console.log(Object.getOwnPropertySymbols(setTimeout));

输出内容

// 保护函数前
function setTimeout() {
    console.log('setTimeout');
}
// 保护函数后
function setTimeout() { [native code] }
[]

可以看到,当我们使用了 funSetNative() 对修改后的函数进行保护之后,toString() 的内容就和真实浏览器一模一样了。

因为以上方法是大部分现在补环境所用到的,里面有一个可能会检测的点就是 const _toStringSymbol = Symbol('toStringFlag('.concat('', ')_', (Math.random()) + '').toString(36)) ,因为核心原理就是随机生成一个 Symbol 对象,然后给这个对象放到函数的属性中,当调用 toString() 的时候去判断是否有这个属性,如果有则返回。

这样做有一个弊端,就是使用 Object.getOwnPropertySymbols(setTimeout) 得到的结果里面会有一个包含 [ Symbol(toStringFlag()_0.7494612829238927) ] 的数组,而浏览器里是没有的,所以才会有重写 Object.getOwnPropertySymbols

Released under the MIT License.