什么是内核级木马? – 作者:丝路安全团队

一、什么是内核级木马

内核级木马一个无进程、无DLL、无启动项的、集多种Rootkit技术特征的独立功能远程控制后门程序。其利用线程注射DLL到系统进程,解除DLL映射并删除自身文件和启动项,关机时恢复。它是内核级的木马程序,主要部分工作在Ring0,因此有很强的隐蔽性和杀伤力。

二、内核级木马产生的意义

目前,传统应用层木马由于当下一些主动防御软件,杀毒的免费使用,各类反病毒工具对其技俩了如指掌,而木马能带来巨大危害的主要原因在其隐蔽性,各类反病毒工具的快速发展就导致木马谋取利益的成本大幅度提高,其带来的利益以及威胁程度呈现逐年下降的趋势。由于传统应用层木马的隐蔽性大幅度下降,黑客为了提升带来的效益以及获得被控主机更加持久的控制权,因而提升木马的隐藏技术就显得更加迫在眉睫。

内核级木马的隐藏主要使用内核Rootkit技术,内核Rootkit程序在使得远程黑客能够更长期的享有目标机器的底层系统控制权的同时在很大程度上不被杀毒软件发现,从而对被控主机造成更加严重的安全威胁,此外内核Rootkit不仅仅存在于Windows系统中,同样存在于LINUX等其他系统中。

而对于信息安全工作者,找出防御内核级木马的最有效途径自然是要深入了解内核木马的工作机制,内核Rootkit攻击技术,内核Rootkit隐藏技术,掌握其规律和特性,才能更好的找到应对此类木马的措施,以及对未来可能出现的安全隐患做好一些提前的防备。

三、内核木马于传统应用层木马隐藏技术对比

内核级木马隐藏主要有进程隐藏,文件隐藏,自启动隐藏,通信隐藏等,主要于木马的架构有关,那么对于一些主从型内核级木马,木马涉及到的功能模块越多自然对应涉及的隐藏项也就越多,甚至有时候还要做注册表隐藏,服务隐藏等。

3.1.进程隐藏

进程隐藏最初技术体系为最简单的混淆字符隐藏,如系统进程名为svchost.exe,木马进程名改为svch0st.exe隐藏,紧接着到注册服务隐藏,dll注入隐藏。现在内核级木马大部分通过进程控制块中的活动进程链表(ActiveProcessLinks)中摘除自身来达到隐藏,或通过从PspCidTable表中摘除自身等方法来达到隐藏目的。

3.2.文件隐藏

文件隐藏最初是通过存放于敏感目录(系统目录)并混淆文件名来实现,后来有些人通过挂钩应用层上的FindFirstFile,FindNextFile等API来实现该目的,现在在内核层隐藏文件方法一般试用FSDHook或FSD Inline Hook来实现。

3.3.自启动隐藏

自启动隐藏先后经历了添加注册表Run值,修改系统启动文件,注册为服务,修改定时程序,感染系统文件技术来实现,现在黑客开始关注于在硬盘固件,bios等地方做手脚来实现自启动隐藏。

3.4.通讯隐藏

对于通信隐藏来说,现在有些研究者已经实现了NDIS小端口驱动层的隐藏,不过主流的木马仍然在TDI层面上通信或者在NDIS中间层上通信。

四、内核级隐藏技术分析

应用层级隐藏技术由于处在ring3层,一般难以抵抗通过从内核层获取相应的信息的一些软件的检测,而在内核级隐藏技术中,由于各种方案有着不同的特点、所要达到的目的不同因而这些方案所使用的隐藏技术也各有差异,这一节中,将重点分析内核级木马可能用到的一些隐藏技术。

4.1自启动隐藏

对于应用层级的木马,自启动的方法可谓是五花八门,有捆绑文件、修改注册表项、加自启动快捷方式等,但是安全软件对应用层的检查比较严格,虽说对于内核层的自启动方法也有检查,不过相对来说,内核级的自启动一般要更有效一些。常见的内核级木马自启动方法主要有以下几点:

