保护内核对象—对象挂钩(Object Hook)

内核对象

每个内核对象实际上都是一个内存块,它是由操作系统内核分配的,并且只能由操作系统内核访问。这个内存块是一个数据结构,它的成员维护着和这个对象相关的信息。但是应用程序不能直接访问和修改这些数据结构,它们需要通过句柄。

先通过系统调用打开/创建内核对象,让当前进程与目标对象之间建立起连接,此时就会返回一个句柄。然后调用Windows提供的一系列API函数就能访问这些内核对象。所以句柄其实相当于一个接口,Ring3层通过它来访问Ring0层的一些数据结构。

内核对象由对象头对象体组成。所有内核对象的对象头是一样的,是_OBJECT_HEADER结构。不同对象的对象体不同,如进程对象的是_EPROCRSS,线程对象的是_ETHREAD,文件对象的是_FILE_OBJECT,等。

0: kd> dt _OBJECT_HEADER
nt!_OBJECT_HEADER
   +0x000 PointerCount     : Int4B	// 对象的指针计数
   +0x004 HandleCount      : Int4B	// 对象的句柄计数
   +0x004 NextToFree       : Ptr32 Void
   +0x008 Lock             : _EX_PUSH_LOCK
   +0x00c TypeIndex        : UChar	// 对象类型,实际上是一个数组的下标(ObTypeIndexTable)
   +0x00d TraceFlags       : UChar
   +0x00e InfoMask         : UChar
   +0x00f Flags            : UChar
   +0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION
   +0x010 QuotaBlockCharged : Ptr32 Void
   +0x014 SecurityDescriptor : Ptr32 Void
   +0x018 Body             : _QUAD	// 对象体,不同类型的对象,对象体不同

当应用程序打开内核对象时,它的指针计数和句柄计数都会被加1;关闭时,都会减1。
另外,内核模块可以通过ObReferenceObject来增加对象的指针计数。一般来说,对象的指针计数会 >= 句柄计数。当指针计数变为0时,这个内核对象会被销毁。

ObTypeIndexTable 是一个数组,其中保存了系统对象类型的信息,数组的每一个元素都是一个Type类型的内核对象。可以通过Windbg查看ObTypeIndexTable:

0: kd> dd ObTypeIndexTable
83f48900  00000000 bad0b0b0 858b87c8 858b8700
83f48910  858b8638 85944040 85944f40 85944e78
83f48920  85944db0 85944ce8 85944c20 85944568
83f48930  85969418 85969350 8596ff78 8596feb0
83f48940  85968408 85968340 8596b588 8596b4c0
83f48950  8596b3f8 8595f040 8595f218 8595f150
83f48960  85963040 85963220 85963158 8596b040
83f48970  8596b220 8596b158 85966b78 85966ab0

// 查看一下 858b8700 这个对象,可以发现它是Type类型的内核对象,对象名称为"Directory"
0: kd> !object 858b8700
Object: 858b8700  Type: (858b87c8) Type
    ObjectHeader: 858b86e8 (new version)
    HandleCount: 0  PointerCount: 2
    Directory Object: 89a05880  Name: Directory
// 再随便看一个,可以发现它是Type类型的内核对象,对象名称为"Mutant"
0: kd> !object 8596ff78
Object: 8596ff78  Type: (858b87c8) Type
    ObjectHeader: 8596ff60 (new version)
    HandleCount: 0  PointerCount: 2
    Directory Object: 89a05880  Name: Mutant

Type类型的内核对象,它的对象体定义如下:

0: kd> dt _OBJECT_TYPE
nt!_OBJECT_TYPE
   +0x000 TypeList         : _LIST_ENTRY
   +0x008 Name             : _UNICODE_STRING
   +0x010 DefaultObject    : Ptr32 Void
   +0x014 Index            : UChar
   +0x018 TotalNumberOfObjects : Uint4B
   +0x01c TotalNumberOfHandles : Uint4B
   +0x020 HighWaterNumberOfObjects : Uint4B
   +0x024 HighWaterNumberOfHandles : Uint4B
   +0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER // 包含和类型相关的详细信息
   +0x078 TypeLock         : _EX_PUSH_LOCK
   +0x07c Key              : Uint4B
   +0x080 CallbackList     : _LIST_ENTRY
       
