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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (27投票s)

2008年3月16日

CPOL

2分钟阅读

viewsIcon

152930

downloadIcon

2044

恶意软件可以躲避 Win32 API,但无法躲避这个工具,因为我们直接转储注册表配置项。

动机

最近我遇到了一个很棘手的蠕虫/Rootkit 问题,自然想知道它在我系统里做了什么改动。所以我开始寻找一些检测注册表改动的工具。我想要一个简单的工具,在感染前后将完整的注册表内容转储到文本文件中,然后通过简单的文本差异比较,就能快速看到改动。但运气不太好。因为我找到的所有注册表工具都使用 Win32 API 获取数据,而那个狡猾的 Rootkit 将 API 重定向到自己,从而隐藏了起来。而且,后来我发现恶意软件甚至不需要那么聪明就能隐藏注册表中的信息,使其无法通过标准的 API 访问。

所以现在我有了系统还原点提供的干净的注册表文件,以及我感染系统中的脏文件。我不断地研究这些配置项,直到我开发出一个简单的工具,能够以简单的文本格式转储和比较它们的内容。我还需要在每个条目中包含完整的注册表路径,这样在使用文本差异比较这些转储文件时,就能清楚地看到改动发生的位置。

dump.JPG

配置项格式

NT/XP 注册表文件(二进制配置项,而不是文本注册表文件)实际上非常简单。它们只是由一堆 4KB 块组成,每个块包含大小可变的块。每个块都以

通常的 4 字节大小和 2 字节类型开头。

hive.JPG

大致就是这样。这就是 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 中的命令行版本非常棒)。

© . All rights reserved.