谈谈注册表项信息隐藏

来源:互联网 发布:东华网络教育平台 编辑:程序博客网 时间:2024/06/04 18:57

理论:
注册表是Windows系统存储关于计算机配置信息的数据库,包括了系统运行时需要调用的运行方式的设置,是系统的核心。操作系统是用特殊的文件(Hive文件)来存储注册表的内容,并提供给用户相关的编程接口(APIs)来进行注册表的操作,例如Advapi32.dll中的Registry Functions(如:RegEnumKey)。在Windows操作系统中,函数的调用有着极其规范的层次结构,如下图所示是系统实现RegEnumKey函数的调用体系。

正常情况下,函数调用返回的结果应该是操作系统提供的实际信息,但是随着间谍软件和Rootkit等恶意代码技术的发展,越来越多的恶意程序都具备了隐藏自身存在信息的功能,这其中最重要就是对注册表项信息的隐藏。注册表隐藏就是将系统呈现给用户的注册表信息进行修改,使得用户或检测工具无法直观地发现事实上存在的注册表内容。目前的注册表隐藏,大都采用的是Hook技术。根据它们不同的运行环境和模式,把常见的注册表隐藏技术称为用户态下注册表隐藏技术和核心态下注册表隐藏技术。它们通常是利用IAT Hook, Inline Hook、远程线程注入、SSDT Hook、中断调度表挂钩、IRP函数表挂钩、原始内核代码修改和特殊指针修改等方式来实现。网上比较多见的核心态隐藏技术是ssdt hook NtEnumerateKey 和inline hook CmEnumerateKey。然而这些方法已经很容易被防御体系发现。

本文是通过修改hive内存结构中的特殊指针hook实现注册表项的隐藏。关于hive文件结构,论坛中曾经有两人写过相关的文章,一篇是炉子大牛写的《HIVE格式解析》,另一篇是HSQ大牛写的《注册表监控弱点演示程序 v0.2 逆向ASM源码及相关资料》,另外,Petter Nordahl-Hagen写过一个hive文件存取库,我整理了下,也附在本篇文章之后,方便大家查阅。

相信看过网上这几篇的朋友一定还有些疑虑,到底注册表的工作原理是怎样的呢?我们今天通过一个驱动代码来揭开它神秘的面纱。

1. 在磁盘上,注册表并不是简单的一个大文件,而是一组称为hive的单独文件。每个hive文件包含了一颗注册表树。有一个键作为该树的根。子键和他们的值存储在根的下面。

当配置管理器(注册表的执行体子系统)加载hive的时候,会在HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/hivelist子键下的注册表值中记录下每个hive的路径。下面是本机注册表hivelist子键导出的信息:

 

[HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/hivelist]

"//REGISTRY//MACHINE//HARDWARE"=""

"//REGISTRY//MACHINE//SECURITY"="//Device//HarddiskVolume1//WINDOWS//system32//config//SECURITY"

"//REGISTRY//MACHINE//SOFTWARE"="//Device//HarddiskVolume1//WINDOWS//system32//config//SOFTWARE"

"//REGISTRY//MACHINE//SYSTEM"="//Device//HarddiskVolume1//WINDOWS//system32//config//SYSTEM"

"//REGISTRY//USER//.DEFAULT"="//Device//HarddiskVolume1//WINDOWS//system32//config//DEFAULT"

"//REGISTRY//MACHINE//SAM"="//Device//HarddiskVolume1//WINDOWS//system32//config//SAM"

"//REGISTRY//USER//S-1-5-20"="//Device//HarddiskVolume1//Documents and Settings//NetworkService//NTUSER.DAT"

"//REGISTRY//USER//S-1-5-20_Classes"="//Device//HarddiskVolume1//Documents and Settings//NetworkService//Local Settings//Application Data//Microsoft//Windows//UsrClass.dat"

"//REGISTRY//USER//S-1-5-19"="//Device//HarddiskVolume1//Documents and Settings//LocalService//NTUSER.DAT"

"//REGISTRY//USER//S-1-5-19_Classes"="//Device//HarddiskVolume1//Documents and Settings//LocalService//Local Settings//Application Data//Microsoft//Windows//UsrClass.dat"

