Windows 中的许多功能都基于各种内核对象。一个这样的对象是目录,不要与文件系统中的目录混淆。Directory 对象在概念上很简单:它是其他内核对象(包括其他 Directory 对象)的容器,从而创建内核的对象管理器用来管理命名对象的层次结构。
原文链接:https://scorpiosoftware.net/2022/12/13/unnamed-directory-objects/
这种安排可以很容易地看到像Sysinternals的WinObj这样的工具:
WinObj 的左侧显示对象管理器目录,其中“存储”了命名对象,并且可以按名称定位。足够清晰和简单。
但是,目录对象可以是未命名的,也可以是命名的。这怎么可能?这是我的对象资源管理器工具(我的系统资源管理器工具也提供了类似的功能)。其中一个视图是所有对象类型及其某些属性(如名称、类型索引、对象和句柄数、对象和句柄的峰值数、泛型访问映射以及从中分配它们的池类型)的“统计”视图。
如果右键单击目录对象类型并选择“所有对象”,您将看到另一个视图,其中显示了系统中的所有目录对象(不一定是全部,但大多数*)。
如果滚动一下,您将看到许多没有名称的未命名目录对象:
这似乎很奇怪,因为没有名称的目录没有意义。然而,这些目录是“真实的”,并且有一个重要目的 – 管理私有对象命名空间。几年前,我在博客上写过关于私有对象命名空间的文章(不幸的是,它在我的旧博客站点中丢失了),但这是它的要点:
对象名称非常有用,因为它们允许在进程之间轻松共享。例如,如果两个或多个进程想要共享内存,它们可以创建一个内存映射文件对象(在内核中称为 section),其名称都是它们都知道的。使用相同名称调用 CreateFileMapping(或其变体之一)将创建对象(由第一个调用方),后续调用方将获取现有对象的句柄,因为它是按名称查找的。
这既简单又有用,但有一个可能的陷阱:由于名称是使用工具或 API “可见”的,因此其他进程可以通过使用该可见名称获取自己的句柄并恶意或意外地“干预”对象来“干扰”对象。
这个问题的解决方案在Windows Vista中以私有对象命名空间的想法出现。一组协作进程可以创建一个只有它们才能使用的私有命名空间,受“机密”名称保护,更重要的是边界描述符保护。细节超出了本文的范围,但都记录在Windows API函数中,如CreateBoundaryDescriptor,CreatePrivateNamespace和friends。下面是使用这些 API 创建包含节对象的私有命名空间的示例(省略错误处理):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
HANDLE hBD = ::CreateBoundaryDescriptor(L"MyDescriptor", 0); BYTE sid[SECURITY_MAX_SID_SIZE]; auto psid = reinterpret_cast<PSID>(sid); DWORD sidLen; ::CreateWellKnownSid(WinBuiltinUsersSid, nullptr, psid, &sidLen); ::AddSIDToBoundaryDescriptor(&m_hBD, psid); // create the private namespace hNamespace = ::CreatePrivateNamespace(nullptr, hBD, L"MyNamespace"); if (!hNamespace) { // maybe created already? hNamespace = ::OpenPrivateNamespace(hBD, L"MyNamespace"); namespace"); } HANDLE hSharedMem = ::CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, 1 << 12, L"MyNamespace\\MySharedMem")); |
此代码片段取自 Windows 10 系统编程第 1 部分书籍中的私人共享代码示例。
如果运行此演示应用程序,并在进程资源管理器或对象资源管理器等工具中查看上述代码中生成的句柄 (),您将看到未给出对象的名称:hSharedMem
不显示全名,无法从用户模式检索。即使可以以某种方式定位它,边界描述符也能提供进一步的保护。让我们在内核调试器中检查此对象。从对象的属性中复制其地址:
将地址粘贴到本地内核调试器中 – 首先使用通用 !object 命令:
1 2 3 4 5 6 |
lkd> !object 0xFFFFB3068E162D10 Object: ffffb3068e162d10 Type: (ffff9507ed78c220) Section ObjectHeader: ffffb3068e162ce0 (new version) HandleCount: 1 PointerCount: 32769 Directory Object: ffffb3069e8cbe00 Name: MySharedMem |
名称在那里,但目录对象也在那里。让我们检查一下:
1 2 3 4 5 6 7 8 9 |
lkd> !object ffffb3069e8cbe00 Object: ffffb3069e8cbe00 Type: (ffff9507ed6d0d20) Directory ObjectHeader: ffffb3069e8cbdd0 (new version) HandleCount: 3 PointerCount: 98300 Hash Address Type Name ---- ------- ---- ---- 19 ffffb3068e162d10 Section MySharedMem |
此目录中有一个对象。目录的名称是什么?我们需要检查对象标头 – 它的地址在上面的输出中给出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
lkd> dt nt!_OBJECT_HEADER ffffb3068e162ce0 +0x000 PointerCount : 0n32769 +0x008 HandleCount : 0n1 +0x008 NextToFree : 0x00000000`00000001 Void +0x010 Lock : _EX_PUSH_LOCK +0x018 TypeIndex : 0x53 'S' +0x019 TraceFlags : 0 '' +0x019 DbgRefTrace : 0y0 +0x019 DbgTracePermanent : 0y0 +0x01a InfoMask : 0xa '' +0x01b Flags : 0 '' +0x01b NewObject : 0y0 +0x01b KernelObject : 0y0 +0x01b KernelOnlyAccess : 0y0 +0x01b ExclusiveObject : 0y0 +0x01b PermanentObject : 0y0 +0x01b DefaultSecurityQuota : 0y0 +0x01b SingleHandleEntry : 0y0 +0x01b DeletedInline : 0y0 +0x01c Reserved : 0x301 +0x020 ObjectCreateInfo : 0xffff9508`18f2ba40 _OBJECT_CREATE_INFORMATION +0x020 QuotaBlockCharged : 0xffff9508`18f2ba40 Void +0x028 SecurityDescriptor : 0xffffb305`dd0d56ed Void +0x030 Body : _QUAD |
获取内核的对象名称有点棘手,这里不会完全描述。第一个要求是成员必须设置位 1,因为这表示存在名称。由于它不是(值为 0xa 或 10 十进制),因此此目录没有名称。我们可以通过查看给定对象原始地址的真实数据结构来更详细地检查目录对象:InfoMask
1 2 3 4 5 6 7 8 9 |
kd> dt nt!_OBJECT_DIRECTORY ffffb3069e8cbe00 +0x000 HashBuckets : [37] (null) +0x128 Lock : _EX_PUSH_LOCK +0x130 DeviceMap : (null) +0x138 ShadowDirectory : (null) +0x140 NamespaceEntry : 0xffffb306`9e8cbf58 Void +0x148 SessionObject : (null) +0x150 Flags : 1 +0x154 SessionId : 0xffffffff |
有趣的部分是成员,它不是 NULL。这表示此目录的用途:作为私有命名空间对象的容器。您也可以单击并在此处找到单个部分对象。NamespaceEntry
HasBuckets
返回到进程资源管理器,启用未命名的对象句柄(“视图”菜单,显示未命名的句柄和映射)并查找未命名的目录对象:
目录的地址与我们查看的地址相同!
指针指向当前未随符号一起提供的未记录结构。但是,只要稍微超出目录的对象结构就会显示一个提示:NamespaceEntry
1 2 3 4 5 6 7 8 9 10 |
lkd> db ffffb3069e8cbe00+158 ffffb306`9e8cbf58 d8 f9 a3 55 06 b3 ff ff-70 46 12 66 07 f8 ff ff ...U....pF.f.... ffffb306`9e8cbf68 00 be 8c 9e 06 b3 ff ff-48 00 00 00 00 00 00 00 ........H....... ffffb306`9e8cbf78 00 00 00 00 00 00 00 00-0b 00 00 00 00 00 00 00 ................ ffffb306`9e8cbf88 01 00 00 00 02 00 00 00-48 00 00 00 00 00 00 00 ........H....... ffffb306`9e8cbf98 01 00 00 00 20 00 00 00-4d 00 79 00 44 00 65 00 .... ...M.y.D.e. ffffb306`9e8cbfa8 73 00 63 00 72 00 69 00-70 00 74 00 6f 00 72 00 s.c.r.i.p.t.o.r. ffffb306`9e8cbfb8 02 00 00 00 18 00 00 00-01 02 00 00 00 00 00 05 ................ ffffb306`9e8cbfc8 20 00 00 00 21 02 00 00-00 00 00 00 00 00 00 00 ...!........... |
名称“MyDescriptor”清晰可见,这是上述代码中边界描述符的名称。
内核调试器的文档指示带有 -p 开关的 !object 命令应显示私有命名空间。但是,这将失败:
1 2 3 |
lkd> !object -p 00000000: Unable to get value of ObpPrivateNamespaceLookupTable |
调试器似乎无法定位全局内核变量。这可能是调试器命令中的一个错误,因为自从在 Windows 10 版本 1607 中引入服务器接收器以来,对象命名空间范围已更改(例如,Docker 在运行 Windows 容器时使用这些接收器)。每个思洛存储器都有自己的对象管理器命名空间,因此旧的全局变量不再存在。我怀疑微软尚未更新此命令开关以支持孤岛。即使没有运行服务器思洛存储器,主机也被视为位于其自己的(全局)思洛存储器中,称为主机思洛存储器。您可以使用 !silo 调试器命令查看其详细信息:
1 2 3 4 5 6 |
kd> !silo -g host Server silo globals fffff80766124540: Default Error Port: ffff950815bee140 ServiceSessionId : 0 OB Root Directory : State : Running |
单击“服务器思洛存储器全局”链接,显示更多详细信息:
1 2 3 4 5 6 7 8 9 |
kd> dx -r1 (*((nt!_ESERVERSILO_GLOBALS *)0xfffff80766124540)) (*((nt!_ESERVERSILO_GLOBALS *)0xfffff80766124540)) [Type: _ESERVERSILO_GLOBALS] [+0x000] ObSiloState [Type: _OBP_SILODRIVERSTATE] [+0x2e0] SeSiloState [Type: _SEP_SILOSTATE] [+0x310] SeRmSiloState [Type: _SEP_RM_LSA_CONNECTION_STATE] [+0x360] EtwSiloState : 0xffff9507edbc9000 [Type: _ETW_SILODRIVERSTATE *] [+0x368] MiSessionLeaderProcess : 0xffff95080bbdb040 [Type: _EPROCESS *] [+0x370] ExpDefaultErrorPortProcess : 0xffff950815bee140 [Type: _EPROCESS *] <truncated> |
ObSiloState
是与对象管理器相关的根对象。单击此按钮显示:
1 2 3 4 5 6 7 |
lkd> dx -r1 (*((ntkrnlmp!_OBP_SILODRIVERSTATE *)0xfffff80766124540)) (*((ntkrnlmp!_OBP_SILODRIVERSTATE *)0xfffff80766124540)) [Type: _OBP_SILODRIVERSTATE] [+0x000] SystemDeviceMap : 0xffffb305c8c48720 [Type: _DEVICE_MAP *] [+0x008] SystemDosDeviceState [Type: _OBP_SYSTEM_DOS_DEVICE_STATE] [+0x078] DeviceMapLock [Type: _EX_PUSH_LOCK] [+0x080] PrivateNamespaceLookupTable [Type: _OBJECT_NAMESPACE_LOOKUPTABLE] |
PrivateNamespaceLookupTable
是此思洛存储器的私有命名空间的根对象(在此示例中为主机思洛存储器)。
欢迎有兴趣的读者进一步深入研究。
如果将专用命名空间列表提升运行并启用了本地内核调试,则专用命名空间列表随 WinObjEx64 工具一起提供,因为它使用内核调试器的驱动程序读取内核内存。
* 大多数对象,因为对象资源管理器的工作方式是枚举句柄并将它们与对象关联。但是,某些对象使用来自内核的引用来保存,句柄为零。对象资源管理器无法检测到此类对象。