再探Rookit-对Festi Rookit样本的详细分析
前言 很老的样本了,github有自行下载。参考国外的研究报告学习的,接着静态分析学习一下。 DriverEntry 基本架构 Festi由单个内核驱动程序组成。驱动程序的入口调用图如下: 前文提过,Festi经Dropper植入到系统中,Festi的内核驱动程序有俩个主要职责:从命令和控制(C&C)服务器请求配置信息,以及以插件的形式下载和执行恶意模块,插件是内核模式驱动程序,不会保存
前言
很老的样本了,github有自行下载。参考国外的研究报告学习的,接着静态分析学习一下。
DriverEntry
基本架构
Festi由单个内核驱动程序组成。驱动程序的入口调用图如下:

前文提过,Festi经Dropper植入到系统中,Festi的内核驱动程序有俩个主要职责:从命令和控制(C&C)服务器请求配置信息,以及以插件的形式下载和执行恶意模块,插件是内核模式驱动程序,不会保存在系统中的任何存储设备上,并且在内存中易失性。因此,当受感染的计算机关闭或重新启动时,插件会从系统内存中消失。

每个插件专用于特定的任务,例如对指定的网络资源执行DDoS攻击,或向C&C服务器提供的电子邮件列表发送垃圾邮件。插件通过明确定义的接口与主驱动程序通信。
Festi使用一个插件管理器管理插件,有一个C&C协议解析器,用于从C&C服务器下载插件。同时还有一个内存管理器和网络套接字组件,上述构成了Festi驱动程序的基本架构:

为了有效的管理下载的插件,Festi维护了一个指针数组,这个指针指向了一个特别定义的PLUGIN_INTERFACE结构。每个结构对应于内存中的一个特定插件,并为bot提供特定的入口点--负责处理从C&C接收的数据的例程。
PLUGIN_INTERFACE结构如下:
struct PLUGIN INTERFACE
{
//初始化 plug-in
PVOID Initialize;
//释放plug-in 执行清除操作
PVOID Release;
//获取plug-in 版本信息
PVOID GetVersionInfo_1;
//获取plug-in 版本信息
PVOID GetVersionInfo_2;
//向TCP 流中写入具体插件信息
PVOID WriteIntoTcpStream;
// 从 TCP 流读取特定于插件的信息并解析数据
PVOID ReadFromTcpStream;
// 保留字段
PVOID Reserved 1;
PVOID Reserved 2;
};
Festi加载后,从C&C服务器接收数据,下载插件到易失性内存中。Festi有俩个内置插件,分别是配置信息管理器和僵尸主机插件管理器。为了与C&C服务器通信,Festi使用了一种定制的网络通信协议,并要保护其不被窃听。
IRP处理
驱动程序初始化。先获取 DriverObject 的 MajorFunction 数组的起始地址。

关于MajorFunction:MajorFunction 是 RIVER_OBJECT 结构的一个成员,它是指向函数的大量指针,这些函数处理 I/O 请求数据包 (IRP)。如下:
0: kd> !drvobj parport 2
Driver object (fffffa80048d9e70) is for:
\Driver\Parport
DriverEntry: fffff880065ea070 parport!GsDriverEntry
DriverStartIo: 00000000
DriverUnload: fffff880065e131c parport!PptUnload
AddDevice: fffff880065d2008 parport!P5AddDevice
Dispatch routines:
[00] IRP_MJ_CREATE fffff880065d49d0 parport!PptDispatchCreateOpen
[01] IRP_MJ_CREATE_NAMED_PIPE fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE fffff880065d4a78 parport!PptDispatchClose
[03] IRP_MJ_READ fffff880065d4bac parport!PptDispatchRead
[04] IRP_MJ_WRITE fffff880065d4bac parport!PptDispatchRead
[05] IRP_MJ_QUERY_INFORMATION fffff880065d4c40 parport!PptDispatchQueryInformation
[06] IRP_MJ_SET_INFORMATION fffff880065d4ce4 parport!PptDispatchSetInformation
[07] IRP_MJ_QUERY_EA fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL fffff880065d4be8 parport!PptDispatchDeviceControl
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL fffff880065d4c24 parport!PptDispatchInternalDeviceControl
[10] IRP_MJ_SHUTDOWN fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[12] IRP_MJ_CLEANUP fffff880065d4af4 parport!PptDispatchCleanup
[13] IRP_MJ_CREATE_MAILSLOT fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER fffff880065d491c parport!PptDispatchPower
[17] IRP_MJ_SYSTEM_CONTROL fffff880065d4d4c parport!PptDispatchSystemControl
[18] IRP_MJ_DEVICE_CHANGE fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP fffff880065d4840 parport!PptDispatchPnp
因此这部分代码就是对所有的IRP请求做一个统一处理,统一拦截下来,sub_12D06分代码:
检查IRQL

