一、前言
在js逆向中补环境这个方向一般是根据网站定制,即所谓的缺什么补什么,但网站补多了就会发现其实很多环境可以通用,如能将这部分抽离以复用,可以减少很大一部分逆向工作。
基于此,本文以window对象为例,参照浏览器端window对象,在本地node环境实现模仿,主要做了以下几点工作:
- window原型继承关系梳理
- window及其原型属性补充
- 实现一键脱环境脚本
二、思路
在js中一个对象由自身属性和原型属性构成,例如:
所以想要模仿构造window对象,也就是要补充其自身属性和原型属性,有趣的地方在于,原型属性也是一个对象,也有自身属性和原型属性,原型属性也是。。。,所有首要是要把window对象的继承关系搞清楚。
然后再为这些对象补上相应的属性,这里有几点需要注意:
- 有些对象的有些属性可以不用补,来看一个例子:
上图中Window对象中的length、arguments、caller、name、prototype
是构造函数建立时就自动生成的,也就是说其实只要补TEMPORARY
和PERSISTENT
,这点通过一个简单的例子可以直接看出,见下图:
同理,构造函数附带的原型对象中的constructor
属性也不需要实现,这同样是自动生成的,见下图案列:
- window对象属性太多了(1000+),全部补完不太现实,因此只补几个属性作为代表。
用思维导图整理梳理要补的对象和属性,作为实现蓝图:
三、实现
3.1 确立window对象的继承关系
//定义各对象,构造函数
let window={}
let Window=function Window(){}
let WindowProperties={}
let EventTarget=function EventTarget(){}
//设置继承关系
Object.setPrototypeOf(window,Window.prototype)
Object.setPrototypeOf(Window.prototype,WindowProperties)
Object.setPrototypeOf(WindowProperties,EventTarget.prototype)
上述代码中的继承关系是依据浏览器控制台打印的输出结果确定:
这里要注意的是WindowProperties
没有constructor
属性,说明可能不是由构造函数创建,因此当成普通对象构建,接下来补充对象属性。
3.2 补充对象属性
//定义对象属性
//window
Object.defineProperty(window, 'atob', {"value": global.atob, "writable": true, "enumerable": true, "configurable": true})
Object.defineProperty(window, 'btoa', {"value": global.btoa, "writable": true, "enumerable": true, "configurable": true})
Object.defineProperty(window, 'name', {"enumerable": true, "configurable": true,"get":function (){return ''},"set":function (){}})
//Window构造函数
Object.defineProperty(Window, 'PERSISTENT', {"value": 1, "writable": false, "enumerable": true, "configurable": false})
Object.defineProperty(Window, 'TEMPORARY', {"value": 0, "writable": false, "enumerable": true, "configurable": false})
//Window原型对象
Object.defineProperty(Window.prototype, 'PERSISTENT', {"value": 1, "writable": false, "enumerable": true, "configurable": false})
Object.defineProperty(Window.prototype, 'TEMPORARY', {"value": 0, "writable": false, "enumerable": true, "configurable": false})
Object.defineProperty(Window.prototype, Symbol.toStringTag, {"value": 'Window', "writable": false, "enumerable": false, "configurable": true})
//WindowProperties
Object.defineProperty(WindowProperties, Symbol.toStringTag, {"value": 'WindowProperties', "writable": false, "enumerable": false, "configurable": true})
//EventTarget构造函数没有需要补的属性,因此无须补
//EventTarget原型对象
Object.defineProperty(EventTarget.prototype, 'addEventListener', {"writable": true, "enumerable": true, "configurable": true, 'value': function addEventListener() {}})
Object.defineProperty(EventTarget.prototype, Symbol.toStringTag, {"value": 'EventTarget', "writable": false, "enumerable": false, "configurable": true})
先看一下和浏览器的对比结果(左:浏览器,右:本地node):
由上图可知,在整体结构上两者相近,但要注意的有3点:
window.name
是一个存取器型属性,即实现了get和set方法的属性,这两个方法从浏览器端无法获得(底层实现是c++,展现在浏览器端是native code
标识),对于这种情况,一般需要手动在网站调试结合js官网api文档来动态补充;EventTarget.prototype
对象addEventListener
属性的value
方法,也是看不到底层源码,对外以native code
显示,因此这里直接赋予了一个空方法,需要用时,调试网站动态实现;是一个解释,
Symbol.toStringTag
这个属性的作用是什么?它是一个浏览器内置属性,代表实例对象的类型,通过调用实例对象.toString()
得到,来看一个例子:
由上图可知,Symbol.toStringTag
的作用是用来标识实例对象的类型,同时也是该原型对象在控制台的显示名称(默认情况下与其构造函数名相同)。
四、脱环境脚本
浏览器环境中的对象颇多,一个个补下来难免伤手劳神,回想第三节中window对象的实现流程,在脑海中缕清思绪后,笔者认为可以分为3种情况:特殊对象、原型函数和实例对象。
4.1 特殊对象
该类对象有:window、WindowProperties等。
window对象属性太多,node环境下globalThis对象作用和window类似,因此可以用来作为代替品。
let window =globalThis;
Object.setPrototypeOf(window,Window.prototype)
WindowProperties因在浏览器端不可访问,因此也列为特殊对象特别处理,如图:
let WindowProperties = {}
Object.defineProperty(WindowProperties, Symbol.toStringTag, {"value": 'WindowProperties', "writable": false, "enumerable": false, "configurable": true})
Object.setPrototypeOf(WindowProperties,EventTarget.prototype)
4.2 原型函数
例如Window,EventTarget等,该类对象的实现可分为4步:
- 定义构造函数
- 设置函数属性
- 设置原型属性
- 为原型对象绑定原型对象
第四点的意思是,原型对象本身既然也是一个对象,那么必然是某个构造函数的实例,具有prototype属性,该属性必然指向一个原型对象,而该原型对象也必定是某个构造函数的实例对象,这样无限套娃直到顶层是Object
为止,下图是一个简单的例子,可以说明这种情况:
脱环境脚本:
// 获取原型函数环境代码
let getProtoFuncEnvCode = function getProtoFuncEnvCode(ProtoFunc, instanceObj) {
// ProtoFunc: 原型函数
// instanceObj: 实例对象, 可选参数
let code = "";
let protoName = ProtoFunc.name;
// 添加注释
code += `// ${protoName}对象\r\n`;
// 1.定义原型函数
code += `let ${protoName} = function ${protoName}(){}\r\n`;
// 2.设置原型函数属性
for (const key in Object.getOwnPropertyDescriptors(ProtoFunc)) {
if (key === "arguments" || key === "caller" || key === "length" || key === "name" || key === "prototype") {
continue;
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(ProtoFunc, key);
code += `Object.defineProperty(${protoName}, "${key}",{"configurable":${propertyDescriptor.configurable},"enumerable":${propertyDescriptor.enumerable},`;
if (propertyDescriptor.hasOwnProperty('writable')) {
code += `"writable":${propertyDescriptor.writable},`
}
if (propertyDescriptor.hasOwnProperty('value')) {
if (propertyDescriptor.value instanceof Object) {
if (typeof propertyDescriptor.value === 'function') {
code += `"value": function ${key}(){},`
} else {
code += `"value":{},` //需要额外实现实例对象的补充,可手动可自动
}
} else if (typeof propertyDescriptor.value === 'symbol') {
code += `"value":${propertyDescriptor.value.toString()},`;
} else if (typeof propertyDescriptor.value === "string") {
code += `"value":"${propertyDescriptor.value}",`;
} else {
code += `"value":${propertyDescriptor.value},`;
}
}
if (propertyDescriptor.hasOwnProperty('get')) {
if (typeof propertyDescriptor.get === 'function') {
let get_result = propertyDescriptor.get.call(instanceObj)
code += `"get": function ${key}_get(){return "${get_result}"},`
} else {
code += `get:undefined, `
}
}
if (propertyDescriptor.hasOwnProperty('set')) {
if (typeof propertyDescriptor.set === 'function') {
code += `"set": function ${key}_set(){},`
} else {
code += `set:undefined, `
}
}
code += '})\r\n'
}
// 3.设置原型对象的属性
code += `Object.defineProperty(${protoName}.prototype, Symbol.toStringTag,{"value":"${protoName}","writable":false,"enumerable":false,"configurable":true})\r\n`;
for (const key in Object.getOwnPropertyDescriptors(ProtoFunc.prototype)) {
if (key === "constructor") {
continue;
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(ProtoFunc.prototype, key);
code += `Object.defineProperty(${protoName}.prototype, "${key}",{"configurable":${propertyDescriptor.configurable},"enumerable":${propertyDescriptor.enumerable},`;
if (propertyDescriptor.hasOwnProperty('writable')) {
code += `"writable":${propertyDescriptor.writable},`
}
if (propertyDescriptor.hasOwnProperty('value')) {
if (propertyDescriptor.value instanceof Object) {
if (typeof propertyDescriptor.value === 'function') {
code += `"value": function ${key}(){},`
} else {
code += `"value":{},`
}
} else if (typeof propertyDescriptor.value === 'symbol') {
code += `"value":${propertyDescriptor.value.toString()},`;
} else if (typeof propertyDescriptor.value === "string") {
code += `"value":"${propertyDescriptor.value}",`;
} else {
code += `"value":${propertyDescriptor.value},`;
}
}
if (propertyDescriptor.hasOwnProperty('get')) {
if (typeof propertyDescriptor.get === 'function') {
let get_result = propertyDescriptor.get.call(instanceObj)
code += `"get": function ${key}_get(){return "${get_result}"},`
} else {
code += `get:undefined, `
}
}
if (propertyDescriptor.hasOwnProperty('set')) {
if (typeof propertyDescriptor.set === 'function') {
code += `"set": function ${key}_set(){},`
} else {
code += `set:undefined, `
}
}
code += '})\r\n'
}
// 4.设置原型对象的原型属性
let protoProtoName = Object.getPrototypeOf(ProtoFunc.prototype)[Symbol.toStringTag];
if (protoProtoName !== undefined) { //undefined说明直接是继承于object
code += `Object.setPrototypeOf(${protoName}.prototype, ${protoProtoName}.prototype);\r\n`;
}
console.log(code);
copy(code);
}
效果:
参数传想要补的原型函数,即可得到本文第三节所做的工作。这段脚本有3处需要注意的地方:
- 对于具体的功能属性,即在属性描述符中
value
类型为function
,需要自行在本地实现相同功能,例如window
的atob
属性。 - 对于属性描述符实现了get/set的属性:
- 当访问某对象属性时,并不会如value函数需要使用
.()
调用,而是以取值赋值形式呈现,例如window.location
会调用get函数,使用window.location=123
会调用set方法。 - 所以最上层函数参数中引入了
instanceObj
,即当前构造函数的实例对象,将原get函数的this指向改到该实例对象,并将结果作为新get函数的返回值。 - set方法用于改变get方法的返回值,由于get已直接返回最终的结果,因此set函数可以忽略。
- 如果想以函数形式实现,需要去查官方文档对此api的介绍结合网站动态调试补充。
- 当访问某对象属性时,并不会如value函数需要使用
- 在属性描述符中value类型为Object(非function)的属性,属于实例对象,存在继承关系链,情况复杂,因此归到第三种情况:实例对象的脱环境脚本。
4.3 实例对象
例如window,location,navigator
等,以location对象为例:
脱环境脚本:
let getObjEnvCode = function getObjEnvCode(obj, objName) {
let code = "";
// 添加注释
code += `// ${objName}对象\r\n`;
// 1. 定义对象
code += `let ${objName} = {}\r\n`;
// 2. 定义对象属性
for (const key in Object.getOwnPropertyDescriptors(obj)) {
let propertyDescriptor = Object.getOwnPropertyDescriptor(obj, key);
code += `Object.defineProperty(${objName}, "${key}",{"configurable":${propertyDescriptor.configurable},"enumerable":${propertyDescriptor.enumerable},`;
if (propertyDescriptor.hasOwnProperty('writable')) {
code += `"writable":${propertyDescriptor.writable},`
}
if (propertyDescriptor.hasOwnProperty('value')) {
if (propertyDescriptor.value instanceof Object) {
if (typeof propertyDescriptor.value === 'function') {
code += `"value": function ${key}(){},`
} else {
code += `"value":{},`
}
} else if (typeof propertyDescriptor.value === 'symbol') {
code += `"value":${propertyDescriptor.value.toString()},`;
} else if (typeof propertyDescriptor.value === "string") {
code += `"value":"${propertyDescriptor.value}",`;
} else {
code += `"value":${propertyDescriptor.value},`;
}
}
if (propertyDescriptor.hasOwnProperty('get')) {
if (typeof propertyDescriptor.get === 'function') {
let get_result = propertyDescriptor.get.call(obj)
code += `"get": function ${key}_get(){return "${get_result}"},`
} else {
code += `get:undefined, `
}
}
if (propertyDescriptor.hasOwnProperty('set')) {
if (typeof propertyDescriptor.set === 'function') {
code += `"set": function ${key}_set(){},`
} else {
code += `set:undefined, `
}
}
code += '})\r\n'
}
// 3. 设置原型
let protoName = Object.getPrototypeOf(obj)[Symbol.toStringTag];
if (protoName !== undefined) { //undefined说明直接是继承于object
code += `Object.setPrototypeOf(${objName}, ${protoName}.prototype);\r\n`;
}
console.log(code);
copy(code);
}
实例对象与原型函数相比少了”补原型函数属性”这一步,其余步骤大体相同,主要注意的是在第三步设置原型时:
- 使用
Object.getPrototypeOf(obj)[Symbol.toStringTag]
拿到实例对象的原型名称; - 如该名称是
undefined
,说明该对象直接上层是Object
,那么无须为其设置原型(所有对象的原型尽头是Object)。
以window.location
为例,结果如下:
五、总结
本文以补window
对象为例,尝试在本地node环境复现window
对象,在这个过程中归纳了补环境对象时的3种情形:
- 特殊对象的处理,如
window
、Windowproperties
,前者属性太多,需要借助node对象实现,后者浏览器端无法直接获取。 - 原型函数的处理,并归纳成了4个步骤:
- 定义构造函数
- 设置原型函数属性
- 设置原型对象属性
- 为原型对象绑定原型
- 实例对象的处理,并归纳了成了3个步骤:
- 定义对象
- 设置对象属性
- 为对象绑定原型
在实际操作中,完成一个对象的补充,往往这三种情况会同时存在,相互嵌套,对于逆向者而言,这是培养耐心和细心的良好道场。
附录
借助一键脱环境脚本得到的window对象:
// EventTarget对象
let EventTarget = function EventTarget(){}
Object.defineProperty(EventTarget.prototype, Symbol.toStringTag,{"value":"EventTarget","writable":false,"enumerable":false,"configurable":true})
Object.defineProperty(EventTarget.prototype, "addEventListener",{"configurable":true,"enumerable":true,"writable":true,"value": function addEventListener(){},})
Object.defineProperty(EventTarget.prototype, "dispatchEvent",{"configurable":true,"enumerable":true,"writable":true,"value": function dispatchEvent(){},})
Object.defineProperty(EventTarget.prototype, "removeEventListener",{"configurable":true,"enumerable":true,"writable":true,"value": function removeEventListener(){},})
//WindowProperties对象
let WindowProperties = {}
Object.defineProperty(WindowProperties, Symbol.toStringTag, {"value": 'WindowProperties', "writable": false, "enumerable": false, "configurable": true})
Object.setPrototypeOf(WindowProperties,EventTarget.prototype)
// Window对象
let Window = function Window(){}
Object.defineProperty(Window, "TEMPORARY",{"configurable":false,"enumerable":true,"writable":false,"value":0,})
Object.defineProperty(Window, "PERSISTENT",{"configurable":false,"enumerable":true,"writable":false,"value":1,})
Object.defineProperty(Window.prototype, Symbol.toStringTag,{"value":"Window","writable":false,"enumerable":false,"configurable":true})
Object.defineProperty(Window.prototype, "TEMPORARY",{"configurable":false,"enumerable":true,"writable":false,"value":0,})
Object.defineProperty(Window.prototype, "PERSISTENT",{"configurable":false,"enumerable":true,"writable":false,"value":1,})
Object.setPrototypeOf(Window.prototype, WindowProperties);
//window对象
let window =globalThis;
Object.setPrototypeOf(window,Window.prototype)
一键脱环境脚本:
第4节中获取属性描述符这部分可以解耦,因此这部分分离出来单独形成一个函数。
// 获取原型函数环境代码
let getProtoFuncEnvCode = function getProtoFuncEnvCode(ProtoFunc, instanceObj) {
// ProtoFunc: 原型函数
// instanceObj: 实例对象,可选
let code = "";
let protoName = ProtoFunc.name;
// 添加注释
code += `// ${protoName}对象\r\n`;
// 1.定义原型函数
code += `let ${protoName} = function ${protoName}(){}\r\n`;
// 2.设置原型函数属性
for (const key in Object.getOwnPropertyDescriptors(ProtoFunc)) {
if (key === "arguments" || key === "caller" || key === "length" || key === "name" || key === "prototype") {
continue;
}
let definePropertyDescriptor = getDescriptor(ProtoFunc, key, instanceObj);
code += definePropertyDescriptor
}
// 3.设置原型对象的属性
code += `Object.defineProperty(${protoName}.prototype, Symbol.toStringTag,{"value":"${protoName}","writable":false,"enumerable":false,"configurable":true})\r\n`;
for (const key in Object.getOwnPropertyDescriptors(ProtoFunc.prototype)) {
if (key === "constructor") {
continue;
}
let definePropertyDescriptor = getDescriptor(ProtoFunc.prototype, key,`${protoName}.prototype`, instanceObj);
code += definePropertyDescriptor
}
// 4.设置原型对象的原型属性
let protoProtoName = Object.getPrototypeOf(ProtoFunc.prototype)[Symbol.toStringTag];
if (protoProtoName !== undefined) { //undefined说明直接是继承于object
code += `Object.setPrototypeOf(${protoName}.prototype, ${protoProtoName}.prototype);\r\n`;
}
console.log(code);
copy(code);
}
//获取实例对象环境
let getObjEnvCode = function getObjEnvCode(obj, objName) {
let code = "";
// 添加注释
code += `// ${objName}对象\r\n`;
// 1. 定义对象
code += `let ${objName} = {}\r\n`;
// 2. 定义对象属性
for (const key in Object.getOwnPropertyDescriptors(obj)) {
let definePropertyDescriptor = getDescriptor(obj, key,objName, obj);
code += definePropertyDescriptor
}
// 3. 设置原型
let protoName = Object.getPrototypeOf(obj)[Symbol.toStringTag];
if (protoName !== undefined) { //undefined说明直接是继承于object
code += `Object.setPrototypeOf(${objName}, ${protoName}.prototype);\r\n`;
}
console.log(code);
copy(code);
}
//获取属性描述符
let getDescriptor = function getDescriptor(obj, prop,objName, instanceObj) {
let code = ''
let propertyDescriptor = Object.getOwnPropertyDescriptor(obj, prop);
code += `Object.defineProperty(${objName}, "${prop}",{"configurable":${propertyDescriptor.configurable},"enumerable":${propertyDescriptor.enumerable},`;
if (propertyDescriptor.hasOwnProperty('writable')) {
code += `"writable":${propertyDescriptor.writable},`
}
if (propertyDescriptor.hasOwnProperty('value')) {
if (propertyDescriptor.value instanceof Object) {
if (typeof propertyDescriptor.value === 'function') {
code += `"value": function ${prop}(){},`
} else {
code += `"value":{},` //需要额外实现实例对象的补充,可手动可自动
}
} else if (typeof propertyDescriptor.value === 'symbol') {
code += `"value":${propertyDescriptor.value.toString()},`;
} else if (typeof propertyDescriptor.value === "string") {
code += `"value":"${propertyDescriptor.value}",`;
} else {
code += `"value":${propertyDescriptor.value},`;
}
}
if (propertyDescriptor.hasOwnProperty('get')) {
if (typeof propertyDescriptor.get === 'function') {
if (instanceObj) {
let get_result = propertyDescriptor.get.call(instanceObj)
code += `"get": function ${prop}_get(){return "${get_result}"},`
}else {
code += `"get": function ${prop}_get(){},`
}
} else {
code += `get:undefined, `
}
}
if (propertyDescriptor.hasOwnProperty('set')) {
if (typeof propertyDescriptor.set === 'function') {
code += `"set": function ${prop}_set(){},`
} else {
code += `set:undefined, `
}
}
code += '});\r\n'
return code
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,如有问题请邮件至2454612285@qq.com。