如何读取/转储/比较注册表配置项






4.73/5 (27投票s)
恶意软件可以躲避 Win32 API,但无法躲避这个工具,因为我们直接转储注册表配置项。
动机
最近我遇到了一个很棘手的蠕虫/Rootkit 问题,自然想知道它在我系统里做了什么改动。所以我开始寻找一些检测注册表改动的工具。我想要一个简单的工具,在感染前后将完整的注册表内容转储到文本文件中,然后通过简单的文本差异比较,就能快速看到改动。但运气不太好。因为我找到的所有注册表工具都使用 Win32 API 获取数据,而那个狡猾的 Rootkit 将 API 重定向到自己,从而隐藏了起来。而且,后来我发现恶意软件甚至不需要那么聪明就能隐藏注册表中的信息,使其无法通过标准的 API 访问。
所以现在我有了系统还原点提供的干净的注册表文件,以及我感染系统中的脏文件。我不断地研究这些配置项,直到我开发出一个简单的工具,能够以简单的文本格式转储和比较它们的内容。我还需要在每个条目中包含完整的注册表路径,这样在使用文本差异比较这些转储文件时,就能清楚地看到改动发生的位置。
配置项格式
NT/XP 注册表文件(二进制配置项,而不是文本注册表文件)实际上非常简单。它们只是由一堆 4KB 块组成,每个块包含大小可变的块。每个块都以
通常的 4 字节大小和 2 字节类型开头。
大致就是这样。这就是 MS 注册表配置项的格式。哦,我差点忘了。第一个块的前 1KB 是注册表头,据我所知,没有有用的信息。
现在,这些大小可变的块里面有什么?
描述注册表的最好方法是将它想象成一个文件系统,其中键是目录,值是文件。正如我们已经知道的,目录和文件都有名称,除了每个文件还可以包含数据。所以有两种基本的块,一种用于键,一种用于值。值得庆幸的是,MS 决定在之前提到的块类型字段中使用人类可读的 2 个字符字符串。因此,如果你在十六进制查看器中打开配置项,可以清楚地看到“nk”表示键块,“vk”表示值块。
使用代码
这是实际转储注册表配置项的代码。我使用了可移植的 C 代码,因此应该可以在 Unix 上编译,无需进行太多修改。
#include <string.h> #include <stdio.h> #include <stdlib.h> struct offsets { long block_size; char block_type[2]; // "lf" "il" "ri" short count; long first; long hash; }; struct key_block { long block_size; char block_type[2]; // "nk" char dummya[18]; int subkey_count; char dummyb[4]; int subkeys; char dummyc[4]; int value_count; int offsets; char dummyd[28]; short len; short du; char name; }; struct value_block { long block_size; char block_type[2]; // "vk" short name_len; long size; long offset; long value_type; short flags; short dummy; char name; }; void walk ( char* path, key_block* key ) { static char* root=(char*)key-0x20, *full=path; // add current key name to printed path memcpy(path++,"/",2); memcpy(path,&key->name,key->len); path+=key->len; // print all contained values for(int o=0;o<key->value_count;o++){ value_block* val = (value_block*)(((int*)(key->offsets+root+4))[o]+root); // we skip nodes without values if(!val->offset) continue; // data are usually in separate blocks without types char* data = root+val->offset+4; // but for small values MS added optimization where // if bit 31 is set data are contained wihin the key itself to save space if(val->size&1<<31) { data = (char*)&val->offset; } // notice that we use memcopy for key/value names everywhere instead of strcat // reason is that malware/wiruses often write non nulterminated strings to // hide from win32 api *path='/'; if(!val->name_len) *path=' '; memcpy(path+1,&val->name,val->name_len); path[val->name_len+1]=0; printf("%s [%d] = ",full,val->value_type); for(int i=0;i<(val->size&0xffff);i++) { // print types 1 and 7 as unicode strings if(val->value_type==1||val->value_type==7) { if(data[i]) putchar(data[i]); // and rest dump as binary data } else { printf("%02X",data[i]); } } printf("\n"); } // for simplicity we can imagine keys as directories in filesystem and values // as files. // and since we already dumped values for this dir we will now iterate // thru subdirectories in the same way offsets* item = (offsets*)(root+key->subkeys); for(int i=0;i<item->count;i++){ // in case of too many subkeys this list contain just other lists offsets* subitem = (offsets*)((&item->first)[i]+root); // usual directory traversal if(item->block_type[1]=='f'||item->block_type[1]=='h') { // for now we skip hash codes (used by regedit for faster search) walk(path,(key_block*)((&item->first)[i*2]+root)); } else for(int j=0;j<subitem->count;j++) { // also ms had chosen to skip hashes altogether in this case walk(path,(key_block*)((&subitem->first)[item->block_type[1]=='i'?j*2:j]+root)); } } } int main(int argc, char** argv) { char path[0x1000]={0}, *data; FILE* f; int size; if(argc<2||!(f=fopen(argv[1],"rb"))) return printf("hive path err"); fseek(f,0,SEEK_END); if(!(size=ftell(f))) return printf("empty file"); rewind(f); data=(char*)malloc(size); fread(data,size,1,f); fclose(f); // we just skip 1k header and start walking root key tree walk(path,(key_block*)(data+0x1020)); free(data); return 0; }
关注点
请记住,它会转储你通常甚至无法访问的值,所以要小心。
它非常适合在软件安装前后转储配置项,然后使用文本差异比较更改(例如,UnixUtils 中的命令行版本非常棒)。