文章目录
VMprotect静态跟踪
(1)虚拟执行特点:
- 虚拟执行是静态分析与动态执行的一个折中办法
- 虚拟执行时对内存访问做了一定的控制以防止出现异常
允许读写静态内存与堆栈访问
忽略其他内存访问与修改
解决了异常问题后,就可以从入口点一直虚拟执行到出口了
(2)执行引擎的虚拟执行
分析虚拟机的一般传统方法:
- 找到关键位置
- 动态执行并使用记录断点记录数据
- 输出记录日志
优点:
- 寻找关键位置时间相对较短
缺点:
- 多路径时只能走其中一条路径
- 分析多个虚拟机时要重复做相同的工作
虚拟执行方法:
- 虚拟执行代码
- 根据已分析字节码灵活控制代码
流程:
- 输出记录日志
优点:
- 虚拟执行不会对系统造成伤害
- 完整的字节码流程
缺点:
- 指令正确但操作数的值不可靠
- 复杂度较高,开发时间较久
(3)分析条件跳转的两条出边
因为虚拟执行是不依赖运行时信息的,所以它无法判断应该走哪一条,必须把两条边都走过一遍。
- 在基本块(
BasicBlock
)执行前备份运行时环境 - 执行到跳转处分析指令流,获得修改路径关键点
- 退回基本块起始位置
- 重新执行并控制路径
字节码反编译
(1)中间表示语言
VMP的Hanlder只能算是低级中间语言,缺少一些例如数据依赖,流程走向等信息,还不满足反编译的条件。需要将其转换为包含更多信息的高级中间语言形式。
Push c
Push b
Add. d
Pop EFL
Pop A
转换后:
Stk1=push c
Stk2=push b
Stk3,Stk4=Add. d stk2,stk1
EFL=stk4
a=stk3
转换后:
Stk1=c
Stk2=b
Stk3,Stk4=Add. d stk2,stk1
EFL=stk4
A=stk3
优点:
- 去掉了对堆栈的依赖,转为直接关联变量
- 表达式被转换成SSA(静态单赋值)形式,方便对指令做优化处理
(2)指令化简和优化
常数收缩:
Push 1A2FBCA0
Push F80499D8
Add. d
Pop EFL
采用常数收缩后:
Push 12345678
活跃变量分析:
Stk0=Ctx24
Stk1=Ctx10
Stk2,EFL1=Add. d Stk0,Stk1
Ctx28=EFL1
Ctx30=Stk2
Stk3=Ctx30
Stk4=1111
Stk5,EFL2=Add. d Stk3,Stk4
Ctx28=EFL2
Ctx34=Stk5
去除中间变量后:
Ctx30,Ctx28=Add. d Ctx24,Ctx10
Ctx34,Ctx28=Add. d Ctx30,1111
去除无用变量后:
Ctx30=Add. d Ctx24,Ctx10
Ctx34,Ctx28=Add. d Ctx30,1111
删除无关代码:
VMP
在生成的字节码中夹杂了一些自己的指令流,这些指令与原汇编代码没有任何关系,且对还原分析没有任何好处,只会起到干扰的作用,需要根据特征制定一些规则来识别这些垃圾指令
(3)转换汇编指令——树模式匹配
- 文本表达转换为树形表达
- 收集转换规则(这是最麻烦的一个过程,需要分析
VMP
将汇编生成字节码的特征来收集将字节码逆向转换回去的规则,这是一个不得不做的体力活) - 使用匹配规则迭代匹配汇编指令
Ctx30=Add. d Ctx24,Ctx10
Ctx34,Ctx28=Add. d Ctx30,1111
匹配结果如下:
(4)归类映射寄存器
经过迭代后的最终结果是这样:
Add Ctx30(Ctx24),Ctx10
Add Ctx34(Ctx30),1111
虽然已经转换为汇编指令,但是还无法确定寄存器到底是哪一个,以目前所知的信息也的确无法判断,步过,我们可以尽可能的确定一些信息,以供后面的分析参考。
在转换规则中,预先明确定义了Add指令的第一个参数与结果是同一个寄存器(其他指令也是差不多,类似xchg
的指令除外),所以可以推理得到,在指定的区别内Ctx34
,Ctx30
,Ctx24
,是同一个寄存器,这样后面在专门针对寄存器识别的分析时,就可以一下确定这四个寄存器所映射的寄存器了
(5)转换汇编指令——动态规划
首先来看两段指令
mov eax ,dword ptr[dei+0x100]
add edi,100
其中第一条里面包含了第二条的指令,第一条的权值应该设得更高
add edi,100
Lea ebx,[edi+100]
两条指令仅仅是目标寄存器不同,两条指令的权值应该相等
所谓动态规划,通俗的讲就是制定一些规则,根据实际情况来选择最终匹配结果。
这里的意思是对每一个匹配规则设一个权值,使用计算后值最大的那个匹配规则来进行转换
第二段的情况有些特殊,其中两条指令唯一的不同只有目标操作数。Add指令认为目标操作数与源操作数1相同,而lea指令无此限制。当出现权值一样的情况时,可以同时作为结果,在识别出寄存器后,再根据实际情况来匹配规则,在这两个指令中选出更像的哪一个。
(6)寄存器染色
要识别前面所代表的寄存器,要从以下几个方面进行分析:
- 初始化虚拟机时各项所映射的寄存器
- 根据汇编转换规则映射或者结束映射某项到某寄存器
- 退出虚拟机时通过弹出各项时确定各项最终映射的寄存器
从这三方面可以大体推理出各项所映射的寄存器
但仅仅是这样的话只有在没有跳转指令的字节码中,成功率才最高。因为还得考虑寄存器轮转
(一)基本块内的寄存器轮转:
基本块内的寄存器轮转比较容易简单,只要转换规则正确,就可以识别出寄存器
(二)基本块间的寄存器轮转:
在执行set.jmp之前,将Context中所有位置的值都临时存放到了堆栈中,跳向目标地址后又再全部把它弹出到不同位置中去,这样就完成了一次轮换
它比基本块内的寄存器轮转更麻烦,因为其中涉及到了二义性的问题
(三)寄存器的二义性问题
寄存器的二义性问题是一个很严肃的问题,因为如果不能正确分析和处理,将会在成一子放错,满盘皆输的局面。
寄存器的二义性由指令的二义性衍生出,要解决指令的二义性,需要先解决寄存器的二义性问题。
Push\Pop的二义性:
Push\Pop在VMP中存在一种二义性,即传值与传引用。
传值:
当pop指令的作用是传值时,表示员项中国的值放到目标项中去,所映射的寄存器不变。
传引用:
当pop指令的作用是传引用时,不但将值从源项放到目标项中去,且目标项所映射的寄存器也将被覆盖
传值是一般情况,即汇编指令的Push\Pop指令时,传引用时特殊情况,如寄存器轮换等。
Add与Lea等其他指令的二义性:
这两条指令广义上讲也是Push\Pop的二义性:
为Add指令时,pop指令的含义为传引用
为Lea指令时,pop指令的含义为传值
(四)识别寄存器的二义性步骤
- 根据转换规则尽可能确定一些特性,缩小可能的寄存器范围
- 无法判断的寄存器将其加入此项的可能性列表中,并建立起传递链表
- 退出虚拟机时可知各项真实的寄存器,排除其他可能的寄存器
- 确定寄存器后,再重新排除有二义性的指令