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






4.65/5 (11投票s)
注册表配置单元读取器和比较器。
介绍
为了查看某些软件是如何操作 Windows 注册表的,我需要一个工具,能够读取两个配置单元文件,加载它们,然后进行比较以显示两者之间的差异。我在网上找到了一些可以比较文件的代码,但没有一个能以美观的图形化方式比较两个注册表配置单元。所以我想用 C# 自己编写一个。(注册表配置单元是二进制文件,我这里指的不是 regedit.exe 生成的基于文本的 .reg 文件。)
背景
Windows 将注册表存储在二进制文件中。这种注册表配置单元的格式并未公开。数据在注册表中的存储方式很简单。它以一个大小为 4096 字节的注册表头开始。然后是逻辑数据块——也称为单元(Cell)。根据逻辑数据块的类型,它会有一个指针列表(称为偏移量 = 从第 4096 字节开始的文件偏移量)。这些指针指向另一个包含更多信息或数据的逻辑单元。广义上说,有两种类型的单元:
- 节点键单元 (Node Key Cell)
- 值键单元 (Value Key Cell)
节点键——顾名思义——是树的节点。它就像 Windows 文件系统中的目录。一个目录可以包含更多的目录和文件。这种节点单元有四个重要的信息片段。
- 此节点(或键)中的子键数量
- 指向一个单元的指针,该单元存储了这些子键的地址。
- 此节点中的值键单元数量。
- 指向一个单元的指针,该单元存储了值键的地址。
值键单元存储数据。值键的行为类似于文件,具有不同类型的格式和信息。值键大致分为包含基于文本的信息或二进制信息两类。
配置单元 (HIVE) 从地址 (4096+32) 处的根节点开始。通过读取第一个节点键类型的单元来读取信息。子键节点信息为我们提供了该节点中存在的子键的链接。我们必须遍历所有节点才能读取整个注册表配置单元。在深入遍历各层级时,我们也会读取值键。
我从这些文章中获得了一些非常有用的信息(以及编码方面的帮助):
- https://codeproject.org.cn/Articles/24415/How-to-read-dump-compare-registry-hives
- http://www.sentinelchicken.com/data/TheWindowsNTRegistryFileFormat.pdf
然后我们将这些节点信息读入一个 WinForms 的 TreeNode 中,并在一个 TreeView 中绘制出来。
一旦我们将两个配置单元加载到两个独立的 TreeView 中,我们便使用一个简单的迭代方法来比较它们。我们根据搜索状态为 TreeNodes 分配颜色:绿色表示未更改的注册表键,红色表示在第二个文件中被删除(或不存在)的键,蓝色表示在第二个文件中添加的键(在第一个文件中不存在),而粉色表示其数据发生更改的值键。
使用代码
代码首先创建一个 WinForm,并向其添加一个 TreeView
、一些 Button
和一些 Label
。Label
提供关于两个注册表配置单元的信息,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
来逐层迭代读取子键。遵循的一般概念是:
- 将根节点地址添加到队列中。
- 启动循环并进行处理,直到队列为空。
- 在处理过程中,从队列中读取一个节点地址,然后读取该地址指向的键节点结构。
- 处理该节点的子节点键信息。如果存在任何子键,则将每个子键的地址添加到队列中。
- 如果存在任何值键,则创建一个带有名称和信息的
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;
};
lf
或 lh
类型的偏移块表示一个一维的节点键地址数组。li
或 ri
类型的偏移块表示一个二维的节点键地址。我们只需处理这个结构,获取到节点键地址。对于我们读取的每个节点键地址,我们只需将该地址推入我们的队列中。随着我们的循环进展并读取队列对象,这些节点将自行得到处理。
通过这种方式,整个文件被逐层扫描,我们为找到的每个对象创建一个 TreeNode
,并将它们添加到同样保存在队列中的 TreeNode
中。
文件比较
为了进行比较,我们首先将配置单元加载到两个独立的 TreeView 中。第一个是可见的,但第二个仅用于比较目的。treenode
和 treenode2
分别是这两个 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 的每个节点。这种搜索只可能出现两种情况:
- 如果在某个层级找到了节点,我们将 Tree1 节点的背景色更改为绿色。
- 如果在某个层级未找到节点,我们只需将正在搜索的整个节点复制到 Tree1 中。(我们已经将整个 Tree2 标记为蓝色,所以这会将该节点标记为稍后添加的数据)。
- 对于我们添加到 Tree1 的整个子节点,我们不再在此子节点内进行搜索。这是通过不将此子节点添加到队列中来实现的,这样整个子节点就自动从处理中移除了。
- 如果被搜索的节点是值类型节点,我们不仅比较节点名称,还比较该节点中的数据。记住我们已将节点名称的长度保存在
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 中搜索其是否存在。
- 为了简化搜索,它首先检查 Tree2 节点是否有子元素。这是通过
Node.Tag
元素来完成的。(请记住,对于值类型键,我们已将键名的长度存入Node.Tag
)。如果此Node.Tag
为 null,则表示它是一个节点键类型的数据。 - 我们使用
Node.FullPath
属性来获取节点的完整地址。我们解析这个名称中的 '\\',以获取每个层级的节点名称。 - 对于每个层级和可用的键名,我们在 Tree1 中进行搜索。如果找到,我们继续前进(并将 Tree1 节点设为绿色)直到最后一层。如果连最后一个也找到了,我们希望处理该节点中的子键,因此允许将此节点键加入队列。
- 如果在任何层级都找不到,我们只需将该层级的节点从 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 在搜索信息时也采用了类似的方法。
历史
我要感谢上面提到的两个链接,它们为制作这个实用工具提供了帮助。对软件所做的更改:
- 加入了窗体缩放功能,以便在更大的屏幕上获得更好的视图。
- 增加了检测键节点是否为空的边界条件。之前这会导致队列中出现空条目,并且在出队时处理不当。
- 添加了一个按钮来移除相同的数据节点。这使得查看更容易一些。我们实际上创建了一个新的 Treenode (=Finaltreenode),并且只将那些未标记为绿色的节点复制进去。为了简化删除相同节点的任务,我们首先删除值键。这样,我们就能清空那些所有子键都被标记为绿色的节点键。
- 然后我们以类似的方式继续删除节点(或目录)键。为此,我们将不得不进行多次迭代。这里的逻辑是,我们删除所有被标记为绿色且没有子节点的节点键。在单次迭代中,节点从最后一层开始删除。然后我们再次迭代,删除更多最后的绿色标记节点,我们一直这样做,直到没有更多的最后绿色节点可以删除。它是这样完成的:
- 这里的想法是我们创建一个新的 Treenode,并复制除最后那些空节点外的所有节点。
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
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);
}
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);
}
}