补环境之补window对象

  1. 一、前言
  2. 二、思路
  3. 三、实现
    1. 3.1 确立window对象的继承关系
    2. 3.2 补充对象属性
  4. 四、脱环境脚本
    1. 4.1 特殊对象
    2. 4.2 原型函数
    3. 4.3 实例对象
  5. 五、总结
  6. 附录

一、前言

在js逆向中补环境这个方向一般是根据网站定制,即所谓的缺什么补什么,但网站补多了就会发现其实很多环境可以通用,如能将这部分抽离以复用,可以减少很大一部分逆向工作。

基于此,本文以window对象为例,参照浏览器端window对象,在本地node环境实现模仿,主要做了以下几点工作:

  1. window原型继承关系梳理
  2. window及其原型属性补充
  3. 实现一键脱环境脚本

二、思路

在js中一个对象由自身属性和原型属性构成,例如:

所以想要模仿构造window对象,也就是要补充其自身属性和原型属性,有趣的地方在于,原型属性也是一个对象,也有自身属性和原型属性,原型属性也是。。。,所有首要是要把window对象的继承关系搞清楚。

然后再为这些对象补上相应的属性,这里有几点需要注意:

  1. 有些对象的有些属性可以不用补,来看一个例子:

上图中Window对象中的length、arguments、caller、name、prototype是构造函数建立时就自动生成的,也就是说其实只要补TEMPORARYPERSISTENT,这点通过一个简单的例子可以直接看出,见下图:

同理,构造函数附带的原型对象中的constructor属性也不需要实现,这同样是自动生成的,见下图案列:

  1. window对象属性太多了(1000+),全部补完不太现实,因此只补几个属性作为代表。

window属性

用思维导图整理梳理要补的对象和属性,作为实现蓝图:

window关系图

三、实现

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点:

  1. window.name是一个存取器型属性,即实现了get和set方法的属性,这两个方法从浏览器端无法获得(底层实现是c++,展现在浏览器端是native code标识),对于这种情况,一般需要手动在网站调试结合js官网api文档来动态补充;

  2. EventTarget.prototype对象addEventListener属性的value方法,也是看不到底层源码,对外以native code显示,因此这里直接赋予了一个空方法,需要用时,调试网站动态实现;

  3. 是一个解释,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步:

  1. 定义构造函数
  2. 设置函数属性
  3. 设置原型属性
  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处需要注意的地方:

  1. 对于具体的功能属性,即在属性描述符中value类型为function,需要自行在本地实现相同功能,例如windowatob属性。
  2. 对于属性描述符实现了get/set的属性:
    • 当访问某对象属性时,并不会如value函数需要使用.()调用,而是以取值赋值形式呈现,例如window.location会调用get函数,使用window.location=123会调用set方法。
    • 所以最上层函数参数中引入了instanceObj,即当前构造函数的实例对象,将原get函数的this指向改到该实例对象,并将结果作为新get函数的返回值。
    • set方法用于改变get方法的返回值,由于get已直接返回最终的结果,因此set函数可以忽略。
    • 如果想以函数形式实现,需要去查官方文档对此api的介绍结合网站动态调试补充。
  3. 在属性描述符中value类型为Object(非function)的属性,属于实例对象,存在继承关系链,情况复杂,因此归到第三种情况:实例对象的脱环境脚本。

4.3 实例对象

例如window,location,navigator等,以location对象为例:

浏览器中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);
}

实例对象与原型函数相比少了”补原型函数属性”这一步,其余步骤大体相同,主要注意的是在第三步设置原型时:

  1. 使用Object.getPrototypeOf(obj)[Symbol.toStringTag]拿到实例对象的原型名称;
  2. 如该名称是undefined,说明该对象直接上层是Object,那么无须为其设置原型(所有对象的原型尽头是Object)。

window.location为例,结果如下:

五、总结

本文以补window对象为例,尝试在本地node环境复现window对象,在这个过程中归纳了补环境对象时的3种情形:

  1. 特殊对象的处理,如windowWindowproperties,前者属性太多,需要借助node对象实现,后者浏览器端无法直接获取。
  2. 原型函数的处理,并归纳成了4个步骤:
    1. 定义构造函数
    2. 设置原型函数属性
    3. 设置原型对象属性
    4. 为原型对象绑定原型
  3. 实例对象的处理,并归纳了成了3个步骤:
    1. 定义对象
    2. 设置对象属性
    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。
跃迁主页