JS逆向之补环境学习笔记(较杂)

下载使用vm2(有安全漏洞,酌情使用)

如果我们直接使用Node环境来运行js代码的话,有些东西是无法规避环境检测的,所以我们需要一个纯净的V8环境来进行补环境的操作。vm2可以理解为一个纯净的脱离Node的V8环境,相当于重新启动了一个进程,然后在进程中不允许加载Node中的一些东西,禁止了一些Node中的特性,相当于V8的沙箱环境。PS:一些node环境中无法删除的node特征可以在vm2中删除,而不是说vm2=v8环境。

npm install vm2 //下载vm2

const fs = require('fs')
const {VM,VMScript} = require('vm2')   //VMScript是用来调试VM环境中的代码的

const file = `${__dirname}/code.js`
const windowfile = `${__dirname}/window.js`
const vm = new VM()
const script = new VMScript(fs.readFileSync(file),'VM2') //第二个参数是指调试名,可以自己任意指定

vm.run(script)

检测环境的例子

  1. 如node环境中有process,而V8环境或者浏览器环境中没有process。
  2. 任何canvas指纹都存在一定误杀率,所以厂商不会去对canvas指纹做强校验,因为不同浏览器canvas底层实现代码不同;其次即便是相同浏览器,它的内核版本不同则取出来的canvas指纹也是不同的:document.createElement('canvas').toDataURL()
  3. toString()
  4. 基于原型链的检测
  5. DOM环境的检测

手动补环境的例子

主要可以参考官方文档来:
https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/getContext

  1. document.createElement
// 补 document.createElement('canvas').toDataURL()
document = {
    createElement: function(tagName){
        var tag = (tagName + '').toLowerCase;
        if (tag == 'canvas')
        {
            return {
                toDataURL:function(){
                    return 'data:image/png;base64,iVBORw0KG...略...RU5ErkJggg=='  //可以手动在浏览器中运行生成获取
                }
            }
        }
        return {}
    }
}
  1. 下面的代码中:
//在node环境中运行下面代码
var window = this;
navigator = {
    userAgent:'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36'
}
const descriptor1 = Object.getOwnPropertyDescriptor(navigator,'userAgent');

此时会返回这样的一个对象:
在这里插入图片描述
但如果是在浏览器中,是拿不到descriptor1的(返回undefined)。
此时我们可以这样补环境:

Object.getOwnPropertyDescriptor_ = Object.getOwnPropertyDescriptor;
Object.getOwnPropertyDescriptor = function(o,p){
    if (o.toLocaleString() == '[object Navigator]'){
        return undefined;
    }
    Object.getOwnPropertyDescriptor_.apply(this,arguments);
}

此时再次运行const descriptor1 = Object.getOwnPropertyDescriptor(navigator,'userAgent');,得到的返回值便是undefined了。

  1. 补原型链环境例
var Window_my = function Window_my() {

}

window_my = new Window_my;

Object.defineProperties(window_my.__proto__,{
    [Symbol.toStringTag]:{
        value:'Window_my',
        configurable:true
    }
});

var WindowProperties_my = function WindowProperties_my() {
    
}

Object.defineProperties(WindowProperties_my.prototype,{
    [Symbol.toStringTag]:{
        value:'WindowProperties_my',
        configurable:true
    }
});

Window_my.prototype.__proto__ = WindowProperties_my.prototype;

var EventTarget_my = function EventTarget_my() {
    
}

Object.defineProperties(EventTarget_my.prototype,{
    [Symbol.toStringTag]:{
        value:'EventTarget_my',
        configurable:true
    }
});

WindowProperties_my.prototype.__proto__ = EventTarget_my.prototype;

如以上代码,便是一个补原型链的环境例子,效果如下:
在这里插入图片描述
4. 补环境例

a = document.createElement('a')
a.href = 'https://www.yuanrenxue.com'
console.log(a.host)   // 输出www.yuanrenxue.com

可见我们修改了href的同时也修改了host,要如何实现这一目的?

