危险漫步博客
新鲜的“黑客思维”就是从全新的角度看待黑客技术,从更高的层面去思考;专注于黑客精神及技术交流分享的独立博客。
文章2306 浏览20583349

DKOM技术初探

本文是讨论比挂钩SSDT更强的通过DKOM技术隐藏进程的方法。危险漫步在写文章的时候,假设读者已经对Windows系统的驱动程序编写有基本的了解。所谓DKOM,即Direct Kernel Obj ect Manipulation,可译为直接内核对象操作,是Rootkit技术的一个方面:首先有必要对“内核对象”这个词做一点粗略的解释。简而言之,这里的“对象”二字基本等价于“结构”。内核对象,是微软公司在描述Windows内核相关的数据结构时采用的词(即英语Object),特别要注意的一点是,这和“类的实例”等面向对象编程概念没有什么关系。

在明晰了内核对象的含义之后,接着描述一下DKOM的概念。什么是DKOM呢?具体来说

DKOM就是通过定位并直接修改内核中用于存储信息的内核对象以达到Rootkit-般目的的技术。操作系统将许多信息以特定的结构保存在内核空间内存中。举个例子,Windows系统在内核中使用特定的内核对象表示活动进程,通过DKOM技术,我们可以修改这些内核对象,进而达到隐藏进程的目的。本文讨论的便是这个过程的细节。

DKOM是很强大的技术,通过它可以对系统做出广泛的修改,而且这些修改往往都是极难检测的。之前讨论过的挂钩SSDT的方法,因为直接修改了系统表,所以某些反Rootkit程序可以发现系统表被修改了,进而检测出Rootkit的存在,因为任何合法的程序都不会更改SSDT,可通过DKOM技术,我们将直接修改内存中记账的内核对象,反Rootkit多对此无能为力。但是,DKOM也不是万能的。关于这点我们以后再谈。此外要了解的一点是,发掘DKOM新技术会遇到很大的阻力——在修改内核之前我们首先必须知道内核对象都有什么,他们的结构定义是什么,他们是如何被系统操作的等一系列细节。可惜,上述大多数必要信息都是无文档可查的,只能通过逆向等方法去发掘,这是高手的工作。虽说如此,使用已知的DKOM技术却是很简单的。如果了解挂钩SSDT技术是读者对脚本小子身份的一次尝试性地突破,那么在了解可DKOM后,就算基本脱离了脚本小子的行列了。

以上就是对DKOM技术的一个简单概括,下面我们将首先作一番纯技术性的说明,之后,通过我为本文编写的一个较为实用的Rootkit范例来进一步讨论解决编程中面临的种种问题。要强调的一点是,以下所进行的讨论都是基于Windows XP SP3系统的,要知道,Rootkit技术同操作系统的版本有着密切的联系。

Windows在内核中使用一个EPROCESS结构表示一个进程内核对象。EPROCESS结构很长,而且没有文档说明。我的得到的一份EPROCESS结构的定义是从张银奎所著的《软件调试》-书中提取的,书作者提及使用WinDbg可以得到这个结构定义。危险漫步研究了一番,无奈没有折腾出来。详细的结构定义读者可以参考《软件调试》一书第169页,或者从在网络上搜索一番。在下面的叙述中,我们只用到其中的两个字段:一是位于偏移+Ox088的ActiveProcessLinks字段,一是位于偏移+Ox174的ImageFileName字段。前者是一个LIST ENTRY结构,后者是一个长度为16字节的缓冲区。LISTENTRY结构定义如下:

系统正常运行时同时会有许多活动进程,每一个进程都对应着一个EPROCESS结构。这些结构并不是孤立存在的,他们在内存中被组织为一个环状的双向链表。如图1所示。系统本身也是通过遍历这个链表而得到相关进程的信息的。组成链表的关键点就是其中包含了一个LIST ENTRY结构。这个结构的两个成员分别指向前一个与后一个EPROCESS结构的LIST_ ENTRY字段。这就是位于偏移+Ox088处地ActiveProcessLinks字段的作用。

我们也知道,每个进程都有一个名字。这个名字通常是进程的位于文件系统中映像文件的名字。这个名字的一部分或者全部被存储在位于偏移+Ox174处地ImageFileName字段。ImageFileName字段本身是一个长度为16字节的缓冲区,而文件系统中映像文件的文件名可以很长,远远不是16字节的缓冲区可以完整表示的,所以我说名字的一部分或者全部被保存在这里。具体的规则却是隐晦的。

再次强调,以上所谈及的两个字段的偏移是与操作系统相关的,不同的操作系统,甚至应用不同Service Pack都有可能改变偏移量,上面的数据是特定于Windows XP SP3的,这也说明了在写Rootkit时判定目标操作系统的重要性。在内核态中运行的程序可以调用PsGetVersion()函数获取操作系统的版本。

