简单的活着

Nitro

Posted on By Mista Cai

Nitro: Hardware-Based System Call Tracing for Virtual Machines

前言

VMI是在hypervisor层面监控和分析虚拟机状态的方法。因此Nitro对客户机OS不可见。

- Guest OS Portability.可移植性,Nitro不依赖GuestOS,因此无需改动就可在客户机间移植。
- Evasion-Resistance.部署好系统之后,攻击者无法逃逸。

为了翻译虚拟机里面底层的二进制状态信息,hypervisor必须借助硬件结构或者GuestOS信息来弥补语义鸿沟。

摘要

虚拟机内省(VMI)描述了从管理员级别监视和分析虚拟机状态的方法。

这非常适合安全应用,尽管英特尔和AMD的硬件虚拟化支持并未考虑到VMI。这为硬件支持的VMI系统的开发人员带来了许多挑战。

本文描述了我们的原型框架Nitro的设计和实现,用于系统调用跟踪和监控。由于Nitro是纯粹基于VMI的系统,因此它与来自客户操作系统的攻击保持隔离,并且不会从客户端直接看到。 Nitro非常灵活,因为它支持Intel x86架构提供的所有三种系统调用机制,并且已被证明可在Windows,Linux,32位和64位环境中运行。

我们系统的高性能允许实时捕获和传播数据,而不会妨碍可用性。通过各种客户操作系统的广泛测试支持这一点。此外,由于称为硬件生根的构造,Nitro可以抵抗规避手段。最后,Nitro在性能和功能上都超越了类似的系统。

实现

KVM分为两部分:

  • 基于QEMU构建的用户应用程序:提供QEMU监视器,它是管理程序的类似shell的接口。 它提供对VM的一般控制。所有Nitro命令都通过此监视器输入。
  • 一组Linux内核模块:这些命令通过I/O控制接口发送到KVM的内核模块部分。最后,通过使用proc文件系统实现输出。

在某些情况下,硬件制造商提供的虚拟化扩展支持捕获您感兴趣的特定事件,这使得工作变得简单。但是,通常情况下,特别是对于安全机制,硬件扩展不支持捕获所需的事件。在这些情况下,我们必须间接地向虚拟机管理程序引入陷阱。找到这些用于捕获所需事件的间接方法通常是一项挑战。

事实证明,在流行的Intel IA-32(即x86)和Intel 64(以前的EM64T)架构上不支持在系统调用事件时捕获到管理程序。在这种情况下,我们必须找到一种间接导致陷阱的方法,如上所述。我们通过强制系统中断(例如,页面错误,一般保护错误等)来实现此目的,英特尔虚拟化扩展(VT-x)支持这些中断。因此,我们已经有效地创建了一种捕获系统调用的机制,即使硬件扩展本身不支持这一点。由此产生的控制流程如图2所示。由于三个系统调用机制的性质完全不同,因此必须为每个系统调用机制设计一个独特的陷阱机制。下面描述这些捕获机制及其实现。

Interrupt-Based System Calls

系统调用可以实现为用户定义的中断。 x86架构通过中断描述符表(IDT)处理中断。该IDT可以具有多达256个条目,每个条目长度为8个字节。 IDT的确切大小与IDT存储在系统内存中的地址一起存储在IDTR中。当发生中断时,硬件通过IDTR查询IDT以确定适当处理程序的位置并继续在那里执行,如图1所示。

英特尔的VT-x扩展允许用户将系统中断(中断0到31)捕获到管理程序,但是它们没有提供用于捕获可能用于系统调用的用户中断(中断32及更高版本)的机制。这意味着我们必须设计一种方法来使这个用户中断产生系统中断。

我们可以通过虚拟化IDT来实现这一点,也就是说,我们将访客的IDT复制到虚拟机管理程序中。然后我们必须操纵IDTR并捕获对它的所有写访问,从而禁止任何进一步的操作。由于存储在IDTR中的IDT大小值被添加到基址以获得IDT的最后一个有效字节的偏移量,我们可以将此大小设置为32·8-1 = 255.这使得所有系统中断不受影响,但是所有调用用户中断的尝试(即中断大于31)都会导致一般保护错误,因为IDT的界限已被超过。这种方法的优点是IDT在内存中不受影响,但实际上会被用户中断忽略。

