Windows内核原理与实现读书笔记(2)注册表和配置管理器,事件追踪,安全性管理(未完)

注册表和配置管理器

Windows系统很多组件都是可以配置的,内核组建通常支持一些参数,甚至有些完全依赖于系统配置信息。例如I/O管理器和即插即用管理器在初始化阶段根据系统设置来例句和加载设备驱动程序。Windows操作系统提供了一个称为“注册表”的中心存储设施来作为系统的配置和管理中心。应用程序和捏合通过访问注册表来读写设置Windows同时提供API供访问注册表,API接到注册表访问请求,转发给系统服务。在内核中,执行体包含一个称为“配置管理器(configuration manager)”组件,是注册表的真正实现。注册表由一组称为储巢(hive)的文件构成,每个储巢内部包含一个树形层次结构,每个储巢可以想象成一个文件系统。

windows注册表是树状结构,每个节点是一个键值。注册表值可以多种类型,绝大多说注册表值类型为REG_DWORD(32位整数),REG_BINARY(二进制数据)和REG_SZ(字符串)。还有REG_LINK(符号链接,执行另一个键或值)。

Windows内核原理与实现读书笔记(2)注册表和配置管理器,事件追踪,安全性管理(未完)

除了HKEY_PERFORMANCE_<XXX>以外,在其他的5个根键中,真正存放系统设置信息的子树是HKLM和HKU。HKLM存放有关系统全局的信息,包括5个子键,分别为HARDWARE(硬件设置)、SAM(本地账户和组的信息)、SECURITY(系统全局范围的安全策略和用户权限设置)、SOFTWARE(系统中的全局配置信息,在系统引导时不需要)和SYSTEM(系统中的全局配置信息,在系统引导时需要,包括设备驱动程序和系统服务等)。HKU为系统中每个加载过的用户轮廓包含一个子键,也包含一个名为.DEFAULT的子键,这是系统的默认轮廓,当登录进程winlogon.exe为第一次登录到系统中的用户创建轮廓时将以此为基础。

Windows内核原理与实现读书笔记(2)注册表和配置管理器,事件追踪,安全性管理(未完)

关于注册表存储结构,注册表是由一组储巢构成的,每个储巢包含了一个由键和值构成的层次结构。上图列出了Windows Server 2003系统中各个储巢的注册表路径和文件路径。一个系统的储巢列表存放在HKLM\SYSTEM\CurrentControlSet\Control\hivelist键下,如下图所示。当系统初始化时,HKLM\SYSTEM总是先被加载进来,然后配置管理器找到hivelist键,继而加载其他储巢,并创建注册表根键,将这些储巢链接起来,从而建立起完整的注册表结构。

Windows内核原理与实现读书笔记(2)注册表和配置管理器,事件追踪,安全性管理(未完)

 储巢的内部结构类似于一个文件系统,而储巢相当于是一个磁盘分区。储巢的基本分配单元称为块(block),类似于文件系统定义的簇(cluster)。当储巢为了存储新的数据而需要扩展时,它总是按照块的粒度来增长。在Windows中,注册表的块的大小为4KB(4096B)。储巢的第一个块称为基本块,它包含了储巢文件标识、最新***、最后一次写操作的时间戳、储巢格式的版本号、校验和,以及储巢的内部文件名。储巢中的注册表数据是按照巢室(cell)来组织的。巢室可大可小,具体取决于它的类型和数据,每个巢室可以存放一个键、值、安全描述符、子键列表或者值列表,对应的巢室分别称为键巢室、值巢室、安全描述符巢室、子键列表巢室和值列表巢室。巢室在储巢文件中的偏移称为该巢室的索引(cell index),其他巢室可以利用此巢室索引来引用它,从而建立起巢室之间的关系。

配置管理器使用了一种类似于Intel x86处理器的页表映射的做法来解决巢室地址转译,一个32位的巢室索引被分成四个组成部分:存储类型、巢室目录索引、巢室表索引和块内偏移。存储类型有两种可能:稳定的(stable,最高位用0表示)和易失的(volatile,最高位用1表示)。每个储巢在内存中有两个巢室目录,分别对应于稳定的和易失的配置数据;每个巢室目录有1024项,每一项指向一个巢室表;每个巢室表包含512个表项,每一项指向一个块。由于配置管理器用巢箱来管理内存分配,而巢箱总是以块为边界(4KB),所以,巢室索引的最后12位指定了一个巢室在块内的偏移。基于这样的巢室索引结构,配置管理器将只为每个储巢映射那些需要用到的巢箱,而不是所有的巢箱。巢室目录和巢室表仍然占用换页内存池的空间,但通常情况下,相比于整个储巢文件,它们要小得多。配置管理器通过这种巢室映射的做法,可有效地降低注册表数据的内存使用量。