// 注意自己伪造的对象应该如何避免被new(在浏览器中也无法被new)
HTMLAnchorElement = function HTMLAnchorElement(val){
    debugger;
    if(val === "此处可以填入一个密钥,当密钥对的时候该对象才会被new出来"){
    }else if(this instanceof HTMLAnchorElement){  //new的时候,this指向实例化对象
        throw TypeError('Illegal constructor')
    }else{        //不使用new来创建对象的时候,this指向的是window
        throw TypeError(`Failed to construct 'HTMLAnchorElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function`)
    }
};
Object.defineProperty(HTMLAnchorElement.prototype, 'href', {
    set: function(){
        debugger;
        this.host = arguments[0].split('/').at(-1);
        this._HTMLAnchorElement_href = arguments[0];
        // return arguments[0]   // 这里不用return  等号表达式在解析的时候会自动向前赋值的,这里不需要return
    },
    get: function(){
        debugger;
        return this._HTMLAnchorElement_href;  //这里不用 return this.href 的原因在于避免无限循环调用(栈溢出),所以我们需要一个中间值_HTMLAnchorElement_href
    },
})

HTMLAnchorElement.prototype.host = '';
HTMLAnchorElement.prototype.href = '';
HTMLAnchorElement.prototype._HTMLAnchorElement_href = '';

document = {
    createElement: function(val){
        if(val === 'a'){
            return new HTMLAnchorElement("此处可以填入一个密钥,当密钥对的时候该对象才会被new出来")
        }
    }
}
  1. form标签对全局变量影响的检测:
备注:
window.__proto__.__proto__[tag.id] = tag这个地方完整的写法是依次去浏览器的window的原型上去找,
发现当window.__proto__.__proto__.__proto__.__yrxform__时为undefined,
然后window.__proto__.__proto__是一个WindowProperties,那么我们考虑从WindowProperties上去补,下面的补法仅仅是个示例,针对以下几行检测的代码所写。
window = global;
let document = {};
let formTag = {
    tagName: "FORM"
};
document.createElement = function (tagName){
    if(tagName === "form"){
        return formTag
    }
};
document.body = {};
document.body.appendChild = function (tag){
    if(tag.tagName === "FORM"){
        window.__proto__.__proto__[tag.id] = tag  
    }
};

form = document.createElement("form");
form.id = "__yrxform__";
form.action = "https://yuanrenxue.com";
document.body.appendChild(form);
if (window["__yrxform__"] !== form){console.log("你被检测了1")}
if (!("__yrxform__" in window) || window.hasOwnProperty("__yrxform__")){console.log("你被检测了2")}
if (!(delete window["__yrxform__"])){console.log("你被检测了3")}
if (window["__yrxform__"] !== form){console.log("你被检测了4")}
if (Object.getOwnPropertyDescriptor(window, "__yrxform__")){console.log("你被检测了5")}
window["__yrxform__"] = 1;
if (window["__yrxform__"] === form){console.log("你被检测了6")}
if (!Object.getOwnPropertyDescriptor(window, "__yrxform__")){console.log("你被检测了7")}
delete window["__yrxform__"];
if (window["__yrxform__"] !== form){console.log("你被检测了8")}
  1. navigator.plugins的检测
分析:
- navigator.plugins 是一个类数组对象,是PluginArray的实例
- navigator.plugins[0] 是一个类数组对象,是Plugin的实例
- 取Plugin下的属性的时候,每次取到的属性都是新的(也就是,每次取都new 了一下),结果是MimeType的实例
- MimeType的实例下面有一个enabledPlugin属性,指向了它的"祖宗" Plugin
function MimeType(_ts){
    this["enabledPlugin"] = _ts
}

function Plugin(){}
Plugin = new Proxy(Plugin, {
    construct: function (){
        let ob = {}
        Object.defineProperty(ob, "0", {
            get: function (){
                return new MimeType(this)
            }
        })
        Object.defineProperty(ob, "1", {
            get: function (){
                return new MimeType(this)
            }
        })
        return ob
    }
})

function PluginArray(){
    this["0"] = new Plugin()
}
navigator = {}
navigator.plugins = new PluginArray()
var dd=navigator.plugins[0]
console.log(dd[0]==dd[0])
console.log(navigator.plugins[0][0]==navigator.plugins[0][0])
console.log(dd[0].enabledPlugin[0]==dd[0])
console.log(navigator.plugins[0][0].enabledPlugin==dd)
console.log(navigator.plugins[0][0].enabledPlugin==dd[1].enabledPlugin)

