1.加载一个rootkit
2.与rootkit通信
3.采用合法的网络通信
4.作者写的一个示例rootkit以及其背后的设计选择
5.执行来自内核的命令
6.掩盖你的rootkit的文件系统踪迹的技巧
Rootkit是一种特殊的恶意软件,它的功能是在安装目标上隐藏自身及指定的文件、进程和网络连接等信息,比较多见到的是Rootkit一般都和木马、后门等其他恶意程序结合使用。Rootkit通过加载特殊的驱动,修改系统内核,进而达到隐藏信息的目的。
Loading a Rootkit
1.利用合法的驱动。有很多脆弱的驱动,利用逆向知识,找到一个驱动的0day漏洞是很容易的。并且只需要几句原语即可提升权限,找到一个脆弱的驱动相对来说是简单的,因为兼容性的问题很难被检测。
缺点:跨操作系统版本的兼容性的主要问题取决于所拥有的原语;最有可能的是稳定性问题;最后一件事是你的恶意软件使得受害者蓝屏。
2.买证书。买一个合法的证书就没有稳定性的问题了,但是可能会揭露自己的身份以及会被拉进黑名单。
3.利用已经泄露的证书。这种方式相比购买证书来说更安全,但是在未来可能被检测,并且根据证书发布时间是有操作系统版本限制的。
Communicating with a Rootkit
1.发给C2服务器。防火墙可以阻止或标记对未知/可疑IP地址或端口的传出请求,先进的网络检查可以捕捉到一些试图“融入噪音”的外渗技术。
2.打开一个端口。一些恶意软件通过C2直接连接到受害者主机来控制它。这样设置相对简单。但是可能会被防火墙发现,并且很难融入噪声。
3.应用程序Hooking技术。更高级的恶意软件可能会选择hook一个特定的应用程序的通信作为一个通信通道。这样就很难被检测,特别是如果用的是合法的协议,但是这样不是特别灵活,机器可能没有暴露出该服务。
我们想要的是有限的检测向量,各种环境下的灵活性,假设受害主机会有一些已经揭露出来的服务,入站和出站访问可能被监控。那么第三种方法非常符合我们的需求,除了灵活性。现在要考虑的是如何不依赖一个应用程序。
如果不直接hook一个应用程序,那么我们hook像wireshark这样的工具的网络通信;在恶意数据包中放置一个特殊的指示器(一个”magic”常数);将这些恶意数据包发送到受害者机器上的合法端口;搜索这个常数的数据包,以将数据传递给我们的恶意软件.
Hooking the User-Mode Network Stack
hook Windows Winsock driver
在用户模式下windows上的许多服务都可以被发现,但是如何全面地获取这些流量是一个问题。与winsock相关的网络是由Afd.sys(该驱动负责将PC连接到网络服务,特别是TCP/IP协议)处理的,mswsock.dll用NtDeviceIoControlFiles
实现与Afd.sys驱动的通信。逆向分析在mswsock.dll中的一些函数显示大量的通信是由IOCTLs完成的,如果我们可以拦截到这些请求,我们可以窥探所收到的数据。
- 在拦截之前要知道IRPs是怎么走的,当对一个设备调用文件句柄上的
NtDeviceIoControlFile
函数时,内核如何确定要调用什么函数?
首先通过IoGetRelatedDeviceObject
检索与Afd驱动程序相关联的设备(检索FILE_OBJECT结构中的DeviceObject成员),然后判断驱动是否支持FastIo(在进行基于IRP 为基础的接口调用前, IO MANAGER 会尝试使用FAST IO 接口来加速各种IO 操作),如果支持那么就调用DriverObject->FastIoDispatch
,如果不支持,那么就分配并填写IRP,然后调用IoCallDriver
来发送IRP,对于IoCallDriver
,内核将通过在DRIVER_OBJECT结构的MajorFunction数组中查找IRP内部指定的“Major function Code”来确定调用哪个函数
拦截IRPs的两种常见方法。替换掉驱动对象中你想hook的主函数;直接对调度处理程序执行代码hook。
要选择hooking最好的方法,要考虑的问题(可能有的检测向量有多少种,方法如何可用,检测这种方法昂贵吗)
Hook驱动对象:memory artifacts;对于稳定性,通过用互锁交换替换单个函数,这种方法应该是稳定的。对于兼容性,驱动程序对象有很好的文档记录,并且易于找到;但是检测起很便宜,所有的防病毒软件都需要枚举已加载的驱动程序,并检查主要功能是否在驱动程序的范围内。
Hook驱动程序的调度函数:memory artifacts;除非函数被导出,否则需要自己找到函数,由于补丁保护程序,并不是所有的驱动程序都与此方法兼容,HVCI不兼容的;检测也比较便宜。
Hook文件对象:没有被文档化;稳定;检测成本相对昂贵,反病毒软件必须复制我们的连接过程,并枚举文件对象,以确定设备/驱动程序对象是否被交换。。
Hook文件对象
1.创建我们自己的设备对象和驱动程序对象。
2.修补驱动程序对象的副本。
3.用我们自己的设备替换我们文件对象的DeviceObject指针
首先我们需要获得Afd设备的文件对象,我们可以通过ZwQuerySystemInformation
函数,特别是使用SystemHandleInformation信息类来查询系统上的每个打开句柄。此信息类将为每个句柄返回以下结构:
有了这些信息我们需要判断句柄是否是Afd设备的。首先通过比较ObjectTypeNumber
成员与已知的文件对象类型索引来判断该句柄是否是一个文件对象的,然后我们再将FILE_OBJECT结构的DeviceObject成员与一致的Afd设备比较。
然后我们需要创建我们自己的假的驱动或者设备对象。创建之前先判断是否已经hook过了,Windows内核使用ObCreateObject
函数来创建对象,内核大概是为了允许其他Windows驱动创建他们自己的对象,导出了该函数。
最后我们需要hook我们的假的驱动对象,可以用前面提到过的方法,将DRIVER_OBJECT结构中的MajorFunctions数组部分替换为我们的hook函数,(要注意我们要hook的是我们自己创建的驱动对象,而不是真正的驱动对象)
最后一步就是将FILE_OBJECT的DeviceObject成员替换成我们自己的设备。
现在文件对象被Hook了,对IoGetRelatedDeviceObject
的任何调用都将返回我们的假设备,IoCallDriver
将使用该设备调用我们的修补过的MajorFunctions数组。
剩下的工作:检查我们是否正在hook被调用的MajorFunction,如果是,则调用为该主函数传递原始设备对象和原始调度函数的hook函数;确保当MajorFunction是IRP_MJ_CLEANUP时保存了原始的DeviceObject。
How the Spectre Rootkit Abuses the User-Mode Network Stack
Abusing the Network
有了前一步的hook,现在我们已经可以拦截Afd驱动的IRPs了。并且我们可以拦截所有用户模式的网络流量,通过任何套接字发送和接收我们自己的数据。
Packet Structure
Processing
收到数据包后先对数据包的magic constant部分进行判断。
在发出包之前,我们需要创建一个完整的数据包
Packet Handlers
其他数据包处理程序继承这个基类。在上面的类中需要注意的两个关键问题是,调度器不仅会在处理程序的构造函数中传递指向自身的指针,而且实际的数据包将被传递给数据包处理程序的ProcessPacket函数。
一旦数据包被填充完,process handler会采取以下步骤
通过将一个指针传递给相关的包处理程序,该包处理程序可以递归地处理一个新的包。示例XorPacketHandler:
此XOR_PACKET实际上并不执行恶意操作。相反,它充当了一个封装的数据包。当Xor软件包处理程序收到一个数据包时,它将使用XorKey消除XorContent;递归地将XorContent分派为一个新的数据包。
该模型允许您创建无限的封装层。
Executing the Commands
首先我们需要了解从用户模式上下文执行命令的过程
关于内核模式,让我们从创建获取输出所需的管道开始,下面是创建管道在后台所做的事情。
现在我们有了管道,我们需要创建实际的流程。我们将使用ZwCreateUserProcess
,因为这是kernelbase.dll使用自己来创建进程的方法。(这一部分在后面详细说)
我们需要从从进程的属性列表开始,我们必须设置的最重要的属性是PsAttributeImageName
。这将指定新进程的映像文件名。
接下来我们需要为进程填充RTL_USER_PROCESS_PARAMETERS
结构,主要填充参数有窗口标志和输出到我们的管道的句柄,当前目录、命令行参数、进程映像路径和默认的桌面名称。这些都封装在函数StartProcess
中。
然后我们就可以用ZwCreateUserProcess
开启进程了,一旦进程退出,类似于我们在用户模式下所做的事情,我们可以调用ZwReadFile
来从未命名的管道中读取输出。
Finding Unexported ZwXx Functions Reliably
如 ZwCreateUserProcess
这样的函数,不能被 Windows 内核可靠导出。需要有一种稳定性的方法去寻找到这养的函数。 ZwCreateUserProcess
函数设置的系统调用号与Ntxx
(用户模式下)中的相同,因此可以用后者的系统调用号寻找Zwxx
。几乎所有的NtXx
实现都惊人的一致,在参数传入后,把系统调用号(在内核寻找真正的处理函数使用)保存至eax内,之后判断cpu是否支持快速调用,如果支持使用syscall进入内核,反之使用中断门进入内核,这两种方式除了使用不同的堆栈切换方式和效率外并未有其他本质区别。(还可以看一下这篇动态获取系统调用)
系统调用号在不同的 Windows 版本之间会发生变化; 硬编码系统调用不是一个好的方法。相反,可以选择自己解析 ntll.dll 来提取系统调用号。
1.rootkit先加载ntdll.dll
2.我们通过枚举 ntdll.dll 模块的导出地址表(EAT)来找到 NtCreateUserProcess 函数。
3.因为Ntxx
函数都是按照以mov r10, rcx``mov eax,[system call number]
这样的汇编代码开始的,所以我们很容易提取其系统调用号。
Zwxx
函数采用的是下列的格式:
1 | push rax |
要找到ZwXx
函数,我们需要在ntoskrnl.exe
的可执行节中搜索上述格式
Hiding a Rootkit
MiniFilter
微过滤驱动通过过滤管理驱动进行注册来实现拦截特定文件I/O.
Minifilter可以用于掩盖我们的根工具包在文件系统上的存在。它可以将某一文件的所有文件访问权限定向到另一个文件。我们可以使用此功能将对驱动程序文件的访问重定向到另一个合法的驱动程序。
利用MiniFilter最简单的方法就是自己成为一个MiniFilter,这个方法的检测向量主要是注册表和内存痕迹,不用担心稳定性和实用性,但是检测比较便宜,除了注册表工件之外,注册为MiniFillter的驱动程序也可以通过FltEnumerateFilters
等API轻松枚举。
第二个方法是Hook一个MiniFilter,有三种方式,第一种是代码Hook现有的过滤器的回调;第二种是在受害驱动程序使用FLT_REFISTRATION结构体获得你自己的回调之前重写该结构;第三种是DKOM(直接内核对象操作技术)一个现有的过滤器实例,并用您的替换原始回调。第一种方式虽然简单,但是有很多缺点。对于第三种方式,这是一种半文档化的方法,可以通过FltEnumerateFilters
和FltEnumerateInstances
这两个API枚举过滤器和实例,为某个操作调用的函数在FLT_INSTANCE
结构中的CallBackNodes
数组中指定。这种方式会有内存痕迹,对于稳定性来说,虽然获得一个FLT_INSTANCE结构是文档化的,但是FLT_INSTANCE结构它自己不是文档化的,并且检测并不昂贵,反病毒软件需要偶尔枚举注册的过滤器在CallBackNodes
数组中的钩子及其实例。
在代码中没有找到利用通过微过滤驱动的部分,只看到inf文件中有以下内容,猜测采用的是第一种方法们也就是自己成为一个MiniFilter.