hivex: Begin implementation of writing to hives.
authorRichard Jones <rjones@redhat.com>
Wed, 3 Feb 2010 17:59:03 +0000 (17:59 +0000)
committerRichard Jones <rjones@redhat.com>
Thu, 4 Feb 2010 11:06:33 +0000 (11:06 +0000)
This implements hivex_node_set_values which is used to
delete the (key, value) pairs at a node and optionally
replace them with a new set.

This also implements hivex_commit which is used to commit
changes to hives back to disk.

hivex/hivex.c
hivex/hivex.h
hivex/hivex.pod

index 44f2998..af36868 100644 (file)
@@ -35,6 +35,7 @@
 #include <assert.h>
 
 #include "full-read.h"
+#include "full-write.h"
 
 #ifndef O_CLOEXEC
 #define O_CLOEXEC 0
@@ -88,6 +89,10 @@ struct hive_h {
   /* Fields from the header, extracted from little-endianness hell. */
   size_t rootoffs;              /* Root key offset (always an nk-block). */
   size_t endpages;              /* Offset of end of pages. */
+
+  /* For writing. */
+  size_t endblocks;             /* Offset to next block allocation (0
+                                   if not allocated anything yet). */
 };
 
 /* NB. All fields are little endian. */
@@ -520,6 +525,10 @@ hivex_close (hive_h *h)
   return r;
 }
 
+/*----------------------------------------------------------------------
+ * Reading.
+ */
+
 hive_node_h
 hivex_root (hive_h *h)
 {
@@ -1399,6 +1408,10 @@ hivex_value_qword (hive_h *h, hive_value_h value)
   return ret;
 }
 
+/*----------------------------------------------------------------------
+ * Visiting.
+ */
+
 int
 hivex_visit (hive_h *h, const struct hivex_visitor *visitor, size_t len,
              void *opaque, int flags)
@@ -1642,3 +1655,374 @@ hivex__visit_node (hive_h *h, hive_node_h node,
   free_strings (strs);
   return ret;
 }
