JS原型链与补环境
原型
__proto__
:在js中,每个实例对象都拥有__proto__
(隐式原型),实例的__proto__.constructor
指明该实例创建时使用的构造方法 。如下所示prototype
: 函数除了拥有__proto__
,还额外拥有prototype
(显示原型)
构造方法
在了解原型链前,先了解js中的构造方法。构造方法也是函数,因此也拥有 prototype
。构造函数首字母通常大写,表示可以用 new
创建实例
下面介绍两种构造函数编写方式
传统方式
function Person(name) { this.name = name; this.introduce = function() { console.log(`Hi, I'm ${this.name}`); }; }
效果如下。使用该方法
new
出来的对象,其__proto__.constructor
指向构造函数通过以上例子,可以得到如下结论(Tips:实例通常用到
__proto__
,构造方法通常用到prototype
)重点1:实例的
__proto__.constructor
与 构造函数的prototype.constructor
指向构造函数自己,即otto.__proto__.constructor === Person
重点2:实例的
__proto__
指向构造函数的prototype
重点3: 构造函数的
prototype.__proto__
指向父类的prototype
,即Person.prototype.__proto__ === Object.prototype
this
方式绑定的方法为实例私有,实例自己修改后不会影响其他实例,可理解为实例方法重点4:绑定在构造方法的
prototype
内的方法是其所有实例共享的,实例可调用该方法。且在该方法修改时,实例调用也会对应变动。可理解为随动的类方法
class方式(ES6引入)。代码来自MDN
class Person { constructor(name) { this.name = name; } introduce() { console.log(`你好,我的名字是 ${this.name}`); } } const otto = new Person("Otto"); otto.introduce(); // 你好,我的名字是 Otto
效果如下。此时
class
中声明的方法,都会绑定到构造方法的prototype
中,即所有实例共享
考虑继承关系时声明类
需要声明某个类时,需按照以下流程
- 明确其父类并声明
- 声明子类(自身)并设置继承关系
- 保证子类原型指向与构造函数指向
注意以下两点
- 在新建一个构造方法时,其
prototype.constructor
默认指向自己,即之前提到的重点2默认是成立的 - 若需要多个构造方法,其中存在继承关系,则需要保证
<子类>.prototype.__proto__ === <父类>.prototype
。采用传统方式需要手动处理,而在处理原型指向后,会影响到重点2构造函数的指向,也需对构造方法指向手动处理
下面以浏览器中 Element -> HTMLElement -> HTMLHtmlElement / HTMLDivElement / ... (html标签) 的继承关系为例( Tips:通过重点2查看父类, getParent = (cls) => {return cls.prototype.__proto__.constructor}
)
传统方式
创建父类
Element
Element = function Element() {} // `Element = ` 是利用全局变量自动绑定到全局的特性 // 在补环境时,`window = global; delete global;` // 这样声明 Element,会实现 window.Element
创建子类
HTMLElement
继承自父类Element
HTMLElement = function HTMLElement() { Element.call(this) // 继承逻辑,调用父类构造函数。 }
设置原型链中的指向关系。如果不进行此步骤。
HTMLElement
的原型链将指向Object
,构造方法依然指向自己// 方法一 // 设置子类原型指向,以实现 HTMLElement.prototype.__proto__ === Element.prototype HTMLHtmlElement.prototype = Object.create(HTMLElement.prototype) // 保证构造函数指向自己。由于子类原型通过父类原型创建,此时子类原型中的构造方法与父类一致,即指向父类自己,需要修改 HTMLHtmlElement.prototype.constructor = HTMLHtmlElement // 方法二 同时保证子类原型指向与构造函数指向的设置方法 HTMLElement.prototype = Object.create(Element.prototype, { constructor: { value: HTMLElement, enumerable: false, writable: true, configurable: true, }, });
class方式,自动实现了原型指向问题,无需额外设置
class Element { constructor () { } } window.Element = Element // 需要手动绑定到 window 上,不然 window.Element 会 undefined class HTMLElement extends Element { // 声明父类 constructor () { super() // 使用父类构造方法 } } window.HTMLElement = Element // 检测 console.log(HTMLElement.prototype.__proto__ === Object.prototype) // 应该为false console.log(HTMLElement.prototype.__proto__ === Element.prototype) // 应该为true console.log(HTMLElement.prototype.constructor === HTMLElement) // 应该为true console.log(window.Element) // 不应该为undefined console.log(Element.toString()) // 当前输出 es6 class 模式。需要输出 function native code,可使用toString检测函数处理 safeFunction(Element)来过检测
常见浏览器内置的继承关系
Object ├── EventTarget │ ├── Node │ │ ├── Document │ │ │ └── HTMLDocument │ │ ├── DocumentFragment │ │ ├── Element │ │ │ ├── HTMLElement │ │ │ │ ├── HTMLHtmlElement │ │ │ │ ├── HTMLCanvasElement │ │ │ │ └── ...(html标签) │ │ │ └── SVGElement │ │ ├── CharacterData │ │ │ ├── Text │ │ │ └── Comment │ │ └── ... │ ├── XMLHttpRequestEventTarget │ │ └── XMLHttpRequest │ ├── Window │ ├── Screen (window.screen) │ ├── Performance │ ├── Clipboard │ ├── MediaDevices │ └── ... │ ├── Navigator (window.navigator) ├── History (window.history) ├── Location (window.location) ├── Storage (localStorage / sessionStorage) ├── Crypto ├── AbortController └── ...
原型链检测
常规补环境。常常使用以下代码进行,其中
userAgent
是navigator
的属性,相当于构造方法中通过this
绑定到实例上的实例私有属性navigator = {} navigator.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
检测代码。浏览器环境中,
navigator
是通过Navigator
构造方法new
出来的实例,而userAgent
是Navigator.prototype
内的属性,对于navigator
实例来说是共享属性。以下检测代码会无法通过// 浏览器环境应该为false,userAgent 不是 navigator 实例的私有属性。而是原型链上的属性(构造方法上的属性) if (navigator.hasOwnProperty('userAgent')){ console.log('invalid') } else { console.log('pass') // undefined 才通过检测 } // 检测是否有属性描述符。原型链上的属性没有描述符 if (Object.getOwnPropertyDescriptor(navigator, 'userAgent')) { console.log('invalid') } else { console.log('pass') // undefined 才通过检测 }
原型链检测的补环境方法
使用构造方法
// 1. 编写构造函数 Navigator = function () { } // 2. 原型链属性补充 Navigator.prototype = { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36' } // 3. 实例化 navigator = new Navigator() // 4. 绑定window。Tips: 在浏览器中,所有定义的全局内容,都是window的属性 window.Navigator = Navigator // eg: 在浏览器中查看原型 如 document.documentElement.__proto__,指向了HTMLHtmlElement // 而 HTMLHtmlElement 继承自 HTMLElement ,HTMLElement 继承自 Element window.Element = function Element() {} window.HTMLElement = function HTMLElement() { Element.call(this) } HTMLElement.prototype = Object.create(Element.prototype) // 设置子类原型指向,以实现 HTMLElement.prototype.__proto__ === Element.prototype。 HTMLElement.prototype.constructor = HTMLElement // 保证构造函数指向自己。由于原型通过父类创 window.HTMLHtmlElement = function HTMLHtmlElement() { HTMLElement.call(this) } HTMLHtmlElement.prototype = Object.create(HTMLElement.prototype) HTMLHtmlElement.prototype.constructor = HTMLHtmlElement
利用 实例的
__proto__
指向构造函数的prototype
的特性var window = {} var navigator = {} navigator.__proto__ = { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36' }
补环境
使用代理明确检测内容
通过代理的方式,在控制台打印出代码读取了目标对象的哪些属性。以下代理代码来源
- 环境代理:将浏览器内置对象进行代理,查看内置对象的调用情况
let setProxyArr = function (proxyObjArr) { // 环境代理 for (let i = 0; i < proxyObjArr.length; i++) { const handler = `{ get: function(target, property, receiver) { console.log("方法:", "get", "对象:", "${proxyObjArr[i]}", "属性:", property, "属性类型:", typeof property, "属性值:", target[property], "属性值类型:", typeof target[property]); return target[property]; }, set: function(target, property, value, receiver) { console.log("方法:", "set", "对象:", "${proxyObjArr[i]}", "属性:", property, "属性类型:", typeof property, "属性值:", value, "属性值类型:", typeof target[property]); return Reflect.set(...arguments); } }`; eval(`try{ ${proxyObjArr[i]}; ${proxyObjArr[i]} = new Proxy(${proxyObjArr[i]}, ${handler}); } catch (e) { ${proxyObjArr[i]} = {}; ${proxyObjArr[i]} = new Proxy(${proxyObjArr[i]}, ${handler}); }`); } } proxyArray = ['window', 'document', 'location', 'navigator', 'history', 'screen']; // ['window','document','location', 'navigator', 'history', 'screen','span', 'div','chrome','SCRIPT', 'canvas','HEAD','body','EventTarget','navigator.mimeTypes','catvm.memory.PluginArray._','localStorage','sessionStorage']; window = global; delete global; delete Buffer; // 需要先补这些对象,再对这些对象挂代理 setProxyArr(proxyArray);
- 通用代理。若发现检测我们补充内容的属性时,可以代理我们补充内容,打印调用该对象的内容,并执行原本的内容
function watch(object) { // 通用代理,当检测补充内容的属性值,用于对补充内容进行代理 const handler = { get: function (target, property, receiver) { if (property !== 'isNaN' && property !== 'encodeURI' && property !== 'Uint8Array' && property !== 'undefined' && property !== 'JSON') { console.log('方法:', 'get', '对象:', target, '属性:', property, '属性类型:', typeof property, '属性值:', target[property], '属性值类型:', typeof target[property]); } return target[property] // return Reflect.get(...arguments) }, set: function (target, property, value, receiver) { console.log('方法:', 'set', '对象', target, '属性:', property, '属性类型:', typeof property, '属性值:', value, '属性值类型:', typeof target[property]); return Reflect.set(...arguments) } } return new Proxy(object, handler) }
常规处理流程
补充基本环境。如
window
,document
,navigator
等浏览器内置对象window = global; delete Buffer; // nodejs自带关键字,删除防止检测 delete global; // 防止检测 window.parent = window.self = window.top = window.window = window; // 窗口检测,是否存在 iframe 等窗口嵌套
挂代理。将基本环境与环境代理放在目标代码前,或放入 env.js 一个js文件中,使用
require("./env.js")
加载目标代码。在补环境后放,可通过
require("./vmp.js")
的方式,将vmp自执行函数运行,方便过格式化检测。在require
后,调用vmp中的加密函数运行代码,查看控制台是否log出检测内容。如果没有,打断点后,在控制台输入
window
,查看window对象是否有代理。如果window已有代理,可能通过<alias> = winodow;
的别名方式对window调用与检测,搜索global
、self
、this
、env
等可能的别名,查看是否有类似别名。将其全局替换为window
后查看控制台是否正常log运行代码,查看是否异常中断。通常需要补环境使代码可以顺利执行完成
注意:不是所有的异常都需要处理。 通过勾选以下内容明确原本代码中是否抛出该异常。如出现读取
buffer
内的属性产生的异常,实际上是检测buffer
,在浏览器中也存在该异常,则无需处理,保持与浏览器一致即可
补充环境。查看调用的 undefined 内容,配合浏览器查看调用内容类型(方法还是属性,是什么类型的属性)。不是每个调用都需要补充,如检测自动化、爬虫框架的特征,配合浏览器环境,此时理应需要结果是
undefined
常见的待补充内容如下
- 方法
document.createElement
- 属性
document.documentElement
- 方法
window/document.addEventListener
- 方法
window.MouseEvent
- 属性
navigator.webdriver = false
- 方法
普通补环境方法。此处以
document.documentElement
为例document = { "createElement": function(e){ console.log("document createElement ",e) // 通过打印确定调用内容 } }
原型链补环境。补充到原型(构造函数prototype)中,详见原型链部分
补属性。绑定到构造函数的原型上使其成为属性。需要先检查属性是否为实例,则声明该实例的构造函数后,使用构造函数实例化,再。此处以
document.documentElement
为例function HTMLDocument() {} document = new HTMLDocument() // 待绑定对象 function HTMLHtmlDocument(){} // 目标属性的构造函数 HTMLDocument.prototype.documentElement = new HTMLHtmlDocument() // 绑定到待绑定类的原型上 // Tips: // 严格情况下,需要考虑继承关系,HTMLDocument -> HTMLHtmlDocument // 常用继承关系与构造函数继承方法,可查看原型链部分
补方法。在对象的构造函数原型上创建该方法。此处以
document.addEventListener
为例function HTMLDocument() {} document = new HTMLDocument() // 待绑定对象 // 绑定到待绑定类的原型上 HTMLDocument.prototype.addEventListener = function addEventListener(){}
对被调用对象进行代理后返回。查看后续可能检测该对象的内容
普通方式
document = { "createElement": function(e){ console.log("document createElement ",e); // 通过打印确定调用内容 if (e=="canvas") { // canvas是对象,先返回空对象,挂上代理,查看可能检测canvas的内容 return getProxy({}, "canvas") } } }
原型链
function HTMLDocument() {} document = new HTMLDocument() function HTMLHtmlDocument(){} HTMLDocument.prototype.documentElement = getProxy(new HTMLHtmlDocument(), 'document.documentElement') // 补对象后挂上代理
勾选通过代理发现调用canvas中的getContext,进行补充(对于创建和获取,日志打印较为关键)
document = { "createElement": function(e){ console.log("document createElement ",e); // 通过打印确定调用内容 if (e=="canvas") { return getProxy({ // 补充的内容 getContext: function(e){ console.log("canvas getContext ",e) // 通过打印确定调用内容 } }, "canvas") } } }
代码没有异常中断后,勾选 断点(变量下面)-> Caught Exception & Uncaught Exception 。对代码自行
catch
的异常进行分析,查看是否存在明显的错误需要调整。一些环境逻辑可能在通过catch处理而不异常中断如
catch
到了一个属性是undefined,控制台log了 getAttribute,说明 getAttribute 为找到,需要补充。可以考虑补充到构造函数原型上。如果获取对象本身是构造函数,则补充到父类的原型上。此处给出几个典型案例// E.G. // HTMLElement.getAttribute class Element { constructor () { } getAttribute() {} } Element.prototype.custom = 'custom' // 在父类的原型上绑定 // tips:方法可以直接写在class中,属性需单独 prototype 赋值 class HTMLElement extends Element { constructor () { super() } } console.log(Element.prototype.getAttribute) console.log(HTMLElement.prototype.getAttribute) // E.G. // 检测 document.getElementByTagName('*') 输出HTMLCollection数组 // 及数组内元素的 方法 get 属性 Symbol(Symbol.iterator) 类型 symbol(补充后发现调用了_elements) // 补 _elements = [] 后 // 控制台输出 HTMLCollection{_elements:Array(0)} 中元素的属性 `(0)[]`,说明取了HTMLCollection._elements[0]。具体属性需要补对象后通过代理确定 function HTMLCollection() { this._elements=[getProxy(new Element())] // 通过代理查看 检测了 _elements[0] 的属性 TagName // 浏览器 document.getElementByTagName('*') 的第一个元素是html标签,查看其 TagName 为 HTML this._elements=[getProxy(new HTMLHtmlElement())] // 然后HTMLHtmlElement中添加tagName this.tagName = 'HTML' } HTMLDocument.prototype.getElementByTagName = function getElementByTagName(tagName) { if (tagName == '*') { // 触发 getElementByTagName('*') 检测时,返回一个HTMLCollection return getProxy(new HTMLCollection(), 'HTMLCollection_obj') } } HTMLCollection.prototype[Symbol.iterator] = function () { // prompt: 不引入库,在 function HTMLCollection() {} 基础上实现属性 Symbol(Symbol.iterator) // 异常时把代码copy并让其修改 let index = 0; const elements = this._elements; return [ next: function () { if (index < elements.length) { return {value: elements[index++], done:false} } else { return {value: undefined, done: true} } } ] } // E.G. then属性报错 -> 调用 then 的 Promise 对象。补 Promise ()。此处示例 navigator.permissions.query // 0. 先发现navigator.permissions undefined Permissions = function Permissions() {} Navigator.prototype.permissions = getProxy(new Permissions()) PermissionStatus = function PermissionStatus() // 1.3 // 1. 补完后发现寻找 navigator.permissions 中的 query ,浏览器查看 navigator.permissions.query 是函数 // 补完 query 发现 query 中没有 then,通过浏览器控制台发现 query 函数返回了 Promise 对象,then 关键字也可以印证 query 返回了 Promise 对象 Permission.prototype.query = function query() { // 1.2 Promise 不正常打印,可能是需要入参,增加参数输出 console.log('Permissions.prototype.query 参数:', arguments[0]) // 1.1 resolve 内容需要通过浏览器控制台注入确定。代码断点时状态为pending,需要跳过断点,使请求正常响应,触发回调 // navigator.permissions.query().then(res=>console.log(res) // 1.3 得到参数后修改注入代码。这里假设入参为字符串'in_params' ,navigator.permissions.query('in_params').then(res=>console.log(res) // 得到响应为 PermissionStatus 对象 return Promise.resolve(watch new PermissionStatus()) } // 浏览器控制台使用此代码测试 then ,需要释放断点,使 Promise 从 pending 状态变为结束状态,从而触发注入代码打印结果 // navigator.permissions.query({name: camera}).then(res=>console.log(res)) // E.G. 补XMLHttpRequest XMLHttpRequest = function XMLHttpRequest() {} XMLHttpRequest.prototype.open = function open(){} XMLHttpRequest.prototype.send = function send(){} XMLHttpRequest.prototype.setRequestHeader = function setRequestHeader(){} XMLHttpRequest.prototype.addEventListener = function addEventListener(){}
一边补充,一边查看结果的长度。长度与加密函数长度一致时,可考虑进行下一步,函数调用
结果检测
进行加密函数调用,验证结果。结果不正确需对代理打印的undefined进行补充(看情况补充,不是所有undefined都补充,此处通常是cookie或localStorage内读取某个内容)
navigator = {}; navigator.webdriver = false; navigator.userAgent = '...'; navigator.plateform = 'Win32'; // window.location 浏览器console直接location location = {} // window.localStorage localStorage = {} localStorage = getProxy(localStorage, "localStorage") localStorage.getItem = function(k) { console.log('localStorage getItem', k) if (k == '') { return {} // getItem获取对应值时undefined,根据if返回对应值 } }
自执行。加密通常为自执行,仍不成功,考虑自执行,查看是否有特殊的undefined内容进行补充 - 此处通过代理得到的代码逻辑是
document.createElement('canvas')
, 然后调用了canvas.getContext("webgl")
,此时的执行结果为undefined,需要补充document = { "createElement": function(e){ console.log("document createElement ",e); if (e=="canvas") { return getProxy({ getContext: function(arg) { if (arg == "webgl") { return getProxy({...}, "webgl") // 在浏览器得到webgl是第一个对象,需要补充 } } }, "canvas") } } }
toString检测
一些环境会对内置函数进行检测。以 createElement
为例,console打印的内容
document.createElement.toString()
// 浏览器环境输出: 'function createElement() { [native code] }'
// 手动补环境输出: 'function createElement() {}'
通过函数使补充内容的toString返回与浏览器一样拥有 native code
标志。一般将所有自定义函数(包括构造函数)都使用此函数进行保护 代码来源
const safeFunction = function safeFunction(func) {
//处理安全函数
Function.prototype.$call = Function.prototype.call;
const $toString = Function.toString;
const myFunction_toString_symbol = Symbol('('.concat('', ')'));
const myToString = function myToString() {
return typeof this === 'function' && this[myFunction_toString_symbol] || $toString.$call(this);
}
const set_native = function set_native(func, key, value) {
Object.defineProperty(func, key, {
"enumerable": false,
"configurable": true,
"writable": true,
"value": value
});
}
delete Function.prototype['toString'];
set_native(Function.prototype, "toString", myToString);
set_native(Function.prototype.toString, myFunction_toString_symbol, "function toString() { [native code] }");
const safe_Function = function safe_Function(func) {
set_native(func, myFunction_toString_symbol, "function" + (func.name ? " " + func.name : "") + "() { [native code] }");
}
return safe_Function(func)
}
// 使用示例
window.addEventListener = function addEventListener() {}
safeFunction(window.addEventListener)
// 本地node环境执行时,在后续代码处断点,控制台输入 window.addEventListener.toString() 查看是否成功加入[native code]