首页
论坛
课程
招聘
[原创] 给"某音"的js虚拟机写一个编译器
2020-8-12 15:10 43308

[原创] 给"某音"的js虚拟机写一个编译器

2020-8-12 15:10
43308

0x0 前言

其实这篇笔记很久之前就写了, 写的零零碎碎的, 最近翻硬盘的时候看到了, 就想着整理一下发出来, 和大家讨论一下, 其中有些逻辑可能自己都不太清楚了(毕竟写代码不注释, 一周不看代码就不属于自己了, 手动狗头), 描述的有问题欢迎指出哈;

0x1 js虚拟机逆向

先看看原始文件是什么样的(一部分opcode片段), 这时候看还是比较乱的...

      .........
      case 71:
          v[x++] = n;
          break;
      case 72:
          v[x++] = +f();
          break;
      case 73:
          u(parseInt(f(), 36));
          break;
      case 75:
          if (v[--x]) {
              b++;
              break
          }
      case 74:
          g = t.charCodeAt(b++) - 32 << 16 >> 16,
          b += g;
          break;
      case 76:
          u(k[t.charCodeAt(b++) - 32]);
          break;
      case 77:
          y = v[--x],
          u(v[--x][y]);
          break;
      case 78:
          g = t.charCodeAt(b++) - 32,
          u(a(v, x -= g + 1, g));
          break;
      case 79:
          g = t.charCodeAt(b++) - 32,
          u(k["$" + g]);
          break;
      .........

如何逆向这部分我这里不细说了, 单步调试的话还是很容易看出每个操作的意义的, 经过一步步单步分析后, 逻辑就清晰了很多, 可以看到不少压栈出栈的操作, 是一个基于堆栈的栈式虚拟机, 利用数组模拟堆栈, 且有作用域管理 (完整的代码我会放在附件)

      .........
    case 71:
        vm_stack[vm_esp++] = pthis;
        console.log('PUSH pthis {obj:%s type:%s}', my_tostring((vm_stack[vm_esp-1])), typeof((vm_stack[vm_esp-1])));
        break;
    case 72:
        vm_stack[vm_esp++] = +vm_substring();
        console.log('PUSH STR {obj:%s type:%s}', my_tostring((vm_stack[vm_esp-1])), typeof((vm_stack[vm_esp-1])))
        break;
    case 73:
        vm_push(parseInt(vm_substring(), 36));
        console.log('PUSH INT {obj:%s type:%s}', my_tostring((vm_stack[vm_esp-1])), typeof((vm_stack[vm_esp-1])))
        break;
    case 75: //判断条件是否成立
        if (vm_stack[--vm_esp]) {
            index++;
            break
        }
    case 74: //不成立则跳过, 或用于循环代码块
        g = codes.charCodeAt(index++) - 32
        g = g << 16 >> 16
        console.log('IDX+=%s', my_tostring(g))
        index += g;
        break;
    case 76:
        var vars_idx = codes.charCodeAt(index) - 32;
        //vars_idx = vars_idx);
        vm_push(vm_vars[codes.charCodeAt(index++) - 32]);
        console.log('PUSH Vars[%s] {obj:%s type:%s}',my_tostring( vars_idx), my_tostring(vm_vars[vars_idx]), typeof((vm_vars[vars_idx])))
        break;
    case 77:
        y = vm_stack[--vm_esp],
        vm_push(vm_stack[--vm_esp][y]);
        console.log('PUSH_OBJECT {obj:%s type:%s}', my_tostring((vm_stack[vm_esp-1])), typeof((vm_stack[vm_esp-1])))
        break;
    case 78:
        g = codes.charCodeAt(index++) - 32
        vm_push(vmNewObject(vm_stack, vm_esp -= g + 1, g));
        console.log('NEW_OBJECT {obj:%s type:%s}', my_tostring((vm_stack[vm_esp-1])), typeof((vm_stack[vm_esp-1])))
        break;
    case 79:
        g = codes.charCodeAt(index++) - 32;
        vm_push(vm_vars["$" + g]);
        console.log('PUSH Vars_2[%s]', my_tostring(g));
        break;
      .........

0x2 如何写一个编译器?

esprima和escodegen的仓库

esprima

escodegen

 

既然已经知道每个opcode的作用, 那么如何针对这些opcode去写一个编译器呢? emmm...我这里选择了一个比较low的方式, 使用esprima把js代码解析成ast, 再使用escodegen解析ast的过程中(修改escodegen.js), 根据不同的type来生成自己的opcode的代码, 最后再写入文件...

 

比如在IfStatement不同的位置插入接口encrypt_code
59.png

 

encrypt_code根据不同的位置来生成opcode
60.png

 

