转:Windows注册表HIVE文件格式解析

Windows注册表HIVE文件格式解析
文章作者:fahrenheit
引言

  相信大家对Windows系统的注册表(registry)一定都不陌生了,我们可以用系统提供的注册表编辑器(regedit)来访问和修改注册表中的数据。直观的讲,注册表呈现出来的是图1所示的形式,它由根键(rootkey)、子键(subkey)、键值(value)和数据(data)组成。数据之间有类型的分别,常见的有:REG_SZ、字符串型,REG_BINARY、二进制型, REG_DWORD、双字型,REG_MULTI_SZ、多字符串值型和REG_EXPAND_SZ、长度可变的数据串型。

  注册表相当于Windows系统中所有32位硬件/驱动和32位应用程序的数据文件,是一个系统信息的数据库。既然是数据文件,那在磁盘上就一定有注册表的影子的存在。Windows 2000/XP的注册表文件在系统设置和缺省用户配置数据的情况下,是存放在/系统文件夹/SYSTEM32/CONFIG目录下的6个文件,DEFAULT、SAM、SECURITY、SOFTWARE、USERDIFF和SYSTEM中,而用户的配置信息存放在系统所在磁盘的/Documents and Setting/目录,包括ntuser.dat,ntuser.ini和ntuser.dat.log。其中每个文件的路径都由注册表项HKLM /SYSTEM/CurrentControlSet/Control/HIVElist下的键值指出。

  我们看到的注册表结构是经过注册表编辑器读取之后呈现给我们的,其磁盘形式并不是一个简单的大文件,而是一组称被为HIVE的单独文件形式,HIVE中文名曰“储巢”。每个HIVE文件可以被理解为一棵单独注册表树,就像Windows的PE格式一样,它也有自己的组织形式。本文的任务就是要分析HIVE 文件的组织形式并完成一个HIVE格式的分析程序。



注册表API工作原理简述

  Windows系统提供了大量的API给用户访问和修改注册表中的数据,regedit就是基于这些API所实现的。注册表API大致分为用户空间的与内核空间的两类,一般用户调用前者,层层调用转移,由内核的注册表API再调用文件系统的驱动等,去访问磁盘上的HIVE文件,并最终返回请求的数据结果。这个过程有点冗长,但是为了注册表里存放数据的安全考虑,损失一些性能表现还是值得的。



HIVE结构解析

  在认识真正的HIVE文件之前,我们先列举HIVE文件的几个主要特征。先入为主的将它们呈现出来将有助于我们对其文件组织和数据结构的理解。
? 注册表由多个HIVE文件组成。
? 一个HIVE文件由多个巢箱(BIN)组成,HIVE文件的首部有一个文件头(基本块、base block),用于描述这个HIVE文件的一些全局信息
? 一个BIN由多个巢室(CELL)组成,CELL可以分为具体的5种(后面介绍),用于存储不同的注册表数据。
本文中,我们并不统一使用HIVE、BIN和CELL的英文单词,而是和对应的中文词汇交替出现。在中文里,它们分别对应储巢、巢箱和巢室三个名词。

  一个储巢被看成是一些称为块(block)的分配单元,类似于将磁盘分为簇的形式。根据定义,每一个注册表块的大小为4096字节(4KB),当新的数据要加入到一个储巢中来时,该储巢总是按照块的粒度来增加。一个储巢的第一个块是基本块(base block),包含了有关该储巢的全局信息,包括一个特征签名“regf”,更新序列号,储巢上一次写操作发生的时间戳,储巢格式版本号、检验和,以及该储巢文件的内部文件名等等。下面的_HBASE_BLOCK就是一个基本块的数据结构还原。
typedef struct _HBASE_BLOCK
{
  ULONG Signature; /* 签名ASCII-"regf" = 0x66676572 (小端序)*/
  ULONG Sequence1; 
  ULONG Sequence2; 
  LARGE_INTEGER TimeStamp; /* 最后一次写操作的时间戳 */
  ULONG Major; /* 主版本号 */
  ULONG Minor; /* 次版本号 */
  ULONG Type; 
  ULONG Format;
  ULONG RootCell; /* 第一个键记录的偏移 */
  ULONG Length; /* 数据块长度 */
  ULONG Cluster; 
  UCHAR name[64]; /* 储巢文件名 */
  ULONG Reserved1[99];
  ULONG CheckSum; /* 校验和 */
  ULONG Reserved2[894];
  ULONG BootType;
  ULONG BootRecover;
} HBASE_BLOCK, *PHBASE_BLOCK;

  Windows将一个储巢所存储的注册表条目组织在一种称为巢室的容器中,当一个巢室加入到一个储巢中,而且该巢室必须经过扩展才能容纳该巢室时,系统将创建一个巢箱的分配单元。巢箱是新巢室正好扩展到下一个块的边界的大小,系统将巢室的尾部和巢箱的尾部之间的任何空间都看作是空闲空间,因而可以分配其他的巢室。

  巢箱也有头部的标识,包含了一个特殊的签名“hbin”,一个记录了该巢箱在储巢文件中偏移量的域,以及该巢箱的大小。下面是巢箱的数据结构。
