纵有疾风起
人生不言弃

[4]Windows内核情景分析—内核对象

写过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就被两个进程共享了。

 

(注意:本篇说的都是内核对象的句柄。像什么hWnd、hDC、hFont、hModule、hHeap、hHook等等其他句柄,并不是指内核对象,因为这些句柄值不是指向进程句柄表中的索引,而是另外一种机制)

 

 

 

各个对象的结构体虽然不同,但有一些通用信息记录在对象头中,看下面的结构体定义

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;

如上,Body就是对象体中的第一个字段,头部后面紧跟具体对象类型的结构体定义

 

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

 

所有有名字的对象都会进入内核中的‘对象目录’中,对象目录就是一棵树。内核中有一个全局指针变量ObpRootDirectoryObject,就指向对象目录树的根节点,根节点是一个根目录。

对象目录的作用就是用来将对象路径解析为对象地址。给定一个对象路径,就可以直接在对象目录中找到对应的对象。就好比给定一个文件的全路径,一定能从磁盘的根目录中向下一直搜索找到对应的文件。

如某个设备对象的对象名(全路径)是”\Device\MyCdo”,那么从根目录到这个对象的路径中:

Device是根目录中的子目录,MyDevice则是Device目录中的子节点。

对象有了名字,应用程序就可以直接调用CreateFile打开这个对象,获得句柄,没有名字的对象无法记录到对象目录中,应用层看不到,只能由内核自己使用。

 

 

内核中各种类型的对象在对象目录中的位置:

目录对象:最常见,就是对象目录中的目录节点(可以作为叶节点) 

普通对象:只能作为叶节点

符号链接对象:只能作为叶节点

注意文件对象和注册表中的键对象看似有文件名、键名,但此名非对象名。因此,文件对象与键对象是无名的,无法进入对象目录中

根目录也是一种目录对象,符号链接对象可以链接到对象目录中的任何节点,包括又链向另一个符号链接对象。

对象目录中,每个目录节点下面的子节点可以是

1、 普通对象节点

2、 子目录

3、 符号链接

该目录中的所有子节点对象都保存在该目录内部的目录项列表中。不过,这个列表不是一个简单的数组,而是一个开式hash表,用来方便查找。根据该目录中各个子对象名的hash值,将对应的子对象挂入对应的hash链表中,用hash方式存储这些子对象以提高查找效率

目录本身也是一种内核对象,其类型就叫“目录类型”,现在就可以看一下这种对象的结构体定义:

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;

看到没,每个目录项记录了指向的对象的地址,同时间接记录了对象名信息

 

下面这个函数用来在指定的目录中查找指定名称的子对象

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中。

 

 

对象类型:

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;

//下面这个结构体描述了类型的关键信息

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内核中有许多预定义的对象类型,程序员也可以自己注册一些自定义的对象类型,就像自注册“窗口类”一样。下面这个函数用来注册一种对象类型(注意对象类型本身也是一种内核对象,因此,‘对象类型’即是‘类型对象’,‘类型对象’即是‘对象类型’)

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”

于是,我们的驱动就可以创建对应类型的对象了。

 

 

下面我们具体看几个重点对象类型的创建过程:

OBJECT_TYPE_INITIALIZER  Oti = {0};

Oti.Length=sizeof(OBJECT_TYPE_INITIALIZER);

Oti.UseDefaultObject=TRUE;

Oti.MaintainTypeList=TRUE;//一般都会维持类型信息,加入到所属类型的链表中

Oti.PoolType=NonPagePool;//所有内核对象,都默认分配在非分页池中

Oti.InvalidAttributes=OBJ_OPENLINK;//一般都不许打开符号链接

 

Oti.DefaultNonPagePoolCharge=sizeof(OBJECT_DIRECTORY);

Oti.UseDefaultObject=FALSE;

ObCreateObjectType(“Directory”,&oti,&ObpDirectoryType);//创建普通的目录对象类型

 

Oti.DefaultNonPagePoolCharge=sizeof(OBJECT_SYMBOLIC_LINK);

Oti.ValidAccessMask=SYMBOLIC_LINK_ALL_ACCESS;