0: kd> dt _OBJECT_TYPE_INITIALIZER
nt!_OBJECT_TYPE_INITIALIZER
   +0x000 Length           : Uint2B
   +0x002 ObjectTypeFlags  : UChar
   +0x002 CaseInsensitive  : Pos 0, 1 Bit
   +0x002 UnnamedObjectsOnly : Pos 1, 1 Bit
   +0x002 UseDefaultObject : Pos 2, 1 Bit
   +0x002 SecurityRequired : Pos 3, 1 Bit
   +0x002 MaintainHandleCount : Pos 4, 1 Bit
   +0x002 MaintainTypeList : Pos 5, 1 Bit
   +0x002 SupportsObjectCallbacks : Pos 6, 1 Bit
   +0x004 ObjectTypeCode   : Uint4B
   +0x008 InvalidAttributes : Uint4B
   +0x00c GenericMapping   : _GENERIC_MAPPING
   +0x01c ValidAccessMask  : Uint4B
   +0x020 RetainAccess     : Uint4B
   +0x024 PoolType         : _POOL_TYPE
   +0x028 DefaultPagedPoolCharge : Uint4B
   +0x02c DefaultNonPagedPoolCharge : Uint4B
   +0x030 DumpProcedure    : Ptr32     void 
   +0x034 OpenProcedure    : Ptr32     long 	// 一个函数指针,当一个内核对象被获取句柄时,系统会调用该内核对象所对应的Type对象的OpenProcedure
   +0x038 CloseProcedure   : Ptr32     void 
   +0x03c DeleteProcedure  : Ptr32     void 
   +0x040 ParseProcedure   : Ptr32     long 
   +0x044 SecurityProcedure : Ptr32     long 
   +0x048 QueryNameProcedure : Ptr32     long 
   +0x04c OkayToCloseProcedure : Ptr32     unsigned char 

保护内核对象

恶意程序可能会破坏内核对象,从而破坏安全软件或系统,这种行为成为对象劫持

安全软件加固自身安全称为自保护

接下来讨论一下如何防止来自用户态的对象劫持。不讨论内核态是因为,一旦恶意的内核模块被加载到内核,它就拥有了最高权限,它能造成的伤害远远高于对象劫持,或者说防止内核态的对象劫持没有意义。

1. 对象的打开

防止重要的内核对象被恶意进程打开。

可以通过Hook打开内核对象相关的函数,在钩子函数中对其进行判断,看被打开的内核对象是否是被保护的,以及当前进程是否是安全的。

保护不同类型的内核对象,需要Hook不同的函数,如NtOpenProcess、NtOpenThread、NtOpenSection等。

2. 对象句柄的复制

除了打开对象,通过复制句柄也能得到该对象的句柄。

BOOL
DuplicateHandle(
    IN HANDLE hSourceProcessHandle,  // 被复制句柄所属进程的句柄
    IN HANDLE hSourceHandle,   		 // 被复制句柄值
    IN HANDLE hTargetProcessHandle,	 // 目标进程的句柄
    OUT LPHANDLE lpTargetHandle,     // 保存复制后的句柄值
    IN DWORD dwDesiredAccess,    
    IN BOOL bInheritHandle,    
    IN DWORD dwOptions);

解决方法有三种:

  • 挂钩内核对象的操作函数,判断是否在不安全进程中操作受保护的内核对象。
  • 挂钩NtDuplicateHandle函数,判断是否正在将一个受保护的对象句柄复制到一个非安全进程。
  • 挂钩NtOpenProcess函数,防止不安全的进程获取安全进程的PROCESS_DUP_HANDLE权限,这样恶意进程就无法获得hSourceProcessHandle。
3. 句柄的继承

除了打开对象和复制句柄,通过句柄继承也能获得对象句柄。