(1)注册为后台服务

跟应层级木马自启动中的注册为服务的原理一样,只不过前者是将应用程序注册为系统服务,这儿是把写好的驱动程序注册为系统服务,进而实现自启动。这种方法主要是把驱动程序注册为系统的一个服务(一般使用SCM ( Services Control Manager)接口来实现服务的安装、启动、停止等,使用这种方式加载的驱动程序是不可分页的,不需要考虑分页的问题,另外,一般也不会由于驱动的加载导致BSOD),在注册表中生成[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\“ServiceName”]子键,再在其中设置好自己的一些启动参数,如ImagePath等。普通的驱动程序一般就是采用这种方法加载的,内核级木马的驱动模块当然也可以使用这种方法,但由于涉及到在注册表中插入键值,首先,在加载的时候可能被拦截,另外,若是加载成功了,也需要在后续的代码中对该注册表项和服务项做隐藏。

(2)使用Native API ZwSetSystemInformation()

该方法使用了未文档化的API函数ZwSetSystemInformation(),可以在不创建注册表键的情况下实现驱动的加载,但是由于驱动程序的分页性,若在驱动运行过程中,通过该种方法加载的内核木马驱动被页换出,则对其的访问就会导致BSOD[11]。因而,若要使用该种方案,还需针对分页问题进行一定的设计,如migbot是通过将全部的操作代码复制到不分页的内存池来实现这个目的。

4.2文件隐藏

一般在应用层级实现的文件隐藏都是基于伪装,而并不是真正意义上的隐藏,也可以说只是在视觉上欺骗了用户,如将文件名修改为酷似系统文件的名字(通过使用一些易混淆的字符来实现)等。在内核中,可以通过修改文件操作时的内核处理流程来实现对文件的隐藏,进而使用户无法在系统中通过工具(使用同种或更上层方法来枚举文件的工具)来查看被隐藏的文件。常见的内核层的文件隐藏技术一般有一下几种:

(1)Hook ZwQueryDirectoryFile()函数

在Windows中,实现文件的枚举操作,最后都是调用Native API(in ntdll.dll)函数NtQueryDirectoryFile()来实现,接着通过中断进入内核,进而通过查询SSDT(System ServicesDescriptor Table,系统服务描述符表)表获得相应的函数ZwQueryDirectoryFile()的地址,进而调用真正的函数执行体来完成对文件的枚举。Hook SSDT表隐藏文件主要是篡改函数在SSDT表中的真实地址,让程序在枚举文件信息时首先进入修改后的地址对应的函数中执行,接着对待隐藏文件的信息进行过滤,让应用层以及通过该种机制获取文件信息的工具法枚举到被隐藏的文件。

(2)文件过滤驱

1) 过滤驱动程序:

对于文件过滤驱动来说,主要是截获上层文件操作请求,根据需要的功能做相应的处理。

2) 文件系统驱动程序(FSD,File System Driver):

它是Windows下标准的核心层驱动程序,主要负责处理用户与磁盘等存储介质之间的存储信息以及维护各种文件系统(如FAT32、NTFS等)的磁盘结构。

3) 设备驱动程序:

主要是跟相应的硬件打交道,直接依附于相应的硬件设备。磁盘及其他的存储驱动程序主要是实现存储设备的管理、存储等。通过文件过滤驱动实现文件的隐藏,主要是通过设置IRP_MJ_DIRECTORY_ CONTROL分发函数,并在其中设置回调函数,这样发送给文件系统中卷设备的IRP,在从文件系统驱动返回时,将会触发回调函数的执行,在回调函数中将需要过滤的文件从返回的FILE_BOTH_DIR_INFORMATION结构链表中删除,这样就可以实现对指定文件隐藏的功能。

4.3进程隐藏