+
+/*----------------------------------------------------------------------
+ * Writing.
+ */
+
+/* Allocate an hbin (page), extending the malloc'd space if necessary,
+ * and updating the hive handle fields (but NOT the hive disk header
+ * -- the hive disk header is updated when we commit).  This function
+ * also extends the bitmap if necessary.
+ *
+ * 'allocation_hint' is the size of the block allocation we would like
+ * to make.  Normally registry blocks are very small (avg 50 bytes)
+ * and are contained in standard-sized pages (4KB), but the registry
+ * can support blocks which are larger than a standard page, in which
+ * case it creates a page of 8KB, 12KB etc.
+ *
+ * Returns:
+ * > 0 : offset of first usable byte of new page (after page header)
+ * 0   : error (errno set)
+ */
+static size_t
+allocate_page (hive_h *h, size_t allocation_hint)
+{
+  /* In almost all cases this will be 1. */
+  size_t nr_4k_pages =
+    1 + (allocation_hint + sizeof (struct ntreg_hbin_page) - 1) / 4096;
+  assert (nr_4k_pages >= 1);
+
+  /* 'extend' is the number of bytes to extend the file by.  Note that
+   * hives found in the wild often contain slack between 'endpages'
+   * and the actual end of the file, so we don't always need to make
+   * the file larger.
+   */
+  ssize_t extend = h->endpages + nr_4k_pages * 4096 - h->size;
+
+  if (h->msglvl >= 2) {
+    fprintf (stderr, "allocate_page: current endpages = 0x%zx, current size = 0x%zx\n",
+             h->endpages, h->size);
+    fprintf (stderr, "allocate_page: extending file by %zd bytes (<= 0 if no extension)\n",
+             extend);
+  }
+
+  if (extend > 0) {
+    size_t oldsize = h->size;
+    size_t newsize = h->size + extend;
+    char *newaddr = realloc (h->addr, newsize);
+    if (newaddr == NULL)
+      return 0;
+
+    size_t oldbitmapsize = 1 + oldsize / 32;
+    size_t newbitmapsize = 1 + newsize / 32;
+    char *newbitmap = realloc (h->bitmap, newbitmapsize);
+    if (newbitmap == NULL) {
+      free (newaddr);
+      return 0;
+    }
+
+    h->addr = newaddr;
+    h->size = newsize;
+    h->bitmap = newbitmap;
+
+    memset (h->addr + oldsize, 0, newsize - oldsize);
+    memset (h->bitmap + oldbitmapsize, 0, newbitmapsize - oldbitmapsize);
+  }
+
+  size_t offset = h->endpages;
+  h->endpages += nr_4k_pages * 4096;
+
+  if (h->msglvl >= 2)
+    fprintf (stderr, "allocate_page: new endpages = 0x%zx, new size = 0x%zx\n",
+             h->endpages, h->size);
+
+  /* Write the hbin header. */
+  struct ntreg_hbin_page *page =
+    (struct ntreg_hbin_page *) (h->addr + offset);
+  page->magic[0] = 'h';
+  page->magic[1] = 'b';
+  page->magic[2] = 'i';
+  page->magic[3] = 'n';
+  page->offset_first = htole32 (offset - 0x1000);
+  page->page_size = htole32 (nr_4k_pages * 4096);
+  memset (page->unknown, 0, sizeof (page->unknown));
+
+  if (h->msglvl >= 2)
+    fprintf (stderr, "allocate_page: new page at 0x%zx\n", offset);
+
+  /* Offset of first usable byte after the header. */
+  return offset + sizeof (struct ntreg_hbin_page);
+}
+
+/* Allocate a single block, first allocating an hbin (page) at the end
+ * of the current file if necessary.  NB. To keep the implementation
+ * simple and more likely to be correct, we do not reuse existing free
+ * blocks.
+ *
+ * seg_len is the size of the block (this INCLUDES the block header).
+ * The header of the block is initialized to -seg_len (negative to
+ * indicate used).  id[2] is the block ID (type), eg. "nk" for nk-
+ * record.  The block bitmap is updated to show this block as valid.
+ * The rest of the contents of the block will be zero.
+ *
+ * Returns:
+ * > 0 : offset of new block
+ * 0   : error (errno set)
+ */
+static size_t
+allocate_block (hive_h *h, size_t seg_len, const char id[2])
+{
+  if (!h->writable) {
+    errno = EROFS;
+    return 0;
+  }
+
+  if (seg_len < 4) {
+    /* The caller probably forgot to include the header.  Note that
+     * value lists have no ID field, so seg_len == 4 would be possible
+     * for them, albeit unusual.
+     */
+    if (h->msglvl >= 2)
+      fprintf (stderr, "allocate_block: refusing too small allocation (%zu), returning ERANGE\n",
+               seg_len);
+    errno = ERANGE;
+    return 0;
+  }
+
+  /* Refuse really large allocations. */
+  if (seg_len > 1000000) {
+    if (h->msglvl >= 2)
+      fprintf (stderr, "allocate_block: refusing large allocation (%zu), returning ERANGE\n",
+               seg_len);
+    errno = ERANGE;
+    return 0;
+  }
+
+  /* Round up allocation to multiple of 8 bytes.  All blocks must be
+   * on an 8 byte boundary.
+   */
+  seg_len = (seg_len + 7) & ~7;
+
+  /* Allocate a new page if necessary. */
+  if (h->endblocks == 0 || h->endblocks + seg_len > h->endpages) {
+    size_t newendblocks = allocate_page (h, seg_len);
+    if (newendblocks == 0)
+      return 0;
+    h->endblocks = newendblocks;
+  }
+
+  size_t offset = h->endblocks;
+
+  if (h->msglvl >= 2)
+    fprintf (stderr, "allocate_block: new block at 0x%zx, size %zu\n",
+             offset, seg_len);
+
+  struct ntreg_hbin_block *blockhdr =
+    (struct ntreg_hbin_block *) (h->addr + offset);
+
+  blockhdr->seg_len = htole32 (- (int32_t) seg_len);
+  if (id[0] && id[1] && seg_len >= 6) {
+    blockhdr->id[0] = id[0];
+    blockhdr->id[1] = id[1];
+  }
+
+  h->endblocks += seg_len;
+
+  /* If there is space after the last block in the last page, then we
+   * have to put a dummy free block header here to mark the rest of
+   * the page as free.
+   */
+  ssize_t rem = h->endpages - h->endblocks;
+  if (rem > 0) {
+    if (h->msglvl >= 2)
+      fprintf (stderr, "allocate_block: marking remainder of page free starting at 0x%zx, size %zd\n",
+               h->endblocks, rem);
+
+    assert (rem >= 4);
+
+    blockhdr = (struct ntreg_hbin_block *) (h->addr + h->endblocks);
+    blockhdr->seg_len = htole32 ((int32_t) rem);
+  }
+
+  return offset;
+}
+
+/* 'offset' must point to a valid, used block.  This function marks
+ * the block unused (by updating the seg_len field) and invalidates
+ * the bitmap.  It does NOT do this recursively, so to avoid creating
+ * unreachable used blocks, callers may have to recurse over the hive
+ * structures.  Also callers must ensure there are no references to
+ * this block from other parts of the hive.
+ */
+static void
+mark_block_unused (hive_h *h, size_t offset)
+{
+  assert (h->writable);
+  assert (IS_VALID_BLOCK (h, offset));
+
+  struct ntreg_hbin_block *blockhdr =
+    (struct ntreg_hbin_block *) (h->addr + offset);
+
+  size_t seg_len = block_len (h, offset, NULL);
+  blockhdr->seg_len = htole32 (seg_len);
+
+  BITMAP_CLR (h->bitmap, offset);
+}
+
+/* Delete all existing values at this node. */
+static int
+delete_values (hive_h *h, hive_node_h node)
+{
+  assert (h->writable);
+
+  hive_value_h *values;
+  size_t *blocks;
+  if (get_values (h, node, &values, &blocks) == -1)
+    return -1;
+
+  size_t i;
+  for (i = 0; blocks[i] != 0; ++i)
+    mark_block_unused (h, blocks[i]);
+
+  free (blocks);
+
+  for (i = 0; values[i] != 0; ++i) {
+    struct ntreg_vk_record *vk =
+      (struct ntreg_vk_record *) (h->addr + values[i]);
+
+    size_t len;
+    len = le32toh (vk->data_len);
+    if (len == 0x80000000)      /* special case */
+      len = 4;
+    len &= 0x7fffffff;
+
+    if (len > 4) {              /* non-inline, so remove data block */
+      size_t data_offset = le32toh (vk->data_offset);
+      data_offset += 0x1000;
+      mark_block_unused (h, data_offset);
+    }
+
+    /* remove vk record */
+    mark_block_unused (h, values[i]);
+  }
+
+  free (values);
+
+  struct ntreg_nk_record *nk = (struct ntreg_nk_record *) (h->addr + node);
+  nk->nr_values = htole32 (0);
+  nk->vallist = htole32 (0xffffffff);
+
+  return 0;
+}
+
+int
+hivex_commit (hive_h *h, const char *filename, int flags)
+{
+  if (flags != 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (!h->writable) {
+    errno = EROFS;
+    return -1;
+  }
+
+  filename = filename ? : h->filename;
+  int fd = open (filename, O_WRONLY|O_CREAT|O_TRUNC|O_NOCTTY, 0666);
+  if (fd == -1)
+    return -1;
+
+  /* Update the header fields. */
+  uint32_t sequence = le32toh (h->hdr->sequence1);
+  sequence++;
+  h->hdr->sequence1 = htole32 (sequence);
+  h->hdr->sequence2 = htole32 (sequence);
+  /* XXX Ought to update h->hdr->last_modified. */
+  h->hdr->blocks = htole32 (h->endpages - 0x1000);
+
+  /* Recompute header checksum. */
+  uint32_t sum = header_checksum (h);
+  h->hdr->csum = htole32 (sum);
+
+  if (h->msglvl >= 2)
+    fprintf (stderr, "hivex_commit: new header checksum: 0x%x\n", sum);
+
+  if (full_write (fd, h->addr, h->size) != h->size) {
+    int err = errno;
+    close (fd);
+    errno = err;
+    return -1;
+  }
+
+  if (close (fd) == -1)
+    return -1;
+
+  return 0;
+}
+
+int
+hivex_node_set_values (hive_h *h, hive_node_h node,
+                       size_t nr_values, const hive_set_value *values,
+                       int flags)
+{
+  if (!h->writable) {
+    errno = EROFS;
+    return -1;
+  }
+
+  if (!IS_VALID_BLOCK (h, node) || !BLOCK_ID_EQ (h, node, "nk")) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Delete all existing values. */
+  if (delete_values (h, node) == -1)
+    return -1;
+
+  if (nr_values == 0)
+    return 0;
+
+  /* Allocate value list node.  Value lists have no id field. */
+  static const char nul_id[2] = { 0, 0 };
+  size_t seg_len =
+    sizeof (struct ntreg_value_list) + (nr_values - 1) * sizeof (uint32_t);
+  size_t vallist_offs = allocate_block (h, seg_len, nul_id);
+  if (vallist_offs == 0)
+    return -1;
+
+  struct ntreg_nk_record *nk = (struct ntreg_nk_record *) (h->addr + node);
+  nk->nr_values = htole32 (nr_values);
+  nk->vallist = htole32 (vallist_offs - 0x1000);
+
+  struct ntreg_value_list *vallist =
+    (struct ntreg_value_list *) (h->addr + vallist_offs);
+
+  size_t i;
+  for (i = 0; i < nr_values; ++i) {
+    /* Allocate vk record to store this (key, value) pair. */
+    static const char vk_id[2] = { 'v', 'k' };
+    seg_len = sizeof (struct ntreg_vk_record) + strlen (values[i].key);
+    size_t vk_offs = allocate_block (h, seg_len, vk_id);
+    if (vk_offs == 0)
+      return -1;
+
+    vallist->offset[i] = htole32 (vk_offs - 0x1000);
+
+    struct ntreg_vk_record *vk = (struct ntreg_vk_record *) (h->addr + vk_offs);
+    size_t name_len = strlen (values[i].key);
+    vk->name_len = htole16 (name_len);
+    strcpy (vk->name, values[i].key);
+    vk->data_type = htole32 (values[i].t);
+    vk->data_len = htole16 (values[i].len);
+    vk->flags = name_len == 0 ? 0 : 1;
+
+    if (values[i].len <= 4)     /* Store data inline. */
+      memcpy (&vk->data_offset, values[i].value, values[i].len);
+    else {
+      size_t offs = allocate_block (h, values[i].len + 4, nul_id);
+      if (offs == 0)
+        return -1;
+      memcpy (h->addr + offs + 4, values[i].value, values[i].len);
+      vk->data_offset = htole32 (offs - 0x1000);
+    }
+
+    if (name_len * 2 > le32toh (nk->max_vk_name_len))
+      nk->max_vk_name_len = htole32 (name_len * 2);
+    if (values[i].len > le32toh (nk->max_vk_data_len))
+      nk->max_vk_data_len = htole32 (values[i].len);
+  }
+
+  return 0;
+}
index 56718b4..6a3cb3a 100644 (file)
@@ -110,6 +110,18 @@ struct hivex_visitor {
 extern int hivex_visit (hive_h *h, const struct hivex_visitor *visitor, size_t len, void *opaque, int flags);
 extern int hivex_visit_node (hive_h *h, hive_node_h node, const struct hivex_visitor *visitor, size_t len, void *opaque, int flags);
 
+extern int hivex_commit (hive_h *h, const char *filename, int flags);
+
+struct hive_set_value {
+  char *key;
+  hive_type t;
+  size_t len;
+  char *value;
+};
+typedef struct hive_set_value hive_set_value;
+
+extern int hivex_node_set_values (hive_h *h, hive_node_h node, size_t nr_values, const hive_set_value *values, int flags);
+
 #ifdef __cplusplus
 }
 #endif
index 5a58144..5df75aa 100644 (file)
@@ -326,6 +326,127 @@ starts at C<node>.
 
 =back
 
+=head2 WRITING TO HIVE FILES
+
+The hivex library supports making limited modifications to hive files.
+We have tried to implement this very conservatively in order to reduce
+the chance of corrupting your registry.  However you should be careful
+and take back-ups, since Microsoft has never documented the hive
+format, and so it is possible there are nuances in the
+reverse-engineered format that we do not understand.
+
+To be able to modify a hive, you must pass the C<HIVEX_OPEN_WRITE>
+flag to C<hivex_open>, otherwise any write operation will return with
+errno C<EROFS>.
+
+The write operations shown below do not modify the on-disk file
+immediately.  You must call C<hivex_commit> in order to write the
+changes to disk.  If you call C<hivex_close> without committing then
+any writes are discarded.
+
+Hive files internally consist of a "memory dump" of binary blocks
+(like the C heap), and some of these blocks can be unused.  The hivex
+library never reuses these unused blocks.  Instead, to ensure
+robustness in the face of the partially understood on-disk format,
+hivex only allocates new blocks after the end of the file, and makes
+minimal modifications to existing structures in the file to point to
+these new blocks.  This makes hivex slightly less disk-efficient than
+it could be, but disk is cheap, and registry modifications tend to be
+very small.
+
+When deleting nodes, it is possible that this library may leave
+unreachable live blocks in the hive.  This is because certain parts of
+the hive disk format such as security (sk) records and big data (db)
+records and classname fields are not well understood (and not
+documented at all) and we play it safe by not attempting to modify
+them.  Apart from wasting a little bit of disk space, it is not
+thought that unreachable blocks are a problem.
+
+=over 4
+
+=item int hivex_commit (hive_h *h, const char *filename, int flags);
+
+Commit (write) any changes which have been made.
+
+C<filename> is the new file to write.  If C<filename == NULL> then we
+overwrite the original file (ie. the file name that was passed to
+C<hivex_open>).  C<flags> is not used, always pass 0.
+
+Returns 0 on success.  On error this returns -1 and sets errno.
+
+Note this does not close the hive handle.  You can perform further
+operations on the hive after committing, including making more
+modifications.  If you no longer wish to use the hive, call
+C<hivex_close> after this.
+
+=item hive_set_value
+
+The typedef C<hive_set_value> is used in conjunction with the
+C<hivex_node_set_values> call described below.
+
+ struct hive_set_value {
+   char *key;     /* key - a UTF-8 encoded ASCIIZ string */
+   hive_type t;   /* type of value field */
+   size_t len;    /* length of value field in bytes */
+   char *value;   /* value field */
+ };
+ typedef struct hive_set_value hive_set_value;
+
+To set the default value for a node, you have to pass C<key = "">.
+
+Note that the C<value> field is just treated as a list of bytes, and
+is stored directly in the hive.  The caller has to ensure correct
+encoding and endianness, for example converting dwords to little
+endian.
+
+The correct type and encoding for values depends on the node and key
+in the registry, the version of Windows, and sometimes even changes
+between versions of Windows for the same key.  We don't document it
+here.  Often it's not documented at all.
+
+=item int hivex_node_set_values (hive_h *h, hive_node_h node, size_t nr_values, const hive_set_value *values, int flags);
+
+This call can be used to set all the (key, value) pairs stored in C<node>.
+
+C<node> is the node to modify.  C<values> is an array of (key, value)
+pairs.  There should be C<nr_values> elements in this array.  C<flags>
+is not used, always pass 0.
+
+Any existing values stored at the node are discarded, and their
+C<hive_value_h> handles become invalid.  Thus you can remove all
+values stored at C<node> by passing C<nr_values = 0>.
+
+Returns 0 on success.  On error this returns -1 and sets errno.
+
+Note that this library does not offer a way to modify just a single
+key at a node.  We don't implement a way to do this efficiently.
+
+=back
+
+=head3 WRITE OPERATIONS WHICH ARE NOT SUPPORTED
+
+=over 4
+
+=item *
+
+Changing the root node.
+
+=item *
+
+Creating a new hive file from scratch.  This is impossible at present
+because not all fields in the header are understood.
+
+=item *
+
+Modifying or deleting single values at a node.
+
+=item *
+
+Modifying security key (sk) records or classnames.  These are not
+well understood.
+
+=back
+
 =head1 THE STRUCTURE OF THE WINDOWS REGISTRY
 
 Note: To understand the relationship between hives and the common
@@ -452,6 +573,10 @@ Registry contains cycles.
 
 Field in the registry out of range.
 
+=item EROFS
+
+Tried to write to a registry which is not opened for writing.
+
 =back
 
 =head1 ENVIRONMENT VARIABLES