那些年病毒用过的损招——反虚拟机技术

这些天因为某些不可抗力的因素开始接触病毒样本分析,分析的过程中遇到了很多不地道的东西,比如说反调试,反虚拟机等。这帮写病毒的人也不是什么等闲之辈,所以今天打算跟大家分享一下病毒的反虚拟机技术。

写病毒的人知道病毒分析师在分析病毒的时候会使用虚拟机沙箱来对病毒进行分析,所以在病毒编写的时候会检测虚拟机,因为病毒一旦进了病毒分析师的虚拟机,攻击价值就没了,所以在病毒层面上,病毒编写者可以设计这么一个逻辑:检测到虚拟机之后,立刻停止所有的感染和发作行为,然后退出。

但是由于最近各种云计算和虚拟化技术大行其道,导致了虚拟化技术使用泛滥成灾,上至50岁高龄程序员,下至10几岁小屁孩(夸张了)都知道虚拟化技术已经成为了非常普遍的一种计算机技术。所以采用反虚拟机技术的病毒越来越少了。但是不代表虚拟机没有病毒的攻击价值,永恒之蓝+WannaCry的出现预示着一件事情,核武器级别漏洞+病毒的组合攻击方式会变得越来越流行,换句话说,如果发现了虚拟机逃逸这种核武器级别的漏洞,结合勒索软件去搞事情,没准估计就是一个数据中心直接被搞了,扯远了,逃逸漏洞+病毒的利用情况内容不在讨论范围之内。

以下的反虚拟机技术我们用了VMWare去做分析。对此我还专门装了个VMWare,我之前一直用的是Parallels。虚拟机环境我们来用Win7 Starter(别问我为啥,本子空间不够了)。


0x01 VMWare痕迹收集

VMware虚拟环境其实在宿主机和虚拟机之间都会留下了很多痕迹,其实VMware Tools就是一个很多痕迹的集中营。所以我要是编写针对反虚拟机机制的时候,首先会考虑从VMWare Tools下手,比如说通过检测存在于操作系统的文件系统、注册表或者进程列表中寻找VMware相关的存在证据。



这里面我们发现了一个进程和三个服务:进程vmtoolsd、服务VMTools、vmvss和VMWare Physical Disk Helper。首先我们先针对vmtoolsd进行分析。

vmtoolsd其实是以Local System的服务形式启动VMWare Tools的服务,恶意代码可通过搜索注册表中安装的服务,或者说是利用cmd中net start命令来识别它。


因为VMware中,硬件驱动都是VMWare模拟出来的驱动程序,所以在注册表中我们会发现很多带有VMware的标示。比如说:


在MAC地址中,我们也会发现一些端倪,00-0C-29开头的MAC地址其实已经相当于告诉了大家:我用的就是VMWare有本事你来搞我啊!



其实在对病毒样本的逆向中,如果我们检测到了探测硬件设备、出现cmd /c net start| findstr VMWare这种不正常的命令执行,基本上就是用来检测当前病毒是不是在一个虚拟机环境中运行的,但是由于最近逃逸漏洞被搞出来的越来越多,也有可能是在检测是不是存在问题的虚拟机软件版本。

其实最好的解决方法就是,把带有VMWare信息的东西全都卸载掉替换掉。


0x02 VMWare痕迹检测怎么去绕过

我们随便找了一个带检测功能的病毒样本,使用Hopper Disassembler打开这个样本,然后在String里面搜索vmware,查看其调用,我们发现在这里存在一个对vmware字符串的检测。


这么看的话可能不太好看,我们把它弄成PseudoCode的形式:

if (sub_4010b0(var_318, "vmware") == 0x0) 
    goto loc_4012f8;

通过PseudoCode,我们知道了sub_4010b0函数中有两个参数,sub_4010b0如下面所示:

int sub_4010b0(int arg0, int arg1) 
{
    stack[2043] = esi;
    stack[2042] = edi;
    esp = esp - 0x8;
    ecx = ecx | 0xffffffff;
    asm{ repne scasb al, byte [edi] };
    var_C = !ecx + 0xffffffff;
    ecx = !ecx + 0xffffffff | 0xffffffff;
    asm{ repne scasb al, byte [edi] };
    var_8 = !ecx + 0xffffffff;
    var_4 = 0x0;
    if (var_8 >= var_C) goto loc_4010fa;
}

根据之前的提交可以知道,如果var_318和vmware相等的话,函数会返回0x0,一旦函数返回值为0x0,经过多次跳转,最后会退出程序。

补充:内存嗅探获取虚拟环境指纹

虚拟机作为虚拟化过程的结果,VMware其实在内存中也会留下一些痕迹,一些关键的处理器结构发生了变化,就会留下可以被识别的指纹,换句话说就是内存指纹也可以用来识别VMware虚拟环境。

0x03 在VMware上查找漏洞指令:

虚拟机监视器VMM(Virtual Machine Monitor)是一个运行在宿主机系统,用来监视虚拟机状态的监视器。VMM为客户及操作系统提供一个完整的虚拟平台。但是VMM也存在一些可以被恶意代码探测到虚拟化的安全缺陷。