Oti.ParseProcedure=ObpParseSymbolicLink;//关键字段。自定义解析后面的路径名

Oti.DeleteProcedure=ObpDeleteSymbolicLink;

ObCreateObjectType(“SymbolicLink”,&oti,&ObSymbolicLinkType);//创建符号链接对象类型

Oti.DefaultNonPagePoolCharge=sizeof(DEVICE_OBJECT);

Oti.ParseProcedure=IopParseDevice; //关键字段。自定义解析后面的路径名

Oti.SecurityProcedure=IopSecurityFile;

ObCreateObjectType(“Device”,&oti,&IoDeviceObjectType);//创建设备对象类型

Oti.DefaultNonPagePoolCharge=sizeof(FILE_OBJECT);

Oti.UseDefaultObject=FALSE;//每个文件对象内部有一个自己的事件对象

Oti.ParseProcedure=IopParseFile; //关键字段。自定义解析后面的路径名

Oti.SecurityProcedure=IopSecurityFile;

Oti.QueryNameProcedure=IopQueryNameFile;//文件对象自己负责ObQueryNameString

Oti.CloseProcedure=IopCloseFile;//关闭文件句柄时调用的函数(句柄关完后生成MJ_Cleanup irp)

Oti.DeleteProcedure=IopDeleteFile;//销毁文件对象时调用的函数(对象销毁前生成MJ_Close irp)

ObCreateObjectType(“File”,&oti,&IoFileObjectType);//创建文件对象类型

 

我们看到,符号链接、设备、文件这三类对象都提供了自定义的路径解析函数。(后文中,这册表键对象也会提供一个自定义解析函数)因为这几种对象,对象后面的剩余路径并不在对象目录中,对象目录中的叶节点到这几种对象就是终点了。比如物理磁盘卷设备对象上的某一文件路径 “\Device\Harddisk0\Partition0\Dir1\Dir2\File.txt” 的解析过程是:

先:顺着对象目录中的根目录,按“\Device\Harddisk0\Partition0”这个路径解析到这一层,找到对应的卷设备对象

再:后面剩余的路径“Dir1\Dir2\File.txt”就由具体的文件系统去解析了,最终找到对应的文件对象

另外注意一下,文件对象在句柄关完后,将产生一个IRP_MJ_CLEANUP;文件对象在引用减到0后,销毁前将产生IRP_MJ_CLOSE。这就是这两个irp的产生时机。简单记忆【柄完清理,引完关闭】

 

 

 

 

句柄:

任意进程,只要每打开一个对象,就会获得一个句柄,这个句柄用来标志对某个对象的一次打开,通过句柄,可以直接找到对应的内核对象。句柄本身是进程的句柄表中的一个结构体,用来描述一次打开操作。

句柄值则可以简单看做句柄表中的索引,并不影响理解。HANDLE的值可以简单的看做一个整形索引值。

每个进程都有一个句柄表,用来记录本进程打开的所有内核对象。句柄表可以简单看做为一个一维数组,每个表项就是一个句柄,一个结构体,一个句柄描述符,其结构体定义如下:

typedef struct _HANDLE_TABLE_ENTRY  //句柄描述符

{

    union

    {

        PVOID Object;//关键字段。该句柄指向的内核对象(注意是其头部)

        ULONG_PTR ObAttributes;//关键字段。该句柄的属性

        PHANDLE_TABLE_ENTRY_INFO InfoTable;

        ULONG_PTR Value;//值(可见值本身是一个复合体),最低3位表示该句柄的属性(Value= Object | ObAttributes)

    };

    union

    {

        ULONG GrantedAccess;//关键字段。该句柄的访问权限

        struct

        {

            USHORT GrantedAccessIndex;

            USHORT CreatorBackTraceIndex;

        };

        LONG NextFreeTableEntry;//当本句柄是一个空闲表项时,用来链接到句柄表中下一个空闲表项

    };

} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

 

句柄表则定义如下:

typedef struct _HANDLE_TABLE    //句柄表描述符