下一步是将所有一般保护错误捕获到虚拟机管理程序,虚拟化扩展本身支持这些故障。但是,我们仍然必须确定我们生成的一般保护错误与自然发生的一般保护错误之间的区别2。这可以通过检查当前指令并确定它是否是int指令以及中断号是否大于31来完成。

如果我们将异常标识为自然,我们会将此异常注入guest虚拟机并允许它继续。但是,如果我们识别出由用户中断引起的异常,我们会查看中断号以确定是否已经捕获了系统调用。如果是这种情况,我们会根据为Nitro数据收集引擎指定的规则收集数据(参见第4.3节)。在任何一种情况下,都必须使用我们从guest虚拟机复制的IDT模拟int指令,并将控制权交给客户操作系统。

SYSCALL-Based System Calls

系统调用也可以使用SYSCALL指令及其模拟对应SYSRET来实现。这两者都依赖于一组MSR,即STAR MSR,CSTAR MSR和LSTAR MSR。究竟使用哪些寄存器取决于guest虚拟机操作系统是以传统模式,长模式还是兼容模式运行。另外,通过在扩展特征启用寄存器(EFER)中设置和取消设置SCE标志,可以有效地打开和关闭该机制。在未设置SCE标志的情况下使用SYSCALLSYSRET会导致无效的操作码异常。

因此,强制此机制导致系统中断是取消设置SCE标志, 并将虚拟机管理程序设置为捕获所有无效操作码异常的问题,虚拟机扩展本身支持这些异常。一旦控制权转移到管理程序,我们必须再次区分自然例外和由我们内省引起的例外。这是通过查看违规指令来实现的,如果该指令不是SYSCALL或SYSRET,我们将一个无效的操作码异常注入到客户操作系统并将控制返回给它。但是,如果违规指令实际上是SYSCALL,则Nitro会收集所需信息,模拟该指令,并将控制权返回给客户操作系统。

除了模拟SYSCALL指令外,Nitro还必须能够处理由SYSRET指令引起的异常并模拟该指令。这是因为对EFER所做的更改会影响SYSRET指令,其影响方式与它们影响SYSCALL指令的方式相同。因此,使用SYSRET指令也会导致无效的操作码异常,因此必须进行相应的处理。这样做,如果应用程序需要此信息,Nitro还能够收集调用的系统调用的返回值。

SYSENTER-Based System Calls

与SYSCALL和SYSRET类似,SYS-ENTER和SYSEXIT指令对也依赖于一组MSR,即SYS-ENTER CS MSR,SYSENTER ESP MSR和SYSENTER EIP MSR。在调用SYSENTER时,每个MSR中的值都会复制到特定的系统寄存器中。对于Ni的开发而言,具体而且最有趣的是,当执行SYSENTER时,SYSENTER CS MSR的值被复制到CS寄存器中,并且尝试使用空值加载CS寄存器会导致一般保护异常。因此,引起系统中断的问题是在管理程序中保存SYSENTER CS MSR寄存器的当前值并加载空值。这将导致每个SYSENTER操作尝试将空值加载到CS寄存器中,从而导致管理程序可以捕获的系统中断。

一旦管理程序捕获了一般保护异常,自然异常和强制异常之间的区别再一次是在异常时检查当前指令的问题。如果我们遇到一个自然异常,就像以前的系统调用机制一样,我们将异常注入到客户操作系统并允许它继续。如果我们遇到一般保护异常并且当前指令是SYSENTER,我们收集相关数据,使用SYSEN- TER CS MSR的保存值模拟指令,并将控制返回给客户操作系统。

与基于SYSCALL / SYSRET的系统调用机制一样,我们对guest虚拟机进行的更改以引发系统中断也会影响SYSEXIT指令。因此,我们还必须模拟此指令,并且有机会轻松提取调用的系统调用的返回值。

4.2 进程识别

能够确定哪个进程产生系统调用始终是重要的。 这要求我们在每次系统调用中断时收集进程特有的信息。 Nitro收集CR3寄存器的值以及相应顶级页面目录中第一个有效条目的值。 这允许我们识别一个过程,因为CR3寄存器中的值(即顶层页面目录的地址)对于单个进程是唯一的。 为了处理新创建的进程接收顶级页面目录的情况,该目录位于先前销毁的进程的顶级页面目录的相同位置,我们还考虑相应顶级的第一个有效条目页面目录,以便创建真正唯一的标识符。

