Appearance
检测点屏蔽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
。