JSVMP(JavaScript Virtual Machine-Based Protect)JS虚拟机保护,VMP技术就是将原有的代码,转化为专用的字节码,由自写解释器进行解释执行,以此大幅提升逆向分析的成本,同样的,由于代码量的增加,运行效率也会大打折扣。
这里谈一谈一个概念,解释执行,就是将源代码逐行转换为机器码然后再执行。类似于JAVA的JVM,将字节码转换成机器码再执行,但是在JSVMP中,这里的的机器码就不再是可供CPU执行的x86/ARM指令,而是我们自定义的虚拟指令序列,实质上还是JS代码。
之前在windows软件、安卓native层中也遇到过VMP加壳,原理完全相同,比较亲切,但是在保护的对象和实际技术应用上,有着很大的区别,这里就不细谈了,等后续博客更新到安卓逆向或者windows 逆向,可能会拿在一起详细讲讲(??哪来的鸽子叫)。
JSVMP识别
JSVMP加密技术:原理、实现与攻防策略解析
https://comate.baidu.com/zh/page/hde0g2brg2j
这篇文章讲的就很不错了,聊个题外话,我一直认为,不论是写技术博客,还是分享CTF的WP等等分享,都需要写一点自己的内容,哪怕你看完别人的文章,按照别人的思路,自己重新复述一遍,那么这篇文章就是有价值的。不要为了完成一篇文章,去写一些自己不清楚的东西,复制粘贴更甚,不要这么做,要真真正正留下来点东西,最不重要的就是毫无意义的文字和浪费掉的时间。
接下来我将写一个简单的JSVMP,来演示一下他的执行流程,就最简单的,二数加法运算。实现之前,让我来讲一讲大体思路:
- 将二数加法操作拆解成三条指令,加载数到栈LOAD、栈上二数相加ADD、和值返回RET,建立指二数加法指令序列
- 创建指令序列解释器
先准备一下指令序列,指令序列按照一定的规则进行保存,可以是特定的编码,这里规定操作码和参数都以join(“/“)进行拼接。先生成一个指令序列以便后续解释器使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| var instructions = [ {op : "LOAD", arg : "a"}, {op : "LOAD", arg : "b"}, {op : "ADD"}, {op : "RET"} ]
var opcode = { LOAD : "_0x01d2c1", ADD : "_0x01d2c2", RET:"_0x01d2c3" }
function compileByInstruction(instructions){ const bytecode = []; for(let instruction of instructions){ bytecode.push(opcode[instruction.op]) if(instruction.arg !== undefined){ bytecode.push(instruction.arg.charCodeAt()); } } return bytecode.join("/"); }
console.log(compileByInstruction(instructions));
|
1 2
| (base) ➜ node demo.js _0x01d2c1/97/_0x01d2c1/98/_0x01d2c2/_0x01d2c3
|
这样一个混淆后的ADD指令集就生成好了,接下来实现一下最关键的指令集解析部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| function JSVMP(){ this.stack = []; this.opcode = { } }
JSVMP.prototype.add = function(_0xa1b2c1, _0x1ab2c2){ return _0x1ab2c2 + _0xa1b2c1; }
JSVMP.prototype.process = function($opcode){ let opcodeArry = $opcode.split("/"); for(let i = 0; i < opcodeArry.length; i++){ $key = opcodeArry[i]; i++; $value = opcodeArry[i]; this.opcode[$key] = $value; } }
JSVMP.prototype.execute = function(content){ let instructions = "_0x01d2c1/97/_0x01d2c1/98/_0x01d2c2/_0x01d2c3"; let instructArry = instructions.split("/"); let $opcode = "LOAD/_0x01d2c1/ADD/_0x01d2c2/RET/_0x01d2c3" this.process($opcode); for(let i = 0; i < instructArry.length; i++){ switch (instructArry[i]){ case this.opcode.LOAD: i++; this.stack.push(content[String.fromCharCode(instructArry[i])]); break; case this.opcode.ADD: let arg1 = this.stack.pop(); let arg2 = this.stack.pop(); this.stack.push(this.add(arg1, arg2)); break; case this.opcode.RET: return this.stack.pop()
} } }
var jsvmp = new JSVMP(); console.log(jsvmp.execute({a:3, b:2}));
|
1 2
| (base) ➜ jsvmp node demo.js 5
|
结果是对的,这个简单的ADD VMP就已经实现了,自定义了指令集、解释器,还具备了一定的混淆。
JSVMP的特征就是:
存在以上结构
存在JSVMP字样
入参传入很长的一串字符串
如何处理JSVMP
根据前人的经验,JSVMP大概有三种方法进行处理,第一种就是直接补环境,第二种就是纯算还原,第三种就是使用RPC进行远程调用。补环境比较方便,直接来像之前那样来补环境吧。
https://www.toutiao.com/

拿一个字节的网站来看看他们是怎么做的JSVMP,关注到,这里有一个_signature参数,来看一下这个是怎么生成的,打断点调试一下。

调试到,是有一个windows.byted_acrawler.sign函数用来生成这个signature值,走进去看一看有什么东西。

