[2025]VMP3.x原理详解与还原思路 huoji VMP,NOVMP,虚拟化,VMP3 2025-04-20 1713 次浏览 1 次点赞 ### 前言 这几天在逐步把github的项目移动到gitlab里面,无意间发现了三年前写的VMP还原的项目。发现那会的思路 笔记什么的都忘记的差不多了,所以趁着现在还记得起来。赶紧写一下记录着。 # VMP内部 让我们先了解一下VMP的具体执行流程.  实际看一下,一旦设置虚拟化后,VM的程序会从text段跳转到vmp0 stub段.然后每一段会进入vmentry:  这个是stub段:  ## vmentry 在进入vm的方法之前,要进行初始化,这部分就叫做vmentry  而vmentry的作用是,把寄存器放在栈上,生成VSP,VIP以及rolling key 让我们标注一下  ### push寄存器进栈 我们可以看到 这些寄存器在被push进栈   ### 分配VIP 而在所有push完成后,第一次mov的一定是vip。如图所示 ```cpp mov rsi, [rsp+50h+arg_38] ; VIP: mov %reg, [rsp + %offset] ``` 这代表rsi就是VIP寄存器.用于存放虚拟机的IP的寄存器。 > VIP不是每次都固定.只是我这个是RSI罢了 ### 分配vsp 第二访问rsp的目的是分配VSP,虚拟机堆栈,这个堆栈大小是0x180,首先VMP会保存原来的RSP到一个寄存器里面。然后分配一个0x180的空间,这个就是虚拟机栈大小: ```cpp mov rbp, rsp ; VSP: mov %reg,rsp ```  这个0x180空间的布局如下:  ### 分配rolling key(滚动密钥) 在最后,VMP会分配一个滚动密钥,顾名思义,没有滚动密钥,是没办法知道下一个vm block跳转位置的,这个会在后面说为什么。  经过一系列复杂的运算,最终会跳转到FDJ里面(Fetch, Decrypt, Jump,也叫做vmblocks)。 ### 总结 让我们再回顾一下: vmp的核心原理其实是 把正常的指令 比如 push rax, mov eax, rax ....等等 转换为一个一个vm blocks.这些vmblocks 在内部进行运算,并且按照原来的指令去增加/修改虚拟机寄存器里面的信息.并且通过滚动密钥计算出下部分的vmblock地址在哪。 而每一个指令都对应了一个 vmblock,这块叫做vmhandle。比如push其实对应了一个vpush,pop对应了一个vpop ## FDJ(Fetch, Decrypt, Jump,也叫做vmblocks或者vmhandle) vmp将完整的代码通过jmp拆分成code block,与此同时配有一个滚动地址偏移(rolling offset key) -即必须执行前面的代码,才能执行下面的代码  在前文rolling key后经过一系列复杂运算    后 最后会push rollingkey reg, retn   这样的运算每个FDJ都会做。这样能确保: 1. 如果不模拟执行,是不知道rollingkey是多少的,也就不知道下一个fdj需要跳转哪里 2. 如果vmblock被破坏篡改,rolling key也会改变,也会导致不对。 我们这边以第一个vmhandle为例子讲解一下:  首先 我们发现 rsi被复制给了eax ```cpp movzx eax, byte ptr [rsi] ; ax = vip,同时也是 vPop: mov %op_reg, %op_size:[VIP] ``` 这其实意味着 ax = vip,ax是新的VIP寄存器了 对VIP的复杂计算我们先按下不表,我们首先发现,无论怎么样,我们的vsp寄存器被add了8字节  这意味着,这个指令实际上是 add vsp,0x8,更直白一点。这个handle是vPop的handle。也就是pop的虚拟化方法 >实际上,vpop有两种 mov %pop_size:%pop_reg, [%vsp] add vsp, %pop_size mov op_size, %op_size:[VIP] 以及 mov %op_reg, %op_size:[VIP] mov %pop_size: %pop_reg,[VSP] add vsp, %pop_size 做完这件事后, 他就会更新滚动密钥,然后到下一个vmblock   有意思的是,你还会发现,这个滚动密钥,还会跟VSP的内容绑定:  这也是VMP的防篡改方法之一 #### 其他 请注意,这里没讨论一个特殊情况: vmexit,实际上在vmp执行一些特定平台指令比如cpuid或者调用winapi的时候,会走到vmexit, 顾名思义退出虚拟机。执行完毕后继续进入虚拟机。但是再次进入后,可能并不是原来的虚拟机了,而是其他的变种虚拟机。  一共有四个:  不过其他三个的虚拟的指令集,其实是没第一个大的。所以对我们还原来说,没什么影响 # 还原思路 这边代码只写一半,但是思路嘛,能用就行 对于VMP的还原,我们其实可以逐步拆解我们的需求,那就是: 1. 匹配到所有被虚拟化后的代码 2. 通过模式匹配,匹配到我们需要符号执行的关键信息,比如VSP,VIP,RollingKey等。 3. 通过符号执行,我们动态的计算出fdj的位置,然后判断是什么类型的vm handle,比如上面分析的是vpop类型的vmhandle,然后记录他。用第三方语言,比如VTIL,LLVMIR表示他. 4. 这样循环,我们能匹配出全部的执行路径的vmhandles,相当于也还原成原本的指令了。 让我们一步一步来介绍 ## 匹配到所有被虚拟化后的代码 没什么好说的,我们可以通过判断call/jmp的目标是否是vmp的区段来判断这个函数是否被VMP了  ## 模式匹配 概念和原理这个就不说了,模式匹配是二进制安全领域最重要的概念壳/去混淆/恶意代码匹配 基本上都涉及到模式匹配这个需求。说白了就是匹配代码,从汇编里面得到需要的信息。以下代码是用于模式匹配的函数。感谢CPP,模板用对了,高效快捷。  ### VIP的匹配 匹配在栈上加密VIP的指令,匹配到了返回%reg和%offset 让我们回顾一下前面说的 > 而在所有push完成后,第一次mov的一定是vip 因此,我们模式匹配到Push后接mov,则代表是mov的dst_reg是 vip  结果:  ### VIP解密过程匹配 还记得我们说过,VIP有非常复杂的解密流程吗,所以我们需要跟踪VIP的寄存器的变化,记录他的执行数学方法,这样我们就能解密VIP了 实际操作就是,记录vip寄存器的执行操作,加减乘除抑或,然后在模拟执行部分计算出结果:  ### VSP的匹配 VSP紧接着VIP背后的RSP操作,所以直接匹配RSP的操作就能匹配到  结果:  ### rolling key匹配 rollingkey的规律是,VIP后第一个mov就是  结果:  ## 符号执行 当我们拿了足够多的信息后,我们就可以通过符号执行开始遍历vmblocks并且保存记录了,符号执行的概念是,符号执行,我们并不是真的执行它(入PE模拟器,unicorn什么的),我们只是要通过汇编代码虚拟执行它。 实际上非常简单,我们只需要 1. 把VIP通过之前我们介绍的数学记录去算出来第一个vmblock的地址:  2. 模式匹配所有的vmhandles  我们以vpop的vmhandle为例: >vpop有两种 mov %pop_size:%pop_reg, [%vsp] add vsp, %pop_size mov op_size, %op_size:[VIP] 以及 mov %op_reg, %op_size:[VIP] mov %pop_size: %pop_reg,[VSP] add vsp, %pop_size 无情的用模式匹配匹配这两种就行了  匹配到之后,我们如法炮制,匹配rolling key的数学表达式:  然后就能计算出下一个地址的位置了,并且我们也知道了,这个是vpop:  如此重复, 保存指令到数组里面. 为之后的还原做准备.(实际上我们知道了vmblock干什么的还原也非常容易了) # 未完待续 虽然我们走向了第一步,但是后续还有很多挑战给我们,比如分支预测会导致梯度爆炸,比如多个虚拟机需要单独匹配模式。这些都需要解决。 介于这是3年前的代码,读起来很费劲,所以如果文章阅读超过5000 马上更新下一期.否则就不更新了,折腾VMP纯体力活。工作忙起来后就没这种精力折腾了。 本文由 huoji 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。 点赞 1
还不快抢沙发