typedef struct _HBIN
{
  ULONG Signature; /* 签名 ASCII-"hbin" = 0x6E696268 (小端序) */
  ULONG FileOffset; /* 本巢箱相对第一个巢箱起始的偏移 */
  ULONG Size; /* 本巢箱的大小 */
  ULONG Reserved1[2];
  LARGE_INTEGER TimeStamp;
  ULONG Spare;
} HBIN, *PHBIN;

  一个巢室可以容纳一个键、一个值、一个安全描述符、一列子键或者一列键值,分别有对应的巢室来存储数据。在巢室数据的开始之处,有一个数据域描述了该巢室数据的类型,具体的数据结构如下:

? 键巢室,包含了一个注册表键(也称为键节点)的巢室,一个键巢室包含一个特征签名(对于一个键是kn,一个符号链接是kl)、该键最近一次更新的时间戳、该键父键巢室的巢室索引、代表该键的子键的子键列表巢室的索引、该键的安全描述符巢室索引、一个代表该键类名的字符串键巢室索引,以及该键的名称。
typedef struct _CM_KEY_NODE
{
USHORT Signature; /* 签名ASCII-"kn" = 0x6B6E (小端序)*/
  USHORT Flags; /* 根键标识: 0x2C, 其他为 0x20 */
  LARGE_INTEGER LastWriteTime;
  ULONG Spare;
  ULONG Parent; /* 父键的偏移 */
  ULONG SubKeyCounts[2]; /* SubKeyCounts[0]为子键的个数 */ 
  union /* 偏移为0x001C 联合体 */
  {
  struct
  {
  ULONG SubKeyLists[2]; /* SubKeyLists[0]为子键列表相差本BIN的偏移 */
  CHILD_LIST ValueList; /* ValueList结构体 */
  };
  ULONG ChildHiveReference[4];
  }; 

ULONG Security; /* 安全描述符记录的偏移 */
ULONG Class; /* 类名的偏移 */ 
ULONG MaxNameLen: 16;
ULONG UserFlags: 4;
ULONG VirtControlFlags: 4;
ULONG Debug: 8;
ULONG MaxClassLen;
  ULONG MaxValueNameLen;
  ULONG MaxValueDataLen;
  ULONG WorkVar;
  USHORT NameLength; /* 键名长度 */
  USHORT ClassLength; /* 类名长度 */
  PBYTE Name; /* 键名称 */
}CM_KEY_NODE, *PCM_KEY_NODE;

? 值巢室,一个巢室,包含了关于一个键的值的信息,该巢室包含一个签名kv,该值的类型,如REG_DWORD或REG_BINARY,以及该值的名称。一个值巢室也包含了另一个值巢室的索引,后者包含了对前者的数据。
typedef struct _CM_KEY_VALUE
{
WORD Signature; /* 签名ASCII-"kv" = 0x6B76(小端序) */
WORD NameLength; /* 名称长度 */
ULONG DataLength; /* 数据长度 */
ULONG Data; /*数据偏移或数据, 如果DataLength最高位为1,那么它就是数据,
且DataLenth&0x7FFFFFFF为数据长度;否则 */
ULONG Type; /* 值类型 */
WORD Flags; 
WORD Spare; 
PWCHAR Name; /* 值名称 */
} CM_KEY_VALUE, *PCM_KEY_VALUE;

? 子键列表巢室,有一系列的键巢室的巢室索引构成的巢室,这些键巢室是同一个父键下面的所有子键。
typedef struct _CM_KEY_INDEX
{
  WORD Signature;
  WORD Count;
  ULONG List[1];
} CM_KEY_INDEX, *PCM_KEY_INDEX;
如果Signature==CM_KEY_FAST_LEAF,签名为“fl”,或者Signature==CM_KEY_HASH_LEAF,签名为“hl”,那么List后是一个结构体:
struct
{
  ULONG offset;
  ULONG HashKey;
}
否则为:ULONG offset;

? 值列表巢室,有一系列的值巢室的巢室索引构成的巢室,这些值巢室是同一个父键下面的所有值。其数据结构即上文说到的结构。即上面_CM_KEY_NODE的联合体中ValueList数据域。
typedef struct _CHILD_LIST
{
  ULONG Count; /* ValueList.Count值的个数 */
  ULONG List; /* ValueList.List值列表相差本BIN的偏移 */
} CHILD_LIST, *PCHILD_LIST;