内核级木马在进程隐藏上,不断发展了好多方法,不过从根本上来说,主要是通过钩挂内核函数、修改内核返回信息(文件链表或句柄表)来实现进程隐藏的。主要的有内核挂钩、DKOM技术、抹掉进程句柄等。

(1)内核挂钩

SSDT实现了从用户态函数到内核态函数的引导。当挂钩 SSDT之后,进程枚举、查询类函数(一般都是挂钩ZwQuerySystemInformation()函数)对应的内核服务函数在SSDT表中的地址被修改。这样,当产生进程枚举的调用时,就会运行修改后的函数,接着在修改后的函数中

首先获取完整的进程信息,然后查找需要隐藏的进程信息,对其实行过滤,再把处理后的结果返回给上层,这样就可以使最终的用户层的用户看不到隐藏后的进程。另外的一种内核钩实现进程隐藏的方法是挂钩IDT ( Interrupt Dispatch Table,中断调度表)。在Windows系统中,用户层的API函数都是通过软中断进入内核的。当软中断出现时,系统就会从IDT表中调用相应的中断服务处理例程,对应INT 2E中断的服务处理程序为KiSystemService()。该程序又会调用SSDT表中的函数实现最终的调用。因而,通过挂钩IDT,修改中断响应过程,就可以实现对进程信息的监视和修改,达到隐藏的目的。

(2)DKOM技术

DKOM(Direct Kernel ObjectManipulation,直接内核对象操作)实现进程隐藏使用的是另外一种机制。跟上面提到的内核钩子相比,隐藏性更好,相比来说,一般也不容易被检测到。说到使用该机制隐藏进程,不得不提到Windows下跟进程相关的一个数据结构EPROCESS。

在Windows中,使用双向链表的典型示例就是进程和线程的数据结构。对于进程来说,每个进程都拥有自己的EPROCESS数据结构,用来描述进程的地址空间、进程名、线程等信息。该结构中的ActiveProcessLinks字段是一个LIST_ENTRY结构,所有进程的EPROCESS结构被LIST_ENTRY链接成一个双向链表。这样,使用该结构,我们可以很容易的遍历该链表,找到需要的LIST_ENTRY结构。

不过,EPROCESS结构在内存中的位置几乎在OS的每个发布版本中都会发生改变,这就给我们在内存找到进程的EPROCESS带来了一些麻烦。好在通过PsGetCurrentProcess()函数始终能找到指向当前运行的进程的指针,进而找到当前进程的EPROCESS结构。然后以当前进程指针为终止判断条件,遍历EPROCESS双向链表,就一定能定位到需要隐藏的进程。

当找到需要隐藏的进程的结构后,通过修改其LIST_ENTRY中的Flink和Blink的值,将待隐藏的进程的前一个LIST_ENTRY结构(Previous Entry)的Flink指针指向下一个LIST_ENTRY结构(Next Entry),而下一个结构的Blink指向前一个结构。另外,为了防止因指针指向无效内存区域导致系统崩溃,将待摘除的进程的LIST_ENTRY结构中的Flink和Blink指向自身。这样就完成了脱链,系统就无法通过这种方法列举被隐藏进程的信息其中实线表示的是实际的进程链表的情形,而虚线表示的是删除待隐藏的进程结构后的进程链表的情形),从而实现进程的隐藏。需要指出的是,摘除进程链表中的被隐藏进程结构后,并不会影响进程的调度执行,这是由于Windows是基于线程来实行任务调度的。

(3)抹掉进程句柄

两种常用的抹掉进程句柄的做法分别是抹掉CSRSS句柄表中的待隐藏进程句柄和抹掉PspCidTable中的待隐藏进程句柄。

(1)抹掉CSRSS

