65.9K
CodeProject 正在变化。 阅读更多。
Home

注册表比较器 (GUI) (C#)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (11投票s)

2012年8月21日

CPOL

10分钟阅读

viewsIcon

39513

downloadIcon

1887

注册表配置单元读取器和比较器。

介绍 

为了查看某些软件是如何操作 Windows 注册表的,我需要一个工具,能够读取两个配置单元文件,加载它们,然后进行比较以显示两者之间的差异。我在网上找到了一些可以比较文件的代码,但没有一个能以美观的图形化方式比较两个注册表配置单元。所以我想用 C# 自己编写一个。(注册表配置单元是二进制文件,我这里指的不是 regedit.exe 生成的基于文本的 .reg 文件。)

背景 

Windows 将注册表存储在二进制文件中。这种注册表配置单元的格式并未公开。数据在注册表中的存储方式很简单。它以一个大小为 4096 字节的注册表头开始。然后是逻辑数据块——也称为单元(Cell)。根据逻辑数据块的类型,它会有一个指针列表(称为偏移量 = 从第 4096 字节开始的文件偏移量)。这些指针指向另一个包含更多信息或数据的逻辑单元。广义上说,有两种类型的单元:

  1. 节点键单元 (Node Key Cell)
  2. 值键单元 (Value Key Cell)

节点键——顾名思义——是树的节点。它就像 Windows 文件系统中的目录。一个目录可以包含更多的目录和文件。这种节点单元有四个重要的信息片段。

  1. 此节点(或键)中的子键数量
  2. 指向一个单元的指针,该单元存储了这些子键的地址。
  3. 此节点中的值键单元数量。
  4. 指向一个单元的指针,该单元存储了值键的地址。

值键单元存储数据。值键的行为类似于文件,具有不同类型的格式和信息。值键大致分为包含基于文本的信息或二进制信息两类。

配置单元 (HIVE) 从地址 (4096+32) 处的根节点开始。通过读取第一个节点键类型的单元来读取信息。子键节点信息为我们提供了该节点中存在的子键的链接。我们必须遍历所有节点才能读取整个注册表配置单元。在深入遍历各层级时,我们也会读取值键。

我从这些文章中获得了一些非常有用的信息(以及编码方面的帮助):

然后我们将这些节点信息读入一个 WinForms 的 TreeNode 中,并在一个 TreeView 中绘制出来。

一旦我们将两个配置单元加载到两个独立的 TreeView 中,我们便使用一个简单的迭代方法来比较它们。我们根据搜索状态为 TreeNodes 分配颜色:绿色表示未更改的注册表键,红色表示在第二个文件中被删除(或不存在)的键,蓝色表示在第二个文件中添加的键(在第一个文件中不存在),而粉色表示其数据发生更改的值键。

使用代码

代码首先创建一个 WinForm,并向其添加一个 TreeView、一些 Button 和一些 LabelLabel 提供关于两个注册表配置单元的信息,Button 则调用一个方法将配置单元加载到一个 TreeNode 中。我们直接将第一个注册表文件作为字节数组加载到内存中。

fs = File.OpenRead(InputputRegistryFile);  // File Stream
byte[] data = new byte[fs.Length];  //data[] has the file in memory as byte[]
fs.Read(data, 0, data.Length);
if (fs.Length < 4096) { MessageBox.Show("Not a reg hive file."); fs.Close(); return; }
     // reg file cant be smaller than 4096 bytes. A simple check to see if file is a hive
fs.Close();  // File closed 

节点加载从创建第一个根节点开始。

TreeNode treenode = new TreeNode();
treenode.Text = "--Registry--Root--" //you could name anything as you like
TreeView treeView1 = new TreeView();  // create a tree view
treeView1.Nodes.Add(treenode); 

该过程首先从 Data[] 数组中读取第一个节点键逻辑单元。节点键单元的结构如下:

[Serializable]
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct key_block  { 
    public int  block_size;

    [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 2)]
    public byte[] block_type;// "nk" identifies this as a NODE KEY (nk) type cell.

    [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 18)]
    public byte[] dum1;
   
    public uint   subkey_count;  // number of sub keys (or sub nodes) in this node
    [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 4)]
    public byte[] dum2;

    public int   offsettosubkeys;  //stores the offset to subkey list
    [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 4)]
    public byte[] dum3;

    public int   value_count;  // number of values in the node or key
    public int   offsettovaluelist; // offset to cell which stores value key information
    [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 28)]
    public byte[] dum4;
   
    public ushort len;
    public ushort du;
    // Name of Node Key is stored at the end of this structure.
    // Length of name is the len variable of this structure.
};

我们将创建一个方法,向其传递 data[] 中的地址并读取此结构。然后,我们使用该结构的 len 变量来读取节点名称。该名称紧跟在结构结束后存储。

我们使用 C# 中的 Queue 来逐层迭代读取子键。遵循的一般概念是:

  1. 将根节点地址添加到队列中。
  2. 启动循环并进行处理,直到队列为空。
  3. 在处理过程中,从队列中读取一个节点地址,然后读取该地址指向的键节点结构。
  4. 处理该节点的子节点键信息。如果存在任何子键,则将每个子键的地址添加到队列中。
  5. 如果存在任何值键,则创建一个带有名称和信息的 TreeNode,并将其添加到树节点(指向 TreeView 的那个)中。

在这里,我们还多做了一件事。我们在队列中添加一个结构体。该结构体只有两项:一个指向节点键单元的指针和当前的 TreeNode。这样做是为了在我们持续读取和处理子键时,保留“当前节点”。每当我们从队列中读取一个子键时,我们也能得到它的父树节点。这有助于在该地址的 TreeNode 中添加信息。

public struct stackobject
{
    public int offset;
    public TreeNode treend;
}
stackobject tstackobject = new stackobject();
public Queue<stackobject> jobstack = new Queue<stackobject>();

tstackobject.offset = 4128; // the first Root node is at this address
tstackobject.treend = treenode; // the root of TreeNode
jobstack.Enqueue(tstackobject);  //Queue the first stackobject
      
presentnode = treenode;   // present node stores information about current node level

while(jobstack.Count!=0)
{
    tstackobject = jobstack.Dequeue(); //read object from queue
    presentkey = tstackobject.offset;  // read the offset where node key is stored
    presentnode = tstackobject.treend;
    readkeyblock(presentkey);  // read keyblock in tmpblock using a method
    tstring = getstringataddress(presentkey + 80,tmpblock.len); //read the key name
    TreeNode newnode = new TreeNode(); // create a new Treenode
    newnode.Text = tstring;
                
    presentnode.Nodes.Add(newnode);
    presentnode = newnode;

一旦名称被添加到 TreeNode,我们现在处理存储在键块中的信息。我们从该节点(或键)中的值键开始。节点键指向一个地址,该地址存储了每个值键块的地址(类似于指向指针的指针)。从节点键中,我们知道值键的数量,所以现在问题简化为简单地读取每个值键的地址,并读取该地址处的值键结构。一个值键块有如下结构:

[Serializable]
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct value_block {
    public int  block_size;

    [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 2)]
    public byte[] block_type;// "vk"
   
    public ushort name_len;
    public uint  size;
    public uint  offset;
    public uint  value_type;
    public ushort flags;
    public ushort dummy;
   // char  name; 
};

值键通过 value_type uinteger 提供有关键类型的信息。值为 1、2、6 和 7 的被视作基于文本的数据,4 是二进制 DWORD,其余是二进制数据。此结构中的偏移量数据是实际信息存储的地址,size 提供了该数据的长度。(OFFSET 是从第 4096 字节开始的偏移量,而不是从文件开头。)同样,值节点的名称紧跟在该结构之后存储,name_len 提供了其长度。

名称长度为零的值键成为在 regedit 中看到的“默认”值键。下面的代码展示了如何读取值节点:

if (tmpblock.value_count != 0) //if number of values in the node key is not zero
{
 for(int v=0;v < tmpblock.value_count; v++){
  ad= (int)readuintataddress(tmpblock.offsettovaluelist + root +4 + 4*v) + root; //root = 4096
  readvalueblock(ad); // read value block structure at address pointed by ad in tvalueblock.
  if (tvalueblock.size > 0)
  {
    TreeNode newnode1 = new TreeNode(); // create a TreeNode for this Value Key
    vstring = getstringataddress(ad + Marshal.SizeOf(tvalueblock), tvalueblock.name_len);
    // store the length of key name in Treenode tag.
    // This will help us in the comparison of the files.
    newnode1.Tag = vstring.Length;
    switch (tvalueblock.value_type)
    {  // read the data based on valuetype

一旦值节点被读取,就该处理当前节点键中的子节点(或子键)了。这里,我们也遵循相同的过程。偏移量提供了存储每个子节点键地址的位置的地址。与值键处理不同,这个指向子节点列表的指针可能是一维或二维的。这意味着,由偏移量数据指向的地址可能是一个子键地址列表,或者是一个存储了指向节点键的实际地址列表的地址列表。这是根据一个简单的 offset 类型结构来决定的。我们只需从读取由节点键偏移量数据指向的这个 offset 类型结构开始。

[Serializable]
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct offsets {   //subkey_list
public int  block_size;
[MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 2)]
public byte[] block_type;// // "lf" "il" "ri"
public  ushort count;   
};

lflh 类型的偏移块表示一个一维的节点键地址数组。liri 类型的偏移块表示一个二维的节点键地址。我们只需处理这个结构,获取到节点键地址。对于我们读取的每个节点键地址,我们只需将该地址推入我们的队列中。随着我们的循环进展并读取队列对象,这些节点将自行得到处理。

通过这种方式,整个文件被逐层扫描,我们为找到的每个对象创建一个 TreeNode,并将它们添加到同样保存在队列中的 TreeNode 中。

文件比较

为了进行比较,我们首先将配置单元加载到两个独立的 TreeView 中。第一个是可见的,但第二个仅用于比较目的。treenodetreenode2 分别是这两个 TreeView 的节点。

所采用的方法与 regedit 在每次搜索时所做的类似。我们不使用 MSDN 网站上发布的递归搜索,而是使用由我们自己管理的队列(而不是递归迭代中使用的系统管理的队列)进行逐层搜索。以下是进行简单的 TreeView 节点逐层数据处理的代码。

public Queue<TreeNode> treequeue = new Queue<TreeNode>();
treequeue.Enqueue(treenode);  //treenode is the root node of TreeView
while (treequeue.Count != 0)
{
   Treenode ttree = treequeue.Dequeue();
     do
    {
        ttree.BackColor = Color.Red;  // process the node anyhow
        if (ttree.FirstNode != null) treequeue.Enqueue(ttree.FirstNode);
        ttree = ttree.NextNode;
    } while (ttree != null) ;
}

在上面的代码中,我们正在更改树的每个节点的背景色。这是逐层完成的。

为了开始比较,我们将 Tree1 的背景色设为红色,Tree2 的背景色设为蓝色。我们将逐层比较 Tree2 的每个节点与 Tree1 的每个节点。这种搜索只可能出现两种情况:

  1. 如果在某个层级找到了节点,我们将 Tree1 节点的背景色更改为绿色。
  2. 如果在某个层级未找到节点,我们只需将正在搜索的整个节点复制到 Tree1 中。(我们已经将整个 Tree2 标记为蓝色,所以这会将该节点标记为稍后添加的数据)。
  3. 对于我们添加到 Tree1 的整个子节点,我们不再在此子节点内进行搜索。这是通过不将此子节点添加到队列中来实现的,这样整个子节点就自动从处理中移除了。
  4. 如果被搜索的节点是值类型节点,我们不仅比较节点名称,还比较该节点中的数据。记住我们已将节点名称的长度保存在 Node.Tag 键中。这有助于我们搜索名称和数据。(另请注意,我们将名称和数据都作为单个字符串存储在 Node.Text 属性中,我们需要一个地方来存储键名的长度——其余部分是其数据)。
treequeue.Enqueue(treenode2);  //Queue up the Root node of Tree2
while (treequeue.Count != 0)
{
   TreeNode ttree = treequeue.Dequeue();
   do
   {
       stackthisnode = 1;
       // find this node in Treeone. If there fine, if not we wont add this
       // to stack for processing. Method adjust the value of stackthisnode to signal its stacking
       findthisnodeintree1(ttree);
       if (ttree.FirstNode != null && stackthisnode == 1) treequeue.Enqueue(ttree.FirstNode);
       ttree = ttree.NextNode;
   } while (ttree != null);
}

最后,方法 Findthisnodeintree1 接收来自 Tree2 的 Treenode,并在 Tree1 中搜索其是否存在。

  1. 为了简化搜索,它首先检查 Tree2 节点是否有子元素。这是通过 Node.Tag 元素来完成的。(请记住,对于值类型键,我们已将键名的长度存入 Node.Tag)。如果此 Node.Tag 为 null,则表示它是一个节点键类型的数据。
  2. 我们使用 Node.FullPath 属性来获取节点的完整地址。我们解析这个名称中的 '\\',以获取每个层级的节点名称。
  3. 对于每个层级和可用的键名,我们在 Tree1 中进行搜索。如果找到,我们继续前进(并将 Tree1 节点设为绿色)直到最后一层。如果连最后一个也找到了,我们希望处理该节点中的子键,因此允许将此节点键加入队列。
  4. 如果在任何层级都找不到,我们只需将该层级的节点从 Tree2 复制到 Tree1,并禁止将该节点加入队列以进行进一步处理。
void searchforthisnodeintree1(TreeNode tn)
{
    searchqueue.Enqueue(treenode);  //treenode is the masternode of Treeview1
    string fullpath = tn.FullPath;
    string shrt = "", nodetext="";
    int index = -1, comparestatus=0, last=0, level=0;
    if (tn.Tag == null)  //find only directory nodes. tag = null only for key nodes
    {
        str2 = tn.Text;
        while (searchqueue.Count != 0)
        {
            index = -1;
            ttree = searchqueue.Dequeue();
            temp = ttree;  //making a copy of ttree
            index = fullpath.IndexOf('\\'); last = 0;
            if (index == -1) { shrt = fullpath; last = 1; }
            else { shrt = fullpath.Substring(0, index); fullpath = fullpath.Substring(index + 1); }
            do
            {
                found = 0;
                if (ttree.Tag == null)//we are not interested in value nodes
                {
                    nodetext = ttree.Text;
                    comparestatus = string.Compare(shrt, nodetext);
                    if (comparestatus == 0)
                    {
                        ttree.BackColor = Color.Green;
                        searchqueue.Enqueue(ttree.FirstNode); 
                        found = 1; if (last == 1) searchqueue.Clear(); break;
                    }
                }
                ttree = ttree.NextNode;
            } while (ttree != null);
            if (found == 0) //if the node is not found,means it was added in second file
            {
                stackthisnode = 0; //disallow queuing of this node
                if (temp.Parent != null) { temp = temp.Parent; temp.Nodes.Add((TreeNode)tn.Clone()); }
                else { treeView1.Nodes.Add((TreeNode)tn.Clone()); }
            }
        }
    }

值节点的搜索方式也类似。唯一的区别是,我们必须先搜索名称,如果找到,再比较数据字符串。如果数据不相似,我们将 Tree1 对象标记为粉色。

关注点

这段代码也展示了如何快速比较两个 Treenode。我认为 Regedit 在搜索信息时也采用了类似的方法。

历史

我要感谢上面提到的两个链接,它们为制作这个实用工具提供了帮助。对软件所做的更改:

  1. 加入了窗体缩放功能,以便在更大的屏幕上获得更好的视图。
  2. 增加了检测键节点是否为空的边界条件。之前这会导致队列中出现空条目,并且在出队时处理不当。
  3. ttree = searchqueue.Dequeue();
    if (ttree == null)
    {
        stackthisnode = 0; 
        temp1.Nodes.Add((TreeNode)tn.Clone()); 
        return;
    }// this happens when the last node is empty, means items were added here
  4. 添加了一个按钮来移除相同的数据节点。这使得查看更容易一些。我们实际上创建了一个新的 Treenode (=Finaltreenode),并且只将那些未标记为绿色的节点复制进去。为了简化删除相同节点的任务,我们首先删除值键。这样,我们就能清空那些所有子键都被标记为绿色的节点键。
  5. treequeue.Clear();
    newqueue.Clear();start = 1;
    treequeue.Enqueue(treenode);
    FinalNode.Text = "--REGISTRY--ROOT--";//"--Registry--Root--";
    FinalNode.BackColor = Color.Green;
    FinalNode.Tag = null;
    newqueue.Enqueue(FinalNode);  
    while (treequeue.Count != 0)
    {
        ttree = treequeue.Dequeue();
        tnn = newqueue.Dequeue();
        do
        {
        if (ttree.Tag != null && ttree.BackColor != Color.Green)  // if value key
            {
                tnn1 = new TreeNode();
                tnn1.Text = ttree.Text;
                tnn1.BackColor = ttree.BackColor;
                tnn.Nodes.Add(tnn1);
                tnn1.Tag = 1;
                tnn1.ImageIndex = ttree.ImageIndex;
            }
            if (ttree.Tag == null )  //if node key
            {  
           if (ttree.FirstNode != null) treequeue.Enqueue(ttree.FirstNode);
                    tnn1 = new TreeNode(); tnn1.Text = ttree.Text;
                    tnn1.Tag = null;
                    tnn1.BackColor = ttree.BackColor;// Color.Green; 
                    if (start == 1) { start = 0; newqueue.Enqueue(FinalNode); }
                    else
                    { tnn.Nodes.Add(tnn1);
                        if (ttree.FirstNode != null) newqueue.Enqueue(tnn1);
                    } 
           } 
           try { ttree = ttree.NextNode; }
            catch { ttree = null; }
        } while (ttree != null);
    }
  6. 然后我们以类似的方式继续删除节点(或目录)键。为此,我们将不得不进行多次迭代。这里的逻辑是,我们删除所有被标记为绿色且没有子节点的节点键。在单次迭代中,节点从最后一层开始删除。然后我们再次迭代,删除更多最后的绿色标记节点,我们一直这样做,直到没有更多的最后绿色节点可以删除。它是这样完成的:
  7. 这里的想法是我们创建一个新的 Treenode,并复制除最后那些空节点外的所有节点。
  8. while(anynodesdeleted !=0) //Run until no more nodes to delete
    {
        treequeue.Clear();
        newqueue.Clear();start = 1;
        treenode.Nodes.Clear();
        treenode = (TreeNode) FinalNode.Clone();
        FinalNode.Nodes.Clear();
        treequeue.Enqueue(treenode);
        FinalNode.Text = "--REGISTRY--ROOT--";//set up root node
        FinalNode.BackColor = Color.Green;
        FinalNode.Tag = null; //all node types have Tag=null; Value keys have integer Tag
        newqueue.Enqueue(FinalNode);  
        anynodesdeleted = 0;
        while (treequeue.Count != 0)
        {
            ttree = treequeue.Dequeue();
            tnn = newqueue.Dequeue();
            do
            {
                if (ttree.Tag != null && ttree.BackColor != Color.Green)// if value key just copy over
                {
                    tnn1 = new TreeNode();
                    tnn1.Text = ttree.Text;
                    tnn1.BackColor = ttree.BackColor;
                    tnn.Nodes.Add(tnn1);
                    tnn1.Tag = 1;
                    tnn1.ImageIndex = ttree.ImageIndex;
                }
                if (ttree.Tag == null )  //if node key
                {
                    if (ttree.FirstNode != null) treequeue.Enqueue(ttree.FirstNode);
                    tnn1 = new TreeNode(); tnn1.Text = ttree.Text;
                    tnn1.Tag = null;
                    tnn1.BackColor = ttree.BackColor;// Color.Green; 
                    if (start == 1) { start = 0; newqueue.Enqueue(FinalNode); }
                    else
                    {
                        if (ttree.BackColor == Color.Green && ttree.FirstNode == null) { anynodesdeleted++; }
                        else tnn.Nodes.Add(tnn1); //copy if not last node
                        if (ttree.FirstNode != null) newqueue.Enqueue(tnn1);
                    }
    	       } 
    	       try { ttree = ttree.NextNode; }
    	       catch { ttree = null; }
    	    } while (ttree != null);
        }
    }
© . All rights reserved.