JS逆向-JSVMP

​ 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("/");
}

//将操作码进行混淆,并且将arg进行ASCCII码转换,使用斜杠进行拼接

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
// 创建堆栈和指令集,指令集加一个混淆,通过this.process进行动态加载,这里先置空,js中使用list来模拟stack非常的方便
function JSVMP(){
this.stack = [];
this.opcode = {
}
}

//在execute外定义ADD指令的逻辑
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核心执行入口,指令序列写死了,指令集也写在这里面
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参数,来看一下这个是怎么生成的,打断点调试一下。

signature生成

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

JSVMP代码

​ 很明显,这里的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处理

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

断点调试

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

protocol

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

查看protocol归属

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

localsessionStorage

​ 经过一步一步的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://www.cnblogs.com/a438842265/p/18332426,感谢大佬分享

​ 至于纯算嘛,扔个链接:https://mp.weixin.qq.com/s/YAL4iT3ECQeK7vZRsLRFaA ,日后研究(!??哪来的鸽子叫)

​ 还得扔点啥,哦,RPC,自个搜搜Chrome RPC 或者 JsRPC怎么用吧。

https://github.com/jxhczhl/JsRpc