这里我举ifelse分支的例子, 看看如何把js代码编译成该虚拟机可以执行的opcode的, 以及opcode的格式

if elseif else

测试代码:

  var a = 11;
  if (a < 10) {
      a = 10;
  } else if (a == 10) {
      a = 11;
  } else {
      a = 12;
  }

使用编译器跑一遍, 在encrypt_code入口打印下的类型, 看一下经过编译器了哪些位置, 以及对应的js代码

Expression            //var a = 11;
IfStatement           //if(a < 10)
EnmaybeBlock
EnBlockStatement      //{      
ExpressionStatement   //a = 10
LvBlockStatement      //}   
LvmaybeBlock
IfStatement           //else if (a == 10)
EnmaybeBlock    
EnBlockStatement      //{      
ExpressionStatement   //a = 11
LvBlockStatement      //}
LvmaybeBlock
IfStatement_ELSEBEGIN //else
EnBlockStatement      //{
ExpressionStatement   //a = 12
LvBlockStatement      //}
IfStatement_ELSEEND

这里可以看到每种类型对应的js代码, 接下来就是根据不同的类型生成opcode, 生成过程用文字描述会比较冗余, 这里我直接把生成的opcode结构贴上来, 就能很直观的理解了

 

以下为生成的opcode (虚拟机解析opcode的时候会先减去-32, 这里的已经处理了):

14,11,83,15,76,15,14,10,66,60,75,6,14,10,83,15,74,20,76,15,14,10,68,2,29,29,75,6,14,11,83,15,74,4,14,12,83,15

直接看又是很蒙是不是, 没事, 我们拆开和js代码比对来看就很清晰了


var a = 11;  >>  14,11,83,15
opcode dest
PUSH(14) (Value)11 //把11压栈
MOV_VARS(83) (Index)15 //把栈顶的值放到变量区[15]
if(a < 10)  >> 76,15,14,10,66,60,75,6
opcode dest
PUSH_VAR(76) (Value)15 //把变量区[15]的值压栈
PUSH(14) (Value)10 //把10压栈
EXPRESSION(66) (Symbol '<') 60 //把栈顶的两个值做运算, 结果放在栈顶
IS_TURE(75) (Value)6 //判断栈顶的值是否为True, 如果为False则把当前opcode_index+Value
{ a = 10 } >>  14,10,83,15,74,20
opcode dest
PUSH(14) (Value)10 //把10压栈
MOV_VARS(83) (Index)15 //把栈顶的值放到变量区[15]
SKIP_BLOCK(74) (Value)20 //跳出Block, 当前opcode_index+Value
else if (a == 10)  >>  76,15,14,10,68,2,29,29,75,6
opcode dest1 dest2
PUSH_VAR(76) (Value)15 //把变量区[15]的值压栈
PUSH(14) (Value)10 //把10压栈
EXPRESSION2(68) (Value)2 //符号数 (Symbol '==') 2929 //把栈顶的两个值做运算, 结果放在栈顶
IS_TURE(75) (Value)6 //判断栈顶的值是否为True, 如果为False则把当前opcode_index+Value
{ a = 11 }  >>  14,11,83,15,74,4
opcode dest
PUSH(14) (Value)11 //把11压栈
MOV_VARS(83) (Index)15 //把栈顶的值放到变量区[15]
SKIP_BLOCK(74) (Value)4 //跳出Block, 当前opcode_index+Value
{ a = 12 }  >>  14,12,83,15
opcode dest
PUSH(14) (Value)12 //把12压栈
MOV_VARS(83) (Index)15 //把栈顶的值放到变量区[15]

 

到这里以上代码整个opcode的解析过程就已经结束了, 在贴一下虚拟机执行的过程吧, 可以清晰的看到分支最后走到了a = 12

vmEnter
vmRun
opcode = 14
PUSH Vlaue{ obj:11 type:number}
opcode = 83
MOV Vars[15], v {v:11 type:number}
opcode = 76
PUSH Vars[15] {obj:11 type:number}
opcode = 14
PUSH Vlaue{ obj:10 type:number}
opcode = 66
    ==Expression: 11 < 10
PUSH expr {obj:false type:boolean}
opcode = 75
IDX+=6
opcode = 76
PUSH Vars[15] {obj:11 type:number}
opcode = 14
PUSH Vlaue{ obj:10 type:number}
opcode = 68
    ==Expression: 11 == 10
PUSH expr_2 {obj:false type:boolean}
opcode = 75
IDX+=6
opcode = 14
PUSH Vlaue{ obj:12 type:number}
opcode = 83
MOV Vars[15], v {v:12 type:number}

0x3 具体例子