很明显,这里的signature生成逻辑已经被JSVMP混淆隐藏了,要在本地运行这个sign函数,可以将整个JSVMP代码全部扣下来,然后进行补环境。根据这个文件名可以发现,这个acrawler.js文件,是存在反爬的,可能会有环境检测,根据经验,进行补环境的过程中需要做一些特别的处理。
浏览器补环境,在node环境中要特别注意一下两个东西,一个是exports,一个是module,这两个在浏览器环境中是undefined,但是在node环境中不是,这里反爬会在这里区别一下环境,防止本地运行,需要手动调整一下代码。
1 2 3
| "undefined" != typeof exports ? exports : void 0, "undefined" != typeof module ? module : void 0, void 0, void 0
|

补环境中经常会遇到这样的length缺失,这就需要找对应上一步的A参数是什么,在对应的地方下一个log断点,和一个条件断点,查看length上一步是做了什么操作,然后再通过条件断点查看需要补什么环境。

然后使用调试,查看上一步的参数是protocol

重新设置条件断点 A==”protocol”,查看需要补的参数,通过断点发现,这个protocol是location里面的东西,直接从浏览器中拿就好

node环境中又出现了错误,这里可以打印出所有的A参数,查看哪些环境是我们忽略掉的,导致位数不一样,浏览器中的signature参数的位数是147,这里是47,显然补环境还没有结束

经过一步一步的S、R、A参数分析,发现缺少了localStorage和sessionStorage参数,以及对应的操作方法。这里遇到的问题比较多,有一段时间,我以为是我node版本的问题,结果重新走了一遍,通了。总之,要耐心,一步一步来,缺啥补啥。
扔一个补的环境代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| var document = { referrer: "https://www.toutiao.com/" }
var window = globalThis;
var location = { href : "https://www.toutiao.com/?wid=1657855126033", protocol: "https:" }
localStorage = { "__pwa_push_show_count": "3", "__tea_cache_first_24": "1", "__click_close_download_panel_ts__": "1768906192550", "tt_scid": "X-K2RhNMYuvyG8ixJ2bcVnTvdOwlrpi8bSZuvi3OMJx6jKSGDtiT1qvXoJcGL0xq80bd", "xmst": "-KuT0_WjztH_2l4g7SBmGR2cLG5uqneDAX1dgWBCATcTCTgf_J5cZwdKwzqb4t-5-1PATg09gUdMUgCu0Kr4KVdZxhMudDDuIUwQi2xyy9MPIjN6IX5Za5vwiMU4n7bl", "__tea_cache_tokens_24": "{\"web_id\":\"7592452048437610022\",\"user_unique_id\":\"7592452048437610022\",\"timestamp\":1768901790381,\"_type_\":\"default\"}", "__pwa_push_show_time": "1768550589920", "_byted_param_sw": "uOv3hoKTqsvlugnaOtw=", "__tea_cache_refer_24": "{\"refer_key\":\"\",\"refer_title\":\"今日头条\",\"refer_manual_key\":\"\",\"routeChange\":false}", "web_runtime_security_uid": "f413de2d-2d08-41db-a6c6-30c5810374de", "__is_visited_home": "1", "SLARDARtoutiao_web_pc": "JTdCJTIydXNlcklkJTIyOiUyMjc1OTI0NTIwNDg0Mzc2MTAwMjIlMjIsJTIyZGV2aWNlSWQlMjI6JTIyNzU5MjQ1MjA0ODQzNzYxMDAyMiUyMiwlMjJleHBpcmVzJTIyOjE3NzY2Nzc3OTE4MDMlN0Q=", "ttcid": "846493ba57fa4d43815d5de2d6d1b8ee20" }
sessionStorage = { "__tea_session_id_24": "{\"sessionId\":\"47e8012b-f317-47ff-96d7-81eaedf56e20\",\"timestamp\":1768906615958}", "_tea_cache_duration": "{\"duration\":11491611,\"page_title\":\"今日头条\"}", "_byted_param_sw": "uOv3hoKTqsvlugnaOtw=", "/": "1", "tt_scid": "X-K2RhNMYuvyG8ixJ2bcVnTvdOwlrpi8bSZuvi3OMJx6jKSGDtiT1qvXoJcGL0xq80bd" }
sessionStorage.getItem = function (arg){ return sessionStorage[arg]; }
localStorage.getItem = function (arg){ return localStorage[arg]; }
|
这里有很多补环境用的Proxy自吐脚本,扔一个,调试起来比较方便
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| function getEnv(proxy_array) { for(let i=0; i<proxy_array.length; i++){ handler = `{ get: function(target, property, receiver) { console.log('方法:get',' 对象:${proxy_array[i]}',' 属性:',property,' 属性类型:',typeof property,' 属性值类型:',typeof target[property]); return target[property]; }, set: function(target, property, value, receiver){ console.log('方法:set',' 对象:${proxy_array[i]}',' 属性:',property,' 属性类型:',typeof property,' 属性值类型:',typeof target[property]); return Reflect.set(...arguments); } }`; eval(` try{ ${proxy_array[i]}; ${proxy_array[i]} = new Proxy(${proxy_array[i]},${handler}); }catch(e){ ${proxy_array[i]}={}; ${proxy_array[i]} = new Proxy(${proxy_array[i]},${handler}); } `); } } proxy_array = ['window','document','locaion', 'navigator', 'history', 'screen', 'history'] getEnv(proxy_array);
|
至于纯算嘛,扔个链接:https://mp.weixin.qq.com/s/YAL4iT3ECQeK7vZRsLRFaA ,日后研究(!??哪来的鸽子叫)
还得扔点啥,哦,RPC,自个搜搜Chrome RPC 或者 JsRPC怎么用吧。
https://github.com/jxhczhl/JsRpc