Windows的属性列表机制(AttributeList) 允许在创建子进程过程中指定从某个进程继承句柄。恶意程序可以通过属性列表的 方式,在创建子进程时,指定从安全进程中继承句柄,然后通过继承的句柄来恶意操作内核对象。

解决方法:

  • 因为继承句柄也涉及到了进程句柄权限,所以也可以通过2中的第三个方法来解决,即挂钩NtOpenProcess函数。
  • 对象挂钩(Object Hook),重点介绍一下这种方法。

在上面我们介绍到了当一个内核对象被获得句柄时,系统会调用该内核对象所对应的Type对象的OpenProcedure
只要Hook了这个OpenProcedure,当该类对象的句柄被获得时,都会调用我们的钩子函数。这种方法就叫做对象挂钩

但是OpenProcedure是未公开的函数,不同系统下参数可能不同。
win7 x86系统中的函数原型如下:

NTSTATUS
OB_OPEN_METHOD(
	IN OB_OPEN_REASON OpenReason,	// 表示当前获得句柄的方式,是个枚举类型
	IN KPROCESS_MODE AccessMode,	// 访问模式,UserMode或KernelMode
	IN PEPROCESS Process OPTIONAL,
	IN PVOID Object,	// 内核对象指针,将要获得的是这个内核对象的句柄
	IN PACCESS_MASK GrandedAccess,	// 获取的权限,可以通过这个值来修改句柄权限
	IN ULONG HandleCount);		// 句柄计数

typedef enum _OB_OPEN_REASON
{
    ObCreateHandle,		// 通过创建内核对象获得句柄
    ObOpenHandle,			// 通过打开内核对象获得句柄
    ObDuplicateHandle,	// 通过复制句柄
    ObInheritHandle,			// 通过继承句柄
    ObMaxOpenReason
} OB_OPEN_REASON;

首先介绍一下如何Hook OpenProcedure,我这里Hook的是进程对象的,代码如下:

#define OPENPROCEDURE_OFFSET	0x5C
BOOLEAN 
HookOpenProcedure(PVOID FakeFunc, PVOID* OriginalFunc)
{
	BOOLEAN IsOk = FALSE;
	PVOID TypeObject = NULL;
	PVOID* HookAddress = NULL;
	if (FakeFunc == NULL || OriginalFunc == NULL)
	{
		return FALSE;
	}
	do
	{
		TypeObject = (PVOID)*PsProcessType;
		HookAddress = (PVOID*)((PUCHAR)TypeObject + OPENPROCEDURE_OFFSET);
		if (HookAddress == NULL)
		{
			break;
		}
		*OriginalFunc = *HookAddress;
		InterlockedExchangePointer(HookAddress, FakeFunc);
		IsOk = TRUE;	
	} while (FALSE);
	return IsOk;
}

不同对象的对象类型,对应不同的全局变量,进程对象的是PsProcessType,其他的可以参考:

extern POBJECT_TYPE* ObpTypeObjectType				// Type
extern POBJECT_TYPE* PsJobType;						// Job
extern POBJECT_TYPE* ObpDirectoryObjectType;		// Directory
extern POBJECT_TYPE* ObpSymbolicLinkObjectType;		// SymbolicLink
extern POBJECT_TYPE* MmSectionObjectType;			// Section
extern POBJECT_TYPE* LpcPortObjectType;				// Port
extern POBJECT_TYPE* LpcWaitablePortObjectType;		// WaitablePort
extern POBJECT_TYPE* IoCompletionObjectType;		// Completion
extern POBJECT_TYPE* IoFileObjectType;				// File
extern POBJECT_TYPE* ExCallbackObjectType;			// Callback
extern POBJECT_TYPE* ExEventObjectType;				// Event
extern POBJECT_TYPE* ExEventPairObjectType;			// EventPair
extern POBJECT_TYPE* ExMutantObjectType;			// Mutant
extern POBJECT_TYPE* ExProfileObjectType;			// Profile
extern POBJECT_TYPE* ExSemaphoreObjectType;			// Semaphore
extern POBJECT_TYPE* ExTimerObjectType;				// Timer
extern POBJECT_TYPE* ExWindowStationObjectType;		// WindowStation
extern POBJECT_TYPE* ExDesktopObjectType;			// Desktop
extern POBJECT_TYPE* CmpKeyObjectType;				// Key
extern POBJECT_TYPE* ExpKeyedEventObjectType;		// KeyedEvent
extern POBJECT_TYPE* SeTokenObjectType;				// Token
extern POBJECT_TYPE* PsProcessType;					// Process
extern POBJECT_TYPE* PsThreadType;					// Thread
extern POBJECT_TYPE* DbgkDebugObjectType;			// Debug
extern POBJECT_TYPE* IoAdapterObjectType;			// Adapter
extern POBJECT_TYPE* IoControllerObjectType;		// Controller
extern POBJECT_TYPE* IoDeviceObjectType;			// Device
extern POBJECT_TYPE* IoDriverObjectType;			// Driver
extern POBJECT_TYPE* IoDeviceHandlerObjectType;		// DeviceHandle
extern POBJECT_TYPE* ObpDeviceMapObjectType;		// DeviceMap
extern POBJECT_TYPE* WmipGuidObjectType;			// Guid