"//REGISTRY//USER//S-1-5-21-1550111154-316279633-4274931122-1006"="//Device//HarddiskVolume1//Documents and Settings//Jason//NTUSER.DAT"

"//REGISTRY//USER//S-1-5-21-1550111154-316279633-4274931122-1006_Classes"="//Device//HarddiskVolume1//Documents and Settings//Jason//Local Settings//Application Data//Microsoft//Windows//UsrClass.dat"

 

通过这个信息我们看到,在C:/WINDOWS/system32/config目录中存储着HKLM所有项的hive文件以及一个HKU的DEFAULT hive文件,其他的HKU下的hive文件存储在c:/Documents and Settings等子目录下。在hivelist记录的这些信息中,我们看到hive文件只能加载到HKLM和HKU下。如图:

 

也就是HKEY_LOCAL_MACHINE和HKEY_USERS外的注册表项没有HIVE文件对应。

配置管理器从逻辑上将一个hive分成一些称为block的分配单元。如图:

 


其方式类似于文件系统将一个磁盘分成簇。根据定义,注册表block的大小为4096字节(4kb)。当新的数据要扩展一个hive时,该hive总是按照block的粒度来增加。hive的第一个block称为base block. 里面包含了一些全局信息,包括一个特征签名regf、更新的序列号,时间戳,hive格式版本号、校验和以及该hive的内部文件名(如:/Device/HarddiskVolume1/WINDOWS/system32/config/SECURITY)。

结构如下:

lkd> dt _hbase_block

nt!_HBASE_BLOCK
   +0x000 Signature         : Uint4B

   +0x004 Sequence1         : Uint4B

   +0x008 Sequence2         : Uint4B

   +0x00c TimeStamp         : _LARGE_INTEGER
    +0x014 Major             : Uint4B
    +0x018 Minor             : Uint4B
    +0x01c Type              : Uint4B
    +0x020 Format            : Uint4B
    +0x024 RootCell          : Uint4B
    +0x028 Length            : Uint4B
    +0x02c Cluster           : Uint4B
    +0x030 FileName          : [64] UChar

   +0x070 Reserved1         : [99] Uint4B
    +0x1fc CheckSum          : Uint4B
    +0x200 Reserved2         : [894] Uint4B
    +0xff8 BootType          : Uint4B
    +0xffc BootRecover       : Uint4B

windows将一个hive所存储的注册表数据组织在一种称为cell的容器中。一个cell可以容纳一个键、一个值、一个安全描述符、一列子键或者一列键值。其对应的结构如下:

 

typedef struct _CELL_DATA
{
     union _u
    {
         CM_KEY_NODE       KeyNode;

         CM_KEY_VALUE      KeyValue;
         CM_KEY_SECURITY   KeySecurity;     // Variable security descriptor length

         CM_KEY_INDEX      KeyIndex;        // Variable sized structure
         CM_BIG_DATA       ValueData;       // This is only for big cells; a list of cells
                                          // all of the length CM_KEY_VALUE_BIG

         HCELL_INDEX       KeyList[1];      // Variable sized array
         WCHAR             KeyString[1];    // Variable sized array
     } u;
} CELL_DATA, *PCELL_DATA;

该结构中嵌套了一个联合体,用于代表cell可以存放的不同类型的数据。

下面给出CELL_DATA可以存放的几种不同类型的cell结构:

lkd> DT _CM_KEY_NODE
nt!_CM_KEY_NODE
    +0x000 Signature         : Uint2B

    +0x002 Flags             : Uint2B
    +0x004 LastWriteTime     : _LARGE_INTEGER

    +0x00c Spare             : Uint4B
    +0x010 Parent            : Uint4B
    +0x014 SubKeyCounts      : [2] Uint4B

    +0x01c SubKeyLists       : [2] Uint4B
    +0x024 ValueList         : _CHILD_LIST

    +0x01c ChildHiveReference : _CM_KEY_REFERENCE
    +0x02c Security          : Uint4B
    +0x030 Class             : Uint4B
    +0x034 MaxNameLen        : Pos 0, 16 Bits

    +0x034 UserFlags         : Pos 16, 4 Bits
    +0x034 VirtControlFlags : Pos 20, 4 Bits
    +0x034 Debug             : Pos 24, 8 Bits
    +0x038 MaxClassLen       : Uint4B
    +0x03c MaxValueNameLen   : Uint4B
    +0x040 MaxValueDataLen   : Uint4B
    +0x044 WorkVar           : Uint4B
    +0x048 NameLength        : Uint2B
    +0x04a ClassLength       : Uint2B
    +0x04c Name              : [1] Uint2B