在内核态下,程序总是可以调用PsGetCurrentProcess()函数获得代表当前进程的EPROCESS结构的地址。通过这个地址,基于上面对LIST ENTRY结构,我们可以遍历内核中环形的EPROCESS链表。通过比较ImageFileName字段判断结构所代表的进程,并对此链表做“适当”的修改,使代表特定进程的EPROCESS结构从链表中脱离,从而达到隐藏进程的目的。

在之前的文章中我已经谈过如何将驱动程序加载到内核中并执行的方法,在此不再赘述了。要补充的一点是,但凡是通过SCM安装并执行的驱动程序调用PsGetCurrentProcess()获得的EPROCESS结构地址总是指向程“System”的,也就是系统进程。基于正常加载的驱动程序具有的这个特性,可以定义一个在不同操作系统通用的发现ImageFileName宇段偏移的函数,即下面的GetOffsetImageFileName()函数。

代码是很简单的,就是通过单纯的内存比较确定偏移地址,读者都是聪明人,所以我不多讲了。

如何从自身所在进程的EPROCESS结构遍历到目标EPROCESS结构呢?为此,我编写了FindProcessEPROCO函数。

这个函数基于给定的名字,通过比较ImageFileName字段发现目标进程,正如我们之前提到的那样。程序首先通过PsGetCurrentProcess0函数得到当前的EPROCESS结构地址,然后遍历链表,寻找其中ImageFileName字段为name的EPROCESS地址,找到则返回,找不到返回O值。通过LISTENTRY的Flink字段跳到下一个EPROCESS结构中去。其中的宏OFFSET-- LIST_ ENTRY等于Ox88。值得一提的一点,EI-ROCESS结构构成的链表是首尾相连的,所以在nCount大于O并且且当前EPROCESS和最初在开始时候得到的EPROCESS相等的时候退出循环。得到目标EPROCESS结构的地址是最最关键的,以上函数完成了几乎所有的工作,理解这个函数对于进一步理解下面的内容至关重要。

说一点题外话,很多初涉驱动编程的人都会对内核导出函数的命名方式表示惊奇,他们都有一个类似于“Ps”、“Io”、“Rtl”等简短前缀,这个前缀使函数便于归类。比如以“Ps”开头的函数必然与进程(Process)有关,以“Rtl”开头的函数必然是“Runtime”系列实用函数等等。更多此类信息可以从WDK文档中获得。

实际的代码大致是怎样写的呢?首先,指定参数调用FindProcessEPROC()函数,设置要隐藏的进程名,名字的长度,以及名字在EPROCESS中的偏移量。当此函数返回后,必不可少的一步是判断其返回值是否是非零的,否则给出一条提示并终止。接下来,通过已知的EPROCESS地址和链表结构的偏移量修改链表。使前一个链表的后继成为后一个链表,后一个链表的前继成为前一个链表。重要的是,把自身的前继后继指针都修改指向本身,关于这点,稍后再细谈。这样一番修改后,目标EPROCESS就从链表中脱离了。此后系统在因为需要查询信息从而遍历链表的时候就不会再遍历到目标进程,于是进程的存在被隐藏了。

以上的说明还是较为笼统而抽象的,接下来我通过分析我为本文准备的一个较为实用的隐藏进程的Rootkit示例来进一步的演示DKOM技术的典型应用。在示例中,我们在驱动程序初始化过程中创建了一个新的系统线程,其线程函数每相同时间间隔后遍历EPROCESS链表一次,从中查找进程名以“TTT”开头的进程,并将其隐藏。代码中涉及的几个新的内核导出函数将在其出现的时候对其进行详细的说明。因篇幅所限,文章这里就不粘贴完整的代码了,但仍然建议读者一边对照本文附带的完整代码一边阅读此分析。

首先看下驱动程序的初始化代码。

其中g_nBingo与g-WaitEvent是全局变量。前者用于控制将要创建的系统线程的终止过程,后者是一个“事件对象”,与前者配合使用。

在初始化过程中,我们设置了一个OnUnload例程,然后初始化了一个事件对象,并创建了一个系统线程。然后关闭了系统线程的句柄。一直讲到“系统线程”这个词,它的含义就是属于进程“System”的进程。这与用户模式编程中创建一个线程没有什么形式上的区别,无非是这样的线程是内核态的线程,权限大些罢了。创建线程的时候指定了线程函数DelayCheck()。线程函数完成隐藏进程的全部功能。“事件对象”又是什么呢?简单的说,就是一种用于线程同步的机制。从纯粹的编程视角来看,就是首先调用Kelnitial/zeEvent0内核函数初始化一个KEVENT类型的变量,这个变量所代表的事件对象是全局的。它有两种状态,即TRUE和FALSE。使用KeSetEvent()函数可以动态的更改事件的状态。如何使用这个所谓的事件对象呢?调用一个函数KeWaitForSingleObject(),这个函数的参数中包含了一个事件对象。这个函数的行为诡异,程序执行到这个函数的时候,它在内部检测事件的状态,如果事件状态为FALSE,它就内陷在函数中不返回,直到事件状态变为TRUE时才返回。

