《Windows内核原理与实现笔记》(二)注册表和配置管理器,事件追踪,安全性管理

本文深入探讨Windows注册表的结构与配置管理机制,解析注册表的初始化过程、存储结构及配置管理器的实现细节。文章揭示了注册表在系统初始化、硬件配置和用户登录中的作用,以及注册表数据的组织方式和访问机制。

注册表和配置管理器

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

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

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

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

 储巢的内部结构类似于一个文件系统,而储巢相当于是一个磁盘分区。储巢的基本分配单元称为块(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函数,其原型如下:

 这两个函数的代码位于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缓冲区中的数据,也可以接收日志文件中的事件数据。

Windows内置了一个内核日志记录器(kernel logger)作为ETW提供者,专门用于记录内核和核心驱动程序的事件。此内核日志记录器是由WMI(Windows Management Instrumentation,Windows管理规范)设备驱动程序实现的,也是内核模块ntoskrnl.exe的一部分。WMI驱动程序的名称为“WMIxWDM”,它是在I/O管理器初始化过程中调用WMI组件的初始化函数(WMIInitialize)而创建的。它除了实现内核日志记录器的功能,也管理用户模式ETW事件类型的注册工作。由于它的实现形式是驱动程序,因此,其他的内核例程或设备驱动程序可以通过I/O接口与它通信。

内核日志记录器是一个事件提供者,它有一个预定义的GUID,即内核变量SystemTraceControlGuid。内核日志记录器支持多种事件类,它采用标志位(flag)来指示是否记录某一类型的事件。

#define PERF_MASK_INDEX         (0xe0000000)
#define PERF_MASK_GROUP         (~PERF_MASK_INDEX)

#define PERF_NUM_MASKS       8
typedef ULONG PERFINFO_MASK;

//
// This structure holds a group mask for all the PERF_NUM_MASKS sets
// (see PERF_MASK_INDEX above).
//

typedef struct _PERFINFO_GROUPMASK {
    ULONG Masks[PERF_NUM_MASKS];
} PERFINFO_GROUPMASK, *PPERFINFO_GROUPMASK;

 当控制器程序指示内核日志记录器记录某些类型的内核事件时,它们需要构造一个PERFINFO_GROUPMASK对象,将需要记录的标志位置上。PERFINFO_GROUPMASK的Masks数组包含8个ULONG成员,每个ULONG的最高3位(即29~31位)是组的索引(0~7),低29位(即0~28位)为组内的标志位。内核日志记录器的标志位安排是由Windows系统定义的,并且允许扩展。Windows Server 2003仅仅使用了一部分标志位,参见WRK的public\internal\base\inc\ntwmi.h文件中的PERF_<XXX>宏定义。

在Windows中,系统的全局组掩码是由全局变量PerfGlobalGroupMask定义的。以环境切换事件为例,当PERF_CONTEXT_SWITCH标志位被置上时,线程切换时就会记录一个CSwitch事件。这发生在SwapContext函数中,缓冲区管理是WMI驱动程序的重要职责之一。WMI驱动程序为环境切换事件定义了一个缓冲区数组(WmipContextSwapProcessorBuffers变量),让每个处理器使用它自己的缓冲区,从而避免缓冲区冲突。一旦属于某个处理器的当前缓冲区满了(不足以再存放一个CSwitch事件),则交由WMI刷新该缓冲区,下次记录新的CSwitch事件时重新申请一个新的缓冲区作为当前缓冲区。WMI为每个记录会话管理缓冲区,它在创建记录会话时根据控制器的指示,创建合理数量的缓冲区。

Microsoft提供了一个性能工具xperf,它既是一个控制器,也是一个消费者。Xperf利用I/O接口(NtDeviceIoControlFile函数)与WMI驱动程序进行通信。Windows SDK提供了事件追踪API(位于advapi32.dll模块中),因而用户模式应用程序可以很方便地操纵和控制WMI驱动程序。Xperf直接在ntdll.dll的ETW接口上工作,并没有使用advapi32.dll中的事件追踪API。

安全性管理

Windows有严格的安全模型,它既实现了以对象为基础的自主访问控制(discretionary access control),又实现了系统级的强制访问控制(mandatory accesscontrol)。在自主访问控制模型中,对象的所有者授权或拒绝哪些人可以访问该对象。而且,Windows还考虑到了对象所有者丢失的情形,即,当由于某些原因对象的所有者不再有效时,特权用户可以将对象的所有权接管过来,从而可以访问该对象,或授权其他的用户访问它。

Windows操作系统中涉及安全性管理的核心组件:winlogon、SRM和lsass,winlogon和lsass是两个用户模式进程,而SRM是Windows执行体中的组件。

SRM(安全引用监视器),负责执行对象的安全访问检查、管理用户特权、生成安全审计消息,并且定义了访问令牌数据结构来表示一个安全环境。

Winlogon,负责响应SAS(安全注意序列),以及管理交互式登录会话。当用户登录到系统中时,winlogon创建一个初始进程,并进一步由它创建外壳(shell)进程。

Lsass(本地安全权威子系统),负责本地系统的安全策略,同时,它也认证用户的身份,以及将安全审计消息发送到系统的事件日志中。

SAM(安全账户管理器)数据库,包含了本地用户和用户组,以及它们的口令和其他属性。它位于注册表的HKLM\SAM下面。由于HKLM\SAM键只允许本地系统账户访问,所以,除非用户在Local System账户下运行regedit.exe工具,否则无法访问HKLM\SAM子树。

LSA策略数据库,包含了有关当前系统的一些信息,譬如谁允许访问系统以及如何访问(交互式登录、网络登录或者以服务方式登录);分配给谁哪些特权;安全审计如何进行等。如同SAM数据库一样,LSA策略数据库也存储在注册表中,位于HKLM\SECURITY下面。同样地,除了Local System账户以外的其他账户均无法访问HKLM\SECURITY子树。

简而言之,在Windows的安全模型中,winlogon负责系统登录,包括对用户身份的认证;lsass负责管理系统本地安全策略,并且将这些策略通知到内核中的SRM。在内核中,SRM负责实现基于对象的访问控制以及系统全局安全策略的实施。SRM的代码,位于base\ntos\se目录下面。在此模型中,lsass和SRM对于系统的安全性至关重要,一旦这两个组件被恶意代码修改或侵入,则系统的安全防线将不复存在。Windows对这两个组件有特殊的保护,普通应用程序无法与它们打交道,它们相互之间通过LPC进行通信。它们的LPC连接在系统初始化时建立,而且,一旦其双向LPC连接建立起来,它们的LPC端口便不再接受任何其他的连接请求,因而其他程序与它们无法建立LPC连接。

Windows的自主访问模型与对象管理器集成在一起,每一种对象类型都定义了一个Security方法,该方法返回一个对象的安全信息。线程在访问一个对象以前必须先打开这个对象,并获得一个指向该对象的句柄。在打开对象的操作中,对象管理器调用SRM的函数,根据调用线程的安全凭证、在打开操作中请求的访问类型(比如读、写、删除等),以及该类型对象的Security方法提供的对象安全信息,来决定此打开操作是否允许。如果可以打开该对象,那么,对象管理器在线程的进程句柄表中创建一个句柄,记录下该对象以及它所请求的访问类型。所以,线程在成功打开对象后获得一个指向该对象的句柄。以后,当该线程访问此对象时,它传递该对象的句柄,而对象管理器将该线程所请求的访问操作与该对象被打开时所获得的访问类型进行比较,如果当前的访问操作允许,则安全检查通过,否则此次对象访问失败。

在这个模型中有两点值得一提。第一,每个线程都有一个安全环境,其中最重要的信息是一个访问令牌,代表了该线程的用户的一次登录;每个对象都有一个自主访问控制列表(ACL),指明了允许谁以何种方式访问该对象,而拒绝哪些用户以何种方式访问它。为了访问一个对象,线程可以不使用它所属进程的安全环境,而是以其他账户身份来运行的安全环境,这称为模仿(impersonation)。第二,同一个进程中的其他线程也可以利用已经得到的句柄来访问该对象。同样地,对象管理器使用调用线程的安全环境和它所请求的访问操作,对照该对象被打开时获得的访问类型进行检查,以决定是否允许该操作。

SRM的安全检查功能

当一个进程打开一个对象时,对象管理器在名字空间中查找到目标对象以后,但是在返回句柄给调用者以前,它调用ObCheckObjectAccess函数检查访问许可。ObCheckObjectAccess是对象管理器的函数,它将对象管理器与SRM的安全监视机制连接起来。该函数的代码位于base\ntos\ob\obse.c文件的387~564行。它首先调用ObGetObjectSecurity函数以获得目标对象的SD(安全描述符),数据类型为SECURITY_DESCRIPTOR。对象的安全描述符包含了对象的所有者的SID(安全标识符)、自主访问控制列表(DACL)和系统访问控制列表(SACL),以及其他一些描述对象安全特征的属性。这里的自主访问控制列表规定了谁可以或不可以以何种方式访问该对象;系统访问控制列表规定了哪些用户的哪些操作应该被记录到安全审计日志中。

对于每一种类型的对象,系统在创建它的类型对象时,可以指定该类对象的Security方法,即SecurityProcedure函数成员;如果在创建类型对象时不指定该函数成员,那么,对象管理器使用一个默认的安全函数SeDefaultObjectMethod。接着,在获得了对象的安全描述符以后,ObCheckObjectAccess函数锁住当前的安全环境,并调用SeAccessCheck函数执行安全访问许可检查。

线程的安全环境是一个SECURITY_SUBJECT_CONTEXT类型的对象,它包含了线程的主令牌(PrimaryToken成员)、模仿级别、进程审计ID(直接用进程对象的地址来表示),以及一个可选的客户令牌(ClientToken)。当线程模仿一个客户时,客户令牌不为空,指向被模仿客户的令牌,在这种情况下,SRM使用客户令牌来检查访问许可。令牌(token)代表了SRM中用到的主体,它描述了一个用户的一次登录,由winlogon进程在认证了用户身份以后创建。同一个用户会话中运行的进程都使用同样的令牌。令牌的数据结构为TOKEN,它包含了令牌ID、认证ID、用户账户的SID和所属组的SID、一组与该令牌关联的特权,以及其他一些用于各种安全操作的属性。

代码如下

BOOLEAN
ObCheckObjectAccess (
    __in PVOID Object,
    __inout PACCESS_STATE AccessState,
    __in BOOLEAN TypeMutexLocked,
    __in KPROCESSOR_MODE AccessMode,
    __out PNTSTATUS AccessStatus
    )

/*++

Routine Description:

    This routine performs access validation on the passed object.  The
    remaining desired access mask is extracted from the AccessState
    parameter and passes to the appropriate security routine to perform the
    access check.

    If the access attempt is successful, SeAccessCheck returns a mask
    containing the granted accesses.  The bits in this mask are turned
    on in the PreviouslyGrantedAccess field of the AccessState, and
    are turned off in the RemainingDesiredAccess field.

Arguments:

    Object - The object being examined.

    AccessState - The ACCESS_STATE structure containing accumulated
        information about the current attempt to gain access to the object.

    TypeMutexLocked - Indicates whether the type mutex for this object's
        type is locked.  The type mutex is used to protect the object's
        security descriptor from being modified while it is being accessed.

    AccessMode - The previous processor mode.

    AccessStatus - Pointer to a variable to return the status code of the
        access attempt.  In the case of failure this status code must be
        propagated back to the user.


Return Value:

    BOOLEAN - TRUE if access is allowed and FALSE otherwise

--*/

{
    ACCESS_MASK GrantedAccess = 0;
    BOOLEAN AccessAllowed;
    BOOLEAN MemoryAllocated;
    NTSTATUS Status;
    PSECURITY_DESCRIPTOR SecurityDescriptor = NULL;
    POBJECT_HEADER ObjectHeader;
    POBJECT_TYPE ObjectType;
    PPRIVILEGE_SET Privileges = NULL;

    PAGED_CODE();

    UNREFERENCED_PARAMETER (TypeMutexLocked);

    //
    //  Map the object body to an object header and the
    //  corresponding object type
    //

    ObjectHeader = OBJECT_TO_OBJECT_HEADER( Object );
    ObjectType = ObjectHeader->Type;

    //
    //  Obtain the object's security descriptor
    //获得目标对象的SD(安全描述符)

    Status = ObGetObjectSecurity( Object,
                                  &SecurityDescriptor,
                                  &MemoryAllocated );

    //
    //  If we failed in getting the security descriptor then
    //  put the object type lock back where it was and return
    //  the error back to our caller
    //

    if (!NT_SUCCESS( Status )) {

        *AccessStatus = Status;

        return( FALSE );

    } else {

        //
        //  Otherwise we've been successful at getting the
        //  object's security descriptor, but now make sure
        //  it is not null.

        if (SecurityDescriptor == NULL) {

            *AccessStatus = Status;

            return(TRUE);
        }
    }

    //
    //  We have a non-null security descriptor so now
    //  lock the caller's tokens until after auditing has been
    //  performed.
    //锁住当前的安全环境

    SeLockSubjectContext( &AccessState->SubjectSecurityContext );

    //
    //  Do the access check, and if we have some privileges then
    //  put those in the access state too.
    //执行安全访问许可检查,真正执行检查任务是在SepAccessCheck函数中。它们的代码位于base\ntos\se\accessck.c文件中。

    AccessAllowed = SeAccessCheck( SecurityDescriptor,
                                   &AccessState->SubjectSecurityContext,
                                   TRUE,                        // Tokens are locked
                                   AccessState->RemainingDesiredAccess,
                                   AccessState->PreviouslyGrantedAccess,
                                   &Privileges,
                                   &ObjectType->TypeInfo.GenericMapping,
                                   AccessMode,
                                   &GrantedAccess,
                                   AccessStatus );

    if (Privileges != NULL) {

        Status = SeAppendPrivileges( AccessState,
                                     Privileges );

        SeFreePrivileges( Privileges );
    }

    //
    //  If we were granted access then set that fact into
    //  what we've been granted and remove it from what remains
    //  to be granted.
    //

    if (AccessAllowed) {

        AccessState->PreviouslyGrantedAccess |= GrantedAccess;
        AccessState->RemainingDesiredAccess &= ~(GrantedAccess | MAXIMUM_ALLOWED);
    }

    //
    //  Audit the attempt to open the object, audit
    //  the creation of its handle later.
    //

    if ( SecurityDescriptor != NULL ) {

        SeOpenObjectAuditAlarm( &ObjectType->Name,
                                Object,
                                NULL,                    // AbsoluteObjectName
                                SecurityDescriptor,
                                AccessState,
                                FALSE,                   // ObjectCreated (FALSE, only open here)
                                AccessAllowed,
                                AccessMode,
                                &AccessState->GenerateOnClose );
    }

    SeUnlockSubjectContext( &AccessState->SubjectSecurityContext );

    //
    //  Free the security descriptor before returning to
    //  our caller
    //

    ObReleaseObjectSecurity( SecurityDescriptor,
                             MemoryAllocated );

    return( AccessAllowed );
}

综上所述,当一个线程打开一个对象时所执行的安全检查过程如图

一旦线程成功地打开了一个对象,以后当它在该对象上执行操作时,将使用句柄来引用该对象,而对象管理器或其他内核组件在将句柄解析成对象时仍然要检查调用者所请求的操作是否已被授权或拒绝。例如,ObReferenceObjectByHandle函数使用宏SeCompute-DeniedAccesses来判断要请求的访问是否被拒绝;ObReferenceFileObjectForWrite函数使用宏SeComputeGrantedAccesses来判断所请求的文件操作是否已被授权。此外,I/O管理器也使用同样的方法来检查它所接到的访问操作。

至此,我们看到了SRM中自主访问控制的实现过程。SRM的强制访问控制是通过特权检查来实施的,SePrivilegeCheck函数是SRM中用于特权检查的内核接口函数。

在Windows中,特权是由LUID对象来标识的,LUID代表一个本地唯一标识符(Locally UniqueIdentifier),由两个LONG成员构成,因此在32位系统中LUID是一个64位的整数。每一个特权都附带一些属性,两者结合起来构成了LUID_AND_ATTRIBUTES数据结构,而属性是一个无符号长整数类型。

在令牌TOKEN数据结构的定义中,有一个Privileges成员,代表该令牌账户的所有特权,因此,SepPrivilegeCheck函数的实现是,用一个二重循环,对调用线程所请求检查的每一个特权,遍历Token参数中的Privileges数组成员,看是否能匹配上。最后检查总共匹配了多少特权,是否所请求的特权均已匹配上,或至少匹配了一个。

Windows内核中定义了一组特权,即类型为LUID的Se<Xxx>全局变量,其定义位于base\ntos\se\seglobal.c文件中,对于每一种特权,当相关联的操作(即需要此特权才能进行的操作)在内核中被适当的组件激发时,应通过SePrivilegeCheck或它的包装函数SeSinglePrivilegeCheck,来检查调用线程是否具有相应的特权。在Windows内核中,与安全相关的函数以“Se”作为前缀,有一些安全函数还存在对应的系统服务。这些系统服务函数的名称以“Nt”作为前缀,后面部分与“Se”函数相同。

 

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值