{

    ULONG TableCode; //表的地址|表的层数(该字段的最后两位表示表的层数)

    PHANDLE_TABLE_ENTRY **Table; 

    PEPROCESS QuotaProcess;//所属进程

    PVOID UniqueProcessId; //所属进程的PID

    EX_PUSH_LOCK HandleTableLock[4];

    LIST_ENTRY HandleTableList;//用来挂入全局的句柄表链表(间接给出了系统中的进程列表)

    EX_PUSH_LOCK HandleContentionEvent;

    ERESOURCE HandleLock;

    LIST_ENTRY HandleTableList;

    KEVENT HandleContentionEvent;

    PHANDLE_TRACE_DEBUG_INFO DebugInfo;

    LONG ExtraInfoPages;

    ULONG FirstFree;//第一个空闲表项的索引位置

    ULONG LastFree;//最后一个空闲表项的索引位置

    ULONG NextHandleNeedingPool;//本句柄表本身占用的内存页数

    LONG HandleCount;//表中的有效句柄总数

    union

    {

        ULONG Flags;

        UCHAR StrictFIFO:1;

    };

} HANDLE_TABLE, *PHANDLE_TABLE;

进程的EPROCESS结构体中有一个字段HANDLE_TABLE* ObjectTable;指的就是该进程的句柄表

 

HANDLE   ExCreateHandle(PHANDLE_TABLE HandleTable,   PHANDLE_TABLE_ENTRY HandleTableEntry)

{

    EXHANDLE Handle;

    NewEntry = ExpAllocateHandleTableEntry(HandleTable,&Handle);//在句柄表中找到一个空闲表项

    *NewEntry = *HandleTableEntry;//复制句柄表项

    return Handle.GenericHandleOverlay;//返回句柄值(也即空闲表项的索引位置)

}

上面这个函数与其说是创建一个句柄,不如说是插入一个句柄。在指定句柄表中找到一个空闲未用的表项,

然后将句柄插入到那个位置,最后返回句柄的“索引”。

 

//下面这个函数用来打开对象,获得句柄

NTSTATUS

ObpCreateHandle(IN OB_OPEN_REASON OpenReason,//4种打开时机

                IN PVOID Object, //要打开的对象

                IN PACCESS_STATE AccessState, //句柄的访问权限

                IN ULONG HandleAttributes, //句柄的属性

                IN KPROCESSOR_MODE AccessMode,

                OUT PHANDLE ReturnedHandle) //返回的句柄值

{

    BOOLEAN AttachedToProcess = FALSE, KernelHandle = FALSE;

    NewEntry.Object = ObjectHeader;//关键。将该句柄指向对应的对象头

    if (HandleAttributes & OBJ_KERNEL_HANDLE)//如果用户要求创建一个全局型的内核句柄

    {

        HandleTable = ObpKernelHandleTable;//改用内核句柄表

        KernelHandle = TRUE;

        //将当前线程挂靠到system进程,也即修改当前的CR3,将页表换成system进程的页表

        if (PsGetCurrentProcess() != PsInitialSystemProcess)

        {

            KeStackAttachProcess(&PsInitialSystemProcess->Pcb, &ApcState);

            AttachedToProcess = TRUE;

        }

    }

    else

        HandleTable = PsGetCurrentProcess()->ObjectTable;//使用当前进程的句柄表

    

 

   //检查是否可以独占打开,检查权限,若各项检查通过才打开对象,递增句柄计数,调用对象的OpenProcedure等等工作

    Status = ObpIncrementHandleCount(Object,

                                     AccessState,

                                     AccessMode,

                                     HandleAttributes,

                                     PsGetCurrentProcess(),

                                     OpenReason);

    if (!NT_SUCCESS(Status))

        return Status;

    

    NewEntry.ObAttributes |= (HandleAttributes & OBJ_HANDLE_ATTRIBUTES);//填上句柄的属性

    DesiredAccess =AccessState->RemainingDesiredAccess|AccessState->PreviouslyGrantedAccess;

    GrantedAccess = DesiredAccess &(ObjectType->TypeInfo.ValidAccessMask);

    NewEntry.GrantedAccess = GrantedAccess;//填上句柄的属性

    Handle = ExCreateHandle(HandleTable, &NewEntry);//将句柄插入到句柄表中

    if (Handle)//if 插入成功

    {

        if (KernelHandle)

 Handle = ObMarkHandleAsKernelHandle(Handle);//将句柄值的最高位设为1,标记为内核句柄

 

        *ReturnedHandle = Handle;

        if (AttachedToProcess)

 KeUnstackDetachProcess(&ApcState);//撤销挂靠

        return STATUS_SUCCESS;

}

Else

{

       return STATUS_INSUFFICIENT_RESOURCES;

}

}

 