检查堆栈

字符匹配

总结一下,这个函数处理特定的IRP请求,根据CurrentLocation状态进行更新,根据不同IRQL进行不同的处理,检查IRP请求是否涉及特定文件名,如果是,调用sub_12C5B回状态码。
进入一下sub_12BBE:

KeStackAttachProcess和KeUnstackDetachProcess和分别用于将当前线程附加到指定进程的地址空间和解除关联,这将允许线程在其他进程的上下文中执行代码。这样做Festi就有可能附加到其它进程,从而在不被检测到的情况下执行操作。
上述代码还存在许多修改偏移量的操作,过修改传入参数的某些偏移量,代码可能改变请求的状态或标志,利用这个Festi可能可以干扰到系统的正常运行。涉及的俩个函数大致作用是通过遍历内存块、查找特定字符串并清除相应内存,实现对内存中数据的清理和隐藏。
动态解析函数
Festi动态解析了一系列WindowsAPI函数


byte的内容是.cdata段的内容,通过IDApython解密:
import idaapi
import idautils
import idc
# 定义四字节的异或密钥
XOR_KEY = 0x9D802CB0
def xor_data(data, key):
key_bytes = key.to_bytes(4, byteorder='little')
result = bytearray(len(data))
for i in range(0, len(data), 4):
for j in range(4):
if i + j < len(data):
result[i + j] = data[i + j] ^ key_bytes[j]
return result
# 查找 .cdata 段
for seg in idautils.Segments():
seg_name = idc.get_segm_name(seg)
if seg_name == ".cdata":
start = idc.get_segm_start(seg)
end = idc.get_segm_end(seg)
# 打印 .cdata 段的起始和结束地址
print(f".cdata segment found at {hex(start)} - {hex(end)}")
# 读取段内容
cdata = idaapi.get_bytes(start, end - start)
if cdata:
# 对段内容进行异或操作
xor_cdata = xor_data(bytearray(cdata), XOR_KEY)
# 将异或后的内容写回 .cdata 段
ida_bytes.patch_bytes(start, bytes(xor_cdata))
print("XORed .cdata content written back to segment.")
else:
print("Failed to read .cdata segment content.")
break
else:
print(".cdata segment not found.")


隐藏技巧
在磁盘上隐藏
为了保护和隐藏存储在硬盘上的恶意内核模式驱动程序的映像,Festi挂载文件系统驱动程序,以便它可以拦截和修改发送到文件系统驱动程序的所有请求,以排除它存在的证据。