0x5C 是OpenProcedure在OBJECT_TYPE中的偏移(0x28 + 0x34)。
我们首先看一下Hook前的数据:

1: kd> dd PsProcessType
849ac104  876ddd28 876dddf0 876ddb98 876ddad0

1: kd> dt _OBJECT_TYPE 876ddd28 
nt!_OBJECT_TYPE
   +0x000 TypeList         : _LIST_ENTRY [ 0x876ddd28 - 0x876ddd28 ]
   +0x008 Name             : _UNICODE_STRING "Process"
   +0x010 DefaultObject    : (null) 
   +0x014 Index            : 0x7 ''
   +0x018 TotalNumberOfObjects : 0x32
   +0x01c TotalNumberOfHandles : 0xf8
   +0x020 HighWaterNumberOfObjects : 0x32
   +0x024 HighWaterNumberOfHandles : 0xf8
   +0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER
   +0x078 TypeLock         : _EX_PUSH_LOCK
   +0x07c Key              : 0x636f7250
   +0x080 CallbackList     : _LIST_ENTRY [ 0x876ddda8 - 0x876ddda8 ]

1: kd> dt _OBJECT_TYPE_INITIALIZER 876ddd28 + 28
nt!_OBJECT_TYPE_INITIALIZER
   +0x000 Length           : 0x50
   +0x002 ObjectTypeFlags  : 0x4a 'J'
   +0x002 CaseInsensitive  : 0y0
   +0x002 UnnamedObjectsOnly : 0y1
   +0x002 UseDefaultObject : 0y0
   +0x002 SecurityRequired : 0y1
   +0x002 MaintainHandleCount : 0y0
   +0x002 MaintainTypeList : 0y0
   +0x002 SupportsObjectCallbacks : 0y1
   +0x002 CacheAligned     : 0y0
   +0x004 ObjectTypeCode   : 0
   +0x008 InvalidAttributes : 0xb0
   +0x00c GenericMapping   : _GENERIC_MAPPING
   +0x01c ValidAccessMask  : 0x1fffff
   +0x020 RetainAccess     : 0x101000
   +0x024 PoolType         : 0 ( NonPagedPool )
   +0x028 DefaultPagedPoolCharge : 0x1000
   +0x02c DefaultNonPagedPoolCharge : 0x2f0
   +0x030 DumpProcedure    : (null) 
   +0x034 OpenProcedure    : 0x84a53e8f     long  nt!PspProcessOpen+0
   +0x038 CloseProcedure   : 0x84ab40b5     void  nt!PspProcessClose+0
   +0x03c DeleteProcedure  : 0x84ab697a     void  nt!PspProcessDelete+0
   +0x040 ParseProcedure   : (null) 
   +0x044 SecurityProcedure : 0x84aa8936     long  nt!SeDefaultObjectMethod+0
   +0x048 QueryNameProcedure : (null) 
   +0x04c OkayToCloseProcedure : (null) 

