在前端安全领域,环境检测与反检测的对抗从未停止。今天我要分享的是一个在JS逆向中经常用到的进阶技巧——通过原型链层次的属性方法伪造来补全环境。这种方法相比简单的变量覆盖或函数重写,具有更好的隐蔽性和稳定性。
现代前端安全检测通常会验证运行环境的真实性,比如检查事件对象是否来自真实的用户交互。以MouseEvent为例,浏览器原生生成的MouseEvent对象会有一个isTrusted属性,当事件由用户真实触发时该属性为true,而通过脚本创建的事件对象则为false。
环境检测代码通常会这样写:
javascript复制document.addEventListener('click', function(e) {
if (!e.isTrusted) {
// 检测到非真实事件,可能是自动化脚本
blockRequest();
}
});
常见的补环境方法是在全局覆盖原生对象:
javascript复制const oldMouseEvent = window.MouseEvent;
window.MouseEvent = class extends oldMouseEvent {
get isTrusted() { return true; }
}
这种方法虽然简单,但存在几个问题:
让我们看一个更完善的实现方案:
javascript复制MouseEvent = function MouseEvent() {
// 定义不可配置的isTrusted属性
Object.defineProperty(this, "isTrusted", {
configurable: false, // 不可配置
enumerable: true, // 可枚举
get: function () { // getter始终返回true
return true;
},
set: undefined // 无setter
});
// 修改getter方法的name属性
Object.defineProperty(
Object.getOwnPropertyDescriptor(this, "isTrusted").get,
"name", {
value: "get isTrusted",
configurable: true,
enumerable: false,
writable: false
}
);
// 修改getter方法的length属性
Object.defineProperty(
Object.getOwnPropertyDescriptor(this, "isTrusted").get,
"length", {
value: 0,
configurable: true,
enumerable: false,
writable: false
}
);
}
属性描述符控制:
configurable: false 使属性不可被删除或重新定义enumerable: true 保持与原生行为一致方法属性伪造:
原型链完整性:
javascript复制temp1 = new MouseEvent();
console.log(temp1.isTrusted); // true
console.log(Object.getOwnPropertyDescriptors(
Object.getOwnPropertyDescriptor(temp1, "isTrusted").get
));
输出结果应该显示getter函数具有正确的属性描述符,与原生实现高度一致。
更完整的实现还应该处理原型链:
javascript复制MouseEvent.prototype = Object.create(OriginalMouseEvent.prototype, {
constructor: {
value: MouseEvent,
enumerable: false,
writable: true,
configurable: true
}
});
// 复制原型方法
for (const key of Object.getOwnPropertyNames(OriginalMouseEvent.prototype)) {
if (!MouseEvent.prototype.hasOwnProperty(key)) {
Object.defineProperty(
MouseEvent.prototype,
key,
Object.getOwnPropertyDescriptor(
OriginalMouseEvent.prototype,
key
)
);
}
}
别忘了处理静态属性和方法:
javascript复制Object.defineProperties(MouseEvent, {
prototype: {
value: MouseEvent.prototype,
enumerable: false,
writable: false,
configurable: false
},
[Symbol.species]: {
get() { return OriginalMouseEvent; },
enumerable: false,
configurable: true
}
});
// 复制静态属性
for (const key of Object.getOwnPropertyNames(OriginalMouseEvent)) {
if (!MouseEvent.hasOwnProperty(key)) {
Object.defineProperty(
MouseEvent,
key,
Object.getOwnPropertyDescriptor(
OriginalMouseEvent,
key
)
);
}
}
环境检测可能会使用以下方式:
toString检测:MouseEvent.toString()Object.getPrototypeOf(new MouseEvent())Object.getOwnPropertyDescriptor(new MouseEvent(), 'isTrusted')Object.getOwnPropertyDescriptor(new MouseEvent(), 'isTrusted').get.namejavascript复制MouseEvent.toString = function() {
return 'function MouseEvent() { [native code] }';
};
javascript复制Object.setPrototypeOf(MouseEvent, OriginalMouseEvent);
javascript复制const getter = Object.getOwnPropertyDescriptor(
new OriginalMouseEvent('click'),
'isTrusted'
).get;
Object.defineProperty(
Object.getOwnPropertyDescriptor(new MouseEvent(), 'isTrusted').get,
'toString', {
value: getter.toString.bind(getter),
enumerable: false,
configurable: true,
writable: true
}
);
执行时机:
兼容性问题:
性能影响:
法律风险:
Object.getOwnPropertyDescriptors全面检查对象javascript复制const monitored = new Proxy(new MouseEvent(), {
get(target, prop) {
console.log('Accessing:', prop);
return target[prop];
}
});
更高级的实现可以根据运行环境动态调整:
javascript复制function getPatchedMouseEvent() {
const OriginalMouseEvent = window.MouseEvent;
return function MouseEvent() {
const event = new OriginalMouseEvent(...arguments);
Object.defineProperty(event, 'isTrusted', {
configurable: false,
enumerable: true,
get: () => true,
set: undefined
});
return event;
};
}
window.MouseEvent = getPatchedMouseEvent();
对于更严格的环境检测,可能需要伪造多个层次:
javascript复制function deepPatchEvent(constructor) {
const handler = {
construct(target, args) {
const instance = new target(...args);
Object.defineProperty(instance, 'isTrusted', {
configurable: false,
enumerable: true,
get: () => true,
set: undefined
});
return instance;
}
};
return new Proxy(constructor, handler);
}
window.MouseEvent = deepPatchEvent(window.MouseEvent);
使补丁更难被检测:
javascript复制let isPatching = false;
const originalDefineProperty = Object.defineProperty;
Object.defineProperty = function(obj, prop, desc) {
if (isPatching && obj === window && prop === 'MouseEvent') {
return originalDefineProperty(obj, prop, {
configurable: true,
enumerable: true,
writable: true,
value: desc.value
});
}
return originalDefineProperty(obj, prop, desc);
};
isPatching = true;
window.MouseEvent = function MouseEvent() { /*...*/ };
isPatching = false;
解决方案:
Object.getOwnPropertyNames对比原生对象解决方案:
解决方案:
javascript复制// patch-event.js
export function patchMouseEvent() {
// 补环境实现
}
// 使用时
import { patchMouseEvent } from './patch-event';
patchMouseEvent();
javascript复制const config = {
isTrusted: true,
timeStamp: Date.now(),
// 其他可配置属性
};
function createPatchedEvent(config) {
// 根据配置生成补丁
}
javascript复制function getBrowserVersion() {
// 检测浏览器版本
return 'chrome-90';
}
const patches = {
'chrome-90': require('./patches/chrome-90'),
'firefox-88': require('./patches/firefox-88')
};
patches[getBrowserVersion()].apply();
在实际项目中,补环境是一项需要细致和耐心的工作。每个属性、每个方法的实现都需要仔细推敲,确保与原生行为一致。我曾在某个项目中花了整整两天时间才完美补全一个事件对象的所有细节,但最终的效果非常值得——我们的自动化脚本能够稳定运行数月而不被检测到。