Windows内核中配置管理器的实现

配置管理器是执行体中的组件,它的实现依赖于内存管理器和缓存管理器(以及文件系统),这意味着它必须要在这些组件初始化以后才能正常工作;然而,在系统初始化的早期(比如I/O子系统的初始化),Windows已经需要使用注册表中的配置信息了,但此时配置管理器尚未被初始化。Windows的做法是,在内核初始化以前,内核加载器(ntldr)已经将整个HKLM\SYSTEM储巢作为一个只读文件加载到了内存中,因而配置管理器在完全初始化以前只需直接把巢室索引加上该储巢的内存映像地址,就可以得到巢室的内存地址。这一做法有一个限制,即,在配置管理器完全初始化以前,系统只能访问HKLM\SYSTEM中的设置,换句话说,Windows必须把初始化早期用到的各种设置存放在HKLM\SYSTEM中。

配置管理器和注册表的初始化过程

配置管理器建立起完全的注册表视图分三个阶段来完成:第一,在内核初始化阶段,建立起HKLM\SYSTEM和HKLM\HARDWARE储巢;第二,由会话管理器(smss.exe进程)建立起HKLM\SAM、HKLM\SECURITY、HKLM\SOFTWARE和HKU\.DEFAULT储巢;第三,当加载用户轮廓时建立起HKU\<用户的SID>储巢,这是由登录进程(winlogon.exe)来完成的。这里第一阶段可以看做配置管理器的初始化,以及注册表的临时初始化;第二阶段可以看做注册表中系统部分的初始化;第三阶段可以看做注册表中用户部分的初始化。

首先来看第一阶段的初始化,它发生在一个关键点上:在内核初始化过程中,在对象管理器和缓存管理器初始化以后,但在I/O子系统初始化以前。内核在这个点以前,不能访问注册表中的任何信息;而在这个点以后,可以访问HKLM\SYSTEM和HKLM\HARDWARE中的设置。执行这一初始化过程的函数为CmInitSystem1,它是在内核初始化过程中由Phase1InitializationDiscard函数调用的。

CmInitSystem1函数(参见base\ntos\config\cmsysini.c文件)负责完成以下事项:

初始化配置管理器的全局变量,包括各种链表和同步对象。

创建注册表键的类型对象CmpKeyObjectType,CmInitSystem1通过调用CmpCreateObjectTypes函数来完成。

创建主储巢CmpMasterHive,这是一个易失储巢,代表了注册表的根。创建储巢的函数为CmpInitializeHive。

用CmpCreateRegistryRoot函数建立起注册表的根:在主储巢中创建节点“\REGISTRY”,并创建一个键对象指向该节点,然后将该对象插入到对象名字空间的根下面。

调用NtCreateKey函数创建“\REGISTRY\MACHINE”和“\REGISTRY\USER”节点。

调用CmpInitializeSystemHive函数创建系统储巢。在CmpInitializeSystemHive函数中,它根据ntldr传递进来的已加载的原始SYSTEM储巢映像,来初始化内存中的SYSTEM储巢。CmpInitializeSystemHive函数调用CmpInitializeHive来初始化SYSTEM储巢,并调用CmpLinkHiveToMaster将它链接到主储巢中。

调用CmpCreateControlSet函数,根据加载信息创建符号链接“\Registry\Machine\System\CurrentControlSet”。

调用CmpInitializeHive,创建HARDWARE储巢,这是一个易失储巢。然后调用CmpLinkHiveToMaster将它链接到主储巢中。

接下来,利用加载块参数,将有关当前这次引导的信息写到注册表中:

  • 调用CmpInitializeHardwareConfiguration,创建“\Registry\Machine\Hardware”节点,并且把硬件信息设置到注册表中。
  • 调用CmpInitializeMachineDependentConfiguration函数,把与机器相关的配置数据设置到注册表HARDWARE储巢中。
  • 调用CmpSetSystemValues,将这次系统启动的信息写到注册表中。
  • 调用CmpSetNetworkValue,将这次启动的网络信息写到注册表中。