lkd> DT _CM_KEY_VALUE
nt!_CM_KEY_VALUE
    +0x000 Signature         : Uint2B

    +0x002 NameLength        : Uint2B
    +0x004 DataLength        : Uint4B
    +0x008 Data              : Uint4B
    +0x00c Type              : Uint4B
    +0x010 Flags             : Uint2B
    +0x012 Spare             : Uint2B
    +0x014 Name              : [1] Uint2B

lkd> dt _CM_KEY_SECURITY
nt!_CM_KEY_SECURITY
    +0x000 Signature         : Uint2B
    +0x002 Reserved          : Uint2B
    +0x004 Flink             : Uint4B
    +0x008 Blink             : Uint4B
    +0x00c ReferenceCount    : Uint4B
    +0x010 DescriptorLength : Uint4B
    +0x014 Descriptor        : _SECURITY_DESCRIPTOR_RELATIVE

lkd> dt _CM_KEY_INDEX
nt!_CM_KEY_INDEX
   +0x000 Signature        : Uint2B
   +0x002 Count            : Uint2B
   +0x004 List             : [1] Uint4B

lkd> dt _CM_BIG_DATA
nt!_CM_BIG_DATA
   +0x000 Signature        : Uint2B
   +0x002 Count            : Uint2B
   +0x004 List             : Uint4B

如果一个cell要加入一个hive,而该hive必须扩充才能装下它时,系统会创建一个 称为 bin的分配单元。该bin的范围包括了新添cell所占空间再延续到下一个block边界。系统将bin中除了cell以外的部分视为可以分配给其他cell的空闲空间。 bin的头部中包含了一个签名hbin、一个存储了bin在hive文件中偏移量的字段以及bin的大小。

typedef struct _HBIN {
    ULONG       Signature;
    ULONG       FileOffset;     // Own file offset (used in checking)
    ULONG       Size;           // Size of bin in bytes, all inclusive
    ULONG       Reserved1[2];   // Old FreeSpace and FreeList (from 1.0)
    LARGE_INTEGER   TimeStamp; // Old Link (from 1.0). Usually trash, but
                                // first bin has valid value used for .log
                                // correspondence testing, only meaningful
                                // on disk.
    ULONG       Spare;          // this used to be MemAlloc. We don't use it anymore as we
                                // can't afford to touch the bin (it's not residing in paged-pool
                                // anymore, so touching it means modifying mnw pages).
                                // Spare is used for the ShiftFreeBins Stuff - in memory only!

    //
    // Cell data goes here
    //

} HBIN, *PHBIN;

所以一个hive的磁盘映像看起来如下图所示。

 

 

是按照4k字节对齐的。

 

 

 

 

配置管理器不会一有注册表访问就访问磁盘中的hive。Windows 2000保存了一份所有hive的副本在内核地址空间中。当某hive初始化后,配置管理器就会检查该hive文件的大小,并在内核分页内存池中分配足够存储它的内存,再将该hive文件读入内存。由于所有加载注册表hives都被读入了分页内存池,所以一般说来在Windows 2000中,注册表数据是分页内存池最大的消费者。(可以使用Poolmon查看分页内存池的分配情况)

在Windows XP和Windows Server 2003中,配置管理器会在需要的时候映射hive文件的某些部分到内存中。它使用cache管理器的文件映射函数,将16kb的视图映射到hive文件中.为防止hive映射占据所有的cache空间,配置管理器试图保持一个被映射hive在任何时候都不超过256个视图,如果超过就将最近最少使用的视图去掉。配置管理器仍然使用分页内存池来存储各种数据结构(包括最不经常使用的视图列表),但比之它在Windows 2000中的用量这只是很小一部分。