内核模式下,VMWare使用二进制翻译技术进行模拟指令的操作,运行于内核态的某些特权指令被解释和模拟,也就是说他们不再物理处理器上运行。相反,在用户模式下,代码直接在处理器运行,几乎和所有硬件交互的指令,要么是特权之灵,要么就是会产生内核态陷阱指令或者是终端指令,vmware截获终端并处理他们,一遍虚拟机仍然认为我是一个真实的主机。

但是在x86架构上,一些指令在获取硬件信息的时候并不产生异常,比如说sldt、cpuid等。VMware为了正确模拟这些指令,就需要在所有指令上进行二进制翻译,这些指令包括但不限于特权指令。这么干的话由于进行了二进制翻译,所以会造成很大的性能损失。VMware的开发人员显然考虑到了这点,所以允许一些特定指令在没有正确虚拟化的前提下运行,这样的话会造成某些特定的指令在VMWare虚拟机会返回和物理机不一样的结果。

处理器使用某些关键的数据结构和表会被加载与真实系统不同的偏移量,也就是全虚拟化的一个副作用。中断描述表IDT是CPU里面的一个用来确保正确响应终端和异常的数据结构。在x86架构下,所有的内存获取要么是通过全局描述表GDT获得,要么就是通过本地描述表LDT获得。这些表中包含段描述符提供每一个端的详细储存信息,比如说段基址类型、长度以及权限等等。其实CPU内部有用来存放IDT、GDT和LDT表的基址和大小的寄存器,也就是IDTR、GDTR和LDTR。这里需要提醒一点,操作系统不需要使用这些表,比如说Windows的平面内存模型就是默认使用GDT不适用LDT。sidt、sgdt和sldt指令可以读取这些表的位置,并且将相应寄存器存入对应的内存地址。虽然说这些指令通常情况下是操作系统使用的,但是x86架构下这三个指令其实不是特权指令,换句话说就是,可以在用户态内存执行。

在x86架构下,IDTR\GDTR\LDTR的值必须对宿主机操作系统有效,同时,他们偏离了虚拟机操作系统的预期值,原因是sidt、sgdt和sldt可以随时被用户态的代码使用,而且不会产生陷阱,这样的话VMWare就不会正确虚拟化这些指令。说了这么多,其实就想说明,这个机制可以用来检测虚拟环境的存在。那么有什么应用呢?


0x04 Red Pill和No Pill反虚拟机技术:

Red Pill通过运行sidt指令获取IDTR寄存器的值。虚拟机监视器必须重新定位Guest系统的IDTR,来避免与Host系统的IDTR冲突。因为在虚拟机中运行sidt指令时,虚拟机监视器不会得到通知,所以会返回虚拟机的IDTR。Red Pill通过测试这种差异来探测VMWare的使用。这种方法存在一个缺陷,由于IDT的值只针对处于正在运行的处理器而言,在单CPU中它是个常量,但当它处于多CPU时就可能会受到影响了,因为每个CPU都有其自己的IDT,这样问题就自然而然的产生了。

针对此问题,有一种方法就是利用Red Pill反复地在系统上循环执行任务,以此构造出一张当前系统的IDT值变化统计图,但这会增加CPU负担;另一种方法就是windows API函数SetThreadAffinityMask()将线程限制在单处理器上执行,当执行此测试时只能准确地将线程执行环境限制在本地处理器,而对于将线程限制在VM处理器上就可能行不通了,因为VM是计划在各处理器上运行的,VM线程在不同的处理器上执行时,IDT值将会发生变化,因此此方法也很少被使用。


需要注意的是Red Pill只在但处理器的主机上才能够有效,针对多核心和多处理器可能会出现是小的情况,因为每一个处理器都会有一个对应的IDT,所以sidt指令结果可能不唯一。


用sgdt和sldt指令去探测VMWare环境的技术叫做No Pill,No Pill实现的原理是:LDT数据结构由处理器分配而不是操作系统分配。正常情况下,Windows操作系统里面不会使用LDT数据结构,但是VMware却提供了LDT的虚拟化支持,在VMWare里面,LDT还和一般的LDT不太一样:宿主机系统中LDT位置的值是0,但是在虚拟机中不是0。所以我们可以简单的检查sldt指令返回结果是不是0来检测是不是在虚拟机里面。想要阻止sldt探测,可以在VMware中禁用加速来完成。


0x05 查询I/O通信的端口:

VMware使用虚拟化I/O端口完成宿主机和虚拟机之间的通信,比如剪切板功能。这个技术的关键在于x86指令集中的in指令,in指令是从一个源操作数指定的端口复制数据到目的操作数指定的内存地址中。在VMware环境下,虚拟化程序会监视in指令的执行并不或目的通信通道端口为0x5668的I/O操作。VMWare同事会检查第二个而操作数是不是0x5668(VX),如果恰好第二个操作数是VX的话,EAX寄存机载入的值是0x564D5868(VMXh),ECX寄存器中必须被载入对应端口上执行性相应操作的值。0xA表示”get VMWare Version type”;0x14表示“get memory size”。这两个值可以用来探测VMware环境。

下面这段代码其实就是一个vmxh检测的实例:

bool IsInsideVMWare()
{
  bool rc = true;

  __try
  {
    __asm
    {
      push   edx
      push   ecx
      push   ebx
      mov    eax, 'VMXh'
      mov    ebx, 0  // 将ebx设置为非幻数’VMXH’的其它值
      mov    ecx, 10 // 指定功能号,用于获取VMWare版本,当它为0x14时用于获取VMware内存大小
      mov    edx, 'VX' // 端口号
      in     eax, dx // 从端口dx读取VMware版本到eax,若上面指定功能号为0x14时,可通过判断eax中的值是否大于0,若是则说明处于虚拟机中
      cmp    ebx, 'VMXh' // 判断ebx中是否包含VMware版本’VMXh’,若是则在虚拟机中
      setz   [rc] // 设置返回值
      pop    ebx
      pop    ecx
      pop    edx
    }
  }
  __except(EXCEPTION_EXECUTE_HANDLER)  //如果未处于VMware中,则触发此异常
  {
    rc = false;
  }

  return rc;
}

对于这种方法的话,我们可用NOP去替换in指令,或者修补条件跳转,这样的话不论比较结果如何都执行到位探测到虚拟机的程序分支。


0x06 str指令:

Str指令是用来从人物及村集中检索段选择子,段选择子只想当前运行任务的任务状态段(Task Status Segment, TSS)。此外,写病毒的人可以利用str的返回值不同来判断是不是在虚拟机环境下。但是还是由于多处理器的原因,会导致str检测的方法失效。

下面这段代码就是用来使用str探测虚拟机环境的实例,其实这段代码的意思是:在虚拟机和真实主机之中,通过STR读取的地址是不同的,当地址等于0x0040xxxx时,说明处于虚拟机中,否则为真实主机。

#include <stdio.h>
int main(void)
{
    unsigned char mem[4] = {0};
    int i;

    __asm str mem;
    printf (" STR base: 0x");
    for (i=0; i<4; i++)
    {
        printf("%02x",mem[i]);
    }

    if ( (mem[0]==0x00) && (mem[1]==0x40))
        printf("\n In VMWare!!\n”);
    else
        printf("\n Not In VMWare\n”);
    return 0;
}

0x07:在IDA中高亮反虚拟机代码:

那么既然有这么多反虚拟机的情况,让一个病毒分析师去搞这些无疑是增加他的痛苦,所以可以在IDA上标记出这些反虚拟机代码可以减轻他的痛苦。之前说了那么多指令可以探测VMWare虚拟机,那么我们直接利用IDA Pro自带的Python解释器来完成对这些指令的标记也就可以完成了反虚拟机检测的工作,直接上代码:

from idautils import *
from idc import *

heads = Heads(SegStart(ScreenEA()), SegEnd(ScreenEA()))
antiVM = [
for i in heads:
	if (GetMnem(i) == "sidt" or GetMnem(i) == "sgdt" or GetMnem(i) == "sldt" or GetMnem(i) == "smsw" or GetMnem(i) == "str" or GetMnem(i) == "in" or GetMnem(i) == "cpuid"):
		antiVM.append(i)

print "Number of potential Anti-VM instructions: %d" % (len(antiVM))

for i in antiVM:
	SetColor(i, CIC_ITEM, 0x0000ff)
	Message("Anti-VM: %08x\n" % i)

0x08:调整VMware的配置:

VMWare当中的vmx文件包含一些配置,我们可以把这些配置修改一下达到伪装的目的:

isolation.tools.getPtrLocation.disable = "TRUE"
isolation.tools.setPtrLocation.disable = "TRUE"
isolation.tools.getVersion.disable = "TRUE"
isolation.tools.setVersion.disable = "TRUE"
monitor_control.disable_directexec = "TRUE"
monitor_control.disable_chksimd = "TRUE"
monitor_control.disable_ntreloc = "TRUE"
monitor_control.disable_selfmod = "TRUE"
monitor_control.disable_reloc = "TRUE"
monitor_control.disable_btinout = "TRUE"
monitor_control.disable_btmemspace = "TRUE"
monitor_control.disable_btpriv = "TRUE"
monitor_control.disable_btseg = "TRUE"

除此之外,还建议卸载VMware Tools、使用修补代码等方法来减少被发现的概率,但是只能缓解并不能彻底解决。

0x09:Summary:

反虚拟机技术和反调试技术一样,都是需要大量的逆向分析经验才能发现的反逆向操作,分析师需要有着敏锐的嗅觉来发现这些机制以便正常的去分析病毒样本。比如说在调试过程和静态分析中发现一个行为可以过早地结束程序,很有可能是病毒开发者是用了某种反逆向的措施。这个时候你就要去攻克这些难题,这也就是安全工程师和研究员在意的:黑客的意义不在于去利用各种黑客工具和技术手段去攻击别人,然后占领上帝视角来欺负手无寸铁的平民,而是在于通过软硬件及其安全机制的分析研究去寻找他们的缺陷,然后去攻克他们、利用他们、修复他们。

部分code来源:虚拟机检测技术攻防 - whatday的专栏 - CSDN博客

编辑于 2017-07-12