打开对象,以得到一个访问句柄。有四种打开时机:

1、 创建对象时就打开,如CreateFile在创建一个新文件时,就同时打开了那个文件对象

2、 显式打开,如OpenFile,OpenMutex,OpenProcess显式打开某个对象

3、 DuplicateHandle这个API间接打开对象,获得句柄

4、 子进程继承父进程句柄表中的句柄,也可看做是一种打开

在这四种情况下,都会调用这个函数来打开对象,得到一个句柄。OpenReason参数就是指打开原因、时机

注意句柄值的最高位为1,就表示这是一个内核全局句柄,可以在各个进程中通用。否则,一般的句柄,只能在对应的进程中有意义。

另外有两个特殊的伪句柄,他们并不表示‘索引’,而是一个简单的代号值

GetCurrentProcessHandle  返回的句柄值是-1

GetCurrentThreadHandle   返回的句柄值是-2

对这两个句柄要特殊处理。《Windows核心编程》一书专门强调了这两个句柄的使用误区

 

句柄不光含有指向对象的指针,每个句柄都还有自己的访问权限与属性,这也是非常重要的。访问权限表示本次打开操作要求的、申请的并且最终得到的权限。句柄属性则表示本句柄是否可以继承,是否是独占打开的,是否是一个内核句柄等属性。

 

 

 

在驱动程序开发中,经常遇到的下面这个结构:

typedef struct _OBJECT_ATTRIBUTES

{

   ULONG Length;//本结构体的长度

   HANDLE RootDirectory;//相对目录(不一定是父目录)

   PUNICODE_STRING ObjectName;//相对RootDirectory这个目录的剩余路径 或者 全路径

   //上面两个字段一起间接构成对象的全路径

   ULONG Attributes;//对象属性与句柄属性的混合

   PVOID SecurityDescriptor;// SD安全描述符

   PVOID SecurityQualityOfService;

} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;

typedef CONST OBJECT_ATTRIBUTES *PCOBJECT_ATTRIBUTES;

创建对象、打开对象时都会用到这个结构。

 

 

 

下面这个函数用来创建一个指定类型的内核对象

NTSTATUS

ObCreateObject(IN POBJECT_TYPE Type,

               IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,

               IN KPROCESSOR_MODE AccessMode,

               IN ULONG ObjectSize,

               IN ULONG PagedPoolCharge OPTIONAL,

               IN ULONG NonPagedPoolCharge OPTIONAL,

               OUT PVOID *Object)

{

   ObjectCreateInfo = ObpAllocateObjectCreateInfoBuffer(LookasideCreateInfoList);

   Status = ObpCaptureObjectCreateInformation(ObjectAttributes,FALSE,ObjectCreateInfo,

&ObjectName);//提取ObjectAttributes中的字段

   if (!PagedPoolCharge)

        PagedPoolCharge = Type->TypeInfo.DefaultPagedPoolCharge;

   if (!NonPagedPoolCharge)

        NonPagedPoolCharge = Type->TypeInfo.DefaultNonPagedPoolCharge;

   ObjectCreateInfo->PagedPoolCharge = PagedPoolCharge;

        ObjectCreateInfo->NonPagedPoolCharge = NonPagedPoolCharge;

   //从对应池中分配内存,创建对应的对象

   Status = ObpAllocateObject(ObjectCreateInfo,&ObjectName,Type,ObjectSize,AccessMode,

&Header);

   return Status;

}

其实真正的工作函数是ObpAllocateObject,它内部调用

ExAllocatePoolWithTag(ObjectType->PoolType, 可选头总大小+ ObjectSize, Tag)分配对象内存,然后初始化设置头部中的Flags等其他工作。(绝大多数内核对象都分配在非分页池中)

 

 

OBJECT_ATTRIBUTES结构体中的Attributes字段是个混合成员,由句柄属性、对象属性、打开属性复合而成,可以取下面的组合

OBJ_INHERIT://句柄属性,表示句柄是否可继承给子进程

OBJ_PERMANENT://指该对象是否永久存在于对象目录中直到对象销毁.(目录\符号链接\设备\文件 都是)

OBJ_EXLUSIVE://对象属性,指该对象同一时刻只能被一个进程独占打开

OBJ_CASE_INSENSITIVE://打开属性,表示本次打开操作查找比较对象名时大小写不敏感

OBJ_OPENIF://打开属性,表示if对象存在就打开

OBJ_OPENLINK://打开属性,表示本次打开是否可以直接打开符号链接

OBJ_KERNEL_HANDLE://句柄属性,表示要求得到一个内核句柄

 

而对象头中的Flags字段则完全表示对象的一些属性标志

OB_FLAG_CREATE_INFO;//表示头部中含有创建时的属性信息

OB_FLAG_CREATOR_INFO;//表示含有创建者进程信息

OB_FLAG_KERNEL_MODE://表示PreviousMode是内核模式的代码创建的本对象

OB_FLAG_EXCLUSIVE://表示同一时刻只能被一个进程独占打开

OB_FLAG_PERMANET://永久性对象,直到对象完全销毁时才脱离对象目录

OB_FLAG_SINGLE_PROCESS://表示含有每进程的句柄统计信息

OB_FLAG_DEFER_DELETE;//标记本对象被延迟删除了

 

创建的对象,如果有名字,就需要插入到对象目录和句柄表中。即使没有名字,也需要插入到句柄表中,这样才能让应用程序得以找到该对象以进行访问。下面这个函数就是做这个的。

NTSTATUS

ObInsertObject(IN PVOID Object,

               IN PACCESS_STATE AccessState OPTIONAL,

               IN ACCESS_MASK DesiredAccess,

               OUT PHANDLE Handle)//返回得到的句柄

{

    ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);

    ObjectCreateInfo = ObjectHeader->ObjectCreateInfo;

    ObjectNameInfo = ObpReferenceNameInfo(ObjectHeader);

    ObjectType = ObjectHeader->Type;

    ObjectName = NULL;

    if ((ObjectNameInfo) && (ObjectNameInfo->Name.Buffer))

        ObjectName = &ObjectNameInfo->Name;

PreviousMode = KeGetPreviousMode();

//无名对象(并且不需要安全控制)就不挂入对象目录,仅仅插入句柄表中

    if ( (ObjectName==NULL) && !(ObjectType->TypeInfo.SecurityRequired))

    {

        ObjectHeader->ObjectCreateInfo = NULL;

        Status = ObpCreateUnnamedHandle(Object,DesiredAccess,ObjectCreateInfo->Attributes,

                                        PreviousMode,Handle);

        return Status;

    }

 

    InsertObject = Object;

    if (ObjectName)//若是一个有名对象

{

    //这个函数有两种用途。

//当Object不为NULL表示将Object插入到对象目录中的指定位置

    //当Object为NULL表示查找指定的对象目录位置处的对象

    //两种用途都将指定目录位置处的对象返回到InsertObject中

        Status = ObpLookupObjectName(ObjectCreateInfo->RootDirectory,

                                     ObjectName,

                                     ObjectCreateInfo->Attributes,

                                     ObjectType,

                                     ObjectCreateInfo->ParseContext,

                                     Object,//要插入的对象

                                     &InsertObject);//返回最终那个位置处的对象

        //如果原位置处已有同名对象,插入失败

        if ((NT_SUCCESS(Status)) && (InsertObject) && (Object != InsertObject))

        {

            OpenReason = ObOpenHandle;//既然插入失败了,那就是要打开对象,获得句柄

            if (ObjectCreateInfo->Attributes & OBJ_OPENIF)//检查本次打开操作的要求

            {

                if (ObjectType != OBJECT_TO_OBJECT_HEADER(InsertObject)->Type)

                    Status = STATUS_OBJECT_TYPE_MISMATCH;

                else

                    Status = STATUS_OBJECT_NAME_EXISTS;//看到没,应用层经常返回这个出错值        

            }

            else

            {

                Status = STATUS_OBJECT_NAME_COLLISION;

            }

            return Status;

        }

    }

    if (InsertObject == Object)//if 插入成功

        OpenReason = ObCreateHandle;//只有第一次创建对象的时候才会插入对象目录中

    ObjectHeader->ObjectCreateInfo = NULL;//不再需要了

    if (Handle)//如果用户要求插入句柄表,就插入句柄表,得到一个句柄

    {

        Status = ObpCreateHandle(OpenReason,InsertObject,AccessState,

                                 ObjectCreateInfo->Attributes, PreviousMode,Handle);

    }

    return  Status;

}

 

下面看一下驱动程序经常调用的那些内核函数

NTSTATUS

ObReferenceObjectByHandle(IN HANDLE Handle,IN ACCESS_MASK DesiredAccess,

                          IN POBJECT_TYPE ObjectType,IN KPROCESSOR_MODE AccessMode,

                          OUT PVOID* Object,OUT POBJECT_HANDLE_INFORMATION HandleInformation)

{

    *Object = NULL;

    //若句柄是一个内核句柄或当前进程、线程的句柄

    if (HandleToLong(Handle) < 0)

    {

        if (Handle == NtCurrentProcess())//若句柄值是当前进程的句柄(-1),特殊处理

        {

            if ((ObjectType == PsProcessType) || !(ObjectType))

            {

                CurrentProcess = PsGetCurrentProcess();

                GrantedAccess = CurrentProcess->GrantedAccess;

  //if内核模式/要求的权限<=进程对象支持的权限(权限检查)

                if ((AccessMode == KernelMode) ||!(~GrantedAccess & DesiredAccess))

                {

                    if (HandleInformation)

                    {

                        HandleInformation->HandleAttributes = 0;

                        HandleInformation->GrantedAccess = GrantedAccess;

                    }

                    ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentProcess);

                    InterlockedExchangeAdd(&ObjectHeader->PointerCount, 1);//递增引用计数

                    *Object = CurrentProcess;//返回得到的对象指针

                    Status = STATUS_SUCCESS;

                }

                Else //权限检查不通过

                    Status = STATUS_ACCESS_DENIED;    

            }

            else

                Status = STATUS_OBJECT_TYPE_MISMATCH;

            return Status;

        }

        else if (Handle == NtCurrentThread())//若句柄值是当前线程的句柄(-2),特殊处理

        {

            if ((ObjectType == PsThreadType) || !(ObjectType))

            {

                CurrentThread = PsGetCurrentThread();

                GrantedAccess = CurrentThread->GrantedAccess;

                if ((AccessMode == KernelMode) ||!(~GrantedAccess & DesiredAccess))

                {

                    if (HandleInformation)

                    {

                        HandleInformation->HandleAttributes = 0;

                        HandleInformation->GrantedAccess = GrantedAccess;

                    }

                    ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentThread);

                    InterlockedExchangeAdd(&ObjectHeader->PointerCount, 1);

                    *Object = CurrentThread;

                    Status = STATUS_SUCCESS;

                }

                else

                    Status = STATUS_ACCESS_DENIED;

            }

            else

                Status = STATUS_OBJECT_TYPE_MISMATCH;

            return Status;

        }

        else if (AccessMode == KernelMode)//若句柄是一个内核句柄

        {

            Handle = ObKernelHandleToHandle(Handle);//去掉最高位的1,转为普通句柄

            HandleTable = ObpKernelHandleTable;//采用内核句柄表

        }

    }

    Else //最典型的情况,普通句柄,就使用当前进程的句柄表

        HandleTable = PsGetCurrentProcess()->ObjectTable;

    //以该句柄的值为“索引”,找到句柄表中对应的句柄表项

    HandleEntry = ExMapHandleToPointer(HandleTable, Handle)

    if (HandleEntry)//如果找到了,这就是一个有效句柄

    {

        ObjectHeader = ObpGetHandleObject(HandleEntry);//关键。获得该句柄指向的对应对象

        if (!(ObjectType) || (ObjectType == ObjectHeader->Type))

        {

            GrantedAccess = HandleEntry->GrantedAccess;

            if ((AccessMode == KernelMode) ||!(~GrantedAccess & DesiredAccess))//通过权限检查

            {

                InterlockedIncrement(&ObjectHeader->PointerCount);

                Attributes = HandleEntry->ObAttributes & OBJ_HANDLE_ATTRIBUTES;

                if (HandleInformation)

                {

                    HandleInformation->HandleAttributes = Attributes;

                    HandleInformation->GrantedAccess = GrantedAccess;

                }

                *Object = &ObjectHeader->Body;//返回的是对象体的地址

                return STATUS_SUCCESS;

            }

            Else //权限检查没通过

                Status = STATUS_ACCESS_DENIED;

        }

        else

            Status = STATUS_OBJECT_TYPE_MISMATCH;      

    }

    Else  //有可能用户给定的句柄值是一个无效句柄,在句柄表中找不到

        Status = STATUS_INVALID_HANDLE;

    *Object = NULL;

    return Status;

}

 

 

如上,这个函数从句柄得到对应的内核对象,并递增其引用计数。

两个特殊情况:

#define NtCurrentProcess()   (HANDLE)-1

#define NtCurrentThread()   (HANDLE)-2

这是两个伪句柄值,永远获得的是当前进程、线程的内核对象。

另外:若句柄值的最高位是1,则是一个内核句柄,各进程通用。内核型句柄是“System”进程的句柄表中的句柄。因此,要获得内核句柄对应的对象,系统会挂靠到“System”进程的地址空间中,去查询句柄表。

 

根据句柄值在句柄表中找到对应的表项是靠ExMamHandleToPointer这个函数实现的,这个函数又在内部调用ExpLookupHandleTableEntry来真正查找。句柄表组织为一个稀疏数组(目的用来节省内存),但可以

简单的看做一个一维数组,不影响理解,句柄值本身也可简单理解为一个索引。

 

NTSTATUS

ObReferenceObjectByPointer(IN PVOID Object,IN ACCESS_MASK DesiredAccess,

                           IN POBJECT_TYPE ObjectType,IN KPROCESSOR_MODE AccessMode)

{

    POBJECT_HEADER Header;

    Header = OBJECT_TO_OBJECT_HEADER(Object);

    if ((Header->Type != ObjectType) && ((AccessMode != KernelMode) ||

 (ObjectType == ObSymbolicLinkType)))

        return STATUS_OBJECT_TYPE_MISMATCH;

    InterlockedIncrement(&Header->PointerCount);//递增对象的引用计数

    return STATUS_SUCCESS;

}

上面这个函数其实是递增对象的引用计数而已(手握一个引用计数后,就可以防止对象被析构释放,因为对象只有在引用计数减到0后才会释放,从而防止因对象析构引起的莫名其妙的崩溃)

 

 

在对象目录中的查找过程:

给定一个对象名,如“\Device\Harddisk0\Partition0\Dir1\Dir2\File.txt”,如何查找到对应的对象呢?

这个路径先在对象目录中一路找到\Device\Harddisk0\Partition0表示的磁盘卷设备对象,然后再沿着剩余路径“Dir1\Dir2\File.txt”找到对应的文件对象,不过后半部的查找过程是文件系统的事了,后面我将详细讲解。这里看前半部的查找,是如何找到对应的卷设备的。

前文我们讲过了一个函数: ObpLookupEntryDirectory,那个函数用来在指定的目录中找到指定名称的子对象,现在就需要沿着路径,反复调用这个函数找到我们的卷设备。下面的函数就是用来这个目的的。可以给定任意一个起点目录,以及相对那个起点目录的任意长的路径,找到指定的对象。这个函数的代码有点长…(请做好心理准备),原函数差不多有800行长,我做了粗略压缩,在我的详尽解释下,相信您可以看明白的。

 

NTSTATUS

ObpLookupObjectName(IN HANDLE RootHandle OPTIONAL,//搜索的起点对象,如果为NULL,则表示根节点

                    IN PUNICODE_STRING ObjectName,//相对RootHandle的路径或者全路径

                    IN ULONG Attributes,//本次查找操作的属性(如是否忽略大小写)

                    IN POBJECT_TYPE ObjectType,//要查的目标对象的类型

                    IN OUT PVOID ParseContext,//其它

                    IN PVOID InsertObject OPTIONAL,//如果不为NULL,则表示找不到就插入这个对象

                    IN OUT PACCESS_STATE AccessState,//传入当前线程的令牌以及申请的访问权限

                    OUT POBP_LOOKUP_CONTEXT LookupContext,//用来返回查找结果细节信息

                    OUT PVOID *FoundObject)//返回最终找到的对象

{

*FoundObject = NULL;//初始为未找到

PVOID Object=NULL; 

//分别指相对当前起点对象的剩余路径、剩余路径中的第一个节点名

    UNICODE_STRING RemainingName, ComponentName;

BOOLEAN Reparse = FALSE, SymLink = FALSE;//分别表示是否需要重新解析、是否为符号链接的标志

//表示搜索路径中当前的目录,父目录,以及起点目录

POBJECT_DIRECTORY Directory = NULL, ParentDirectory = NULL, RootDirectory;

//表示最终要插入到那个位置的叶目录及父目录

    POBJECT_DIRECTORY ReferencedDirectory = NULL, ReferencedParentDirectory = NULL;

    OB_PARSE_METHOD ParseRoutine;//对象自带的解析函数

    ULONG MaxReparse = 30;//符号链接等,总的重新解析次数最大不能超过30次

NTSTATUS  Status = STATUS_SUCCESS;//预期找到成功

 

    ObpInitializeLookupContext(LookupContext);

    if (!(ObjectType) || (ObjectType->TypeInfo.CaseInsensitive))

        Attributes |= OBJ_CASE_INSENSITIVE;//检查本类对象是否支持对象名可以忽略大小写

        

    if (RootHandle)//如果给定了搜索起点

    {

        Status = ObReferenceObjectByHandle(RootHandle,AccessMode, &RootDirectory,…);

        ObjectHeader = OBJECT_TO_OBJECT_HEADER(RootDirectory);//得到起点对象

        if (ObjectHeader->Type != ObDirectoryType)//if给定的起点对象不是目录

        {

            ParseRoutine = ObjectHeader->Type->TypeInfo.ParseProcedure;

            if (!ParseRoutine)  return STATUS_INVALID_HANDLE;//非目录对象必须自带解析函数

            MaxReparse = 30;

            while (TRUE)

            {

                RemainingName = *ObjectName;//当前的剩余路径=初始路径

                Status = ParseRoutine(RootDirectory,//起点

                                      ObjectType,

                                      AccessState,

                                      AccessCheckMode,

                                      Attributes,

                           IN、OUT    ObjectName,//传入初始全路径,可能传出新的初始路径

                           IN、OUT    &RemainingName,//传入当前剩余路径,返回最终的剩余路径

                                      ParseContext,

                                      &Object);//返回找到的对象

  // if自带解析函数解析完毕了,不要求重新解析

                if ((Status != STATUS_REPARSE) && (Status != STATUS_REPARSE_OBJECT))

                {

                    if (!NT_SUCCESS(Status))//如果未能解析

                        Object = NULL;

                    else if (!Object)//如果未能找到目标对象

                        Status = STATUS_OBJECT_NAME_NOT_FOUND;

                    *FoundObject = Object;//有可能找到的不是最终的目标对象

未经允许不得转载:起风网 » [4]Windows内核情景分析—内核对象
分享到: 生成海报

评论 抢沙发

评论前必须登录!

立即登录