在2020年8月,我们发布了一篇文章,内容涉及到如何使用Speakeasy仿真框架来模拟用户模式的恶意软件,例如Shellcode。我们建议还没有读过这篇文章的读者首先阅读这篇文章。
除了用户模式仿真之外,Speakeasy还支持内核模式Windows二进制文件的仿真。当恶意软件作者使用内核模式的恶意软件时,通常会采用设备驱动程序的形式,其最终目标是完全感染目标系统。该恶意软件通常不与硬件交互,而是利用内核模式完全破坏系统并保持隐蔽性。
动态分析内核恶意软件面临的挑战
理想情况下,可以使用反汇编程序之类的工具对内核模式的样本进行静态分析。但是,二进制加壳程序和用户模式样本一样,容易混淆内核恶意软件。另外,静态分析通常成本较高,且耗费大量时间。如果我们的任务是自动分析同一恶意软件家族的多个变种,那么可以动态分析恶意驱动程序样本。
与用户模式样本相比,对内核模式恶意软件的动态分析可能会涉及更多内容。为了调试内核恶意软件,需要创建适当的环境。这个过程通常会涉及到将两个独立的虚拟机设置为调试器和被调试器。然后,可以将该恶意软件作为按需的内核服务进行加载,这里可以使用WinDbg之类的工具远程调试驱动程序。
此外,还存在一些使用挂钩或其他监视技术的沙箱型应用程序,但通常以用户模式应用程序为目标。对于内核模式代码来说,也有类似的沙箱监视工作,会需要用到比较深入的系统级挂钩,这可能会产生很大的噪音。
驱动程序仿真
对于恶意程序来说,仿真是一种有效的分析技术。通过这种方式,我们不需要自定义设置,并且可以大规模模拟驱动程序。此外,与沙箱环境相比,这种方式更容易实现更大的代码覆盖范围。通常,rootkit可能会通过I/O请求数据包(IRP)处理程序(或其他回调)公开恶意功能。在普通的Windows系统上,当其他应用程序或设备向驱动程序发送输入/输出请求时,将会执行这些例程。这里包括一些常见的任务,例如读取、写入或将设备I/O控制(IOCTL)发送给驱动程序以执行某种类型的功能。
使用仿真的方式,可以使用IRP数据包直接调用这些入口点,以便在rootkit中标识尽可能多的功能。正如我们在第一篇Speakeasy文章中所讨论的,在发现其他入口点时会对其进行仿真。驱动程序的DriverMain入口点负责初始化一个函数分配表,该表将被调用以处理I/O请求。在主入口点完成后,Speakeasy将尝试通过提供虚拟IRP来仿真这些函数。此外,按顺序模拟所有创建的系统线程或工作项,以尽可能覆盖更多的代码。
仿真内核模式植入工具
在这篇文章中,我们将展示Speakeasy在仿真真实内核模式植入工具(Winnti)的一个案例。尽管我们之前就使用过这个示例,但在本文中还是选择了它,因为它以比较透明的方式实现了一些经典的rootkit功能。这篇文章并不是对恶意软件本身进行分析,因为恶意软件已经过时了,相反,我们将专注于仿真期间捕获到的事件。
我们所分析的Winnti样本的SHA-256哈希值为c465238c9da9c5ea5994fe9faf1b5835767210132db0ce9a79cb1195851a36fb,其原始文件名为tcprelay.sys。在本文的大部分案例中,我们都将检查Speakeasy生成的仿真报告。注意,由于内核补丁程序保护(PatchGuard)可以防止对关键内核数据结构进行修改,因此这个32位rootkit所采用的许多技术在现代64位版本的Windows上将不起作用。
首先,我们将使用Speakeasy按照下图所示的命令行来仿真内核驱动程序。我们指示Speakeasy创建一个完整的内核转储(使用“-d”标志),以便后续可以获取内存。这里还加上了内存跟踪标志(“-m”),它将记录恶意软件执行的所有内存读取和写入操作。这对于检测挂钩和直接内核对象操纵(DKOM)等非常有帮助。
用于仿真恶意驱动程序的命令行:
随后,Speakeasy将开始仿真恶意软件的DriverEntry函数。驱动程序的入口点负责设置被动回调例程,该例程将为用户模式I/O请求以及用于设备添加、移除和卸载的回调提供服务。我们可以查看恶意软件DriverEntry函数的仿真报告(在JSON报告中以“epry_point”的“ep_type”标识),表明该恶意软件找到了Windows内核的基址。该恶意软件通过使用ZwQuerySystemInformation API来找到所有内核模块的基址,然后查找一个名为“ntoskrnl.exe”的工具来实现此目的。随后,恶意软件会手动找到PsCreateSystemThread API的地址。然后,它用于启动系统线程以执行其实际功能。下图展示了从恶意软件入口点调用的API。
tcprelay.sys入口点中的关键功能:
隐藏驱动程序对象
该恶意软件会在执行其主系统线程之前尝试隐藏自身。恶意软件首先在自己的DRIVER_OBJECT结构中查找“DriverSection”字段。该字段包含一个带有所有已加载内核模块的链表,恶意软件尝试将自身取消链接,从而在列出已加载驱动程序的API中隐藏。在下图Speakeasy报告中的“mem_access”字段中,我们可以看到在其前后分别对DriverSection条目进行了两次内存写入,这会将其自身从链表中删除。
表示tcprelay.sys恶意软件的内存写入事件,尝试将自身取消链接以实现隐藏:
如之前的Speakeasy文章所述,当在运行时创建线程或其他动态入口点时,框架会跟随它们进行仿真。在这种情况下,恶意软件会创建一个系统线程,而Speakeasy会自动对其进行仿真。
我们转到新创建的线程(由“system_thread”的“ep_type”标识),可以看到恶意软件开始了真正的功能。该恶意软件首先遍历主机上所有正在运行的进程,然后查找名为services.exe的服务控制器进程。最重要的是,通过对JSON配置文件进行配置,可以记录仿真样本的进程列表。有关这些配置选项的更多信息,可以参见我们GitHub仓库上的Speakeasy README。这个可配置进程列表的示例如下图所示。
提供给Speakeasy的进程列表配置字段:
转到用户模式
一旦恶意软件找到了services.exe进程,它将附加到进程上下文,并开始检查用户模式内存,以便找到导出的用户模式函数的地址。恶意软件会这样做,以便可以将编码的、驻留在内存的DLL注入到services.exe进程中。下图展示了rootkit用于解决其用户模式导出的API。
tcprelay.sys rootkit用于解决其用户模式植入工具导出问题的日志API:
在解决了导出函数之后,rootkit准备注入用户模式DLL组件。接下来,恶意软件手动将内存中的DLL复制到services.exe进程地址空间中。这些内存写入事件被捕获到,如下图所示。
将用户模式植入复制到services.exe时捕获到的内存写入事件:
Rootkit用于执行用户模式代码的常用技术依赖于一种名为“异步过程调用”(Asynchronous Procedure Calls,APC)的Windows功能。APC是在提供的线程的上下文中异步执行的函数。使用APC,内核模式应用程序可以将代码以队列方式在线程的用户模式上下文中运行。恶意软件通常会希望注入用户模式,因为这样一来,就可以更容易地访问Windows内的许多常用功能(例如网络通信)。此外,如果在用户模式下运行,在代码Bug排查过程中被发现的概率较小。
为了让APC按照队列顺序以用户模式启动,恶意软件必须将线程定位为Alertable的状态。当线程将执行交给内核线程调度程序并通知内核可以分配APC时,通常就被称为是“可警告的”(线程等待状态)。恶意软件会在services.exe进程中搜索线程,一旦检测到可警告的线程,它将为DLL分配内存以记性呢注入,然后将APC按照队列顺序执行。
Speakeasy模拟这个过程中涉及到的所有内核结构,特别是为Windows系统上的每个线程分配的执行线程对象(ETHREAD)结构。恶意软件可能会尝试遍历这种不透明的结构,以识别何时设置了线程的警告标志(APC的有效候选对象)。下图展示了Winnti恶意软件在services.exe进程中手动解析ETHREAD结构以确认其处于可警告状态时记录的内存读取事件。在撰写本文时,默认情况下,仿真器中的所有线程都将自己表示为可警告的状态。
tcprelay.sys恶意软件确认线程是否处于可警告状态时记录的事件:
接下来,恶意软件可以使用此线程对象执行所需的任何用户模式代码。其中未记录的函数KeInitializeApc和KeInsertQueueApc将分别初始化并执行用户模式APC。下图显示了恶意软件用于将用户模式模块注入到services.exe进程的API集。该恶意软件执行一个Shellcode stub作为APC的目标,然后将为注入的DLL执行一个加载程序。所有这些都可以从内存转储中恢复,并在后续进行分析。
tcprelay.sys rootkit用于通过APC注入到用户模式的日志API:
网络挂钩
在注入到用户模式后,内核组件将尝试安装网络混淆挂钩(推测是用于隐藏用户模式植入工具)。Speakeasy跟踪并标记仿真空间中的所有内存。在内核模式仿真的上下文中,这包括所有内核对象(例如驱动程序、设备对象以及内核模块本身)。我们观察到恶意软件将其用户模式植入后,立即开始尝试挂钩内核组件。根据静态分析的结果,我们确认它用于网络隐藏。
仿真报告的内存访问部分显示,该恶意软件修改了netio.sys驱动程序,特别是在名为NsiEnumerateObjectsAllParametersEx的导出函数中的代码。当系统上的用户运行“netstat”命令时,最终会调用这个函数,与此同时恶意软件可能会对这个函数进行挂钩,以隐藏受感染系统上已连接的网络端口。这个内联挂钩使用了下图所示的事件标识。
恶意软件设置的内联函数挂钩,用于隐藏网络连接:
此外,恶意软件还会挂钩TCPIP驱动程序对象,以实现其他的网络隐藏功能。具体而言,该恶意软件将TCPIP驱动程序的IRP_MJ_DEVICE_CONTROL处理程序挂钩。查询活动连接时,用户模式代码可能会将IOCTL代码发送给这个函数。通过查找对关键内核对象的内存写入,可以使用Speakeasy轻松识别这种挂钩,如下图所示。
用于挂钩TCPIP网络驱动程序的内存写入事件系统服务调度表挂钩:
系统服务分配表挂钩
最后,rootkit将尝试使用系统服务分派表(SSDT)修补这种近乎古老的技术来隐藏自身。Speakeasy分配了伪造的SSDT,以便恶意软件可以与其进行交互。SSDT是一个函数表,可以将内核功能公开给用户模式代码。下图中的事件表明,SSDT结构在运行时已经被修改。
Speakeasy检测到的SSDT挂钩:
如果我们在IDA Pro中查看恶意软件,就可以确认该恶意软件已经修改了ZwQueryDirectoryFile和ZwEnumerateKeyAPI的SSDT条目,以用于隐藏自身,不会在文件系统分析和注册表分析过程中被发现。
IDA Pro中显示的用于文件隐藏的SSDT修改后函数:
在设置完这些挂钩后,系统线程将会推出。驱动程序中的其他入口点(例如IRP处理程序和DriverUnload例程)不是太值得分析,其中包含的基本都是示例驱动程序代码。
获取注入的用户模式植入工具
现在,我们已经知道了驱动程序在系统上隐藏自身的方式,就可以用Speakeasy创建的内存转储来获取前面所说的注入的DLL。打开我们在仿真时创建的ZIP文件,可以找到上稳重引用过的内存标签。我们迅速确认了该内存块具有有效的PE标头,并且能够成功地将其加载到IDA Pro中,如下图所示。
从Speakeasy内存转储中恢复的注入用户模式DLL:
总结
在这篇文章中,我们探索了如何使用Speakeasy有效地从内核模式二进制文件中自动识别rootkit活动。Speakeasy可以用于快速分类内核二进制文件,解决了通常情况下难以进行动态分析的问题。如果想了解更多信息或查看源代码,欢迎访问我们的GitHub仓库( https://github.com/fireeye/speakeasy )。
本文翻译自:
https://www.fireeye.com/blog/threat-research/2021/01/emulation-of-kernel-mode-rootkits-with-speakeasy.html