代理是什么?

参考文章:https://www.jianshu.com/p/63507d282dfe

  • 使用一个简易封装的代理例:
function vmProxy(o)
{
    return new Proxy(o,{
        set(target,property,value)
        {
            console.log('set->',target,property,value);
            return Reflect.set(...arguments);
        },
        get(target,property,receiver)
        {
            console.log('get->',target,property,receiver);
            return target[property];
        }
    })
}
//测试一下↓
var navigator = vmProxy({})
navigator.test = 111
console.log(navigator.test)
  • 对于Proxy,有一个要点是后代理的检测不到先代理的。所以我们自己伪造的框架中我们是先代理的,无法被检测代码检测到。

一些零碎的知识点

  1. 当var window = global时,可以用下面这个方法修改window的名字
Object.defineProperties(window,{
     [Symbol.toStringTag]:{
         value:'window',
         configurable:true
     }
});
  1. 创建对象的几种方式:
  • {}
  • Object.create({})
  • class f{}
  • function f(){}
  1. document.all的检测与绕过
  • document.all 是类数组对象。也有属性和方法,怎么看都应该是个数组,但typeof document.all = undefined;
  • 它具备以下特性:1.有length 2.隐式转换结果为 ‘[object HTMLAllCollection]’ 3.typeof document.all 结果是 ‘undefined’ 4.可以取值
  • 以上这些特性可以用来检测环境,但typeof目前无法在Js层面解决,可以通过魔改底层nodejs代码实现。
  • 现阶段纯js只能通过“对付”绕过,例如:
// 先通过调试分析的手段,找到每一次调用document.all进行了什么操作,然后针对性进行hook处理
// 例如下面的代码,调用了三次document.all,依次返回undefined,undefined,6就可以通过检测,那么就Hook它,针对性绕过该检测
document = {}

_all_number = 0

Object.defineProperty(document, "all", {
    get: function (){
        return [undefined, undefined, [1,2,3,4,5,6]][_all_number++]
    }
})

console.log(typeof document.all);
console.log(typeof document.all);
a = document.all.length;
console.log(a);
  1. 纯净的V8环境中在this下没有setTimeout、setinterval,但在浏览器里又被放在了this(window)下。
  2. 使用webRTC获取本机真实IP
    注意事项:因为浏览器本身是不被授权获取本机IP的,所以我们需要手动打开授权设置。拿谷歌举例:在导航栏里输入 chrome://flags/ 进入设置页面 搜索“Anonymize local IPs exposed by WebRTC” 按钮点击修改为 disabled。
  3. Navigator.getBattery()获取设备电池状态:navigator.getBattery().then((res)=>{console.log(res)});
  4. 蜜罐如何处理: 1.笨方法:浏览器和nodejs联调,一步一步跟栈排查;2.取巧方法,有时候能很快定位到:搜索条件分支(a. if else b. switch case c. 三目表达式 d. try catch e. && ||)
  5. 使用谷歌浏览器调试nodejs: node --inspect-brk .\demo.js
  6. 在jsvmp中插桩时,或者平时逆向时可能会遇到console被重写的情况,这时候我们使用日志断点就没法打印出想要的东西,这时候我们可以使用Object.assign(),对一个对象或多个对象进行深拷贝,彻底切断两个对象间的内存之间的联系。比如针对console.log被重写的情况,我们直接在代码开头处进行hook,var _console = Object.assign(console);即可 。
  7. 在Nodejs里global.__proto__为空对象:{}。
  8. 要注意this指向的问题:
let a = {a: 100}
a.__proto__ = location
console.log(a.href)

这段代码在浏览器中会报错,但在nodejs中不会报错,可以类似于下面这样使用proxy处理:

Object.defineProperty(location,'href',{
	get: function(){
			if (this != location){
				throw TypeError("Illegal invocation")		
			}
		}
})

参考

志远开源二期 付费课程
vm2实现原理分析
JS Proxy(代理)
猿人学sec4

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值