剖析Windows NT/2000内核对象组织
WebCrazy(http://webcrazy.yeah.net/)
对象管理器在Windows NT/2000内核中占了极其重要的位置,其一个最主要职能是组织管理系统内核对象。在Windows NT/2000中,内核对象管理器大量引入了C++面向对象的思想,即所有内核对象都封装在对象管理器内部,除对象管理器自己以外,对其他所有想引用内核对象结构成员的子系统都是不透明的,也即都需通过对象管理器访问这些结构。Microsoft极力推荐内核驱动代码遵循这一原则(用户态代码根本不能直接访问这些数据),她提供了一系列以Ob开头的例程供我们使用。但是也不是说只有对象管理器即这些函数才能访问这些数据。我下面提供的代码则直接存取这些结构,这种情况下,我们只能祈祷Microsoft永远保持这些对象头,对象体结构的不变,这一般不大可能,所以我提供的代码只供学习Windows NT/2000内核之用,无其它实际应用意义。
先谈谈内核已命名对象吧,命名对象存于系统全局命名内核区,与传统的DOS目录与文件组织方式相似,对象管理器也采用树状结构管理这些对象,这样即可快速检索内核对象。这个优点很容易体现,对象检索速度快,这样会大大提高系统性能,举个例子吧:譬如进程A某个时刻已经使用某个文件,即A在内核中拥有了指向这个文件的一个内核对象(FILE_OBJECT),而如果这时另一进程B也试图访问这个文件,则B通过CreateFile API等打开这个文件时,系统指引对象管理器查找对象存储空间,如已存在此对象,则查看对象头,是否已独占访问,假如是的话,则会出现失败,否则的话,增加此对象的引用计数或句柄计数。系统中这种搜索对象的情况无处不在,因为Windows NT/2000中将所有的需SECURITY_DESCRIPTOR等 结构进行保护的可操作的数据结构都当作对象处理,如常见的进程对象(EPROCESS/KPEB)、线程对象(ETHREAD/KTEB)、驱动程序对象(DRIVER_OBJECT)等等。当然这种树状结构组织内核已命名对象,还有另一个优点,就是使所有已命名对象组织的十分有条理,如设备对象处于\Device下,而对象类型名称处于\ObjectTypes下等等。再者这样也能达到用户态进程仅能访问\??与\BaseNamedObjects下的对象,而内核态代码则没有任何限制的目的。至于系统内部如何组织管理这些已命名对象,其实Windows NT/2000内部由内核变量ObpRootDirectoryObject指向的Directory对象指向根目录,使用哈希表(HashTable)来组织管理这些命名内核对象。先看看I386KD的分析,再让我们来看看我写的代码吧。
kd> !object \
Object: 8148e210 Type: (814c5820) Directory
ObjectHeader: 8148e1f8
HandleCount: 0 PointerCount: 39
Directory Object: 00000000 Name: \
99 symbolic links snapped through this directory
HashBucket[ 00 ]: 8148a350 Directory 'ArcName'
814a8f10 Device 'Ntfs'
HashBucket[ 01 ]: e2390040 Port 'SeLsaCommandPort'
HashBucket[ 03 ]: e1012030 Key '\REGISTRY'
HashBucket[ 06 ]: e1394560 Port 'XactSrvLpcPort'
HashBucket[ 07 ]: e13682e0 Port 'DbgUiApiPort'
HashBucket[ 09 ]: 84305760 Directory 'NLS'
.
.
.
kd> !object 814a8f10
Object: 814a8f10 Type: (814b5ac0) Device
ObjectHeader: 814a8ef8
HandleCount: 0 PointerCount: 2
Directory Object: 8148e210 Name: Ntfs
实现代码如下:
//-----------------------------------------------
//
// Dump Windows 2000 Kernel Object
// Only test on Windows 2000 Server Chinese Edition
// Build 2195(Free)!Programmed By WebCrazy
// (tsu00@263.net ) on 11-04-2000!
// Welcome to http://webcrazy.yeah.net !
//
//-----------------------------------------------
ULONG ObpRootDirectoryObject=0x8148e210; //fetch from symbol file
void DumpDirectoryObject(PVOID DirectoryObject)
{
ULONG HashBucket;
ULONG *Hash;
for(HashBucket=0;HashBucket<=0x24;HashBucket++)
{
Hash=(ULONG *)((ULONG)DirectoryObject+HashBucket*4);
if(*Hash==0) continue;
DbgPrint("\n HashBucket[%02X]\n",HashBucket);
do
{
PUNICODE_STRING ObName,ObTypeName;
PVOID Object,ObPreHeader,ObStandardHeader;
Hash=(ULONG *)(*Hash);
Object=(PVOID)(*(ULONG *)((ULONG)Hash+4));
ObPreHeader=(PVOID)((ULONG)Object-0x28);
ObStandardHeader=(PVOID)((ULONG)Object-0x18);
ObName=(PUNICODE_STRING)((ULONG)ObPreHeader+4);
DbgPrint("\tDump Object:%08X\n",(ULONG)Object);
DbgPrint("\t Name:%S",ObName->Buffer);
ObTypeName=(PUNICODE_STRING)((ULONG)(*(ULONG *)((ULONG)ObStandardHeader+8))+0x40);
DbgPrint("\t Type:%S(%08X)\n",ObTypeName->Buffer,
*(ULONG *)((ULONG)ObStandardHeader+8));
DbgPrint("\t PointerCount:%d HandleCount:%d\n",*(ULONG *)ObStandardHeader,
*(ULONG *)((ULONG)ObStandardHeader+4));
}while(*Hash!=0);
}
}
void DumpObject(PVOID Object)
{
PUNICODE_STRING ObName,ObTypeName;
UNICODE_STRING temp;
PVOID ObPreHeader=(PVOID)((ULONG)Object-0x28),
ObStandardHeader=(PVOID)((ULONG)Object-0x18);
if(((USHORT)NtBuildNumber)!=2195){
DbgPrint("Only test on Windows 2000 Server Build 2195!\n");
return;
}
ObName=(PUNICODE_STRING)((ULONG)ObPreHeader+4);
DbgPrint(" Dump Object:%08X\n Name:%S",(ULONG)Object,ObName->Buffer);
ObTypeName=(PUNICODE_STRING)((ULONG)(*(ULONG *)((ULONG)ObStandardHeader+8))+0x40);
DbgPrint(" Type:%S(%08X)\n",ObTypeName->Buffer,*(ULONG *)((ULONG)ObStandardHeader+8));
DbgPrint(" PointerCount:%d HandleCount:%d\n",*(ULONG *)ObStandardHeader,
*(ULONG *)((ULONG)ObStandardHeader+4));
RtlInitUnicodeString(&temp,L"Directory");
if(!RtlCompareUnicodeString(&temp,ObTypeName,FALSE))
DumpDirectoryObject(Object);
}
void DumpRootDirectoryObject()
{
DumpObject((PVOID)ObpRootDirectoryObject);
}
看过上面Windbg的输出或是用过Softice的objdir命令的人,应该都知道代码的用处,所以我就不加以介绍。这段代码直接读取系统全局命名对象区,没有考虑同步等,而且在引用对象成员时也没有对PointerCount进行加一,另外目前也没有考虑OBJECT_TYPE中定义的CallBack函数,这样在用户驱动代码或操作系统挂接一些额外的应用时,可能会导致错误,不过因为代码中我对对象区的数据结构有了较为清楚的定义,对理解Windows NT/2000的内核对象组织较有帮助,所以我就将它列出(这段代码我理解了很久后才写的)。代码中我使用PVOID指针,且将指针转化为ULONG类型,直接将结构的偏移地址进行相加。不使用如(ULONG *)Ob++这种语法,这样您可以直接从代码中提取一些结构重要成员的偏移,但这也造成代码不够简洁。关于ObpRootDirectoryObject这个最重要的Windows NT/2000内核变量,我直接从Symbols文件中提取,您必须根据实际情况予以调整。或者使用ObReferenceObjectByName获得。
其实现在已经有很多应用程序可以实现这种列举对象的功能,除了上面的windbg/i386kd的!object、SoftICE的objdir命令,另外还有DDK中的objdir.exe、Mark Russinovich的winobj.exe等等。如果你搜索命名对象存储区域,就可以实现系统设备(Device)对象与驱动程序(Driver)对象的枚举,即可实现SoftICE中device与driver命令的功能。必须指出的是,不是所有的device对象都位于\Device下,也不是所有的driver对象都位于\Driver下,所以必须通过递归调用DumpDirectoryObject寻找整个空间,我不知道为什么Microsoft不限制device与driver对象的存放位置。
以上谈及的命名内核对象可在进程间共享,这也是Windows NT/2000中LPC(Local Procedure Call)的最基本原理,LPC使用到内核中称为Port的内核对象。为什么命名对象能在进程间共享,而以下要谈及的未命名对象却不能在进程间共享呢?通过我的理解,我觉得既然两者都位于系统内核4G内存的高端,理论上应该均可以在进程间共享。只是因为Microsoft提供诸如ObReferenceObjectByName直接根据对象名查找对象,而她提供的ObReferenceObjectByHandle却没有指定进程的参数,所以未命名对象在这种程度上是不能共享的。另外,Microsoft不提供这种参数的另一个最主要的原因我想应该是为了系统更健壮、更稳定。下面我就来说说“句柄”这种管理未命名内核对象的机制吧。
为什么要引入“句柄”的概念呢?为防止用户代码对内核对象造成破坏,Windows NT/2000中通过“句柄”这一机制避免将内核对象指针直接返回给用户进程,而“句柄”除了此功能外,它还管理着进程特定拥有的所有内核对象,当然也包含未命名对象。SoftICE中的proc命令加-o选项可以得到特定进程拥有的所有句柄。但在检索某一指定句柄时,SoftICE在使用过程中也感到不便,我曾试着使用Numega提供的KD2SYS.EXE将Windbg Extension DLL转化成SoftICE使用的.SYS文件,但在使用过程中也经常出现问题。后来,在分析了ObReferenceObjectByHandle内核例程后,我在SoftICE中定义了如下的宏(关于ObReferenceObjectByHandle分析过程与代码我不列于此,您可以仔细认真跟踪跟踪):
// 定义宏handle
// handle <kpeb> <handle index>
// 参数kpeb指定特定进程,handle index是句柄索引
:macro handle="addr %1;what (@(@(@(@(@(%1+128)+8)))+(%2>>2)*8))|80000000;."
Macro: 'handle' defined
// 显示System进程中handle index为4的句柄对象指针与对象类型名
:handle dword(@PsInitialSystemProcess) 4
The value 8148EBC8 is (a) Kernel Process object (handle=0004) for System(08)
-------- --------------------- ---- -------------
|_句柄头地址 |_句柄对应对象的类型 | |_指明此句柄隶属System进程
|_handle index
这个宏我仅在Microsoft Windows 2000 Server Build 2195 Chinese Edition上测试通过。如上显示输出结果,其只显示对象指针与对象类型名。要查看对象更详细的资料您只能深入内核对象头与对象体结构或是使用Windbg的!handle命令。这个宏已经将进程的句柄表的结构说明的很清楚了,而且对象的很多结构在我上面提供的代码中也有应用到,我就不将具体实现代码列出了,建议看看Mark Russinovich的HandleEx。
关于标题所提到的Windows NT/2000内核对象组织还有非常多的内容,如内核对象的安全性设置等等,限于水平与精力,我也不可能在此都写出。另外应该提出的是本文未涉及用户模式的GDI对象等,其在组织上也有另一套的方式。在对Windows NT/2000的分析过程中,我初步感觉到Microsoft设计Windows NT/2000的许多先进思想,也越来越体会到分析的难度。很希望您在有所心得后,也能与大家交流交流,或是联系我(tsu00@263.net)!
参考资料:
1.David Solomom《Inside Windows NT,2nd Edition》
2.Mark Russinovich相关文档