From: Richard Jones Date: Wed, 3 Feb 2010 17:59:03 +0000 (+0000) Subject: hivex: Begin implementation of writing to hives. X-Git-Tag: 1.1.0~32 X-Git-Url: http://git.annexia.org/?a=commitdiff_plain;h=7de5891b393d710f43cb334bbe4f4321424413e6;p=hivex.git hivex: Begin implementation of writing to hives. 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. --- diff --git a/hivex/hivex.c b/hivex/hivex.c index 44f2998..af36868 100644 --- a/hivex/hivex.c +++ b/hivex/hivex.c @@ -35,6 +35,7 @@ #include #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; +} diff --git a/hivex/hivex.h b/hivex/hivex.h index 56718b4..6a3cb3a 100644 --- a/hivex/hivex.h +++ b/hivex/hivex.h @@ -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 diff --git a/hivex/hivex.pod b/hivex/hivex.pod index 5a58144..5df75aa 100644 --- a/hivex/hivex.pod +++ b/hivex/hivex.pod @@ -326,6 +326,127 @@ starts at C. =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 +flag to C, otherwise any write operation will return with +errno C. + +The write operations shown below do not modify the on-disk file +immediately. You must call C in order to write the +changes to disk. If you call C 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 is the new file to write. If C then we +overwrite the original file (ie. the file name that was passed to +C). C 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 after this. + +=item hive_set_value + +The typedef C is used in conjunction with the +C 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. + +Note that the C 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. + +C is the node to modify. C is an array of (key, value) +pairs. There should be C elements in this array. C +is not used, always pass 0. + +Any existing values stored at the node are discarded, and their +C handles become invalid. Thus you can remove all +values stored at C by passing C. + +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