虚拟机概述
一个正常的二进制文件,假设为x,那么执行他的是Windows操作系统,也就是解释器,假设为A,
那么vmp加壳的时候,会根据加壳文件的代码,转化为自己的代码,但行为不变,假设push eax,的字节码为0x03,那么vmp就会修改这个值,假设为0x04,但实际执行的时候还是回执行push eax,现在出现了一个新的问题,push eax的字节码被修改了,解释器A,无法将0x04解释为push eax。在vmp将原字节码转换为vmp的字节码之后,vmp还会生成一个解释器,假设为B,解释器B就是来解释,经过vmp修改过后的代码,也就是上文中0x04这个字节码,实际的执行顺序如下
解释器A – 执行 – 解释器B – 执行 – vmp修改过后的代码。
当我们分析vmp代码的时候,实际上分析的是解释器B。
既然解释器B是在执行虚拟机的代码,而虚拟机代码是模拟的vmp加壳之前的代码,加壳之前的代码是需要寄存器来执行的,而我们在调试中看到的寄存器已经被解释器B使用了。
那么在堆栈中会出现一个结构体,我们称之为VMContext,下文会详细介绍。
还有就是vmcode有自己的堆栈地址,并不是保存在esp中。
加密源码
开启变态加密
未开启反调试
生成后文件
源文件3KB,加密后551KB,一个惊人的增长
代码分析
初始化
上图为vmp开始的代码,我们只关心push指令,它将所有的寄存器压入栈,最后将0压入堆栈,
此时堆栈保存的数据顺序如下
1 | 0 |
继续F7单步
上面这段代码,主要的作用,取出vmcontext中的key,进行计算,而这个key计算结束的值就是vmcode的代码位置。
还有两行代码我们需要注意
1 | 00429382 8BFC mov edi,esp |
F7单步继续
上面这段代码中最重要的就是
1 | lea esi, dword ptr ds :[0x462ABD] |
这个地址是第一个handler的地址,这个handler指的是执行vmcode的代码,也就是在vmcode中取出代码,在handler中执行,在上图中jmp之前的代码就是在vmcode的地址中取出下一行要执行的代码。
继续单步
我们看到ebp进行了加4,可以看出虚拟机的代码是倒着走的。
单步一直走,走到带有ret的语句。
上图中的ecx是经过计算后的偏移,和esi相加就是第一个handler的地址。
在以前的版本中会有一个VMDispatcher,来决定下一次handler跳转的位置,而在新版本中,去掉了这个功能,很多脱壳神器也就都失效了,换而取代的是 push .. ret或者jmp esi 等等。
跳到第一个handler地址,继续单步。
上图为第一个handler的代码,执行到此,我们还需要说一下vmcode中的堆栈问题,vmcode的堆栈地址并不是固定保存在一个寄存器中的,vmcode的地址,和handler的地址也是一样,并不是固定在一个寄存器中的,它可能在某一个handler中就保存到其他的寄存器中了。
上图中的寄存器对应如下
edi保存了vmcode的堆栈首地址
ebp保存了vmcode的首地址
handler的地址在esi中。
esp保存了vmcontext的首地址,vmcontext的地址将永远保存在esp中不会改变
上图代码主要工作如下
1 | mov edx,dword ptr ds:[edi] |
F7单步经过一个jmp之后的代码如下
上图中高亮代码,是将在栈顶取出来的值保存到vmcontext的偏移的位置中,我们刚有提过esp中保存的永远都是vmcontext的地址,上图中的ecx的值为0x10,edx为上一次在堆栈中取出的值为0。
而在这之后在ebp中取出4个字节的代码,并把ebp进行加4,然后edx解密得到偏移,加上esi就是下一个handler的地址。
在最后一行代码跳转过去的代码为jmp esi 跳转到下一个handler。
这个handler的代码作用与原理同上,在堆栈中取出内容保存到vmcontext,只是保存位置有变化,保存在0x4的偏移中,然后在ebp中取出代码经过计算通过jmp esi跳转到下一个handler。
后面的几个handler的都是在做这件事情(初始化vmcontext),避免篇幅过大,就不贴出代码了。
初始化结束后的vmcontext如下
1 | [vmcontext + 0x10] = 0 |
至此 vmcontext初始化完毕。
正式执行代码
在上一行代码高亮的地方
1 | 0044CAC5 8DBF FCFFFFFF lea edi,dword ptr ds:[edi-0x4] |
继续F7单步走
每次压栈后,在跳转到下一个handler之前都会有一个当前vmcode栈空间的判断,如果当前栈顶超过了esp+60,就要进行栈空间分配。
分配的代码如下
混肴代码太多
上图代码总结
- 开辟空间
- edi esi eflags 寄存器保存
- 利用 esi 和 edi 寄存器,保存开辟空间之前,和开辟空间之后的位置
- 通过ecx 和 rep mosb 指令将vmcontext进行恢复
- 恢复 edi esi eglags寄存器
- 跳转到下一个handler
到此基本上了解了虚拟机的执行流程。
接下来的代码就没那么幸运了,因为在vmcode中,包含了很多混肴代码,做一些无用的工作,大大浪费了分析的空间,我通过在栈地址中使用硬件断点跳过这些代码,直接看vmp调用call的过程。
将保存在vmcontext的值取出来赋值给原寄存器
此时的堆栈如下
执行完成API返回到401026继续虚拟机指令。
Good Job.
需要注意的地方
- 同样的应用程序生成进行两次vmp加壳,所生成的两个EXE,那么这两个EXE的代码不论是在初始化还是在其他地方代码都会有很多不同,但是他们的目的是一致的,假设在初始化部分之前的寄存器压栈,两个不同的exe的jmp指令的多少或者位置都会不同,但它们都是在做同一件事情,就是将key、寄存器、0压入栈。
- 在不同的handler中,除了vmcontext的地址是固定保存在esp中之外,其他的均不会固定保存在一个寄存器中,而是在不同的寄存器中进行轮询。
- vmcontext的偏移位置在每次编译的时候也不同