因此,CmInitSystem1函数将注册表结构初步建立起来,它构造了主储巢、HKLM\SYSTEM和HKLM\HARDWARE三个储巢,并且也建立起与这次启动有关的符号链接和配置信息,为系统的进一步初始化提供了基本的配置信息。

再来看注册表的进一步初始化。数组CmpMachineHiveList包含6个储巢,对应于表2.6中的前6个储巢。这些储巢(包括HKLM\SYSTEM和HKLM\HARDWARE)是由会话管理器进程(smss.exe)通过NtInitializeRegistry系统服务加载和初始化的。在一次正常启动过程中,它调用CmpCmdInit函数执行注册表的进一步初始化。在正常启动情形下,CmpCmdInit函数调用CmpInitializeHiveList来初始化储巢列表中的指定储巢,以及建立相应的符号链接。

由于CmpInitializeHiveList是在会话管理器进程环境中执行的,而加载和初始化储巢的动作必须在System进程中完成,因此,CmpInitializeHiveList会为储巢列表中的每一个储巢创建一个系统线程,由该系统线程来初始化该储巢。系统线程的主例程为CmpLoadHiveThread,参数为每个储巢在CmpMachineHiveList数组中的索引。

在CmpLoadHiveThread函数中,对于尚未加载的储巢,包括HKLM\SAM、HKLM\SECURITY、HKLM\SOFTWARE和HKU\.DEFAULT,它会调用CmpInitHiveFromFile来完成储巢的加载和初始化;而对于已经被初始化的非易失储巢,即HKLM\SYSTEM,则调用CmpOpenHiveFiles打开系统储巢文件,因为在此之前系统储巢文件实际上一直没有被通过文件系统打开过。经过这一步以后,系统储巢被完全初始化。

随着系统的进一步引导,当需要特定于用户的配置信息时,注册表的HKU子树下的用户储巢也必须建立起来。这些储巢是按需加载和初始化的,由登录进程(winlogon.exe)在建立起用户运行环境时完成,譬如当用户登录到系统中,或者系统以特定的用户身份来启动一个进程或服务时。Winlogon通过NtLoadKey系统服务将一个储巢文件链接到注册表中,而NtLoadKey又进一步调用CmLoadKey来完成实际的加载和链接操作。

以上讨论了配置管理器的初始化以及Windows注册表的建立过程。储巢是配置管理器的核心概念,也是注册表存储结构中的文件实体。WRK包含了配置管理器的完整代码,储巢的数据类型为CMHIVE,其内嵌的HHIVE成员是它的数据管理结构。

巢内部的数据管理类似于一个文件系统,它的数据存储单元按照巢箱来分配,而巢箱以块(4KB大小)为边界;储巢内部的逻辑数据结构为巢室,巢室有不同的类型,其大小亦不尽相同。在配置管理器的实现中,巢室的数据结构为HCELL,巢箱的数据结构为HBIN。空闲的巢箱形成一个空闲链表。实际上,HHIVE数据结构包含两个Storage成员,分别对应于稳定的储巢和易失的储巢;在Storage成员中,有空闲巢箱链表,以及一套用于转译巢室索引的巢室目录和巢室表。

注册表的层次结构形成了一个名字空间,配置管理器定义了一个以“Key”命名的对象类型,从而将该名字空间与对象管理器的全局名字空间整合起来。配置管理器在初始化阶段调用CmpCreateObjectTypes函数,创建了类型对象全局变量CmpKeyObjectType。配置管理器充分利用了对象管理器提供的对象管理框架,让注册表中的每个键自动成为对象管理器中的一个对象。对于每个打开的注册表键,配置管理器分配一个键控制块(key control block),其数据结构为CM_KEY_CONTROL_BLOCK,它包含了该控制块所引用的键节点所在的储巢和巢室索引。配置管理器将所有的键控制块放在一张散列表(全局变量CmpCacheTable)中,因而可以快速地根据名称来搜索已有的键控制块。散列表CmpCacheTable实际上是一个包含2048个元素的数组,散列表的键ID是由键控制块所引用的键对象的名称通过计算而获得。每个键控制块然后被放到散列表的相应桶中,放到同一个散列桶中的所有键控制块形成一个链表。

当内核或应用程序访问一个注册表键时对象管理器和配置管理器的名称解析过程。这涉及两个常用的操作:系统服务NtOpenKey和NtQueryValueKey,或者ZwOpenKey和ZwQueryValueKey。根据内核函数的命名约定,我们知道,Nt<Xxx>函数供用户模式应用程序使用,而Zw<Xxx>函数供内核代码直接调用。NtOpenKey和NtQueryValueKey函数,其原型如下:

Windows内核原理与实现读书笔记(2)注册表和配置管理器,事件追踪,安全性管理(未完)

 这两个函数的代码位于base\ntos\config\ntapi.c文件中。NtOpenKey系统服务接收到的对象名称位于ObjectAttributes.ObjectName中,它检查KeyHandle和对象名称参数是否可以正确地访问,然后将打开注册表键对象的操作全盘交给对象管理器的ObOpenObjectByName函数来完成。从这里也可以看出,注册表的接口与实现,都跟对象管理器的框架融合在一起。

ObOpenObjectByName函数通过ObpLookupObjectName函数来完成对象打开操作,它层层递进解析一个名称串,若碰到目录对象,则在目录中查询剩余的名称串;若碰到支持Parse方法的对象,则交给Parse方法来解析剩余的名称串。在NtOpenKey的情形中,它的ObjectAttributes参数可能已经指定了一个搜索根目录,即RootDirectory;也可能直接从全局名字空间的根下开始查找,此时调用者应该指定注册表键的全路径名。注册表键的全路径名以“\Registry”作为开始,例如,HKLM\SYSTEM\CurrentControlSet\services的全路径名为“\Registry\Machine\System\CurrentControlSet\services”。

由于配置管理器已经在全局名字空间的根下创建了一个名为“REGISTRY”的键对象,所以,当ObpLookupObjectName函数解析一个注册表键的全路径名称时,它首先在根目录下找到“REGISTRY”键对象,然后调用键对象类型的Parse方法来解析剩余的名称字符串。键对象类型的Parse方法CmpParseKey函数。CmpParseKey函数的实现并不难理解,它首先调用CmpBuildHashStackAndLookupCache函数,在散列表中查找已经打开的键对象,若能直接找到,则无须进一步名称解析;否则,需要顺序解析剩余的名称串,对于路径上的每一个子键,逐个为它们创建键控制块(通过调用CmpCreateKeyControlBlock函数)。最后,CmpParseKey调用CmpDoOpen函数打开此注册表键,并根据需要创建一个键控制块。

ObOpenObjectByName函数接收到一个指向键对象的句柄,键对象的数据结构为CM_KEY_BODY,其内部指向一个键控制块。如果两个应用程序打开同一个注册表键的话,它们都会接收到一个键对象,但这两个键对象指向一个公共的键控制块。键控制块有一个引用计数用于跟踪一个键被多少个客户引用。当引用计数为零时,表明该键控制块已不再被使用了,于是配置管理器将它从散列表中移除,并且回收该键控制块。

NtQueryValueKey函数相对要简单得多,因为它的参数KeyHandle已经指示了要查询哪个键,所以,它只需调用ObReferenceObjectByHandle函数即可获得目标键的键对象。然后它调用CmQueryValueKey函数从目标键中读取指定的值的信息。

最后值得一提的是,配置管理器提供了注册表键的变化通知机制。应用程序通过调用NtNotifyChangeKey或NtNotifyChangeMultipleKeys系统服务,可以监视一个或多个注册表键的创建、删除和修改动作。实现注册表键变化通知机制的关键在于,每个键对象都有一个类型为CM_NOTIFY_BLOCK的通知块成员,它描述了一个键对象的哪些事件以何种方式被通知到注册方。由于配置管理器提供了这种变化通知能力,因而对于想要监视注册表行为的应用程序,它们无须频繁地检查注册表来判断感兴趣的键是否已被修改。这对于一些安全保护或者注册表行为分析等程序有显著的意义。

事件追踪(ETW)

Windows提供了统一的跟踪和记录事件的机制,称为ETW(EventTracing for Windows)。用户模式应用程序和内核模式驱动程序都可以使用ETW来记录事件。ETW是直接由内核支持的事件记录机制,在它的框架结构中,共有三种组件:· 控制器(controller)。负责启动、停止或配置事件记录会话。· 提供者(provider)。负责向ETW注册自己的事件类,并接受控制器的命令,以便启动或者停止它们所负责的事件类的记录过程。· 消费者(consumer)。负责有针对性地读取它们想要的事件数据,选择一个或多个记录会话。它们既可以实时地接收ETW缓冲区中的数据,也可以接收日志文件中的事件数据。