我们可以发现,类型是进程的内核对象,对应的OpenProcedure是PspProcessOpen,地址是0x84a53e8f。
我们要做的就是将这个地址换成我们自定义的钩子函数。
注意,在修改之前要先保存原地址,方便后续放行。

Hook之后再次查看这个位置,发现它已经被修改了,改成了我们自定义的FakeOpenProcedure。

3: kd> dt _OBJECT_TYPE_INITIALIZER 876ddd28 + 28
nt!_OBJECT_TYPE_INITIALIZER
   ......
   +0x034 OpenProcedure    : 0x95a7d060     long  DriverTest!FakeOpenProcedure+0
   +0x038 CloseProcedure   : 0x84ab40b5     void  nt!PspProcessClose+0
   +0x03c DeleteProcedure  : 0x84ab697a     void  nt!PspProcessDelete+0
   ......

到此为止,Hook操作就成功了。那我们的FakeOpenProcedure函数中具体要实现什么功能呢?
主要就是一系列的判断,首先获得正在操作的内核对象的名称,看是不是我们要保护的对象,如果是的话,再判断一下当前进程是否安全,安全则放行(调用原函数),不安全则拦截(返回STATUS_ACCESS_DENIED)。
注意,保护对象列表和安全进程判断,都需要视具体情况判断,这里只留了接口。

NTSTATUS
FakeOpenProcedure(
	IN OB_OPEN_REASON OpenReason,
	IN KPROCESSOR_MODE AccessMode,
	IN PEPROCESS Process OPTIONAL,
	IN PVOID Object,
	IN PACCESS_MASK GrandedAccess,
	IN ULONG HandleCount)
{
	NTSTATUS Status = STATUS_UNSUCCESSFUL;
	ULONG ReturnLength = 0;
	POBJECT_NAME_INFORMATION ObjectNameInfo= NULL;
	HANDLE ProcessId = NULL;
	do
	{
		// 注意,在win7系统中,继承操作的AccessMode一般是KernelMode,
		// 所以对于继承操作来说,不用管AccessMode是什么,都需要处理
		// 对于其他的方式,我们只处理UserMode
		if (OpenReason != ObInheritHandle && AccessMode != UserMode)
		{
			break;
		}
		if (GrandedAccess == NULL || Object == NULL)
		{
			break;
		}
		// 首先通过对象指针获得对象名称
		ObQueryNameString(Object, NULL, 0, &ReturnLength);
		if (ReturnLength == 0)
		{
			break;
		}
		ObjectNameInfo = ExAllocatePoolWithTag(NonPagedPool, ReturnLength, 'he');
		if (ObjectNameInfo == NULL)
		{
			break;
		}
		memset(ObjectNameInfo, 0, ReturnLength);
		Status = ObQueryNameString(Object, ObjectNameInfo, ReturnLength, &ReturnLength);
		if (!NT_SUCCESS(Status))
		{
			break;
		}
		if (ObjectNameInfo->Name.Buffer == NULL || ObjectNameInfo->Name.Length == 0)
		{
			break;
		}
		// 得到名称后,判断它是否是受保护的内核对象
		if (RtlCompareUnicodeString(&ObjectNameInfo->Name, &gProtectName, TRUE) &&
			RtlCompareUnicodeString(&ObjectNameInfo->Name, &gProtectNameBase, TRUE))
		{
			break;
		}
		// 获得当前进程ID,判断它是不是安全进程,具体的判定规则要视情况而定。	
		ProcessId = PsGetCurrentProcessId();
		if (IsSafeProcess(ProcessId))
		{
			break;
		}
		// 黑名单,则拦截
		Status = STATUS_ACCESS_DENIED;
	} while (FALSE);
	if (ObjectNameInfo != NULL)
	{
		ExFreePoolWithTag(ObjectNameInfo, 'he');
		ObjectNameInfo = NULL;
	}
	if (gOriginalFunc != NULL &&
		Status != STATUS_ACCESS_DENIED)
	{
		// 白名单,则放行
		Status = gOriginalFunc(OpenReason, AccessMode, Process, Object, GrandedAccess, HandleCount);
	}
	return Status;
}
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值