Code virtualization is the MOST
powerful protection scheme of modern protectors
What Is Code Virtualization
(汇编)代码虚拟化是指保护者将原先的汇编代码语义进行拆解,使用自己实现的虚拟机代码来模拟执行,并且在中途或虚拟机退出时修改程序状态(寄存器状态与内存状态),使得执行完一条虚拟指令后,程序主要状态与执行原始汇编的结果一样。
平时所见的VM大多数指代的是OS层面的虚拟化软件(如VMWare,ExSi等),我们这里说的VM指的是代码虚拟化后执行虚拟化代码的虚拟机(Virtual Machine)。
关系:VM执行VMCode,VMCode是由Code Virtualization技术编译而来。
当然代码虚拟化不一定是汇编层面的虚拟化,如下面的例子:
一个简单的python栈虚拟机
VM 的基本组成
Handlers
中文翻译为 处理函数,是一小段代码片段,是VM中的最小单位。
如下面的两个函数就是虚拟机的Handler
Instructions
Dispatcher(s)
vRegisters
用于存储进入VM前的寄存器状态或者可以是VM内的(局部变量)
vStack
所有的栈虚拟机(如VMProtect)都有虚拟栈,用于VM内的Push/Pop操作,也有平栈虚拟机(如Themida)。
栈虚拟机:Handler从栈上获取操作数,把结果放入栈上
非栈虚拟机:Handler从vRegisters获取操作数
注意,上面两种虚拟机的操作数还有一部分来源于Instruction。
Why VM Hard
万用门
运算拆分
寄存器轮转
Handler过多或者过长
Instructions加密
变种Dispatcher
局部代码混淆
…
程序加壳 还是 代码加密
我们一般把主程序外的一端代码片段叫为 “壳”。
壳一般具有压缩效果 or 反调试效果等。
程序启动时,会优先运行 壳 然后其解压缩/解密主程序代码,然后跳到main函数。
代码加密其实指的就是代码虚拟化
代码虚拟化与壳不冲突,代码虚拟化是替换掉原先的代码片段,然后在原程序末尾新增一个区段用来填虚拟化后的代码。
壳是最后加的,把原先的代码+虚拟化的代码统一打包。
可以这么理解壳与代码加密的关系:壳相当于是Zip压缩包,未加密的代码是压缩包中的.c,虚拟化后的代码相当于是压缩包中由.c生成的.dll,不过架构是虚拟化工具自己定义的。
商业保护器
商业保护器往往具有非常强力的保护措施,在各方面做的都非常完备(如VMProtect和Themida)。
注:我们这次仅分析代码虚拟化,而不分析其反调试的措施与脱壳。
VMProtect
这是什么
VMProtect 是一款商业级的软件保护工具,主要用于保护应用程序代码不被逆向工程和破解。它通过虚拟化技术和代码混淆来增加软件的分析难度。
主要特性
代码虚拟化:
将原始机器代码转换为虚拟机的字节码
使用自定义的虚拟CPU指令集
执行时通过虚拟机解释器运行
我不会脱壳,怎么研究VMP
方法1:VMProtect里面通过勾选单选框,可以取消代码压缩,资源加密与反调试检查与虚拟机检查。
方法2:学习脱壳,有关脱壳本文不再赘述,自行去各大论坛寻找相关资料。
How VMP Works
清空虚拟化片段原代码,在头部
填充虚拟机进入指令,其他地方填充
随机字节
代码分片
通过xor+jns实现jmp(防止静态分析)
通过一系列jmp来实现代码分片
右边一系列图片是进入虚拟机前的指令序列
* 具有大量的垃圾指令,比如说r15被覆盖
其他特征:pushfq, non-ret-call
前面都是准备,这是真正
进入虚拟机了 >>>>>>>
花指令?没有
VMProtect没有花指令,因为这个没有任何保护效果。
垃圾指令
Dispatcher 平坦化
VMProtect 3.x 不再具有循环调度的唯一Dispatcher,而是根据上一个handler进行Relative Instruction Pointer寻址,相当于每一个Handler都是Dispatcher,这样分析者无法通过Hook单一函数进行VM分析。
VMProtect的状态保存在栈上与特定寄存器上,有的寄存器保存VM字节码地址,有的地址保存本Handler开头的地址。
在Handler结束的时候,会解密VM字节码中下一条Handler的相对偏移,然后根据本Handler的地址来进行运算,最后直接跳转。
一般Handler尾部的跳转都是Push reg+Ret或者JMP reg。
寄存器轮转
VMProtect独有的功能,其虚拟寄存器全都保存在当前线程的栈中,vm进入的时候会push所有通用寄存器,vm退出的时候会pop所有的寄存器,这中间通用寄存器与虚拟寄存器的对应关系是由字节码所决定的,所以只要在字节码生成的时候随机一下对应关系,就可以实现寄存器随机映射,增加分析难度。
VM 分析
指令 Trace
通过记录所有指令运行后的寄存器状态,然后追踪来进行分析。
优点:无视FakeJCC,无视花指令,适合快速追踪单一数据,可以非常直观的了解到VM的结构。
缺点:VM内部的循环结构会使得Trace非常的长,难以分析。
局部数据追踪
追踪局部数据最常用的方案,对进入VM前的关键数据下软件/硬件断点,然后运行起来,当断点命中后分析局部代码,或者对变换后的数据再次进行追踪。
优点:非常快,适合于解题,可以快速定位到关键代码段或者是Handler片段。
缺点:不适用于某些充满Trash的VM,这些VM会高频率访问关键数据,但是访问后就会Drop掉。
黑箱测试
我们知道,代码虚拟化只能处理用户指定的部分或者是OEP等关键位置,但是程序大部分代码其实是没有进行保护的,因为高速代码段(如string的构造,cpp的new等)会在程序各个部分进行调用,可能1s内就会有上千次调用,如果VM这些片段,会造成程序运行速度缓慢,用户体验感极差。而且最重要的是,代码虚拟化无法处理第三方库以及系统库的代码,如memcpy等,这就给我们留下了一些分析VM的机会。
另外,还有一些指令(SSE/AVX512指令或是带条件的mov等),虚拟机可能无法处理,这时候就应该还原状态,跳出虚拟机,执行完后立马返回虚拟机,这也造成了VM的一些缺陷。
下面我们来讲解 VM_Exit 与 VM_Exit_Unvm_Insn
VM_Exit & VM_Exit_Unvm_Insn
虚拟机退出的时候一定会还原原先寄存器状态,因为用户只虚拟化了部分的代码,其他代码不知道该虚拟机的存在,如果虚拟化后的代码行为与原先代码不同,则程序逻辑会被改变,虚拟机没有存在的意义。
一般VM_Exit长这样(下图)
VM_Exit具有以下目的:
虚拟机结束,返回未虚拟化的用户代码
遇到未虚拟化的指令,暂时退出虚拟机去执行
CALL 系统API
进入另一段虚拟机
具体Themida分析:看本博客的其他文章
侧信道攻击
不同分支的代码数量不同
不同分支的代码种类可能不同
代码数量不同造成执行时间不同
比如字符串判断,是线性操作,通过Bruteforce下一个字符,看哪个字符造成的指令数量明显与其他的不同,就可以知道它是正确的。
不适用于以下情况
循环判断字符串,字符串如果正确,
则 flag &= 1,否则 flag &= 0,在最后对flag变量进行检查,这样会导致无论哪个分支,代码数量近似一致。
污点分析
这里借用 这里的图片 (BV1Jt4y1r7nC)
指令频率分析
循环分支的vJCC会多次执行,关键分支的vJCC大多数只会执行一次。
结语
VM分析需要汇编功底足够,在看Trace的时候可以快速定位到可疑点。
没有VM是安全的,只要公开出来短时间内就会有人去攻陷。