4.3 收集系统调用数据

根据我们的经验,系统调用跟踪的不同应用程序取决于不同数量的信息。在某些情况下,没有参数的简单系统调用数序列可能就足够了,而其他场景可能需要详细的信息,包括寄存器值,基于堆栈的参数以及来自一小部分系统调用的返回值。由于我们无法预见每个客户操作系统类型和系统调用跟踪的可能应用,因此Nitro不会为每个系统调用提供固定的数据集。相反,它允许用户定义灵活的规则,以便以细粒度的方式在系统调用跟踪期间控制数据收集。

例如,用户可以指定客户OS存储系统调用号的确切位置(通常在EAX寄存器中)。然后,Nitro可以提取此系统调用号以及第4.2节中描述的进程标识符。对于某些用于检测过程中恶意软件或恶意行为的机器学习技术,此信息通常已足够[9,6]。

在其他情况下,参数指向的系统调用参数甚至是解引用的内存变量都是至关重要的。为了满足这些要求,Nitro的规则足以表达基于堆栈和基于寄存器的参数传递,以及直接打印寄存器值或解除它们。此规则的语法采用以下格式:

add scmon rule CONDITION REG CONDITION VAL ACTION REG OFFSET ACTION,

其中CONDITION REG包含应该测试以确定是否应该收集信息的寄存器的名称,CONDITION VAL包含CONDITION REG应包含的值以便收集进一步的信息,ACTION REG包含包含的寄存器的名称 我们感兴趣的基值,OFFSET包含ACTION REG中的偏移量(正或负),用于收集解除引用值时我们感兴趣的数据,ACTION定义是否应该取消引用ACTION REG以及输出的格式 应采取。 这可能导致以十六进制,整数,无符号整数或字符串打印或取消数据。 我们在附录A中以Backus-Naur形式提供了规则的描述。例如,每当用户进程在Linux中使用write系统调用时,很容易指定一个规则来取消引用并输出正在写入的字符串。 客人。 该规则如下:

add scmon rule rax 4 rcx 0 derefstr

这种基于规则的请求信息的方法使Nitro非常灵活,并有助于其操作系统不可知性。

考虑到Nitro的设计目标,我们只收集其位置和格式由硬件规范定义或指定规则的信息。但是,Nitro的灵活设计允许轻松整合客户操作系统特定知识,以便收集有关呼叫过程的其他信息。我们已成功将Nitro与我们研究组内的其他项目相结合,将流程和用户ID等信息纳入输出。但是,我们将这些项目分开,以使Nitro尽可能简单灵活。这使得Nitro可用于更广泛的应用。当与我们调用InSight的内存分析工具结合使用时,我们能够生成输出,如图3所示。这提供了额外的客户操作系统特定信息,例如写入的描述符类型,同时允许Nitro保持适用于广泛一系列客户操作系统。

Jun 20 17:58:20: sys_write : unsigned int fd , const char __user *buf, size_t count
	fd: unsigned int: 0x3 → (socket) → [...]: (SOCK_STREAM) flags: ()
	buf: const char __user*: buffer content hex (of
0x7FFF702FF320 → size 107):
		47 45 72 2d 20 28 74 3a 65 2e 65 65
		54 20 2f 20 48 41 67 65 6e 74 6c 69 6e 75 78 20 2a 2f 2a da 64 65 da 43 6f 70 		2d 41 6c 69 54 54 50 2f 3a 20 57 67 2d 67 6e 75 48 6f 73 74 6e 6e 65 63 76 65 		da da 31 2e 30 da 55 73 65 65 74 2f 31 2e 31 32 29 da 41 63 63 65 70 3a 20 67 		6f 6f 67 6c 74 69 6f 6e 3a 20 4b
	buffer content string:
		GET / HTTP /1.0
		User-Agent: Wget/1.12 (linux-gnu) Accept: */*
		Host: google.de
		Connection: Keep-Alive
count: size_t: 0x6B