用于安装钩子的例程如下:
NTSTATUS __stdcall SetHookOnSystemRoot(PDRIVER_OBJECT DriverObject, void **a2)
{
NTSTATUS v2; // ecx
NTSTATUS result; // eax
struct _DEVICE_OBJECT *RelatedDeviceObject; // eax
struct _DEVICE_OBJECT *v5; // esi
_DWORD *v10; // eax
int v12; // [esp-Ch] [ebp-48h] BYREF
_BYTE v13[12]; // [esp+0h] [ebp-3Ch] BYREF
struct _OBJECT_ATTRIBUTES ObjectAttributes; // [esp+Ch] [ebp-30h] BYREF
struct _IO_STATUS_BLOCK IoStatusBlock; // [esp+24h] [ebp-18h] BYREF
struct _UNICODE_STRING DestinationString; // [esp+2Ch] [ebp-10h] BYREF
PDEVICE_OBJECT TargetDevice; // [esp+34h] [ebp-8h]
PDEVICE_OBJECT DeviceObject; // [esp+38h] [ebp-4h] BYREF
RtlInitUnicodeString(&DestinationString, SourceString);// 初始化一个UNICODE_STRING结构,指向\SystemRoot
ObjectAttributes.Length = 24;
ObjectAttributes.RootDirectory = 0;
ObjectAttributes.Attributes = 64;
ObjectAttributes.ObjectName = &DestinationString;
ObjectAttributes.SecurityDescriptor = 0;
ObjectAttributes.SecurityQualityOfService = 0;
v2 = IoCreateFile( // 获取系统根目录SystemRoot的句柄
&Handle,
0x80000000,
&ObjectAttributes,
&IoStatusBlock,
0,
0,
3u,
1u,
1u,
0,
0,
CreateFileTypeNone,
0,
0x100u);
if ( v2 < 0 )
return v2;
result = ObReferenceObjectByHandle(Handle, 1u, 0, 0, &FileObject, 0);// 通过SystemRoot的句柄引用文件对象,获得指向FILE_OBJECT的指针
if ( result >= 0 )
{
RelatedDeviceObject = IoGetRelatedDeviceObject((PFILE_OBJECT)FileObject);// 给定一个文件对象,IoGetRelatedDeviceObject例程返回指向相应设备对象的指针。
// 这里通过FILE_OBJECT,获取指向Device_OBJETC的指针。
v5 = RelatedDeviceObject;
TargetDevice = RelatedDeviceObject;
if ( !RelatedDeviceObject )
return -1073741823;
ObfReferenceObject(RelatedDeviceObject); // ObfReferenceObject例程会增加对给定对象的引用计数。
result = IoCreateDevice(DriverObject, 0xCu, 0, v5->DeviceType, v5->Characteristics, 0, &DeviceObject);// 创建一个新的设备对象
if ( result >= 0 )
{
DeviceObject->Flags &= ~0x80u;
DeviceObject->Flags |= v5->Flags & 0x2014;
*a2 = DeviceObject->DeviceExtension;
memset(*a2, 0, 0xCu);
_CF = __CFADD__(&v12, 12);
_OF = __OFADD__(12, &v12);
_ZF = v13 == 0;
_SF = (int)v13 < 0;
*(_DWORD *)*a2 = DeviceObject;
v10 = *a2;
__asm { pushfw }
v10[2] = 0;
++v10[2];
__asm { popfw }
*((_DWORD *)*a2 + 1) = IoAttachDeviceToDeviceStack(DeviceObject, TargetDevice);// 前面初始化设备扩展,将其附加到获得的设备对象指针。
if ( !*((_DWORD *)*a2 + 1) )
{
*(_DWORD *)*a2 = 0;
IoDeleteDevice(DeviceObject); // 如果连接失败,删除设备对象
*a2 = 0;
return -1073741823;
}
return 0;
}
}
return result;
}
如上述代码,Festi首先通过RtlInitUnicodeString初始化一个UNICODE_STRING结构,指向系统根目录\SystemRoot;用IoCreateFile尝试获取SystemRoot的句柄;然后执行ObReferenceObjectByHandle系统例程,调用SystemRoot的句柄引用文件对象,获得指向FILE_OBJECT的指针。接着执行IoGetRelatedDeviceObject系统例程,该例程引用一个文件对象返回指向相应设备对象的指针,即通过FILE_OBJECT获得指向DEVICE_OBJECT的指针。IoCreatDevice创建一个新的设备对象,对其进行初始化扩展,使用IoAttachDeviceToDeviceStack将其附加到获得的设备对象指针。
FILE_OBJECT是操作系统用来管理设备对象访问的特殊数据结构。它包含有关文件或设备的信息,如文件的位置、当前偏移量、访问模式、缓冲区状态等,其中就包括一个指向相关DEVICE_OBJECT的指针。DEVICE_OBJECT是表示设备驱动程序所管理的设备的结构。它包含指向设备驱动程序调度入口点的指针,这些入口点用于处理系统向设备发送的 I/O请求 。
根据Windows的I/O堆叠驱动程序模型,这种模型允许多个驱动程序层共同处理 I/O 请求。每个驱动程序层称为一个过滤器驱动程序,它们共同堆叠在一起,形成一个I/O请求处理堆栈。当应用程序发起I/O请求时,系统创建一个 IRP 并将其传递给驱动程序堆栈的顶层驱动程序。顶层驱动程序可以处理 IRP,修改其内容,或将其传递给下层驱动程序。

而经过上述的操作,Festi成功被挂载到了文件系统设备栈的顶部,这意味着每一个IRP都要经过Festi恶意设备才能到达底层系统根目录,同样经过底层驱动程序处理的返回数据也要经过Festi恶意设备,所以Festi可以以此拦截和修改发送到文件系统驱动程序的IRP请求和相应的返回数据。
保护注册表项
位于HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services中的注册表项包含Festi的驱动程序类型和文件系统上驱动程序映像的路径,这使得它很容易被安全软件检测,因此Festi必须隐藏密钥。
Festi通过挂载ZwEnumerateKey例程,来修改系统服务描述符表(SSDT)查询指定注册表项的信息并返回它所有的子键。

