博客文章:深入理解JS逆向代理与环境监测
1. 引言
首先要明确JavaScript(JS)在真实网页浏览器环境和Node.js环境中有很多使用特性的区别。尤其是在环境监测和对象原型链的检测方面。本文将探讨如何使用JS的代理(Proxy)模式来手动补充环境,Node环境和浏览器this
环境,以及如何通过原型链检测来增强代码的安全性以及在。
2. 代理补环境
代理是ES6引入的一种新特性,它允许你定义对象的行为,例如属性的读取和设置。以下是一个简单的代理补环境的实现:
// 代理补环境(缺啥补啥)
function vmProxy(object, objName) {
// 创建代理对象,捕获对对象的访问和赋值操作
return new Proxy(object, {
get: function(target, property, receiver) {
// 打印属性访问信息
console.log(objName, "get: ", property, target[property]);
// 返回目标对象的属性值
return target[property];
},
set: function(target, property, value) {
// 打印属性赋值信息
console.log(objName, "set: ", property, value);
// 使用Reflect.set实现属性赋值
return Reflect.set(...arguments);
}
});
}
// 模拟浏览器环境
navigator = {
userAgent: 'qh'
};
document = {};
// 使用代理来增强navigator和document对象
navigator = vmProxy(navigator, 'navigator');
document = vmProxy(document, 'document');
// 测试代理效果
console.log(navigator.userAgent); // 应输出代理访问信息
console.log(navigator.platform); // 将触发代理的get方法
console.log(document.cookie); // 将触发代理的get方法
console.log(document.createElement); // 将触发代理的get方法
3. Node环境与浏览器this
环境监测
在Node.js中,global
对象是全局对象,而在浏览器中,window
是全局对象。检测这些环境可以通过以下方式实现:
// 确保window指向全局对象
window = globalThis;
// 补充window对象的方法
window['addEventListener'] = function() {};
// 初始化window的navigator和document属性
window['navigator'] = {};
window['document'] = {};
// 使用代理来增强window对象
// 错误写法1:
// window = vmProxy(window, 'window'); // 代理器会影响对象本身的指向,代理window对象本身并不是错误,但关键在于不应该覆盖原有的window对象。代理可以用来增强或监测window对象的行为,但不应该改变其身份。
// 错误写法2:
// window ={'addEventListener': function() {},'navigator':{},'document' {}}; // 直接赋值window为一个新对象会覆盖原有的window对象,这会丢失所有原有的全局属性和方法,包括继承来的属性。
// 错误写法3 重新赋值会影响指向 将window赋值为一个空对象同样会覆盖原有的window对象,导致丢失所有属性和方法。
// window={}
// 测试eval.call方法是否正确指向window
!function (){
function test(){
// 测试eval.call的this指向
console.log(eval["call"](undefined, this) === window); // 应输出true
}
test.apply(null);
}();
这段代码的功能是测试eval.call
方法的this
指向是否正确。在JavaScript中,eval
函数可以计算一个字符串表达式的值。当使用call
方法调用eval
时,可以指定this
的值。在这个例子中,想要测试eval.call
是否能够正确地将this
指向window
对象得到true的结果证明现在node环境和浏览器环境是一致的。
下面是对上面代码的详细拆分和解读,如果已经理解就跳过。
错误写法1解释:
// 错误写法1:
// window = vmProxy(window, 'window'); // 代理器会影响对象本身的指向
这行代码是错误的,因为window
对象是全局对象,其原型链上有很多内置属性和方法。将window
重新赋值为vmProxy
的返回值会切断window
与原有原型链的联系,导致丢失原有的全局属性和方法。此外,这行代码试图将window
对象自身作为代理的目标,这在逻辑上是有问题的,因为window
对象本身不应该被代理。如下图高亮紫色是原有全局属性是继承来的,
在浏览器中打印window对象时,你可能会注意到属性颜色的差异,这通常是由于浏览器的开发者工具中的颜色编码。在Chrome的开发者工具中,全局对象window的属性通常分为两类:
原有全局属性:这些属性是window对象直接定义的,通常是浅色显示,表示它们是window对象的自有属性,而不是通过原型链继承的。
继承来的属性:这些属性是window对象通过原型链从其原型Window.prototype继承的,通常会以高亮紫色显示,以区分自有属性。
例如,navigator对象是window对象的一个自有属性,而navigator对象本身继承自Navigator的属性。在浏览器的控制台中打印window对象时,你会看到类似下面的结构:
Window {
window: Window { ... },
self: Window { ... },
document: document,
name: "name",
location: Location { ... },
history: History { ... },
navigator: Navigator { ... }, // 继承自 Navigator 的属性将显示为高亮紫色
// ... 其他自有属性和继承属性
}
在这个例子中,document、location、history等是window对象的自有属性,而navigator对象的属性,如userAgent,是继承自Navigator的属性。
代码示例:
以下是如何在控制台中查看这些属性的示例代码:
console.dir(window); // 打印window对象及其属性
使用console.dir可以打印出对象的详细信息,包括原型链上的属性。
错误写法2解释:
// 错误写法2:
// window ={'addEventListener': function() {},'navigator':{},'document' {}}; // 直接赋值window为一个新对象会覆盖原有的window对象,这会丢失所有原有的全局属性和方法,包括继承来的属性。
这行代码同样是错误的,因为它试图将window
重新定义为一个具有单个属性addEventListener
的对象,这个属性的值是一个空字符串。正确的做法是为window
添加方法,而不是重新定义window
对象。
错误写法3解释:
// 错误写法3 重新赋值会影响指向 将window赋值为一个空对象同样会覆盖原有的window对象,导致丢失所有属性和方法。
// window = {};
这行代码是错误的,因为它将window
重新赋值为一个空对象,这会覆盖原有的全局window
对象,导致所有原有的全局属性和方法丢失。
衡量错误的标准:
在JS逆向中,衡量错误的标准是在Node环境中是否成功模拟了浏览器环境。正确的做法应该是在不破坏原有window
对象的基础上,补充或修改其属性和方法,以模拟浏览器环境。
加代理正确的做法1:
// 确保window指向全局对象
window = globalThis;
// 补充window对象的方法
if (typeof window.addEventListener === 'undefined') {
window['addEventListener'] = function() {};
}
// 初始化window的navigator和document属性,但不要覆盖原有的属性
if (!window.navigator) {
window['navigator'] = {};
}
if (!window.document) {
window['document'] = {};
}
// 使用代理来增强window对象,但不要重新赋值window
window = vmProxy(window, 'window');
在这个修正的代码中,我们首先检查window
对象上是否已经有addEventListener
方法,如果没有,我们才添加它。同样,我们检查navigator
和document
是否存在,如果不存在,我们才初始化它们。最后,我们使用vmProxy
来增强window
对象,而不是重新赋值它。
加代理正确的做法2:
// 创建window的代理,而不是重新赋值window
const originalWindow = window;
const proxiedWindow = new Proxy(originalWindow, {
get(target, property, receiver) {
if (property === 'navigator') {
console.log('Accessing navigator');
}
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`Setting ${property} to ${value}`);
return Reflect.set(target, property, value, receiver);
}
});
// 使用代理对象进行操作,而不是直接操作window
proxiedWindow.navigator.userAgent; // 这将触发get陷阱,并打印日志
创建了一个window的代理,而不是直接修改window对象。这样,我们可以在不改变window对象本身的情况下,监测和增强其行为。
测试eval.call方法:
!function (){
function test(){
// 测试eval.call的this指向
console.log(eval["call"](undefined, this) === window); // 应输出true
}
test.apply(null);
}();
这个测试函数test
使用apply
方法将this
指向null
,然后通过eval.call
将this
指向window
。如果eval.call
正确地将this
指向了window
,那么console.log
应该输出true
。
4. 原型链检测
原型链是JS中实现继承的关键机制。## 4. 原型链检测的深入分析
原型链是JavaScript中实现继承的核心机制。每个JavaScript对象都有一个原型对象,这个原型对象可以是另一个对象或者null
。当访问一个对象的属性时,如果该属性在对象上不存在,JavaScript引擎会沿着原型链向上查找,直到找到该属性或到达原型链的末端(null
)。
代码拆分
定义构造函数和原型属性
// 定义构造函数Navigator
Navigator = function Navigator(){};
// 在Navigator.prototype上定义userAgent属性
Object.defineProperty(
Navigator.prototype,
'userAgent',
{
set: undefined, // 不允许赋值
enumerable: true, // 可枚举
configurable: true, // 可配置
get: function() {
return "Custom UserAgent"; // 自定义getter函数
} // 自定义getter
}
);
// 创建navigator对象,其原型指向Navigator.prototype
navigator = {};
navigator.__proto__ = Navigator.prototype;
// 检测navigator的属性描述符
console.log(Object.getOwnPropertyDescriptors(navigator)); // 应输出undefined
console.log(Object.getOwnPropertyDescriptors(Navigator.prototype)); // 显示实际的属性描述符
截图示例:在浏览器的控制台中,可以看到Navigator
构造函数和其prototype
上的userAgent
属性定义。
创建并链接自定义navigator
对象
// 创建navigator对象,其原型指向Navigator.prototype
var navigator = {};
Object.setPrototypeOf(navigator, new Navigator()); // 更现代的方法来设置原型
截图示例:在控制台中,通过Object.getPrototypeOf(navigator)
可以看到navigator
的原型现在指向Navigator.prototype
。
属性描述符检测
// 检测navigator的属性描述符
console.log(Object.getOwnPropertyDescriptors(navigator)); // 应输出undefined,因为navigator上没有直接定义userAgent属性
console.log(Object.getOwnPropertyDescriptors(Navigator.prototype)); // 显示实际的属性描述符,包括userAgent的定义
截图示例:在控制台中,Object.getOwnPropertyDescriptors
的调用结果展示了Navigator.prototype
上的userAgent
属性描述符。
JS逆向原型链相关知识
在JavaScript逆向工程中,了解原型链对于分析和修改代码行为至关重要。以下是一些与原型链检测相关的逆向知识点:
- 原型链遍历:逆向工程师可以通过遍历对象的原型链来查找对象的来源和构造方式。
- 原型链污染:通过修改对象的原型链,可以引入新的行为或属性,这在某些情况下可以用于绕过安全限制。
- 构造函数欺骗:通过修改构造函数的
prototype
,可以改变通过该构造函数创建的所有新对象的行为。 - 属性拦截:使用
Proxy
对象,可以在访问或设置属性时进行拦截和自定义行为,这可以用来模拟或监测对象的行为。 - 环境检测:通过检测对象的原型链,可以判断代码运行在何种环境中(浏览器或Node.js),并据此调整代码行为。
通过这些技术,逆向工程师可以深入了解和操纵JavaScript代码的运行时行为,实现代码审计、安全测试或功能增强。然而,这些技术也应谨慎使用,以避免潜在的安全风险和代码维护问题。
5. 环境监测点案例
以下是一些Node和浏览器环境监测点的案例:
- 监测全局对象类型:
typeof global !== 'undefined' ? 'node' : 'browser'
- 监测Node.js特有的模块:
require.main === module
- 监测浏览器特有的对象:
typeof window !== 'undefined' && !!window.document
- 监测浏览器的BOM和DOM:
typeof document !== 'undefined' && !!document.createElement
- 监测环境支持的ES6特性:
'startsWith' in String.prototype
7. 高级监测点:使用eval
和Proxy
进行环境监测
在JavaScript中,eval
函数允许你执行字符串中的代码。然而,使用eval
通常被认为是不安全的,因为它可以执行任意代码。但是,在某些情况下,我们可以通过修改eval
的行为来增强环境监测。以下是一个示例代码,展示了如何重写eval.call
方法,以监测和区分代码执行环境:
// 重写eval.call方法以监测执行环境
eval['call'] = function (){
debugger; // 启动调试模式,便于开发者调试
if (arguments[1].toString() === '[object Window]'){
debugger; // 再次启动调试模式,如果this指向Window对象
// 如果调用环境是浏览器的window对象,则直接返回window
return window;
} else {
// 否则,执行原始的eval函数
return eval(arguments);
}
};
代码解释
eval['call']
: 我们通过eval['call']
访问eval
函数的call
方法,并对其进行重写。debugger
: 这是一个调试语句,当代码执行到这里时,如果正在调试模式下,执行会暂停,允许开发者检查当前的执行环境和变量状态。arguments[1].toString()
:arguments
对象包含了调用函数时传入的所有参数。在这里,我们检查arguments[1]
(即this
的值),并使用toString()
方法获取它的类型描述。'[object Window]'
: 这是一个特定的字符串,用来检测this
是否指向浏览器的window
对象。return window
: 如果检测到this
是window
对象,我们直接返回window
,这样eval.call
调用的结果将总是指向全局的window
对象,而不是局部作用域中的this
。
使用场景
这种技术可以用于确保在执行动态代码时,this
总是指向预期的全局对象,从而避免潜在的作用域问题。此外,它还可以用于调试和测试,帮助开发者更好地理解代码在不同环境下的行为。
7.与局部加密方法对象导出到全局对象的区别
使用代理补环境和原型链检测是一种在运行时动态地修改对象的行为和结构的方法。与之相比,将局部加密方法对象导出到全局对象是一种静态的修改,通常在代码的编写阶段就已经确定。代理补环境提供了一种灵活的方式来监测和修改对象的行为,而局部加密方法对象的导出则是一种更静态、更难以在运行时改变的方法。
结语
通过本文的探讨,我们了解到了JS代理的强大功能以及如何使用它来监测和增强JS运行环境。同时,我们也学习了如何通过原型链检测来提高代码的安全性。这些技术不仅能够帮助开发者更好地理解和控制JS代码的行为,还能够在开发过程中提供更多的灵活性和安全性。