在Windows XP和Windows Server 2003中,如果某block的大小超过了256KB,配置管理器就会将它存储在分页内存池中。 如果hive从来不增长,配置管理器就会以一种内存模式管理注册表,就当hive是文件。给定一个cell索引,配置管理器就能可以计算出该cell在内存中的位置,只要用该cell索引(在hive中的偏移量)加上该hive在内存中的基址即可。在系统引导的早期,Ntldr就是这么处理SYSTEM hive的:Ntldr将SYSTEM hive以只读的方式读入内存,再用cell索引加hive基址的方式定位cell。不幸的是,当hive添加新键或值后会变大,这意味着系统需要为存储新键/值的bin分配分页内存池。因此,存储注册表数据的分页内存池不必是连续的。

 

为了处理内存中存放hive数据的地址不连续的的问题,配置管理器采取了一种类似Windows内存管理器将虚存地址映射到内存时所使用的策略。配置管理器用了一个两级系统。

 

 

 

 

如图所示,该系统以一个cell索引作为输入而后返回cell索引所在的block的内存地址以及cell所在的block的内存地址作为输出。请记得一个bin中可以包含一个或多个block且hive以bin为单位增长,所以Windows用连续的区域存储bin。因此,一个bin中的所有block会在同一个cache管理器视图中(Windows XP和Windows Server 2003)或者分页内存池的同一部分(Windows 2000)。

为了实现映射,配置管理器将cell索引逻辑上分为若干字段,与内存管理器将虚地址分成若干字段使用的是相同方法。Windows将cell索引的第一个字段作为一个hive的cell映射目录的索引。该cell映射目录中包含了1024个记录,每一个记录指向一个有512个记录的cell映射表。 cell索引的第二个字段被定义为该表的索引(又得到一条记录)。该记录定位了目标cell所在的bin及block内存地址。在Windows XP和Windows Server 2003中,不是所有的bin都需要被映射进内存,而如果某策略来查询得到的地址为0,配置管理器就会将该bin映射进内存,如果需要,就将它维护的最近最少使用列表中的一个取代。

在译址的最后一步,配置管理器将cell索引的最后一个字段作为之前定位的block的偏移量找到目标策略来在内存中的确实位置。当一个hive初始化后,配置管理器会动态地创建映射表,为该hive的每一个block指定一个映射记录,还会根据hive大小的变化从cell目录中添加或删除表。

为了方便依据cell的索引来计算出cell的内存位置,在windows内核提供了一个计算函数。这个函数存放在一个称谓hhive的结构中。这个结构如下所示:

lkd> dt   _HHIVE
nt!_HHIVE
    +0x000 Signature         : Uint4B

    +0x004 GetCellRoutine    : Ptr32      _CELL_DATA*
    +0x008 ReleaseCellRoutine : Ptr32      void
    +0x00c Allocate          : Ptr32      void*
    +0x010 Free              : Ptr32      void
    +0x014 FileSetSize       : Ptr32      unsigned char

    +0x018 FileWrite         : Ptr32      unsigned char
    +0x01c FileRead          : Ptr32      unsigned char
    +0x020 FileFlush         : Ptr32      unsigned char
    +0x024 BaseBlock         : Ptr32 _HBASE_BLOCK

    +0x028 DirtyVector       : _RTL_BITMAP
    +0x030 DirtyCount        : Uint4B
    +0x034 DirtyAlloc        : Uint4B
    +0x038 RealWrites        : UChar
    +0x03c Cluster           : Uint4B
    +0x040 Flat              : UChar
    +0x041 ReadOnly          : UChar

    +0x042 Log               : UChar
    +0x044 HiveFlags         : Uint4B

    +0x048 LogSize           : Uint4B
    +0x04c RefreshCount      : Uint4B

    +0x050 StorageTypeCount : Uint4B
    +0x054 Version           : Uint4B
    +0x058 Storage           : [2] _DUAL

GetCellRoutine就是计算函数,函数返回 _CELL_DATA 指针,即cell的地址。这个函数原型是

typedef struct _CELL_DATA * (*PGET_CELL_ROUTINE)(
    struct _HHIVE   *Hive,
    HCELL_INDEX Cell
    );

GetCellRoutine是保存在hhive结构中的PGET_CELL_ROUTINE类型的函数指针。参数2 HCELL_INDEX cell就是我们前面讲的cell索引。