SSDT 是Windows操作系统内核中的一个重要数据结构,包含了所有系统服务(即系统调用)的地址。每个系统调用都有一个唯一的索引,用户模式下的系统调用通过这个索引来找到对应的内核模式函数。如下:

Festi通过SetHookOnZwEnumerateKey函数将原地址修改为钩子的地址。代码如下:


这里先修改了函数的IRQL权限等级,最后禁用了写保护,这将允许内核代码修改只读页面。接下来分析NewZwEnumerateKey钩子,代码如下:

OldZwEnumerateKey 存放的是原ZwEnumerateKey函数的地址,这里调用原始的ZwEnumerateKey函数并获取结果,检查 KeyInformation 和 KeyInformationClass,根据不同的类进行处理。关键在于FindToString函数,该函数的大概定义是在字符串a1中寻找子字符串a3,并忽略大小写(注册表项不区分大小写)。

根据该函数的返回值,决定是否递归执行ZwEnumerateKey函数。
NewZwEnumerateKey钩子监视发送HKLM\System\CurrentControlSet\Service服务注册表项的请求,该注册表项包含与系统上安装的内核模式驱动程序相关的子键,包括Festi驱动程序。Festi修改钩子中的子键列表以排除与其驱动程序相对应的条目。任何依赖于ZwEnumerateKey获取已安装内核模式驱动程序列表的软件都不会注意到Festi恶意驱动程序。
如果注册表被安全软件发现并删除,Festi还能够替换注册表。

Festi创建一个设备对象,紧接着执行IoRegisterShutdownNotification系统例程,以便在系统关机时接收关闭通知。Festi检查关闭通知处理程序,查看系统中是否存在恶意驱动程序和相应的注册表项,如果不存在(被清除),将其恢复确保它在重启过程中一直存在。
通信保护
安全软件一般通过连接ZwCreateFile拦截所有打开设备的尝试,或者通过附加到\Device\Tcp or \Device\Udp以拦截所有发送到IRP请求来监控设备对象访问。Festi为了和C&C服务器的正常通信,没有使用ZwCreateFile系统服务,而是自定义了一个系统服务,如图所示:

Festi手动创建了一个文件对象来与被打开的设备对象通信,并直接向传输驱动程序发送一个IRP_MJ_CREATE请求。这样,所有连接到\Device\Tcp 或 \Device\Udp的设备都会错过请求,安全设备也不会注意到这一操作,实现代码如下:


如果要直接向\Device\Tcp 或 \Device\Udp发送请求,Festi还需要获取指向相应设备对象的指针,代码如下:

Festi通过执行ObReferenceObjectByName例程来获取一个指向tcpip.sys的指针。之后Festi遍历与驱动程序对象对应的设备对象列表,并将其与\Device\Tcp 或 \Device\Udp进行比较。以此来获得指向相应设备对象的指针。
其它函数
整个DriverEntry大概都分析了一遍,最底下这个函数看看,,


StartRoutine例程点进去

都是一些分配内存池的操作,使用了ExAllocatePoolWithTag例程,进行了一些初始化和传参。推测是用于初始化数据结构。sub_21DE4函数跟进去:

发现一处RtlInitUnicodeString,初始化了一个unicode_string,还有一个反调试,其它都不知道是啥。
反调试技术

如上图代码,上写指令对调试寄存器执行写操作。Festi通过定期将调试寄存器dr0通过dr3调零来抵消系统调试器。这些寄存器用于存储断点的地址,删除硬件断点会妨碍调试过程。
DGA算法
域名生成算法(DGA),当无法访问C&C服务器的配置数据中的域名时,Festi会用DGA生成一个伪随机的域名。如下代码:



该函数将表示日期的结构作为种子,并基于此创建一个 12 个字符的.com域。使用chrfun_X函数中的预定义映射,每个种子值都转换为字符。实现DGA功能使僵尸网络有能力拦截攻击,哪怕主C&C服务器域被禁用,Festi仍然能够通过回滚到DGA上重新获取对僵尸网络的控制。
参考:
《Rootkit和Bootkit 现代恶意软件逆向分析和下一代威胁》
https://web-assets.esetstatic.com/wls/200x/king-of-spam-festi-botnet-analysis.pdf
https://www.eset.com/fileadmin/eset/US/resources/docs/white-papers/white-papers-avar-2012-festi-botnet-analysis-investigation.pdf
https://www.slideshare.net/slideshow/festi-botnet-analysis-and-investigation/15672650#20
https://research.checkpoint.com/2018/return-festi-rootkit/