句柄表中的进程信息一个进程在创建的时候,系统会为其分配一个句柄表。该句柄表主要是供内核对象调用。实际上,其是一个结构体数组,数组中的每个元素包含了以下三项:指向一个内核对象的指针、访问掩码、与对象句柄的继承相关的一些标志等。对于进程对象来说,其对象指针也放在句柄表中,通过遍历CSRSS句柄表,可以根据需要过滤的特定进程的信息,实现进程的隐藏。

(2) 抹掉PspCidTable中的进程信息

PspCidTable是一个特殊的句柄表,跟普通的句柄表相比,主要区别在于该句柄表中存放的是系统中的进程对象体EPROCESS、线程对象体ETHREAD等。因而通过该句柄表可以遍历系统中的所有进程(IceSword工具就是使用这种方法来枚举进程的)。不过该句柄表并未导出,因而使用该方法隐藏进程需要自己定位PspCidTable的地址。一般是通过PsLookupProcessByProcessId()函数来搜索定位的。定位好后,就可以从中过滤掉待隐藏进程的句柄,不过使用该方法时,有一点需要注意的是若被隐藏的进程退出时,需要重新恢复原先的进程对象(恢复原先的表的结构),否则,当调用ExDestroyHandle()销毁句柄,若遇到不存在的句柄(内核中访问未知的内存地址),就会导致BSOD。

4.4通讯隐藏

同内核模式的网络编程接口相比,用户模式的网络编程虽说在隐蔽性上稍微差一些,不过其实现起来简单、可以使用大量的内置函数。但是,为了追求更好的隐蔽性,内核级的隐藏技术显得尤为重要,毕竟一个木马不可能没有通信模块,否则其功能将无法实现。在Windows中,OSI的网络模型与Windows系统结构的概略映射。

其中网络应用程序即用户编写的基于Winsock API的通讯代码;网络API DLL则给上层的应用程序提供网络的接口,以便通过进一步调用下层的协议驱动等完成最后的通信;TDI(Transport DataInterface,传输数据接口)客户是网络API的核心态的驱动程序,它可根据相应的网络标准格式化相应的数据包,传送给协议驱动;TDI传送器即NDIS(Network Driver Interface Specification,网络驱动接口规范)协议驱动程序,它接受上层传送过来的数据包,接着根据不同的需求,按照不同的协议(TCP、IPX等)给数据包添加相应的协议头,以便提供给下层做进一步的处理;NDIS库则在为其上下层提供接口,同时也隐藏了设备的细节,使协议驱动程序跟网卡驱动程序通信时不用考虑设备相关性,直接通过这些接口透明地进行通信。

通过以上的分析,一般在内核层,要实现通信隐藏,要么是Hook网络API共同调用的函数,要么就得深入更内核,在TDI、NDIS协议驱动或NDIS中间层驱动程序等上做过滤操作。常见的通信隐藏方法有以下几种:

(1)Hook ZwDeviceIoControlFile()

Netstat、Fport等获取端口列表的工具,一般是通过iphlpapi.dll中提供的函数AllocateAndGetTcpTableFromStack()等来获取端口相关的信息的。而这些函数最后又都是通过调用NtDeviceIoControlFile()这个Native API 来进入内核的。因而在SSDT中通过Hook该函数对应的函数ZwDeviceIoControlFile(),对返回的端口信息进行过滤,就可以将特定的端口隐藏掉。让用户无法通过一些常用的工具枚举到木马开启的通信端口,进而实现端口隐藏来进行隐蔽通信。不过对于像卡巴斯基防火墙等通过TDI过滤驱动来获取网络连接、保证通信安全的软件,该方法一般难以逃避检测。

(2)TDI过滤驱动

应用层的网络应用程序要发送或接收数据,都得通过下层的驱动程序来格式化这些数据。那么只要在上层的IRP到达之后,根据自己的需求设定过滤规则,对特定的信息进行过滤,会使用户层的一些检测工具或使用同种机制实施过滤的防火墙无法查看到自己已经处理过的通信信息。不过该过滤驱动位于TcpIp.sys之上,属于UpperDriver,若一些检测工具或防火墙是通过TcpIp.sys来获取通信的信息的,那么该种方法就无法对特定的数据包实施过滤,也就难以逃避检测。

