给流行视频软件的JS虚拟机写一个编译器

  • A+
所属分类:安全开发
给流行视频软件的JS虚拟机写一个编译器

本文为看雪论坛优秀文章

看雪论坛作者ID:StriveMario





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:


给流行视频软件的JS虚拟机写一个编译器

 

在encrypt_code根据不同的位置来生成opcode:

给流行视频软件的JS虚拟机写一个编译器

 

这里我举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)EnmaybeBlockEnBlockStatement      //{     ExpressionStatement   //a = 10LvBlockStatement      //}  LvmaybeBlockIfStatement           //else if (a == 10)EnmaybeBlock   EnBlockStatement      //{     ExpressionStatement   //a = 11LvBlockStatement      //}LvmaybeBlockIfStatement_ELSEBEGIN //elseEnBlockStatement      //{ExpressionStatement   //a = 12LvBlockStatement      //}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


给流行视频软件的JS虚拟机写一个编译器


if(a < 10)  >> 76,15,14,10,66,60,75,6


给流行视频软件的JS虚拟机写一个编译器


{ a = 10 } >>  14,10,83,15,74,20


给流行视频软件的JS虚拟机写一个编译器


else if (a == 10)  >>  76,15,14,10,68,2,29,29,75,6

给流行视频软件的JS虚拟机写一个编译器


{ a = 11 }  >>  14,11,83,15,74,4


给流行视频软件的JS虚拟机写一个编译器


{ a = 12 }  >>  14,12,83,15


给流行视频软件的JS虚拟机写一个编译器


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


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





0x3 具体例子



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


MD5


源码执行:

给流行视频软件的JS虚拟机写一个编译器

 

编译后vm执行:


给流行视频软件的JS虚拟机写一个编译器


CRC


源码执行:

给流行视频软件的JS虚拟机写一个编译器

 

编译后vm执行:


给流行视频软件的JS虚拟机写一个编译器





0x4 源码



jsvm(https://github.com/StriveMario/jsvm

 

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





0x5 最后



最后想说这个东西只是个玩具,发出来是想和大家交流一下,文章中因为篇幅的原因没有描述太多细节,大家想了解更多的细节可以直接去看看源码。


还有就是想吐槽下这种实现方式调试很痛苦,中间很多一部分直接都花在找bug上。



给流行视频软件的JS虚拟机写一个编译器
- End -


给流行视频软件的JS虚拟机写一个编译器


看雪ID:StriveMario

https://bbs.pediy.com/user-773600.htm

  *本文由看雪论坛 StriveMario 原创,转载请注明来自看雪社区。



推荐文章++++

给流行视频软件的JS虚拟机写一个编译器

* Windows不太常见的进程注入学习小记(一)

* 脚本类恶意程序的快速分析技巧

* 萌新逆向学习笔记——消息钩子键盘记录

* 关于Kimsuky的一次恶意样本分析小记

* HellsingAPT分享







给流行视频软件的JS虚拟机写一个编译器
公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]


求分享

求点赞

给流行视频软件的JS虚拟机写一个编译器

求在看


给流行视频软件的JS虚拟机写一个编译器
“阅读原文一起来充电吧!

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: