刘收获

导航

内核对象

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的产生时机。简单记忆【柄完清理,引完关闭】

posted on 2018-01-22 20:00  沉疴  阅读(1247)  评论(3编辑  收藏  举报