而对于本函数的两个参数,又是怎样的得到呢?下面我们就谈谈两个重要的结构。即:
键对象(key object)和键控制块(key control block)

 

当一个应用程序打开或者创建一个注册表键的时候,对象管理器会给应用程序一个句柄,让它通过这个句柄来引用该键。 该句柄所代表的key object是配置管理器在对象管理器的协助下分配的。有了对象管理器的支持,配置管理器就可以利用对象管理器所提供的安全及引用计数功能了。

对于每个打开的注册表键,配置管理器还会为它分配一个key control block。一个key control block中存储了该键的完整路径名、该key control block所对应的键节点的cell索引以及一个标识用以指示在该键的最后一个句柄被关闭后,配置管理器是否要删除此key control block所对应的的key cell。Windows将所有的key control block放进一个哈希表中好用名字对存在的key control block进行快速查找。一个key object指向一个与它相关的key control block,因此,如果有两个应用程序打开同一个注册表键,这两个程序各会获得一个key object,而这两个key object都会指向同一个key control block。

我们先看看key object的结构:

lkd> DT _cm_key_body
nt!_CM_KEY_BODY
    +0x000 Type              : Uint4B

    +0x004 KeyControlBlock   : Ptr32 _CM_KEY_CONTROL_BLOCK
    +0x008 NotifyBlock       : Ptr32 _CM_NOTIFY_BLOCK
    +0x00c ProcessID         : Ptr32 Void
    +0x010 Callers           : Uint4B
    +0x014 CallerAddress     : [10] Ptr32 Void

    +0x03c KeyBodyList       : _LIST_ENTRY

在该结构中有一项KeyControlBlock ,就是配置管理其分配的key control block。我们看下这个结构:

lkd> dt _CM_KEY_CONTROL_BLOCK
nt!_CM_KEY_CONTROL_BLOCK
    +0x000 RefCount          : Uint2B
    +0x004 ExtFlags          : Pos 0, 8 Bits
    +0x004 PrivateAlloc      : Pos 8, 1 Bit
    +0x004 Delete            : Pos 9, 1 Bit
    +0x004 DelayedCloseIndex : Pos 10, 12 Bits

    +0x004 TotalLevels       : Pos 22, 10 Bits
    +0x008 KeyHash           : _CM_KEY_HASH

    +0x008 ConvKey           : Uint4B
    +0x00c NextHash          : Ptr32 _CM_KEY_HASH

    +0x010 KeyHive           : Ptr32 _HHIVE
    +0x014 KeyCell           : Uint4B
    +0x018 ParentKcb         : Ptr32 _CM_KEY_CONTROL_BLOCK

    +0x01c NameBlock         : Ptr32 _CM_NAME_CONTROL_BLOCK
    +0x020 CachedSecurity    : Ptr32 _CM_KEY_SECURITY_CACHE
    +0x024 ValueCache        : _CACHED_CHILD_LIST
    +0x02c IndexHint         : Ptr32 _CM_INDEX_HINT_BLOCK

    +0x02c HashKey           : Uint4B
    +0x02c SubKeyCount       : Uint4B

    +0x030 KeyBodyListHead   : _LIST_ENTRY
    +0x030 FreeListEntry     : _LIST_ENTRY
    +0x038 KcbLastWriteTime : _LARGE_INTEGER

    +0x040 KcbMaxNameLen     : Uint2B
    +0x042 KcbMaxValueNameLen : Uint2B

    +0x044 KcbMaxValueDataLen : Uint4B
    +0x048 KcbUserFlags      : Pos 0, 4 Bits
    +0x048 KcbVirtControlFlags : Pos 4, 4 Bits

    +0x048 KcbDebug          : Pos 8, 8 Bits
    +0x048 Flags             : Pos 16, 16 Bits

在其中有两个重要的项
   +0x014 KeyCell           : Uint4B 就是我们要隐藏的注册表项对应的cell索引。
   +0x010 KeyHive           : Ptr32 _HHIVE 就是我们前面刚刚讲过的HHIVE结构指针。

 

现在我们要隐藏注册表中的一个键,就很容易了。由于篇幅太长,后续内容见下篇。

原创粉丝点击