系统线程的退出方式很独特,要么等待操作系统将要关闭,System进程将要退出的时候同其所属进程一起退出,要么在线程函数内调用PsTerminateSystemThread0自己退出。此外,驱动程序在卸载的时候必须要关闭自己创建的系统线程,否则给你个大蓝脸,提示你驱动卸载却没有回收资源之类的信息。我设计这个示例的时候考虑了驱动应该能被卸载的问题,可是在OnUnload例程中怎么退出系统线程呢?基于上面的叙述,读者可以发现似乎系统并没有提供一种在系统线程外终止系统线程的方法,所以我设计在OnUnload中设置一个全局变量,即g_nBingo变量。这个变量在初始化的时候被赋值为O,线程函数在每轮循环结束的时候检测g_nBingo变量的值是否被重设为1,如果是的话就自我终极。我在OnUnload例程中将其设置为1,向线程函数通知驱动即将被卸载了。然后,例程陷入KeWaitForSingleObject0函数中等待线程函数发给其准备退出的信息。线程函数在退出之前设置了事件对象,接着,OnUnload例程的阻塞状态消失,驱动就被安全的卸载了。

有的读者可能考虑到应该在OnUnload例程阻塞状态消失后应该再等待一小段事件,以便保证线程函数确实在驱动卸载之前结束掉了。但是在我历次试验的过程中并没有出现什么异常情况,本着简约主张,我在那里没有添加延时代码,觉得有必要的读者完全可以自己动手在那里添加一段空循环以达到适当延时的目的。

通过上面的叙述,想必读者对OnUnload()已经很明白了。下面重点放在DelayCheck()上。函数的主题代码是一个无限循环。在循环中,’首先使用FindProcessEPROC()尝试获得一个名称以“TTT”前缀开头的进程的EPROCESS结构地址,检查获得IYJeproc值是否有效,有效的话,做出一番修改,使得此结构脱离链表,否则什么也不做。然后呢,函数检查了变量g_nBingo是否被设置,设置的话就退出。这套简单的机制在上文中描述的很详尽了。

最后,通过调用KeDelayExecutionThread()函数,在开始下一轮循环之前,稍微“休息”一会儿。最后的延时是很重要的,通过比较无延时和有延时时的不同表现就可以知道,无延时情况下,只要此驱动在运行,CPU使用率就必定是100%,全部被“System”进程占有了。这样的话,系统的性能被严重影响了。有延时时,表现良好,一切正常如从前,只是任务管理器中少显示了几个进程信息,呵呵。修改链表的代码貌似很复杂,实际上很简单,无非是把当前的节点从链表中删掉,并将当前节点的前继后继都指向本身。基于之前给出的对此段代码功能的表述,聪明的读者应该很容易理解他们。然后,给出陌生函数的原型。

再允许我说点题外话,我向来不喜在文章中不描述某个函数的用法,因为这是一篇技术文章,不是函数用法参考文档,况且篇幅有限。所以我更多的把讲述的重心放在描述代码段的功能上,而不是放在如何调用某某函数上。具体的函数调用方法完全可以通过函数参考文档获得完整的了解。可这种讲述方式可能使一部分读者感到困惑。若读者不习惯我的风格,还望见谅。

至此,读者应该基本理解了上述采用DKOM技术隐藏进程的方法。读者可以将上述示例程序自由的使用,更改代码逻辑以增加更强大的功能,秉承传统黑客精神,与大家分享自,己的代码。在文末再介绍一个使用工具吧,通过它可以很方便的将驱动从加载并运行。工具名字叫做InstDvr,运行后显示一个小小的对话框,要求手工输入驱动的绝对地址。建议读者将驱动访问本地磁盘的根目录下,这样省事些。四个按钮的功能很简单,分别是安装,启动,终止,删除。基本的用法是,首先安装,安装时工具根据驱动程序名生成一个服务名,安装成功后就可以单击启动按钮。启动的过程就是执行DriverEntry()的过程。对于启动的驱动,可以随时终止,当然前提是驱动必须注册了OnUnload例程。删除按钮就是将驱动及其注册的服务从系统中删除。有了这样一个小工具后,在试运行自制的Rootkit时就很方便了。当然,这个工具不是为危险漫步的作品。其实自己动手做一个也很简单,有兴趣的读者可以试着做做。

相关推荐