内核对象
0x01 对象
在计算机中,“对象”是个专有名词,其定义是“一个或一组数据结构及定义在其上的操作” 。
对于几乎所有的内核对象,windows都提供一个统一的操作模式,就是先通过系统调用打开或创建目标对象,让当前进程与目标对象之间建立起连接,然后再通过别的系统调用进行操作,最后通过系统调用关闭对象。实际上是关闭进程与目标对象的联系。
常见的内核对象有:
Job、Directory(对象目录中的目录)、SymbolLink(符号链接),Section(内存映射文件)、Port(LPC端口)、IoCompletion(Io完成端口)、File(并非专指磁盘文件)、同步对象(Mutex、Event、Semaphore、Timer)、Key(注册表中的键)、Token(用户/组令牌)、Process、Thread、Pipe、Mailslot、Debug(调试端口)等
内核对象就是一个数据结构,就是一个struct结构体,各种不同类型的对象有不同的定义,
所有内核对象都遵循统一的操作模式:
第一步:先创建对象;
第二步:打开对象,得到句柄(可与第一步合并在一起,表示创建时就打开)
第三步:通过API(系统调用)访问对象;
第四步,关闭句柄,递减引用计数;
第五步:句柄全部关完并且引用计数降到0后,销毁对象。
句柄就是用来维系对象的票据,就好比N名纤夫各拿一条绳,同拉一艘船。每打开一次对象就可拿到一个句柄,表示拿到该对象的一次访问权。
内核对象是全局的,各个进程都可以访问,比如两个进程想要共享某块内存来进行通信,就可以约定一个对象名,然后一个进程可以用CreatFileMapping(”SectionName”)创建一个section,而另一个进程可以用OpenFileMapping(”SectionName”)打开这个section,这样这个section就被两个进程共享了。
各个对象的结构体虽然不同,但有一些通用信息记录在对象头中,对象头的结构体定义:
typedef struct _OBJECT_HEADER { LONG PointerCount;//引用计数 union { LONG HandleCount;//本对象的打开句柄计数(每个句柄本身也占用一个对象引用计数) volatile VOID* NextToFree;//下一个要延迟删除的对象 }; OBJECT_TYPE* Type;//本对象的类型,类型本身也是一种内核对象,有人称之为‘类型对象’ UCHAR NameInfoOffset;//对象名的偏移(无名对象没有Name) UCHAR HandleInfoOffset;//各进程的打开句柄统计信息数组 UCHAR QuotaInfoOffset;//对象本身实际占用内存配额(当不等于该类对象的默认大小时要用到这个) UCHAR Flags;//对象的一些属性标志 union { OBJECT_CREATE_INFORMATION* ObjectCreateInfo;//来源于创建对象时的OBJECT_ATTRIBUTES PVOID QuotaBlockCharged; }; PSECURITY_DESCRIPTOR SecurityDescriptor;//安全描述符(对象的拥有者、ACL等信息) QUAD Body;//通用对象头后面紧跟着真正的结构体(这个字段是后面真正结构体中的第一个成员) } OBJECT_HEADER, *POBJECT_HEADER;
typedef struct _OBJECT_HEADER_NAME_INFO { POBJECT_DIRECTORY Directory;//对象目录中的父目录(不一定是文件系统中的目录) UNICODE_STRING Name;//相对于Directory的路径或者全路径 ULONG QueryReferences;//对象名查询操作计数 … } OBJECT_HEADER_NAME_INFO, *POBJECT_HEADER_NAME_INFO; typedef struct _OBJECT_HEADER_CREATOR_INFO { LIST_ENTRY TypeList;//用来挂入所属‘对象类型’中的链表(也即类型对象内部的对象链表) PVOID CreatorUniqueProcess;//表示本对象是由哪个进程创建的 … } OBJECT_HEADER_CREATOR_INFO, *POBJECT_HEADER_CREATOR_INFO;
对象头中记录了NameInfo、HandleInfo、QuotaInfo、CreatorInfo这4种可选信息。如果这4种可选信息全部都有的话,整个对象的布局从低地址到高地址的内存布局为:
QuotaInfo-> HandleInfo->NameInfo->CreatorInfo->对象头->对象体;这4种可选信息的相对位置倒不重要,但是必须记住,他们都是在对象头中的上方(也即对象头上面的低地址端)。以下为了方便,不妨叫做“对象头中的可选信息”、“头部中的可选信息”。
于是有宏定义:
//由对象体的地址得到对象头的地址
#define OBJECT_TO_OBJECT_HEADER(pBody) CONTAINING(pBody,OBJECT_HEADER,Body)
//得到对象的名字
#define OBJECT_HEADER_TO_NAME_INFO(h)
h->NameInfoOffset?(h - h->NameInfoOffset):NULL
//得到对象的创建者信息
#define OBJECT_HEADER_TO_CREATOR_INFO(h)
h->Flags & OB_FLAG_CREATOR_INFO?h-sizeof(OBJECT_HEADER_CREATOR_INFO):NULL
Windows Object完整的结构图:
+----------------------------------------------------------------+
+------->| ( OBJECT_HEADER_QUOTA_INFO ) |
| +---->| ( OBJECT_HEADER_HANDLE_INFO ) |
| | +->| ( OBJECT_HEADER_NAME_INFO ) |
| | | | ( OBJECT_HEADER_CREATOR_INFO ) |
| | | +------------------------[ Object Header ]-----------------------+
| | | | nt!_OBJECT_HEADER |
| | | | +0x000 PointerCount : Int4B |
| | | | +0x004 HandleCount : Int4B |
| | | | +0x004 NextToFree : Ptr32 Void |
| | | | +0x008 Type : Ptr32 _OBJECT_TYPE |
| | +--| +0x00c NameInfoOffset : UChar |
| +-----| +0x00d HandleInfoOffset : UChar |
+--------| +0x00e QuotaInfoOffset : UChar |
| +0x00f Flags : UChar |
| +0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION |
| +0x010 QuotaBlockCharged : Ptr32 Void |
| +0x014 SecurityDescriptor : Ptr32 Void |
| +0x018 Body : _QUAD |
+-------------------------[ Object Body ]------------------------+
| OBJECT_DIRECTORY, DRIVER_OBJECT, DEVICE_OBJECT, FILE_OBJECT... |
+----------------------------------------------------------------+
0x02 对象目录
所有有名字的对象都会进入内核中的‘对象目录’中,对象目录就是一棵树。树中的每个节点都是对象。内核中有一个全局指针变量ObpRootDirectoryObject,就指向对象目录树的根节点,根节点是一个根目录。
对象目录的作用就是用来将对象路径解析为对象地址。给定一个对象路径,就可以直接在对象目录中找到对应的对象。就好比给定一个文件的全路径,一定能从磁盘的根目录中向下一直搜索找到对应的文件。
如某个设备对象的对象名(全路径)是”\Device\MyCdo”,那么从根目录到这个对象的路径中:
Device是根目录中的子目录,MyDevice则是Device目录中的子节点。
对象有了名字,应用程序就可以直接调用CreateFile(也有其他的API进行打开不同的对象)打开这个对象,获得句柄,没有名字的对象无法记录到对象目录中,应用层看不到,只能由内核自己使用。
树的根是一个目录对象(OBJECT_DIRECTORY),树中的所有中间节点,必须是目录对象或者符号链接对象,而普通对象则只能成为“叶节点”。
目录本身也是一种内核对象,其类型就叫“目录类型”,这种对象的结构体定义:
typedef struct _OBJECT_DIRECTORY { struct _OBJECT_DIRECTORY_ENTRY* HashBuckets[37];//37条hash链 EX_PUSH_LOCK Lock; struct _DEVICE_MAP *DeviceMap; … } OBJECT_DIRECTORY, *POBJECT_DIRECTORY;
如上,目录对象中的所有子对象按hash值分门别类的安放在该目录内部不同的hash链中
其中每个目录项的结构体定义为:
typedef struct _OBJECT_DIRECTORY_ENTRY { struct _OBJECT_DIRECTORY_ENTRY * ChainLink;//下一个目录项(即下一个子节点) PVOID Object;//对象体的地址 ULONG HashValue;//所在hash链 } OBJECT_DIRECTORY_ENTRY, *POBJECT_DIRECTORY_ENTRY;
每个目录项记录了指向的对象的地址,同时间接记录了对象名信息。
ObpLookupEntryDirectory函数用来在指定的目录中查找指定名称的子对象:
VOID* ObpLookupEntryDirectory(IN POBJECT_DIRECTORY Directory, IN PUNICODE_STRING Name, IN ULONG Attributes, IN POBP_LOOKUP_CONTEXT Context) { BOOLEAN CaseInsensitive = FALSE; PVOID FoundObject = NULL; //表示对象名是否严格大小写匹配查找 if (Attributes & OBJ_CASE_INSENSITIVE) CaseInsensitive = TRUE; HashValue=CalcHash(Name->Buffer);//计算对象名的hash值 HashIndex = HashValue % 37;//获得对应的hash链索引 //记录本次是在那条hash中查找 Context->HashValue = HashValue; Context->HashIndex = (USHORT)HashIndex; if (!Context->DirectoryLocked) ObpAcquireDirectoryLockShared(Directory, Context);//锁定目录,以便在其中进行查找操作 //遍历对应hash链中的所有对象 AllocatedEntry = &Directory->HashBuckets[HashIndex]; LookupBucket = AllocatedEntry; while ((CurrentEntry = *AllocatedEntry)) { if (CurrentEntry->HashValue == HashValue) { ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentEntry->Object); HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader); if ((Name->Length == HeaderNameInfo->Name.Length) && (RtlEqualUnicodeString(Name, &HeaderNameInfo->Name, CaseInsensitive))) { break;//找到对应的子对象 } } AllocatedEntry = &CurrentEntry->ChainLink; } if (CurrentEntry)//如果找到了子对象 { if (AllocatedEntry != LookupBucket) 将找到的子对象挂入链表的开头,方便下次再次查找同一对象时直接找到; FoundObject = CurrentEntry->Object; } if (FoundObject) //如果找到了子对象 { ObjectHeader = OBJECT_TO_OBJECT_HEADER(FoundObject); ObpReferenceNameInfo(ObjectHeader);//递增对象名字的引用计数 ObReferenceObject(FoundObject);//注意递增了对象本身的引用计数 if (!Context->DirectoryLocked) ObpReleaseDirectoryLock(Directory, Context); } //检查本次函数调用前,查找上下文中是否已有一个先前的中间节点对象,若有就释放 if (Context->Object) { ObjectHeader = OBJECT_TO_OBJECT_HEADER(Context->Object); HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader); ObpDereferenceNameInfo(HeaderNameInfo); ObDereferenceObject(Context->Object); } Context->Object = FoundObject; return FoundObject;//返回找到的子对象 }
如上,hash查找子对象,找不到就返回NULL。
注意由于这个函数是在遍历路径的过程中逐节逐节的调用的,所以会临时查找中间的目录节点,记录到Context中。
0x03 对象类型
对象是有分类的,也就是有类型(type)的。前面已经列举了一些常见的windows对象类型。用户可以通过安装内核模块即sys模块来达到增加新对象类型的目的。
对象类型_OBJECT_TYPE结构体定义:
typedef struct _OBJECT_TYPE { ERESOURCE Mutex; LIST_ENTRY TypeList;//本类对象的链表,记录所有同类对象 UNICODE_STRING Name;//类型名 PVOID DefaultObject;//指本类对象默认使用的同步事件对象 ULONG Index;//本类型的索引,也即表示这是系统中第几个注册的对象类型 ULONG TotalNumberOfObjects;//对象链表中总的对象个数 ULONG TotalNumberOfHandles;//所有同类对象的打开句柄总数 ULONG HighWaterNumberOfObjects;//历史本类对象个数峰值 ULONG HighWaterNumberOfHandles; //历史本类对象的句柄个数峰值 //关键字段。创建类型对象时,会将类型信息拷贝到下面这个字段中 OBJECT_TYPE_INITIALIZER TypeInfo; ULONG Key;//事实上用作内存分配的tag,同类对象占用的内存块都标记为同一个tag ERESOURCE ObjectLocks[4]; } OBJECT_TYPE;
WINDOWS内核为新类型对象的定义提供了一个全局的_OBJECT_TYPE_INITIALIZER结构,作为需要填写和递交的申请单:
typedef struct _OBJECT_TYPE_INITIALIZER { USHORT Length;//本结构体本身的长度 BOOLEAN UseDefaultObject;//是否使用全局默认的同步事件对象 BOOLEAN CaseInsensitive;//指本类对象的对象名是否大小写不敏感 ULONG InvalidAttributes;//本类对象不支持的属性集合 GENERIC_MAPPING GenericMapping;//一直懒得去分析这个字段 ULONG ValidAccessMask;// 本类对象支持的属性集合 BOOLEAN SecurityRequired;//本类对象是否需要安全控制(另外:凡是有名字的对象都需要安全控制) BOOLEAN MaintainHandleCount;//对象头中是否维护句柄统计信息 BOOLEAN MaintainTypeList;//是否维护创建者信息(也即是否需要挂入到所属对象类型的链表中) POOL_TYPE PoolType;//本类对象位于分页池还是非分页池(一般内核对象都分配在非分页池中) ULONG DefaultPagedPoolCharge; //对象占用的分页池总体大小 ULONG DefaultNonPagedPoolCharge;//对象占用的非分页池总体大小 OB_DUMP_METHOD DumpProcedure;//? OB_OPEN_METHOD OpenProcedure;//打开对象时调用,非常重要 OB_CLOSE_METHOD CloseProcedure;//关闭句柄时调用,非常重要 OB_DELETE_METHOD DeleteProcedure;//销毁对象时调用,非常重要 OB_PARSE _METHOD ParseProcedure;//自定义的路径解析函数(设备、文件、键都提供了此函数) OB_SECURITY_METHOD SecurityProcedure;//查询、设置对象安全描述符的函数 OB_QUERYNAME_METHOD QueryNameProcedure;//文件对象提供了自定义的QueryNameString函数 OB_OKAYTOCLOSE_METHOD OkayToCloseProcedure;//每次关闭句柄前都会调用这个函数检查可否关闭 } OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER;
Windows内核中有许多预定义的对象类型,程序员也可以自己注册一些自定义的对象类型,就像自注册“窗口类”一样。ObCreateObjectType这个函数用来注册一种对象类型(注意对象类型本身也是一种内核对象,因此,‘对象类型’即是‘类型对象’,‘类型对象’即是‘对象类型’)
NTSTATUS ObCreateObjectType(IN PUNICODE_STRING TypeName, IN POBJECT_TYPE_INITIALIZER ObjectTypeInitializer, OUT POBJECT_TYPE *ObjectType) { ObpInitializeLookupContext(&Context); //若 \ObjectTypes 目录下已经创建过了这种对象类型。返回失败 ObpAcquireDirectoryLockExclusive(ObpTypeDirectoryObject, &Context); if (ObpLookupEntryDirectory(ObpTypeDirectoryObject, TypeName, OBJ_CASE_INSENSITIVE, FALSE, &Context)) { ObpReleaseLookupContext(&Context); return STATUS_OBJECT_NAME_COLLISION;//不能重复创建同一种对象类型 } ObjectName.Buffer = ExAllocatePoolWithTag(PagedPool,TypeName->MaximumLength,tag); ObjectName.MaximumLength = TypeName->MaximumLength; RtlCopyUnicodeString(&ObjectName, TypeName); //分配一块内存,创建类型对象 Status = ObpAllocateObject(NULL, //CreateInfo=NULL &ObjectName,//对象的名字 ObpTypeObjectType,//类型对象本身的类型 sizeof(OBJECT_TYPE),//对象的大小 KernelMode, (POBJECT_HEADER*)&Header); LocalObjectType = (POBJECT_TYPE)&Header->Body; LocalObjectType->Name = ObjectName;//类型对象的自身的名称 Header->Flags |= OB_FLAG_KERNEL_MODE | OB_FLAG_PERMANENT;//类型对象全由内核创建并有永久性 LocalObjectType->TotalNumberOfObjects =0; LocalObjectType->TotalNumberOfHandles =0; //本类对象的个数与句柄个数=0 //拷贝类型信息(这个TypeInfo就是类型描述符) LocalObjectType->TypeInfo = *ObjectTypeInitializer; LocalObjectType->TypeInfo.PoolType = ObjectTypeInitializer->PoolType; //类型对象的对象体上面的所有头部大小 HeaderSize = sizeof(OBJECT_HEADER) + sizeof(OBJECT_HEADER_NAME_INFO)+(ObjectTypeInitializer->MaintainHandleCount ?sizeof(OBJECT_HEADER_HANDLE_INFO) : 0); if (ObjectTypeInitializer->PoolType == NonPagedPool) LocalObjectType->TypeInfo.DefaultNonPagedPoolCharge += HeaderSize; else LocalObjectType->TypeInfo.DefaultPagedPoolCharge += HeaderSize; //查询、设置对象安全描述符的函数 if (!ObjectTypeInitializer->SecurityProcedure) LocalObjectType->TypeInfo.SecurityProcedure = SeDefaultObjectMethod; if (LocalObjectType->TypeInfo.UseDefaultObject) { LocalObjectType->TypeInfo.ValidAccessMask |= SYNCHRONIZE;//本对象可用于同步操作 LocalObjectType->DefaultObject = &ObpDefaultObject;//其实是个全局的Event对象 } //文件对象的结构体中可自带一个事件对象,WaitForSingleObject(FileObject)等待的就是那个事件 else if ((TypeName->Length == 8) && !(wcscmp(TypeName->Buffer, L"File"))) LocalObjectType->DefaultObject =FIELD_OFFSET(FILE_OBJECT,Event);//偏移 else if ((TypeName->Length == 24) && !(wcscmp(TypeName->Buffer, L"WaitablePort"))) LocalObjectType->DefaultObject = FIELD_OFFSET(LPCP_PORT_OBJECT,WaitEvent);//偏移 else LocalObjectType->DefaultObject = NULL; InitializeListHead(&LocalObjectType->TypeList); CreatorInfo = OBJECT_HEADER_TO_CREATOR_INFO(Header); if (CreatorInfo) //将这个类型对象注册、加入全局链表中,注意这两个TypeList的含义是不一样的 InsertTailList(&ObpTypeObjectType->TypeList,&CreatorInfo->TypeList); LocalObjectType->Index = ObpTypeObjectType->TotalNumberOfObjects; //将这个类型对象加入全局数组中 if (LocalObjectType->Index < 32)//对象类型较少,一般够用 ObpObjectTypes[LocalObjectType->Index - 1] = LocalObjectType; //将类型对象插入 \ObjectTypes 目录中(目录内部的指定hash链中) bSucc=ObpInsertEntryDirectory(ObpTypeDirectoryObject, &Context, Header); if (bSucc) { ObpReleaseLookupContext(&Context); *ObjectType = LocalObjectType; return STATUS_SUCCESS; } Else { ObpReleaseLookupContext(&Context); return STATUS_INSUFFICIENT_RESOURCES; } }
如上,大致的流程就是创建一个对象类型,然后加入对象目录中的 \ObjectTypes目录中。
内核中的对象管理器在初始化的时候,会初始化对象目录。先注册创建名为“Directory”、“SymbolicLink”的对象类型,然后在对象目录中创建根目录“\”,“\ObjectTypes”目录,“\DosDevices”目录等预定义目录。
内核中的IO管理器在初始化的时候,会注册创建名为“Device”、“File”、“Driver”等对象类型,由于对象类型本身也是一种有名字的对象,所以也会挂入对象目录中,位置分别为:
“\ObjectTypes\Device”
“\ObjectTypes\File”
“\ObjectTypes\Driver”
于是,我们的驱动就可以创建对应类型的对象了。
符号链接、设备、文件这几类对象都提供了自定义的路径解析函数。因为这几种对象,对象后面的剩余路径并不在对象目录中,对象目录中的叶节点到这几种对象就是终点了。比如物理磁盘卷设备对象上的某一文件路径 “\Device\Harddisk0\Partition0\Dir1\Dir2\File.txt” 的解析过程是:
先:顺着对象目录中的根目录,按“\Device\Harddisk0\Partition0”这个路径解析到这一层,找到对应的卷设备对象
再:后面剩余的路径“Dir1\Dir2\File.txt”就由具体的文件系统去解析了,最终找到对应的文件对象
另外注意一下,文件对象在句柄关完后,将产生一个IRP_MJ_CLEANUP;文件对象在引用减到0后,销毁前将产生IRP_MJ_CLOSE。这就是这两个irp的产生时机。简单记忆【柄完清理,引完关闭】