1 摘要
1.1 关于代码优化与还原
关于还原,我认为难点是工作量大,需要自动化提升效率
还原和混淆是一对反义词,相同点是保证代码功能相近,不同是一个是使代码更易读,后者则相反
而代码优化非常类似,也要保证代码功能相近,不同是减少代码的体积或运行速度
所以我感觉还原和代码优化有很多共通点
然后尝试了一下从编译的角度去做自动化还原,这里分享一下思路,算是画一个不太完美的句号吧
1.2 还原流程
我的还原流程简单来说就三步:
- 识别汇编对应的语义(翻译虚拟机字节码)
- 虚拟指令转换成C
- 二次编译,利用编译器优化
第三步可以针对性的实现一些优化,因为vmp是一个基于栈的虚拟机,编译器的优化效果有限
第一步是我做的比较多的一部分,在后面的实现过程会说具体思路
2 实现过程
2.1 Handler语义识别
这一步说的是怎么判断Handler对应的虚拟机指令
2.1.1 浅谈VMP的CFG
Handler识别首先绕不开一个问题,怎么找到Handler在哪
关于VMP 3.X的架构这里简单说一下
在VMP2中会有一个分发器,所有Handler的地址都存在一个数组中,很容易就能把所有Handler找出来;但到了3,分发方式变成从字节码中解码出下一条指令的地址
2.1.2 模拟执行输出虚拟指令
目前分析到两种跳转方式:
或
我的思路是模拟执行,遇到jmp reg
或者push ; ret
时就代表一条Handler已经结束,reg中的是下一条Handler的地址
所以可以构建一个Handler 虚拟地址
到虚拟指令
的映射
模拟执行还有一个好处,对于不同的虚拟指令,在Handler中下断,让Handler自己解密字节码中的内容,然后提取出来
2.1.3 Handler识别
关于Handler的语义是什么就省略了
根据jmp reg
或push ; ret
把Handler提取出来后,现在就需要识别其对应的虚拟机指令
两种思路:
2.1.3.1 正则匹配
这是我目前正在用的方案,对汇编代码使用正则表达式匹配
矛盾点是正则规则越严格,漏判越严重,规则越宽松,误判越严重
缓解方案是对汇编代码先进行一次优化,参考编译原理中的死代码消除,对寄存器的使用进行分析
以一个加法的Handler为例:
比如优化前的Handler:
其中4、5、10、11行连续对rdi寄存器进行了写入,显然前三条写入是无效的
优化后的Handler:
正则匹配:
2.1.3.2 DAG匹配
这部分只是做一个尝试
同样是加法的例子,这是其DAG图(不太严格,因为x86复杂指令集有点麻烦)
蓝色下划线是从栈获取的操作数
绿色下划线是将结果和RFLAGS放回栈
2.1.4 识别结果
模拟执行顺序执行的片段:
2.2 控制流还原
2.2.1 虚拟机指令DU分析
先分析每条虚拟机指令对栈的读写,然后构建DU链
接着利用DU链进行一次简单的优化,包括常量传播,折叠一些变量在VM栈和VM寄存器上的移动,还有简单的MBA表达式优化(简化接下来的判断分支等步骤)
2.2.2 判断是否为分支
进行到这里就可以判断是jmp还是jcc
jcc的例子:
区别就是RET之前的一条语句PUSH的是否为一个立即数(依赖前面的常量传播优化)
2.2.3 获取分支去向
接下来就可以通过DU链,获取分支的两条去向分别是什么
依据是VMP的分支跳转伪代码为:
1
2
3
4
|
mask
=
-
1
+
flag
a1
=
mask & FAddr
a2
=
~mask & TAddr
jmp
=
a1
+
a2
|
这里是识别的例子:
2.2.4 获取分支条件(未完善)
这里我大致分成了两步:
一个比较标准的test x-y
,然后判断CF的例子
绿色框是上一步的跳转地址计算
黄色框是rflags标志位的判断
红色框是计算x-y
的rflags
一个and x, x
,判断是否为0的例子
绿框是上一步的跳转地址计算
黄框是判断其ZF位
红框是读取内存,然后获取其and x, x
的rflags,没识别到
2.2.5 控制流还原杂谈
在前面Handler语义识别的时候,难免会有错漏,出现识别不了的语句
在模拟执行还原控制流时,妥协做法是停止该分支的分析
这里截取了一段控制流
每个圈圈是一个虚拟指令基本块
这里绿色箭头的是前面flag=1
分支、红色箭头是前面flag=0
的分支
红色圈圈的是遇到未知虚拟指令或模拟执行错误,停止分析的块
2.3 还原成C(做的不太好)
这一步随便水水了,只做了一部分,主要工作量太大了
将虚拟指令输出成对应的C语言代码,然后上编译器编译
给个加法的例子吧:
3 结尾(欢迎指教)
3.1 收获
比较喜欢写代码吧,vmp代码还原的自动化又是个需要写很多代码的工程,就比较感兴趣,断断续续大学花了不少时间在这上面
最大的收获是经验吧,写的时候花了很多时间在debug上,实际写的时间根本没多少
我也明白,先设计好再写代码可以减少很多写代码和debug的时间,但缺乏还原经验,设计的时候无从入手,也考虑不周全,只能边写边想
算是积累了一些经验吧
然后实践了一下编译原理的入门知识,一个非常有意思的领域,希望以后有机会继续深入学习下去吧
3.2 关于分析深度和还原难度
在还原的过程中,我发现对虚拟机架构的分析越多,获得更多关于壳的信息,就能写出更容易实现、更有针对性、更有效果的优化
有点类似窥孔优化的思路,牺牲通用性,以便实现和提高效果
4 相关链接
VMP架构与虚拟机指令:
虚拟机分支分析:
编译原理(哪本书不记得了):
加密与解密