上面描述了如何实现一个简单的逻辑运算,分支派发的实现方式, 根据上述思路, 目前实现逻辑运算, IFELSE, 函数调用, 循环, 数组等等基本js语法, 我也找了md5, crc等一些算法测试, 改了下都可以正常跑

MD5

源码执行
61.png

 

编译后vm执行
62.png

CRC

源码执行
63.png

 

编译后vm执行
64.png

0x4 源码

jsvm

 

用法其实也很简单, 安装完需要的模块后, 把修改后的escodegen.js替换node_modules\escodegen中的, 修改encrypt.js中需要编译的js文件路径再执行, 编译后会在./build/vm.js

0x5 最后

最后想说这个东西只是个玩具, 发出来是想和大家交流一下, 文章中因为篇幅的原因没有描述太多细节, 大家想了解更多的细节可以直接去看看源码(写的很随性, 别吐槽...), 还有就是想吐槽下这种实现方式调试很痛苦, 中间很多一部分直接都花在找bug上, 大佬们有好想法可以不吝赐教哈...


看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~

最后于 2020-8-12 18:59 被StriveMario编辑 ,原因:
收藏
点赞9
打赏
分享
打赏 + 2.00雪花
打赏次数 1 雪花 + 2.00
 
赞赏  orz1ruo   +2.00 2020/08/18 助人为乐~
最新回复 (14)
雪    币: 4407
活跃值: 活跃值 (809)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wanttobeno 活跃值 2020-8-12 15:26
2
0
感谢大佬分享!
雪    币: 1877
活跃值: 活跃值 (3296)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
virjar 活跃值 1 2020-8-12 15:58
3
1
建议看雪老板们,开一个js安全板块emmm
雪    币: 1282
活跃值: 活跃值 (1330)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
StriveMario 活跃值 2020-8-12 16:40
4
0
virjar 建议看雪老板们,开一个js安全板块emmm
我觉得可以
雪    币: 3651
活跃值: 活跃值 (375)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
逻辑错误 活跃值 1 2020-8-13 09:49
5
0
orz , 膜拜一下.
雪    币: 1282
活跃值: 活跃值 (1330)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
StriveMario 活跃值 2020-8-13 10:16
6
0
逻辑错误 orz , 膜拜一下.
 大佬好
雪    币: 175
活跃值: 活跃值 (319)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
glider菜鸟 活跃值 1 2020-8-13 11:16
7
0
谢谢大佬分享。这种直接从AST到vm指令,遇到break和continue这种语句很麻烦。还有个小问题,这种实现方法默认了var变量也有块级作用域,下面这样的代码,在加密前后会不一致:function test() { var a = 1; { var a = 2; console.log(a); }console.log(a);} test();
还有个问题请教下:这种vm加密如果只对一个js文件中的特定函数加密,怎么处理这个函数内使用了全局变量的情况?
雪    币: 1282
活跃值: 活跃值 (1330)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
StriveMario 活跃值 2020-8-13 11:35
8
0
glider菜鸟 谢谢大佬分享。这种直接从AST到vm指令,遇到break和continue这种语句很麻烦。还有个小问题,这种实现方法默认了var变量也有块级作用域,下面这样的代码,在加密前后会不一致:function ...
可以把函数编译后, 放在window里, 在外面调用
雪    币: 2343
活跃值: 活跃值 (1562)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
TUGOhost 活跃值 2020-8-14 17:37
9
0
膜拜
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Nanda 活跃值 2020-8-14 18:45
10
0
之前做头条系列爬取的时候,就一直没明白那是个什么样的反爬产品,胡乱修改js解决后也没感觉收获到什么东西,感谢大佬指明方向,受益匪浅。
雪    币: 1110
活跃值: 活跃值 (14392)
能力值: ( LV9,RANK:221 )
在线值:
发帖
回帖
粉丝
0x指纹 活跃值 4 2020-8-17 23:29
11
0
学习学习
雪    币: 3345
活跃值: 活跃值 (565)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
cnzzh 活跃值 2020-8-19 17:34
12
0
感谢分享~
雪    币: 2198
活跃值: 活跃值 (714)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
rushmaster 活跃值 1 2020-11-17 12:37
13
0
现在各种网站使用栈式虚拟机来加密的情况越来越多了,急需这方面的资料。看了大佬的帖子受益匪浅。get了不少思路。。
雪    币: 36
活跃值: 活跃值 (401)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
芃杉 活跃值 2020-11-17 13:55
14
0
mark
雪    币: 0
活跃值: 活跃值 (68)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
breakM 活跃值 2022-5-25 12:24
15
0
mark
游客
登录 | 注册 方可回帖
返回