一.前言
Windows中有很多像进程对象、线程对象、文件对象等等这样的对象,我们称之为Windows内核对象。内核对象是系统地址空间中的一个内存块,由系统创建并维护,这个内存对象是一个数据结构,维护着与对象相关的信息(如计数器)。内核对象为内核所拥有,而不为进程所拥有,所以不同进程可以访问同一个内核对象。
二.内核对象结构
每个对象都有对象头和对象体组成。所有类型的对象头结构都是相同的,而结构体部分却各不相同的。下面是内核对象的结构图:
内核对象结构图
图中灰色部分是可能出现的。每个对象中是否存在这些部分主要由OBJECT_HEADER结构中的相关标志来指定。上面的5个结构的格式是固定的;而OBJECT结构体部分却是各个对象各不同的。需要注意的是:指向对象的指针POBJECT是指向对象体部分,而不是指向对象头的。所以,若需要访问OBJECT_HEADER,需要将POBJCECT减去0x18而获得。
下面是OBJECT_HEADER的结构
typedef struct _OBJECT_HEADER
{
DWORD PointerCount; // 指针引用的数目
DWORD HandleCount; // 打开句柄的数目
POBJECT_TYPE ObjectType; //指向类型对象的指针
BYTE NameOffset; //对象名的偏移
BYTE HandleDBOffset; // HANDLE DB的偏移
BYTE QuotaChargesOffset; //QUOTA CHARGES的偏移
BYTE ObjectFlags; // 对象标志
union
{ // 对象标志中OB_FLAG_CREATE_INFO ? ObjectCreateInfo : QuotaBlock
PQUOTA_BLOCK QuotaBlock;
POBJECT_CREATE_INFO ObjectCreateInfo;
};
PSECURITY_DESCRIPTOR SecurityDescriptor;
}OBJECT_HEADER, *POBJECT_HEADER;
三.目录对象
WINDOWS中有20几类无数的内核对象,它们都独立地存在于系统地址空间中。系统利用目录对象将所有的这些对象组织起来。目录对象是一个有37个数组元素组成的哈希(HASH)树。数据结构如下:
Typedef struct _OBJECT_DIRECTORY_ENTY
{
Struct _OBJECT_DIRECTORY_ENTRY *NextEntry;
POBJECT Object
}OBJECT_DIRECTORY_ENTRY, *POBJECT_DIRECTORY_ENTRY,**PPOBJECT_DIRECTORY_ENTRY;
Typedef struct _OBJECT_DIRECTORY
{
POBJECT_DIRECTORY_ENTRY HashTable[37];
POBJECT_DIRECTORY_ENTRY CurrentEntry;
BOOLEAN CurrentEntryValid;
BYTE Reserved1;
WORD Reserved2;
DWORD Reserved3;
}OBJECT_DIRECTORY, *POBJECT_DIRECTORY;
系统将对象名称进行一定的算法得出一个HASH值,算法如下:
//根据名字计算HASH值。
hash = 0;
p = (PSHORT)wStr; //存放名称的一个WCHAR数组
while(*p)
{
Symb = (CHAR)*p;
hash = hash * 3 + (hash >> 1);
if (Symb < 'a') //<a
hash= hash + Symb;
else if (Symb <= 'z') //即 a~z
hash = hash + Symb - 0x20;
else // > z
hash = hash + (CHAR)RtlUpcaseUnicodeChar((WCHAR)*p);
p ++;
}
hash = hash % 37; //最终的hash值。
系统将所有相同HASH值的对象链接到响应的数组项中,于是系统中所有元素将排列成如下的结构图:
系统根目录的对象的指针由ObpRootDirectoryObject来指定。
按理说,系统中只需要一个目录对象就够了,系统中所有的内核对象都将链接在这个目录对象上。但是不知什么原因,系统中并不是这样,系统中存在着多个目录对象,它们以根目录对象为根,组成一个“对象树”。每个目录对象中的哈希树的hash值的计算规则都是一样的。
我们可以根据系统中“对象树”的结构来遍历系统中所有的对象。
四.类型对象
内核对象中还有一种比较特殊的对象——类型对象。系统中每种类型对象只有一个类型对象,也就是说,系统中最多只有20几个类型对象。每种类型的对象都在其对象体中存在一个指向其类型对象的指针,因为一种类型对象只有一个实体,所以每种类型对象的指针都是固定的,这样我们就可以通过对象体中的类型对象指针来判断和访问对象的类型了。
各个类型对象的对象体内并没有链表结构使得它们相互链接起来。但是假如对象头部前面有OBJECT_CREATOR_INFO结构(见下表),则相同类型的对象就可以通过它的成员ObjectList相互链接起来了。但是,不幸的是:缺省情况下,只有Port和WaitPort两中类型的对象有这种结构。所以一般情况下,我们是不能通过类型对象来遍历这个系统中所有对象的。
typedef struct _OBJECT_CREATOR_INFO
{
LIST_ENTRY ObjectList; // OBJECT_CREATOR_INFO
HANDLE UniqueProcessId;
WORD Reserved1;
WORD Reserved2;
}OBJECT_CREATOR_INFO, *POBJECT_CREATOR_INFO, **PPOBJECT_CREATOR_INFO;
五.对象的遍历
上面分析过了,下面可以目录对象的遍历,来进行系统中所有对象的遍历。
//写一个递归函数。用来分析树型目录。
void AnalyseDirectory(POBJECT_DIRECTORY pDirectory, ULONG DirectoryType, int Level)
{
POBJECT_DIRECTORY_ENTRY pDirectoryEntry;
POBJECT_HEADER pObjectHeader;
POBJECT_NAME pObjectName;
PWCHAR wStr[200];
char Space[100]; //为生成空格用的。
int i, j;
for(i = 0; i < 36; i ++) //DIR对象的对象体(BODY)是37个元素的数组。
{
pDirectoryEntry = pDirectory->HashTable[i];
while(pDirectoryEntry)
{
pObjectHeader = (POBJECT_HEADER)((ULONG)pDirectoryEntry->pObject - sizeof(OBJECT_HEADER));
//生成空格
RtlZeroMemory(Space, 100);
for(j = 0; j < 5 * Level; j ++)
Space[j] = ' ';
if (pObjectHeader->NameOffset)
{
pObjectName = (POBJECT_NAME)((ULONG)pObjectHeader - pObjectHeader->NameOffset);
RtlZeroMemory(wStr, 200 * sizeof(WCHAR));
RtlCopyMemory(wStr, pObjectName->Name.Buffer, pObjectName->Name.Length);
DbgPrint("%s pObject: 0x%08X Name: %S", Space, pDirectoryEntry->pObject, wStr);
}else
DbgPrint("%s pObject: 0x%08X Name: noname", Space, pDirectoryEntry->pObject);
//pObject对象是属性对象吗
if ((ULONG)pObjectHeader->pObjectType == DirectoryType)
AnalyseDirectory(pDirectoryEntry->pObject, DirectoryType, Level + 1);
pDirectoryEntry = pDirectoryEntry->NextEntry;
}
}//end of 遍历37个记录
}
六.对象的访问
内核中知道了内核对象的地址就可以直接访问这个内核对象了,但是在用户程序中却不能这样访问。Windows为内核对象的访问提供了一系列的函数。当调用一个用于创建内核对象的函数时,函数调用完便返回一个句柄值。句柄值是进程独立的,一个进程中的句柄值在另一个进程中是无效的。
句柄值是一个进程句柄表的索引。每个进程都有一个进程句柄表,而所有进程的句柄表串成一个句柄表链。这个链的头部地址保存在内核变量HandleTableListHead中。
下面具体看一下句柄表结构。系统将句柄表组织成和线性地址解析一样的结构。句柄表是个三层的表结构,而句柄值也被分成三部分,用来分别索引这三个部分。下面是句柄解析图:
七.总结
本文可以说是一个读书笔记。在参考了很多文章的基础上,然后作一些试验才完成本文的。内核对象是Windows内部的重要数据结构。通过本文可以大致了解Windows是如何组织众多的对象的。
八. 实践
一、进程如何操作内核对象
应用程序通过句柄访问内核对象,多进程需要共享内存对象才能进行跨进程调用句柄。
二、内核对象何时销毁
操作系统内核通过计数器知道有多少进程在使用这个内核对象.(因为进程终止后,内核对象不一定销毁,只有当计数器归0 时,操作系统内核才会销毁内核对象)
三、内核对象安全性
所有创建内核对象的函数都指向一个SECURITY_ATTRIBUTES结构作为参数。
四、理解进程内核对象句柄表
进程在初始化时,系统会为它分配一个句柄表,此句柄表仅供内核对象使用,像用户对象和GUI对象都无法使用,句柄表的结构是由一个数据结构组成的数组,这个数据结构包含指向内核对象的指针、一个访问掩码和一些标志
五、如何创建内核对象
1、一个进程在初始化的时候,句柄表是空的,当进程里面的一个线程调用一个创建内核对象的函数时,操作系统内核就会为这个内核对象分配一个内存块,并且扫描进程的句柄表,找到一个空的记录项,对其进行初始化。这个记录项的指针指针成员将会被设置成内核对象的数据结构的内部内存地址。
2、任何创建内核对象的函数都会返回一个句柄,这个句柄可以由这个进程内的所有线程使用。句柄的值除以4(右移两位),就是内核对象的信息保存在这个进程句柄表中的具体位置,实际是一个索引。
3、因为句柄值实际是一个索引,它需要右移4位才是实际索引值,所以句柄值可能会出现4、8之类的值。如果创建内核对象函数调用失败,那么返回的句柄值是0(NULL)。注意:有几个函数在调用失败后会返回句柄值-1(INVALID_HANDLE _VALUE),所以需要仔细检查创建内存对象函数的返回值是-1还是0。
六、函数如何操作句柄
1、调用一个函数,如果函数的传入参数是句柄,那么这个函数将会根据句柄查找进程的句柄表,通过句柄表的成员指针,获得内存对象的地址,再进行操纵内存对象。
2、如果作为传入参数的句柄是无效的,GetLastError会返回6(ERROR_INVALID_HANDLE)。
七、为什么句柄不能跨进程调用
1、这个句柄的值是作为进程句柄表中的索引来使用的,所以这个句柄是与当前进程相关的,其他进程无法使用。
2、如果跨进程调用句柄,那么实际引用到的只是这个进程句柄表中的位于同一个索引项的内核对象———只是索引值相同而已
八、创建内存对象函数调用失败的原因
1、系统内存不足。
2、遇到一个安全问题
九、关闭内核对象
1、无论是什么方式创建内核对象的,都需要用CloseHandle向系统表明我们已经结束使用对象。
2、在这个CloseHandle 函数内部,首先检查主调进程的句柄表,验证“传给CloseHandle函数的句柄值 ”标识的是“主调进程确实有权访问的一个对象”。如果这个句柄是有效的,那么操作系统内核将根据进程的句柄表获得内核对象的数据结构的地址,并将结构中的“使用计数”成员递减,如果计数为0,内核对象将被销毁名并且从内存中去除。
3、如果传给CloseHandle 函数的是一个无效句柄,(1)如果进程是正常运行的,CloseHandle将会返回FLASE,而GetLastError返回ERROR_INVALID_HANDLE。(2)如果进程正在被调试,那么系统将会抛出0xC0000008异常(“指定了无效句柄”)。
九.参考
1.《Undocumented Windows 2000 Secrets》
2.Anathema《Inside Windows Nt Object Manager》
3.webcrazy《剖析Windows NT/2000内核对象组织》
4.《Inside Windows 2000》
5.《Windows核心编程》
6. WINDOWS内核对象_misterliwei的博客-CSDN博客
7. windows 内核对象理解_AndyQiu222的博客-CSDN博客
注意:本节描述的句柄是再WIN2K下的句柄.WINXP下句柄表结构已经完全不同.