? 安全描述符巢室,包含了一个安全描述符巢室,其首部的特征签名为ks,以及一个引用计数,该引用计数值记录了所有共享安全描述符的键节点数目,多个键巢室可以共享同样的安全描述符巢室。
typedef struct _CM_KEY_SECURITY
{
  WORD Signature; /* 签名ASCII-"sk" = 0x6B73 (小端序)*/
  WORD Reserved;
  ULONG Flink; /*上一个"sk"记录的偏移 */
  ULONG Blink; /*下一个"sk"记录的偏移 */
  ULONG ReferenceCount; /* 引用计数 */
  ULONG DescriptorLength; /* 数据大小 */
  SECURITY_DESCRIPTOR_RELATIVE Descriptor; /* 数据 */
} CM_KEY_SECURITY, *PCM_KEY_SECURITY;

  储巢的结构是通过一些链接建立起来的,这些链接称为巢室索引(cell index)。每个巢室索引是一个巢室在储巢文件中的偏移。因此,巢室索引就像是一个指针,从一个巢室指向另一个巢室,配置管理器将巢室索引解释为相对于储巢起始处的偏移。因此,假如你想找到子键A的键巢室,并且A的父键是B,那么就必须先利用B的巢室中的子键列表巢室索引,找到包含B的所有子键列表的那个巢室,然后再利用该子键列表巢室中的巢室索引列表,找到B的每个子键的巢室,随即找到A。

  巢室,巢箱和块之间的区别很容易让人混淆,所以我们来看一个简单的注册表储巢的布局示例,如图5。该示例中包含了一个基本块和两个巢箱,第一个巢箱是空的,第二个巢箱包含了几个巢室。该巢室有两个键,一个是根键Root,另一个是Root的子键——SubKey。Root有两个值,Val1和Val2,通过一个子键列表巢室,可以定位到根键的子键,通过一个值列表巢室,可以定位到根键的值。第二个巢箱中,空闲的空间属于空的巢室。



获得HIVE文件

  知道了HIVE们的存放位置,自然就想到要把它们抓过来,逐个解剖,好好研究一番了。但是如果直接去打开或者复制C:/WINDOWS/system32/config/SYSTEM,就会看到图6的出错提示,这是系统的独占资源。

  抓捕工作看似棘手,但解决起来也很简单。HIVE是Windows的重要资源,自启动以来就只能被系统独占访问。我们换一种思路,在另外一个系统中启动,如同一台机器上的Linux,那目前系统的HIVE文件不是就可以访问了吗?但是这里如果你非要在当前系统访问这个HIVE文件,就只有求助于文件系统驱动了。后者已经超出了本文的讨论范围,故我们不作考虑。
不过,为了示例学习的需要,我们总希望HIVE文件能相对简单一些,让我们把它的结构看个清楚明白。系统内部的HIVE文件一般都不太适合,从其文件尺寸已经达到MB级别,就可看出其数据量的巨大。因此,初期我们需要自己建立一个小型的HIVE以供学习之用。
为了以后叙述的方便,我们在HKLM/SAM下建立了子键test_root,然后在test_root下再建立两个子键1test和2test,并且在 1test下新建了五种不同的值,并填写了相应的数据.然后,用RegSaveKey函数编个小程序,把test_root保存为HIVE文件。这样 test_root就变成这个HIVE文件的根键。小程序已经放在了文章的附件里。接下来,我们就可以细细剖析HIVE文件中每一个部分的结构和功能了。



HIVE格式实例分析

  我们用一个16进制编辑器打开test_root文件,首先就可以看到基本块的签名——“regf”字符串,这是registry file的缩写,标志它是个注册表文件...

  本节中用图片和例子详细解释了一个名为test_root的注册表HIVE文件,具体情况可以参见杂志。



HIVE文件读取程序

  基于上面的结构解释和分析,我们可以写出一个HIVE文件的读取程序,放在文章附件里。针对上述分析示例的读取效果,如图13所示,test_root下有子键1test_subkey和2test,前者有1_REG_SZ、2_REG_BINARY、3_REG_DWORD、 4_REG_MULIT_SZ和5_REG_EXPAND_SZ 5个键值。[]内的是键值的类型,()内的是值的长度,以字节为单位,对于REG_SZ型数据是打印出其unicode的编码。图14是针对本机的名为 Software的HIVE文件的分析结果,首先看到的第一项是360安全卫士的注册表信息。


后记

  本文中HIVE的数据格式大部分来源于网络和自己的整理,因为缺乏Microsoft官方的文档支持,所以我们并不能保证分析程序在所有情况下都是正确的。Petter Nordahl-Hagen曾经写过一个NT Registry Hive access library,但年代稍显久远,Windows可能会随版本变化修改这些结构的组织形式,其对XP系统已有局部不再适用。注册表在安全方面有很多应用场景,如果需要更加深入的学习该领域的知识,可以借助windbg,利用其导出的Windows内核数据结构进一步了解注册表文件的组织形式,同时也欢迎大家和我一起探讨,共同学习,共同进步。 

发布了30 篇原创文章 · 获赞 5 · 访问量 6万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章