(3)NDIS过滤驱动

NDIS是Microsoft 和3COM公司联合制定的网络驱动接口规范,其包括NDISProtocol Driver(NDIS协议驱动)、NDIS Intermediate Driver(NDIS中间层驱动)、NDIS MiniPortDriver(NDIS微端口驱动,有的文献中也称之为NDIS小端口驱动)。在这三个层次的驱动中,在后两个上可以获取系统的网络通信的相关信息,如源地址、目的地址等,这样就可以在这两个层次较为方便的获得关于通信隐藏的依据,进而将这些信息直接发往自己的木马功能模块或者调用原先的流程继续传递。直接发往木马模块的通信信息由于没有通过系统的常规流程,因而,基于这些流程中间一些层次上的检测工具或防火墙将无法截获到这些信息。

4.5驱动模块隐藏

驱动模块隐藏主要实现对内核通信模块、木马功能模块等驱动程序的模块的隐藏,防止用户通过一些软件查看到系统中已加载的驱动模块,进而发现非法的木马驱动模块。对于驱动模块的隐藏,使用的是DKOM技术,其跟前面分析的用于进程隐藏的DKOM技术原理一样,只不过实现进程隐藏操作的是EPROCESS结构,而要实现驱动模块的隐藏操作的却是MODULE_ENTRY结构链表。

在MODULE_ENTRY结构(以下简称为“结构1”)中,记录了内核中驱动的一些信息,如驱动路径、驱动名等,该结构实际上是_LDR_DATA_TABLE_ENTRY(以下简称为“结构2”)的一个简化结构,只是选取了跟模块相关的一些字段来组成结构体MODULE_ENTRY的,其中变量1对应于结构2中的InInitializationOrderLinks;变量2是为了保证跟_LDR_DATA_TABLE_ENTRY中字段的偏移一致而添加的一个变量;变量3、4、5、6、7对应分别是结构2中的DllBase、EntryPoint、SizeOfImage、FullDllName、BaseDllName。从MODULE_ENTRY的结构可以看出其第一个参数是LIST_ENTRY结构。对应的_LIST_ENTRY的结构如下:

typedef struct _LIST_ENTRY{

struct _LIST_ENTRY*Flink;

struct _LIST_ENTRY*Blink;

} LIST_ENTRY, *PLIST_ENTRY;

其中Flink参数实现指向下一个链表结构,若无后续节点,则将其指向链表头,若当前的节点是链表头结点,则Flink指向链表的第一个节点,若链表为空,则Flink指向表头本身;Blink

参数实现指向前一个链表结构,若无前驱节点,则其指向链表的头节点,若当前的节点是链表头,则Blink指向链表的最后一个节点,若链表为空,则Blink指向链表头本身。

这样,只要顺着MODULE_ENTRY双向链表,就可以找到准备隐藏的驱动模块,只要从中删除该节点,使用这种机制获取驱动模块列表的工具将无法找到被隐藏的驱动模块。删除前后的MODULE_ENTRY,实线是未修改的链表结构,虚线是修改后的链表结构。同进程删除链表的处理类似,为了防止因指针指向无效内存区域导致系统崩溃,删除操作后,将待摘除的模块的LIST_ENTRY结构中的Flink和Blink指向自身。

4.6注册表隐藏

若要实现对注册表中一些键值的隐藏,只需打破这种正常的调用机制,在其中某个易于操作的环节将键值过滤掉即可。对于内核层来说,通过挂钩或修改SSDT表、ntoskrnl.exe等中的相关函数等,就可以完成该功能。

*本文作者:丝路安全团队,转载请注明来自FreeBuf.COM

来源:freebuf.com 2019-08-09 09:00:02 by: 丝路安全团队

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论