- A+
1 引言
当前的病毒分析主要分为静态分析和动态分析。 静态分析并不执行病毒样本,而是用W32Dasm等反汇编工具将病毒样本文件反汇编后,用逆向技术对其汇编代码进行分析。然而病毒作者可以使用花指令,加壳脱壳,与加密解密等方法让指令不可读从而使静态分析变得非常困难。动态分析执行病毒样本,然后分析人员跟踪病毒具体做了些什么,比如创建了哪些文件,修改了哪些注册表信息,连接了哪些ip,发送和接收了什么数据等等。
当然,运行病毒样本存在一定的风险。由于大多数病毒都有一定的破坏性,一旦机器被感染,那么清除病毒或者还原到未感染状态往往需要耗费大量的时间和精力。笔者还记得以前在网吧作网管的时候,用ghost做备份和还原,怎么也要花5-10分钟还原一台机器把。
所以动态分析病毒需要特殊的环境。目前比较常见的是使用虚拟机,因为用虚拟机很容易建立一个快照(snapshot)来保存当前的状态,玩坏了以后可以一键快速恢复回该状态。笔者就用cuckoo和virtualbox搭建了一个动态分析的环境, 从而可以自动批量运行大量的病毒样本。
然而,道高一尺,魔高一丈。病毒作者自然不希望他们的病毒被分析,从而各种虚拟机侦测技术孕育而生。虽然对于绝大多数用户来说,使用虚拟机和实体机并没两样,但是在细节上两者还是有一定区别的。病毒可以依据这些蛛丝马迹来判断现在其是否运行在一个虚拟环境中,如果在一个虚拟环境中,那就立马退出。这样,分析人员就没法用动态分析追踪其真实行为。这有点像电影黑客帝国里的一个场景:Morpheus 给Neo吃了红色药丸,以至于Neo理解了当前的世界是个虚拟世界,而非真实世界。
虚拟机侦测并不是一个新的技术,Riusksk早在10年有一篇很好的总结 (虚拟机检测技术剖析http://bbs.pediy.com/showthread.php?t=119969)。 根据赛门铁克的统计,2014年初大约28%的病毒样本会对虚拟环境进行侦测,如果发现在Vmware环境下就立即停止执行。以下图片统计了具有侦测vmware能力的病毒的数量, 样本总数为200000,时间从2012年一月到2014年二月。图片来自赛门铁克http://www.symantec.com/connect/blogs/does-malware-still-detect-virtual-machines
2 Pafish
本文介绍Pafish。Pafish是一个用来侦测虚拟环境的开源工具,除了虚拟环境以外,Pafish还能侦测主流的沙箱和debug环境。由于Pafish是开源的,病毒作者们可以很容易的重用其代码。Pafish的github主页在https://github.com/a0rtega/pafish。 目前是V05.3版。
运行Pafish以后,Pafish会对各项特征进行检查,如果没有发现匹配特征,就显示一个绿色的OK, 如果发现了,就显示一个红色的traced! 如图所示,Pafish检测到当前运行环境匹配virtualbox的各项特征。
看一下pafish的github目录,包含以下文件
基本上,Pafish用一个c文件来对应一个侦测模块。Pafish包含如下模块:
·基于cpu的侦测: cpu.c
·侦测 调试环境: debuggers.c
·基本侦测: gensandbox.c
·侦测主流商业虚拟环境:比如vitualbox,VMware等
3 模块介绍
3.1 基于cpu的侦测
用法是,先后执行两次RDTSC,记下两个 64-bit 整数 ret 和 ret2,那么 ret2-ret1 代表了这期间 所花费的CPU 时钟周期数。
由于架构差异,同样的代码在虚拟机的运行速度往往慢于实体机。Pafish用RDTSC指令来计算执行一段代码所花费的平均CPU运行周期数。RDTSC 指令可以以极小的代价获得高精度的 CPU 时钟周期数(Time Stamp Counter)。static inline unsigned long long rdtsc_diff() { unsigned long long ret, ret2; unsigned eax, edx; __asm__ volatile("rdtsc" : "=a" (eax), "=d" (edx)); ret = ((unsigned long long)eax) | (((unsigned long long)edx) << 32); __asm__ volatile("rdtsc" : "=a" (eax), "=d" (edx)); ret2 = ((unsigned long long)eax) | (((unsigned long long)edx) << 32); return ret2 - ret; }
然后这个取差值的过程重复10次,得到平均值。如果这个平均周期差小于750的话,就认为是实体机环境,否则则认为是虚拟机环境。笔者估计Pafish选择750作为平均周期差的是根据实践经验。
int cpu_rdtsc() { int i; unsigned long long avg = 0; for (i = 0; i < 10; i++) { avg = avg + rdtsc_diff(); Sleep(500); } avg = avg / 10; return (avg < 750 && avg > 0) ? FALSE : TRUE; }
另一种方法是用cpuid指令得到cpu的名称和型号。如果名称中包含一些特殊的字符串,比如KVM, VMware等,则确定在虚拟环境中。
static inline void cpuid_vendor_00(char * vendor) { int ebx, ecx, edx; __asm__ volatile("cpuid" \ : "=b"(ebx), \ "=c"(ecx), \ "=d"(edx) \ : "a"(0x00)); sprintf(vendor , "%c%c%c%c", ebx, (ebx >> 8), (ebx >> 16), (ebx >> 24)); sprintf(vendor+4, "%c%c%c%c", edx, (edx >> 8), (edx >> 16), (edx >> 24)); sprintf(vendor+8, "%c%c%c%c", ecx, (ecx >> 8), (ecx >> 16), (ecx >> 24)); vendor[12] = 0x00; } int cpu_known_vm_vendors(char * vendor) { const int count = 4; int i; string strs[count]; strs[0] = "KVMKVMKVMKVM"; strs[1] = "Microsoft Hv"; strs[2] = "VMwareVMware"; strs[3] = "XenVMMXenVMM"; for (i = 0; i < count; i++) { if (!memcmp(vendor, strs[i], 12)) return TRUE; } return FALSE; }
3.2 侦测 调试环境:
第一种方法很简单,只需要看一下IsDebuggerPresent的返回值。IsDebuggerPresent是一个kernel32.dll中的函数,通过它可以检测当前进程是否正在被调试(用户模式)。
int debug_isdebuggerpresent() { if (IsDebuggerPresent()) return TRUE; else return FALSE; }
第二种方法用到OutputDebugString。OutputDebugString用来把调试信息输出到调试器的输出窗口。但是OutputDebugString只能在调试环境下执行。而在非调试环境下,OutputDebugString会产生一个错误。Pafish先设置一个错误代码(99)作为基准值,然后调用OutputDebugString,再取出错误代码与99比较。如果运行在一个调试环境中,那么应该没有新错误产生,所以取出的错误代码还是99。
int debug_outputdebugstring() { DWORD err = 99; /* Random error */ SetLastError(err); /* If we're been debugging, this shouldn't drop an error. */ OutputDebugString("useless"); if (GetLastError() == err){ return TRUE; } else { return FALSE; } }
3.3 基本侦测:
由于硬件的限制,很多人在创建虚拟机的时候往往只选择够用的配置,而不是高配置。(至少配置不会高于实体机)基本侦测是基于鼠标的移动,CPU的数量,硬盘和内存的大小等等。比如用户在一段时间内没有移动鼠标,系统只有一个CPU,硬盘小于60G,内存小于1G,这些都可能说明是一个虚拟环境。这种侦测方法不会非常准确,比如一个服务器可能没有配备鼠标,或者一个配置很差的旧电脑都会被误认为是虚拟环境。但是从统计上看来,还是有一定意义的。
int gensandbox_one_cpu_GetSystemInfo() { SYSTEM_INFO siSysInfo; GetSystemInfo(&siSysInfo); return siSysInfo.dwNumberOfProcessors < 2 ? TRUE : FALSE; }
第二种方法是如果当前系统的用户名包含sandbox,virus,malware等关键字,则认为是在虚拟机中,当然这个检测方法可以很容易的绕过。
int gensandbox_username() { char username[200]; size_t i; DWORD usersize = sizeof(username); GetUserName(username, &usersize); for (i = 0; i < strlen(username); i++) { /* case-insensitive */ username[i] = toupper(username[i]); } if (strstr(username, "SANDBOX") != NULL) { return TRUE; } if (strstr(username, "VIRUS") != NULL) { return TRUE; } if (strstr(username, "MALWARE") != NULL) { return TRUE; } return FALSE; }
3.4 侦测商业虚拟环境
除了基本的虚拟环境以外,Pafish还可以根据指纹侦测几种主流的虚拟机,包括sandboxie,Qemu,Virtualbox, Vmware and wine.侦测方法大同小异,这里只介绍针对virtualbox的侦测方法。
在原有的基础上,Virtualbox提供了一些增强功能,比如virtualbox guest Additions 和共享文件夹。virtualbox guest Additions可以自动调节窗口的分辨率,把实体机的字符串复制到虚拟机里面。而共享文件夹解决了虚拟机和实体机共享文件的问题。这些增强功能极大的提高了virtualbox的易用性。然而,福兮祸所伏。若想使用virtualbox guest Additions,用户需要在虚拟机上安装一些特殊的驱动和应用程序。而这些驱动和应用基本不可能出现在实体机上。所以Pafish可以查找这些驱动和应用来判断当前运行环境是否是virtualbox。
查找注册表,看看有没有VirtualBox Guest Additions的字符串。
int vbox_reg_key3() { return pafish_exists_regkey(HKEY_LOCAL_MACHINE, "SOFTWARE\\Oracle\\VirtualBox Guest Additions"); }
查找有没有VirtualBox Guest Additions的相关进程 (vboxservice.exe 和vboxtray.exe)
查找右下角有没有关于VboxTrayTool的任务栏托盘窗口.
int vbox_traywindow() { HWND h1, h2; h1 = FindWindow("VBoxTrayToolWndClass", NULL); h2 = FindWindow(NULL, "VBoxTrayToolWnd"); if (h1 || h2) return TRUE; else return FALSE; }
查找是否存在以下文件
"C:\\WINDOWS\\system32\\vboxdisp.dll"; "C:\\WINDOWS\\system32\\vboxhook.dll"; "C:\\WINDOWS\\system32\\vboxmrxnp.dll"; "C:\\WINDOWS\\system32\\vboxogl.dll"; "C:\\WINDOWS\\system32\\vboxoglarrayspu.dll"; "C:\\WINDOWS\\system32\\vboxoglcrutil.dll"; "C:\\WINDOWS\\system32\\vboxoglerrorspu.dll"; "C:\\WINDOWS\\system32\\vboxoglfeedbackspu.dll"; "C:\\WINDOWS\\system32\\vboxoglpackspu.dll"; "C:\\WINDOWS\\system32\\vboxoglpassthroughspu.dll"; "C:\\WINDOWS\\system32\\vboxservice.exe"; "C:\\WINDOWS\\system32\\vboxtray.exe"; "C:\\WINDOWS\\system32\\VBoxControl.exe"; "C:\\program files\\oracle\\virtualbox guest additions\\"; "C:\\WINDOWS\\system32\\drivers\\VBoxMouse.sys"; "C:\\WINDOWS\\system32\\drivers\\VBoxGuest.sys"; "C:\\WINDOWS\\system32\\drivers\\VBoxSF.sys"; "C:\\WINDOWS\\system32\\drivers\\VBoxVideo.sys";
用WNetGetProviderName检查共享文件夹的网络类型名称,如果是"VirtualBox Shared Folders",则检查到virtualbox的共享文件夹
int vbox_network_share() { unsigned long pnsize = 0x1000; char provider[pnsize]; int retv = WNetGetProviderName(WNNC_NET_RDR2SAMPLE, provider, &pnsize); if (retv == NO_ERROR) { if (lstrcmpi(provider, "VirtualBox Shared Folders") == 0) { return TRUE; } else { return FALSE; } } return FALSE; }
以上,我们可以看到如果在虚拟机上安装了virtualbox guest Additions 和共享文件夹,该虚拟机的其他进程可以很容易的发现其相关的窗口,文件,驱动,服务信息。这就相当于李鬼在脸上写着“我不是李逵,我是李鬼”。那么,是不是不安装virtualbox guest Additions 和共享文件夹就安全了呐?答案是未必。一些默认的硬件信息仍然可以被作为指纹用于检测virtualbox的存在。比如virtualbox默认的网卡MAC地址前缀为08:00:27,这前3字节是virtualbox分配的唯一标识符OUI,以供其虚拟网卡使用。
int vbox_mac() { /* VirtualBox mac starts with 08:00:27 */ return pafish_check_mac_vendor("\x08\x00\x27"); }
另外,还可以读取注册表信息,通过检测特定的硬件信息,比如SCSI, systembiosversion, videobiosversion, ACPI,如果这些信息 包含VBOX关键字,那么可以断定是virtualbox虚拟环境。
int vbox_reg_key1() { return pafish_exists_regkey_value_str(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\Scsi\\Scsi Port 0\\Scsi Bus 0\\Target Id 0\\Logical Unit Id 0", "Identifier", "VBOX"); }
还可以检测其系统bios生成日期,如果是1999年6月23号,那么很可能是virtualbox
int vbox_reg_key10() { return pafish_exists_regkey_value_str(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System", "SystemBiosDate", "06/23/99"); }
4. 总结
综上,Pafish给出了一些很实用的检测虚拟机的方法。虚拟的毕竟就是虚拟的,总归会留下蛛丝蚂迹。不过很多人认为我们也可以利用虚拟机检测来免疫病毒,比如在实体机上设置一些虚假信息让病毒误认为这个实体机是一个虚拟机,从而跳过“做坏事”的阶段。对于这个观点,有人认为靠谱,有人认为不靠谱,大家怎么看?
* 作者/nickchang,属FreeBuf原创奖励计划文章,未经许可禁止转载