摘要:本文描述了一种检测内核与用户级rootkit的新技术。此技术利用处理器的单步执行模式,来测定系统内核与DLL中执行指令的数量,从而达到检测rootkit和后门的目的。同时还讨论了其在Win2k下的代码实现。
背景知识
一个在计算机安全领域中重要的问题是,如何判断给定的主机是否已被入侵。由于以下两点这项工作变的非常困难:
1、攻击者可以利用未知漏洞进入系统。
2、当进入系统后,入侵者可通过安装rootkit和后门来隐藏自身(例如:隐藏进程,通讯渠道,文件等)。本文将集中讨论在Win2K系统下rootkit的检测问题。
传统rootkit检测技术中的一些问题
传统的rootkit检测程序(那些我们经常在UNIX系统中见到的)只能检测一些已知的rootkit(这点使它变的像反病毒程序)或进行一些内核存储的扫描。例如Linux中就有一些工具扫描内核中的syscall table。这显然不够好,因为已经有了很多并不更改syscall table的rootkit,而Win2k下也可开发出类似的rootkit。
那检测程序是不是还应该扫描内核代码空间?这样我们就有了一个运行在内核模式中的Tripwire。但这还不够好,因为在大多数的操作系统中,我们可以写出即不更改SST(syscall table)也不更改代码的内核级rootkit。在系统中有很多可以勾连的函数指针(例见[2])。
以我看,存储扫描技术决不会成为rootkit检测的终结。这主要是因为我们不能确定具体的监测存储区域。
那到底怎样检测出我们系统中的入侵者呢?
首先我们以rootkit中所使用的技术,将其分为两类:
*通过更改系统结构来隐藏某对象的(如运行的进程)和
*更改内核执行路径(例:勾连那些负责枚举活动进程的内核函数)来达到同样目的的。
更改系统数据结构的rootkit
这类的rootkit不太多。有意思的例子包括fu rootkit(见[3]),通过删除内核中PsActiveProcessList链上的进程对象来隐藏进程。ZwQuerySystemInformation等函数是不能发现这些隐藏进程的。但同时,因为Windows的线程分派器(dispatcher, scheduler)使用另外的数据结构,这些"隐藏"进程仍可运行(被分配到CPU使用时间)。Windows的线程分派器使用以下三个(注1)数据结构:
*pKiDispatcherReadyListHead
*pKiWaitInListHead
*pKiWaitOutListHead
后两个是处于"等待"状态的线程链头。他们之间稍有不同,但对我们来说并不重要。
从上面的信息我们可以找到一种检测隐藏进程的方法。及读取线程分派器使用的数据结构,而不是PsActiveProcessList。
当检测rootkit时我们应该尽可能的触及更底层的内核数据结构。
有一点应该注意,直接从线程分派器使用的链中删除要隐藏的进程是不可能的,因为隐藏的进程将分配不到CPU使用时间。
更改执行路径的rootkit
这类的rootkit较为普及。他们通过修改或增加内核或系统DLL中的指令来达到目的。检测这类rootkit的问题是,我们不知道rootkit在什么地方做了那些修改。它可以勾连DLL中的函数,系统服务列表(System Service Table),改变内核函数的内容或修改内核中一些奇怪的函数指针。
执行路径分析(Execution Path Analysis)
EPA关注这样一个事实:如果入侵者通过修改执行路径隐藏了一些对象,那么当调用一些典型的系统和库的函数时,系统将运行一些多余的指令。
举个例子,如果入侵者为了隐藏文件和进程而修改了ZwQueryDirectoryFile()和ZwQuerySysteminformation(),那样和干净的系统相比,这些系统函数就会执行更多的指令。不管入侵者采用勾连或在代码中加入jmp指令,或其他任何方法。指令增加的原因是rootkit必须进行它的任务(在这个例子中是隐藏文件和进程)。
但是Windows 2000的内核是一个非常复杂的程序,即使在干净的系统中,某些系统函数每次运行的指令个数也是不同的。然而我们可以用统计学来解决这个问题。但首先,我们需要一种测定指令个数的方法......
指令计数器的实现
单步执行模式是Intel处理器的一个好的特性,我们可以用它来实现指令计数。当处理器处在这个模式时,每执行完一条指令,系统将产生一个除错异常(debug exception, #DB)。要进入这个模式,需要设置EFLAGS寄存器中的TF位(详见[6])。
当执行int指令时,处理器会自动清TF位并进行权限切换。这意味着,如果要进行内核模式下的指令计数,则必须在中断处理程序开头设置TF位。因为我们将要测定一些系统服务中的指令个数,勾连中断向量0x2e,也就是Windows 2000下的系统服务调用门会变得很有效。
但是,因为存在用户模式的rootkit,也应该测定在ring3级运行的指令个数。只要在用户模式下设置TF位一次就可以了,从内核返回时,处理器会自动恢复这一位。
以上的计数方法通过内核驱动器实现。如图2所示,驱动加载后勾连IDT 0x1和0x2e。为和用户级程序交互。驱动勾连一个系统调用,用户级程序通过这个特定的系统调用来开关计数器。
一些测试
我们可以使用上一段中描述的方法来测定任意系统服务中执行的指令个数。
例如,来检查是否有人试图隐藏任意文件,我们可以开始一个简单的测试:
pfStart();
FindFirstFile("C:\\WINNT\\system32\\drivers", &FindFileData);
int res = pfStop();
如果有rootkit隐藏任意文件,则执行的指令数要比干净的系统多。
如果运行这个测试上百次,并计算执行指令数的平均值,我们会发现这个值是非常不确定的。考虑Win2k的复杂性,这一点也不让人吃惊。但对于我们的检测目的来说,这种现象是不能接受的。但如果我们将得到的那些值的频率分布用条形图表示出来,会发现图中有一个明显的频率高点。如图4和5中所表示那样的,即使是在系统负载很大时,频率高点所对应的数值保持不变。很难解释这个令人吃惊的现象,可能是因为在循环中同一个系统服务被调用几百次后,与这个系统服务相关的缓冲最后会被填入固定的值。
假想现在有人安装了隐藏文件的rootkit,如果我们重复测试并绘制相应的条形图,就会发现频率高点向右移了,这是因为rootkit需要进行隐藏文件的工作。
在现在的代码实现中,只进行了少量的测试,包括典型的服务如:文件系统读取,枚举进程,枚举注册表项以及Socket读取。
这些测试将有效地检测出著名的NTRootkit(见[1]),或最近比较流行的Hacker Defender(见[4]),包括它自带的网络后门,当然还包括很多其他的后门。但要检测出一些更好的后门还要加入一些新的测试。
误报和执行路径跟踪
虽然对频率高点的检测有助于我们处理系统的不确定因素,但有时会发现测试得到的值有小的差值,一般来说不大于20。
有时这会是一个很严重的问题,因为我们不能确定那些多出来的指令意味着被入侵或只是正常的误差。
为解决这个问题,我们使用了执行路径记录模式。和单一的EPA模式比较,系统增加了对执行路径的记录(包括地址和运行的指令),首先,系统记录下正常情况下的执行路径,以后的每一次运行将产生diff文件(正常系统和现行系统之间的比较)。
我们应该使用好的反编译器来分析那些不一样的地方,以此判定他们是否可疑。图6是一个diff文件的例子。
现阶段的diff文件只记录下指令的地址,以后可能将两次测试的不同结果存为PE格式文件,并可用IDA等工具分析。
检测 ”offset-in-the-code” 的变化
想象有这样一个rootkit,它基本和上面提到的 fu rootkit (见[3]) 一样,但不从PsActiveProcessList中,而是从分派器使用的数据结构中移除进程。我说过那不可能,因为隐藏的进程将分配不到运行时间......
然而,rootkit可以同时更改分派器代码中所使用数据结构的地址(offset),换句话说,就是使其使用不同的链表。但只有分派器使用这个”新的” 链表,而系统其他地方还是使用”旧的”链表...... (见图7)。
虽然这种技术不会改变执行指令的个数,我们还是能检测到它,但需要进一步的完善现有的工具。这项功能现在还没有实现,但应该不是很难。
针对EPA的攻防
我们可以想到一些能骗过EPA类检测工具的方法,先把它们分为两类。
1、针对特定工具的欺骗
2、对EPA类技术的通用攻击
首先,我们考虑一下通用的攻击方法和怎样防止这类攻击。接着讨论针对特定工具的攻击以及怎样通过多态来预防。
对EPA类技术的通用攻击
首先,恶意程序可以勾连包含除错处理程序(debug handler)地址的IDT入口1,这样将暂停记录运行的指令数。当它完成工作时,再恢复 IDT入口1。这样rootkit中所执行的指令数不会被记录。
我们可以使用intel的除错寄存器来防止这类的攻击。可以使用DR0和DR1寄存器对IDT入口1进行写保护。并且为防止rootkit向除错处理程序的开始处写入Jmp指令,还需对其进行读保护。换句话说,我们不想让rootkit发现除错处理程序的地址。但单纯对IDT入口进行读保护是不行的,系统会蓝屏。但有一简单的解决方法,就是增加额外的一层。见图9。
还有一种攻击的方法,在rootkit运行时,其将TF位清零,并在恶意操作完成时恢复TF位,这样检测工具也只能发现运行的指令数和正常的系统有细微差别。
另外,rootkit还能检查TF位, 如发现被跟踪,则不进行恶意操作。这种行为并不会影响rootkit的正常工作,因为只有被检测的进程才被设置TF位。
我们可以防止这种攻击,应该注意到的是运行每一个系统指令前,都会运行我们的除错处理程序。以下是简单的防预方法:
如果除错处理程序发现上一个运行指令是pushf(将EFLAGS寄存器压入堆栈),则运行