Richard W.M. Jones
2010-Mar-29 13:38 UTC
[Libguestfs] [PATCH 0/3] Export and merge into Windows Registry
As described here previously: https://www.redhat.com/archives/libguestfs/2010-March/msg00129.html Here is the three part patch to reimplement virt-win-reg to support exporting and merging Windows Registry entries in the 'regedit' format. Tested by me on a local Windows VM. Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones virt-top is 'top' for virtual machines. Tiny program with many powerful monitoring features, net stats, disk stats, logging, etc. http://et.redhat.com/~rjones/virt-top
Richard W.M. Jones
2010-Mar-29 13:39 UTC
[Libguestfs] [PATCH 1/3] Win::Hivex::Regedit module for importing and exporting regedit format files.
-- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones New in Fedora 11: Fedora Windows cross-compiler. Compile Windows programs, test, and build Windows installers. Over 70 libraries supprt'd http://fedoraproject.org/wiki/MinGW http://www.annexia.org/fedora_mingw -------------- next part -------------->From b97d0534ee4ba5e0cf00fdb63b486bdd6235e326 Mon Sep 17 00:00:00 2001From: Richard Jones <rjones at redhat.com> Date: Thu, 25 Mar 2010 12:03:36 +0000 Subject: [PATCH 1/2] Win::Hivex::Regedit module for importing and exporting regedit format files. --- configure.ac | 2 +- perl/lib/Win/Hivex/Regedit.pm | 661 +++++++++++++++++++++++++++++++++++++++++ perl/t/510-regedit-load.t | 24 ++ perl/t/550-regedit-export.t | 102 +++++++ perl/t/560-regedit-import.t | 154 ++++++++++ perl/t/570-regedit-import2.t | 82 +++++ po/POTFILES.in | 1 + 7 files changed, 1025 insertions(+), 1 deletions(-) create mode 100644 perl/lib/Win/Hivex/Regedit.pm create mode 100644 perl/t/510-regedit-load.t create mode 100644 perl/t/550-regedit-export.t create mode 100644 perl/t/560-regedit-import.t create mode 100644 perl/t/570-regedit-import2.t diff --git a/configure.ac b/configure.ac index 853cf40..11f14ea 100644 --- a/configure.ac +++ b/configure.ac @@ -203,7 +203,7 @@ AC_CHECK_PROG([PERL],[perl],[perl],[no]) dnl Check for Perl modules that must be present to compile and dnl test the Perl bindings. missing_perl_modules=no -for pm in Test::More Test::Pod Test::Pod::Coverage ExtUtils::MakeMaker; do +for pm in Test::More Test::Pod Test::Pod::Coverage ExtUtils::MakeMaker IO::Stringy; do AC_MSG_CHECKING([for $pm]) if ! perl -M$pm -e1 >/dev/null 2>&1; then AC_MSG_RESULT([no]) diff --git a/perl/lib/Win/Hivex/Regedit.pm b/perl/lib/Win/Hivex/Regedit.pm new file mode 100644 index 0000000..ee0e72f --- /dev/null +++ b/perl/lib/Win/Hivex/Regedit.pm @@ -0,0 +1,661 @@ +# Win::Hivex::Regedit +# Copyright (C) 2009-2010 Red Hat Inc. +# Derived from code by Petter Nordahl-Hagen under a compatible license: +# Copyright (c) 1997-2007 Petter Nordahl-Hagen. +# Derived from code by Markus Stephany under a compatible license: +# Copyright (c)2000-2004, Markus Stephany. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +=pod + +=head1 NAME + +Win::Hivex::Regedit - Helper for reading and writing regedit format files + +=head1 SYNOPSIS + + use Win::Hivex; + use Win::Hivex::Regedit qw(reg_import reg_export); + + $h = Win::Hivex->open ('SOFTWARE', write => 1); + + open FILE, "updates.reg"; + reg_import (\*FILE, $h); + $h->commit (undef); + + reg_export ($h, "\\Microsoft\\Windows NT\\CurrentVersion", \*OUTFILE, + prefix => "HKEY_LOCAL_MACHINE\\SOFTWARE"); + +=head1 DESCRIPTION + +Win::Hivex::Regedit is a helper library for reading and writing the +Windows regedit (or C<.REG>) file format. This is the textual format +that is commonly used on Windows for distributing groups of Windows +Registry changes, and this format is read and written by the +proprietary C<reg.exe> and C<regedit.exe> programs supplied with +Windows. It is I<not> the same as the binary "hive" format which the +hivex library itself can read and write. Note that the regedit format +is not well-specified, and hence deviations can occur between what the +Windows program can read/write and what we can read/write. (Please +file bugs for any deviations found). + +Win::Hivex::Regedit is the low-level Perl library. There is also a +command line tool for combining hive files and reg files +(L<hivexregedit(1)>). If you have a Windows virtual machine that you need +to merge regedit-format changes into, use the high-level +L<virt-win-reg(1)> tool (part of libguestfs tools). + +=head2 FUNCTIONS + +=cut + +package Win::Hivex::Regedit; + +use strict; +use warnings; + +use Carp qw(croak confess); +use Encode qw(encode); + +require Exporter; + +use vars qw(@EXPORT_OK @ISA); + + at ISA = qw(Exporter); + at EXPORT_OK = qw(reg_import reg_export); + +=head2 reg_import + + reg_import ($fh, ($h|$map), [encoding => "UTF-16LE"]); + +This function imports the registry keys from file handle C<$fh> either +into the hive C<$h> or via a map function. + +The hive handle C<$h> must have been opened for writing, ie. +using the C<write =E<gt> 1> flag to C<Win::Hivex-E<gt>open>. + +In the binary hive file, the first part of the key name (eg. +C<HKEY_LOCAL_MACHINE\SOFTWARE>) is not stored. You just have to know +(somehow) that this maps to the C<SOFTWARE> hive. Therefore if you +are given a file containing a mixture of keys that have to be added to +different hives, you have to have a way to map these to the hive +handles. This is outside the scope of the hivex library, but if the +second argument is a CODEREF (ie. reference to a function) then this +C<$map> function is called on each key name: + + map ($keyname) + ==> ($h, $keyname) + +As shown, the function should return a pair, hive handle, and the true +key name (with the prefix stripped off). For example: + + sub map { + if ($_[0] =~ /^HKEY_LOCAL_MACHINE\\SOFTWARE(.*)/i) { + return ($software_h, $1); + } else ... + } + +C<encoding> is the encoding used by default for strings. If not +specified, this defaults to C<"UTF-16LE">, however we highly advise +you to specify it. See L</ENCODING STRINGS> below. + +As with the regedit program, we merge the new registry keys with +existing ones, and new node values with old ones. You can use the +C<-> (minus) character to delete individual keys and values. This is +explained in detail in the Wikipedia page on the Windows Registry. + +Remember you need to call C<$h-E<gt>commit (undef)> on the hivex +handle before any changes are written to the hive file. See +L<hivex(3)/WRITING TO HIVE FILES>. + +=cut + +sub reg_import +{ + local $_; + my $fh = shift; + my $hmap = shift; + my %params = @_; + + my $encoding = $params{encoding} || "utf-16le"; + + my $state = "outer"; + my $newnode; + my @newvalues; + my @delvalues; + my $lineno = 0; + + while (<$fh>) { + # Join continuation lines. This is recipe 8.1 from the Perl + # Cookbook. Note we allow spaces after the final \ because + # this is fairly common in pasted regedit files. + $lineno++; + chomp; + if (s/\\\s*$//) { + $_ .= <$fh>; + redo unless eof ($fh); + } + + #print STDERR "reg_import: parsing <<<$_>>>\n"; + + if ($state eq "outer") { + # Ignore blank lines, headers. + next if /^\s*$/; + + # .* is needed before Windows Registry Editor Version.. in + # order to eat a possible Unicode BOM which regedit writes + # there. + next if /^.*Windows Registry Editor Version.*/; + next if /^REGEDIT/; + + # Ignore comments. + next if /^\s*;/; + + # Expect to see [...] or -[...] + # to merge or delete a node respectively. + if (/^\[(.*)\]\s*$/) { + $state = "inner"; + $newnode = $1; + @newvalues = (); + @delvalues = (); + } elsif (/^-\[(.*)\]\s*$/) { + _delete_node ($hmap, \%params, $1); + $state = "outer"; + } else { + croak (_unexpected ($_, $lineno)); + } + } elsif ($state eq "inner") { + if (/^(".*)=-\s*$/) { # delete value + my $key = _parse_quoted_string ($_); + croak (_parse_error ($_, $lineno)) unless defined $key; + push @delvalues, $key; + } elsif (/^@=-\s*$/) { # delete default key + push @delvalues, ""; + } elsif (/^".*"=/) { # ordinary value + my $value = _parse_key_value ($_, $encoding); + croak (_parse_error ($_, $lineno)) unless defined $value; + push @newvalues, $value; + } elsif (/^@=(.*)/) { # default key + my $value = _parse_value ("", $1, $encoding); + croak (_parse_error ($_, $lineno)) unless defined $value; + push @newvalues, $value; + } elsif (/^\s*$/) { # blank line after values + _merge_node ($hmap, \%params, $newnode, \@newvalues, \@delvalues); + $state = "outer"; + } else { + croak (_unexpected ($_, $lineno)); + } + } + } # while + + # Still got a node left over to merge? + if ($state eq "inner") { + _merge_node ($hmap, \%params, $newnode, \@newvalues, \@delvalues); + } +} + +sub _parse_key_value +{ + local $_ = shift; + my $encoding = shift; + my ($key, $_) = _parse_quoted_string ($_); + return undef unless defined $key; + return undef unless substr ($_, 0, 1) eq "="; + return _parse_value ($key, substr ($_, 1), $encoding); +} + +# Parse a double-quoted string, returning the string. \ is used to +# escape double-quotes and other backslash characters. +# +# If called in array context and if there is anything after the quoted +# string, it is returned as the second element of the array. +# +# Returns undef if there was a parse error. +sub _parse_quoted_string +{ + local $_ = shift; + + # No initial quote character. + return undef if substr ($_, 0, 1) ne "\""; + + my $i; + my $out = ""; + for ($i = 1; $i < length; ++$i) { + my $c = substr ($_, $i, 1); + if ($c eq "\"") { + last + } elsif ($c eq "\\") { + $i++; + $c = substr ($_, $i, 1); + $out .= $c; + } else { + $out .= $c; + } + } + + # No final quote character. + return undef if $i == length; + + $_ = substr ($_, $i+1); + if (wantarray) { + return ($out, $_); + } else { + return $out; + } +} + +# Parse the value, optionally prefixed by a type. + +sub _parse_value +{ + local $_; + my $key = shift; + $_ = shift; + my $encoding = shift; # default encoding for strings + + my $type; + my $data; + + if (m/^dword:([[:xdigit:]]{8})$/) { # DWORD + $type = 4; + $data = _dword_le (hex ($1)); + } elsif (m/^hex:(.*)$/) { # hex digits + $type = 3; + $data = _data_from_hex_digits ($1); + return undef unless defined $data; + } elsif (m/^hex\(([[:xdigit:]]+)\):(.*)$/) { # hex digits + $type = hex ($1); + $data = _data_from_hex_digits ($2); + return undef unless defined $data; + } elsif (m/^str:(".*")$/) { # only in Wine fake-registries, I think + $type = 1; + $data = _parse_quoted_string ($1); + return undef unless defined $data; + $data = encode ($encoding, $data); + } elsif (m/^str\(([[:xdigit:]]+)\):(".*")$/) { + $type = hex ($1); + $data = _parse_quoted_string ($2); + return undef unless defined $data; + $data = encode ($encoding, $data); + } elsif (m/^(".*")$/) { + $type = 1; + $data = _parse_quoted_string ($1); + return undef unless defined $data; + $data = encode ($encoding, $data); + } else { + return undef; + } + + my %h = ( key => $key, t => $type, value => $data ); + return \%h; +} + +sub _dword_le +{ + pack ("V", $_[0]); +} + +sub _data_from_hex_digits +{ + local $_ = shift; + s/[,[:space:]]//g; + pack ("H*", $_) +} + +sub _merge_node +{ + local $_; + my $hmap = shift; + my $params = shift; + my $path = shift; + my $newvalues = shift; + my $delvalues = shift; + + my $h; + ($h, $path) = _map_handle ($hmap, $path); + + my $node = _node_lookup ($h, $path); + if (!defined $node) { # Need to create this node. + my $name = $path; + $name = $1 if $path =~ /([^\\]+)$/; + my $parentpath = $path; + $parentpath =~ s/[^\\]+$//; + my $parent = _node_lookup ($h, $parentpath); + if (!defined $parent) { + confess "reg_import: cannot create $path since parent $parentpath does not exist" + } + $node = $h->node_add_child ($parent, $name); + } + + # Get the current set of values at this node. + my @values = $h->node_values ($node); + + # Delete values in @delvalues original and values that are going + # to be replaced. + my @delvalues = @$delvalues; + foreach (@$newvalues) { + push @delvalues, $_->{key}; + } + @values = grep { ! _imember ($h->value_key ($_), @delvalues) } @values; + + # Get the actual values from the hive. + @values = map { + my $key = $h->value_key ($_); + my ($type, $data) = $h->value_value ($_); + my %h = ( key => $key, t => $type, value => $data ); + $_ = \%h; + } @values; + + # Add the new values. + push @values, @$newvalues; + + $h->node_set_values ($node, \@values); +} + +sub _delete_node +{ + local $_; + my $hmap = shift; + my $params = shift; + my $path = shift; + + my $h; + ($h, $path) = _map_handle ($hmap, $path); + + my $node = _node_lookup ($h, $path); + # Not an error to delete a non-existant node. + return unless defined $node; + + # However you cannot delete the root node. + confess "reg_import: the root node of a hive cannot be deleted" + if $node == $h->root (); + + $h->node_delete_child ($node); +} + +# Call the map function, if necessary. +sub _map_handle +{ + local $_; # called function may use this + my $hmap = shift; + my $path = shift; + my $h = $hmap; + + if (ref ($hmap) eq "CODE") { + ($h, $path) = &$hmap ($path); + } + return ($h, $path); +} + +sub _imember +{ + local $_; + my $item = shift; + + foreach (@_) { + return 1 if lc ($_) eq lc ($item); + } + return 0; +} + +sub _unexpected +{ + local $_ = shift; + my $lineno = shift; + + "reg_import: parse error: unexpected text found at line $lineno near\n$_" +} + +sub _parse_error +{ + local $_ = shift; + my $lineno = shift; + + "reg_import: parse error: at line $lineno near\n$_" +} + +=head2 reg_export + + reg_export ($h, $key, $fh, [prefix => $prefix]); + +This function exports the registry keys starting at the root +C<$key> and recursively downwards into the file handle C<$fh>. + +C<$key> is a case-insensitive path of the node to start from, relative +to the root of the hive. It is an error if this path does not exist. +Path elements should be separated by backslash characters. + +C<$prefix> is prefixed to each key name. The usual use for this is to +make key names appear as they would on Windows. For example the key +C<\Foo> in the SOFTWARE Registry, with $prefix +C<HKEY_LOCAL_MACHINE\SOFTWARE>, would be written as: + + [HKEY_LOCAL_MACHINE\SOFTWARE\Foo] + "Key 1"=... + "Key 2"=... + +The output is written as pure 7 bit ASCII, with line endings which are +the default for the local host. You may need to convert the file's +encoding using L<iconv(1)> and line endings using L<unix2dos(1)> if +sending to a Windows user. Strings are always encoded as hex bytes. +See L</ENCODING STRINGS> below. + +Nodes and keys are sorted alphabetically in the output. + +This function does I<not> print a header. The real regedit program +will print a header like: + + Windows Registry Editor Version 5.00 + +followed by a blank line. (Other headers are possible, see the +Wikipedia page on the Windows Registry). If you want a header, you +need to write it out yourself. + +=cut + +sub reg_export +{ + my $h = shift; + my $key = shift; + + my $node = _node_lookup ($h, $key); + croak "$key: path not found in this hive" unless $node; + + reg_export_node ($h, $node, @_); +} + +=head2 reg_export_node + + reg_export_node ($h, $node, $fh, ...); + +This is exactly the same as L</reg_export> except that instead +of specifying the path to a key as a string, you pass a hivex +library C<$node> handle. + +=cut + +sub reg_export_node +{ + local $_; + my $h = shift; + my $node = shift; + my $fh = shift; + my %params = @_; + + confess "reg_export_node: \$node parameter was undef" unless defined $node; + + # Get the canonical path of this node. + my $path = _node_canonical_path ($h, $node); + + # Print the path. + print $fh "["; + my $prefix = $params{prefix}; + if (defined $prefix) { + chop $prefix if substr ($prefix, -1, 1) eq "\\"; + print $fh $prefix; + } + print $fh $path; + print $fh "]\n"; + + # Get the values. + my @values = $h->node_values ($node); + + foreach (@values) { + use bytes; + + my $key = $h->value_key ($_); + my ($type, $data) = $h->value_value ($_); + $_ = { key => $key, type => $type, data => $data } + } + + @values = sort { $a->{key} cmp $b->{key} } @values; + + # Print the values. + foreach (@values) { + my $key = $_->{key}; + my $type = $_->{type}; + my $data = $_->{data}; + + if ($key eq "") { + print $fh '@=' # default key + } else { + print $fh '"', _escape_quotes ($key), '"=' + } + + if ($type eq 4 && length ($data) == 4) { # only handle dword specially + my $dword = unpack ("V", $data); + printf $fh "dword:%08x\n", $dword + } else { + # Encode everything else as hex, see encoding section below. + printf $fh "hex(%x):", $type; + my $hex = join (",", map { sprintf "%02x", ord } split (//, $data)); + print $fh "$hex\n" + } + } + print $fh "\n"; + + my @children = $h->node_children ($node); + @children = sort { $h->node_name ($a) cmp $h->node_name ($b) } @children; + reg_export_node ($h, $_, $fh, @_) foreach @children; +} + +# Escape " and \ when printing keys. +sub _escape_quotes +{ + local $_ = shift; + s/\\/\\\\/g; + s/"/\\"/g; + $_; +} + +# Look up a node in the registry starting from the path. +# Return undef if it doesn't exist. + +sub _node_lookup +{ + local $_; + my $h = shift; + my $path = shift; + + my @path = split /\\/, $path; + shift @path if @path > 0 && $path[0] eq ""; + + my $node = $h->root (); + foreach (@path) { + $node = $h->node_get_child ($node, $_); + return undef unless defined $node; + } + + return $node; +} + +# Return the canonical path of node in the hive. + +sub _node_canonical_path +{ + local $_; + my $h = shift; + my $node = shift; + + return "\\" if $node == $h->root (); + $_ = $h->node_name ($node); + my $parent = $h->node_parent ($node); + my $path = _node_canonical_path ($h, $parent); + if ($path eq "\\") { + return "$path$_" + } else { + return "$path\\$_" + } +} + +=head1 ENCODING STRINGS + +The situation with encoding strings in the Registry on Windows is very +confused. There are two main encodings that you would find in the +binary (hive) file, 7 bit ASCII and UTF-16LE. (Other encodings are +possible, it's also possible to have arbitrary binary data incorrectly +marked with a string type). + +The hive file itself doesn't contain any indication of string +encoding. Windows probably guesses the encoding. + +We think that regedit probably either guesses which encoding to use +based on the file encoding, or else has different defaults for +different versions of Windows. Neither choice is appropriate for a +tool used in a real operating system. + +When using L</reg_import>, you should specify the default encoding for +strings using the C<encoding> parameter. If not specified, it +defaults to UTF-16LE. + +The file itself that is imported should be in the local encoding for +files (usually UTF-8 on modern Linux systems). This means if you +receive a regedit file from a Windows system, you may sometimes have +to reencode it: + + iconv -f utf-16le -t utf-8 < input.reg | dos2unix > output.reg + +When writing regedit files (L</reg_export>) we bypass this madness +completely. I<All> strings (even pure ASCII) are written as hex bytes +so there is no doubt about how they should be encoded when they are +read back in. + +=cut + +1; + +=head1 COPYRIGHT + +Copyright (C) 2010 Red Hat Inc. + +=head1 LICENSE + +Please see the file COPYING.LIB for the full license. + +=head1 SEE ALSO + +L<Win::Hivex(3)>, +L<hivexregedit(1)>, +L<virt-win-reg(1)>, +L<iconv(1)>, +L<dos2unix(1)>, +L<unix2dos(1)>, +L<hivex(3)>, +L<hivexsh(1)>, +L<http://libguestfs.org>, +L<Sys::Guestfs(3)>. + +=cut diff --git a/perl/t/510-regedit-load.t b/perl/t/510-regedit-load.t new file mode 100644 index 0000000..feebe42 --- /dev/null +++ b/perl/t/510-regedit-load.t @@ -0,0 +1,24 @@ +# Win::Hivex::Regedit tests -*- perl -*- +# Copyright (C) 2010 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +use strict; +use warnings; +use Test::More tests => 1; + +BEGIN { + use_ok ("Win::Hivex::Regedit"); +} diff --git a/perl/t/550-regedit-export.t b/perl/t/550-regedit-export.t new file mode 100644 index 0000000..2099157 --- /dev/null +++ b/perl/t/550-regedit-export.t @@ -0,0 +1,102 @@ +# Win::Hivex::Regedit test -*- perl -*- +# Copyright (C) 2010 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +use strict; +use warnings; + +use Encode qw(from_to); +use IO::Scalar; + +use Test::More tests => 8; + +use Win::Hivex; +use Win::Hivex::Regedit qw(reg_export); + +my $srcdir = $ENV{srcdir} || "."; + +my $h = Win::Hivex->open ("$srcdir/../images/minimal", write => 1); +ok ($h); + +my $root = $h->root (); +ok ($root); + +$h->node_add_child ($root, "B"); +ok (1); + +$h->node_add_child ($root, "A"); +ok (1); + +my $b = $h->node_get_child ($root, "B"); +ok ($b); + +# Encode a string as UTF16-LE. +sub utf16le +{ + my $s = shift; + from_to ($s, "ascii", "utf-16le"); + $s; +} + +# Convert a 32 bit integer to a little endian 4 byte data field. +sub dwordle +{ + pack ("V", $_[0]); +} + +my @values = ( + # Values are entered in a random order here, but they should be + # sorted on export. + { key => "Key2", t => 2, value => utf16le ("DEF") }, + { key => "", t => 1, value => "Default" }, + { key => "Key3", t => 4, value => dwordle (0xff876543) }, + { key => "Key1", t => 1, value => "ABC" }, + ); +$h->node_set_values ($b, \@values); +ok (1); + +my $fh = new IO::Scalar; +reg_export ($h, "\\", $fh, prefix => "HKEY_LOCAL_MACHINE\\SOFTWARE\\"); + +my $expected = '[HKEY_LOCAL_MACHINE\\SOFTWARE\\] + +[HKEY_LOCAL_MACHINE\\SOFTWARE\\A] + +[HKEY_LOCAL_MACHINE\\SOFTWARE\\B] +@=hex(1):44,65,66,61,75,6c,74 +"Key1"=hex(1):41,42,43 +"Key2"=hex(2):44,00,45,00,46,00 +"Key3"=dword:ff876543 + +'; + +ok (${$fh->sref} eq $expected); + +$fh = new IO::Scalar; +reg_export ($h, "\\B", $fh); + +$expected = '[\\B] +@=hex(1):44,65,66,61,75,6c,74 +"Key1"=hex(1):41,42,43 +"Key2"=hex(2):44,00,45,00,46,00 +"Key3"=dword:ff876543 + +'; + +ok (${$fh->sref} eq $expected); + +# don't commit because that would overwrite the original file +# $h->commit (); diff --git a/perl/t/560-regedit-import.t b/perl/t/560-regedit-import.t new file mode 100644 index 0000000..b0c5254 --- /dev/null +++ b/perl/t/560-regedit-import.t @@ -0,0 +1,154 @@ +# Win::Hivex::Regedit test -*- perl -*- +# Copyright (C) 2010 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +use strict; +use warnings; + +use Encode qw(from_to); +use IO::Scalar; + +use Test::More tests => 16; + +use Win::Hivex; +use Win::Hivex::Regedit qw(reg_import reg_export); + +my $srcdir = $ENV{srcdir} || "."; + +my $h = Win::Hivex->open ("$srcdir/../images/minimal", write => 1); +ok ($h); + +my ($data, $expected); + +# Note that we don't clear the hive between tests, so results of +# next test depend on the previous test. + +$data = ' +[\A] + +[\B] + +[\C] +"Key1"=hex(2):48,00,65,00,6c,00,6c,00,6f,00 +"Key2"=str(2):"Hello" +"Key3"=hex:48,00,65,00,6c,00,6c,00,6f,00,\ + 48,00,65,00,6c,00,6c,00,6f,00 +"Key4"=dword:ff123456'; +$expected = '[\] + +[\A] + +[\B] + +[\C] +"Key1"=hex(2):48,00,65,00,6c,00,6c,00,6f,00 +"Key2"=hex(2):48,00,65,00,6c,00,6c,00,6f,00 +"Key3"=hex(3):48,00,65,00,6c,00,6c,00,6f,00,48,00,65,00,6c,00,6c,00,6f,00 +"Key4"=dword:ff123456 + +'; + +run_test ($data, $expected); + +$data = ' +[\A] +@="Hello" + +-[\B] +'; +$expected = '[\] + +[\A] +@=hex(1):48,00,65,00,6c,00,6c,00,6f,00 + +[\C] +"Key1"=hex(2):48,00,65,00,6c,00,6c,00,6f,00 +"Key2"=hex(2):48,00,65,00,6c,00,6c,00,6f,00 +"Key3"=hex(3):48,00,65,00,6c,00,6c,00,6f,00,48,00,65,00,6c,00,6c,00,6f,00 +"Key4"=dword:ff123456 + +'; + +run_test ($data, $expected); + +$data = ' +[\A] +@=- + +-[\C] + +[\A\B] +'; +$expected = '[\] + +[\A] + +[\A\B] + +'; + +run_test ($data, $expected); + +$data = ' +[\A] +"NotExistant"=- + +[\A\B] +"Key\"Containing\"Quotes"=hex(0): +'; +$expected = '[\] + +[\A] + +[\A\B] +"Key\"Containing\"Quotes"=hex(0): + +'; + +run_test ($data, $expected); + +$data = ' +[\A\B] +"Key\"Containing\"Quotes"=- + +-[\A] +'; +$expected = '[\] + +'; + +run_test ($data, $expected); + +#---------------------------------------------------------------------- + +sub run_test { + my $data = shift; + my $expected = shift; + + my $fh = new IO::Scalar \$data; + reg_import ($fh, $h); + ok (1); + + $fh = new IO::Scalar; + reg_export ($h, "\\", $fh); + ok (1); + + my $actual = ${$fh->sref}; + warn "\n\n----- ACTUAL -----\n$actual\n----- EXPECTED -----\n$expected\n\n" + if $actual ne $expected; + + ok ($actual eq $expected) +} diff --git a/perl/t/570-regedit-import2.t b/perl/t/570-regedit-import2.t new file mode 100644 index 0000000..a952fb0 --- /dev/null +++ b/perl/t/570-regedit-import2.t @@ -0,0 +1,82 @@ +# Win::Hivex::Regedit test -*- perl -*- +# Copyright (C) 2010 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +use strict; +use warnings; + +use Encode qw(from_to); +use IO::Scalar; + +use Test::More tests => 6; + +use Win::Hivex; +use Win::Hivex::Regedit qw(reg_import reg_export); + +my $srcdir = $ENV{srcdir} || "."; + +my $h = Win::Hivex->open ("$srcdir/../images/minimal", write => 1); +ok ($h); + +my $data; + +# Note: These tests are supposed to fail. + +# Need a blank line between sections. +$data = ' +[A] +[B]'; +run_test ($data); + +# Invalid header. +$data = ' +[A]B'; +run_test ($data); + +# Must create intermediate nodes first. +$data = ' +[A\B\C\D]'; +run_test ($data); + +# Invalid quoting. +$data = ' +[A] +"Quote"it"="Hello"'; +run_test ($data); + +$data = ' +[A] +"Quote it\"="Hello"'; +run_test ($data); + +# Invalid hex -- fails, 'pack' processes it anyway. +#$data = ' +#[A] +#"Key"=hex(1):xy'; +#run_test ($data); + +#---------------------------------------------------------------------- + +sub run_test { + my $data = shift; + + eval { + my $fh = new IO::Scalar \$data; + reg_import ($h, $fh); + }; + #warn "$@\n"; + ok ($@); +} diff --git a/po/POTFILES.in b/po/POTFILES.in index f806581..6ad1bcb 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -3,5 +3,6 @@ lib/hivex.c ocaml/hivex_c.c perl/Hivex.c perl/lib/Win/Hivex.pm +perl/lib/Win/Hivex/Regedit.pm sh/hivexsh.c xml/hivexml.c -- 1.6.6.1
Richard W.M. Jones
2010-Mar-29 13:39 UTC
[Libguestfs] [PATCH 2/3] hivexregedit: Low-level tool for merging and export in regedit format.
-- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming blog: http://rwmj.wordpress.com Fedora now supports 80 OCaml packages (the OPEN alternative to F#) http://cocan.org/getting_started_with_ocaml_on_red_hat_and_fedora -------------- next part -------------->From d5434c35de33a8f12524e4a9169d788635acefb6 Mon Sep 17 00:00:00 2001From: Richard Jones <rjones at redhat.com> Date: Fri, 26 Mar 2010 11:46:44 +0000 Subject: [PATCH 2/2] hivexregedit: Low-level tool for merging and export in regedit format. --- .gitignore | 2 + Makefile.am | 2 +- README | 3 + configure.ac | 1 + regedit/Makefile.am | 43 +++++++ regedit/hivexregedit | 296 ++++++++++++++++++++++++++++++++++++++++++++++++++ regedit/run-locally | 52 +++++++++ 7 files changed, 398 insertions(+), 1 deletions(-) create mode 100644 regedit/Makefile.am create mode 100755 regedit/hivexregedit create mode 100755 regedit/run-locally diff --git a/.gitignore b/.gitignore index 997bbcf..bd99122 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ hivex-*.tar.gz html/hivex.3.html html/hivexget.1.html html/hivexml.1.html +html/hivexregedit.1.html html/hivexsh.1.html images/large images/mklarge @@ -95,6 +96,7 @@ po/remove-potcdate.sin python/*.pyc python/hivex-py.c python/hivex.py +regedit/hivexregedit.1 sh/*.1 sh/hivexsh stamp-h1 diff --git a/Makefile.am b/Makefile.am index 1a34e15..95b1dbb 100644 --- a/Makefile.am +++ b/Makefile.am @@ -24,7 +24,7 @@ SUBDIRS += ocaml endif if HAVE_PERL -SUBDIRS += perl +SUBDIRS += perl regedit endif # Not yet written. diff --git a/README b/README index ffcbb26..ae3d159 100644 --- a/README +++ b/README @@ -92,6 +92,9 @@ python/ are welcome (please modify generator/generator.ml to add them). +regedit/ + Regedit-like registry merging tool. + sh/ Interactive shell. This also contains the old 'hivexget' diff --git a/configure.ac b/configure.ac index 11f14ea..8d24848 100644 --- a/configure.ac +++ b/configure.ac @@ -433,6 +433,7 @@ AC_CONFIG_FILES([Makefile ocaml/Makefile ocaml/META perl/Makefile perl/Makefile.PL po/Makefile.in + regedit/Makefile sh/Makefile xml/Makefile]) AC_OUTPUT diff --git a/regedit/Makefile.am b/regedit/Makefile.am new file mode 100644 index 0000000..ab286cd --- /dev/null +++ b/regedit/Makefile.am @@ -0,0 +1,43 @@ +# hivex +# Copyright (C) 2010 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +EXTRA_DIST = \ + hivexregedit \ + run-locally + +bin_SCRIPTS = hivexregedit + +man_MANS = hivexregedit.1 + +hivexregedit.1: hivexregedit + $(POD2MAN) \ + --section 1 \ + -c "Windows Registry" \ + --name "hivexregedit" \ + --release "$(PACKAGE_NAME)-$(PACKAGE_VERSION)" \ + $< > $@-t; mv $@-t $@ + +noinst_DATA = \ + $(top_builddir)/html/hivexregedit.1.html + +$(top_builddir)/html/hivexregedit.1.html: hivexregedit + mkdir -p $(top_builddir)/html + cd $(top_builddir) && pod2html \ + --css 'pod.css' \ + --htmldir html \ + --outfile html/hivexregedit.1.html \ + regedit/hivexregedit diff --git a/regedit/hivexregedit b/regedit/hivexregedit new file mode 100755 index 0000000..0ad1352 --- /dev/null +++ b/regedit/hivexregedit @@ -0,0 +1,296 @@ +#!/usr/bin/perl -w +# Copyright (C) 2010 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +use warnings; +use strict; + +use Win::Hivex; +use Win::Hivex::Regedit qw(reg_import reg_export); + +use Pod::Usage; +use Getopt::Long; + +=encoding utf8 + +=head1 NAME + +hivexregedit - Merge and export Registry changes from regedit-format files. + +=head1 SYNOPSIS + + hivexregedit --merge [--prefix prefix] [--encoding enc] \ + hivefile [regfile] + + hivexregedit --export [--prefix prefix] hivefile key > regfile + +=head1 DESCRIPTION + +Please note hivexregedit is a low-level tool for manipulating hive +files directly. To merge or export registry changes to Windows +virtual machines it's better to use L<virt-win-reg(1)>. + +Given a local binary ("hive") file, there are two modes. C<--merge> +imports (merges) changes from a regedit-format file into the hive. It +is similar to using the C</s> switch in Windows regedit.exe. + +C<--export> exports a Registry key (recursively) into the regedit format. + +=head2 ENCODING + +C<hivexregedit> expects that regedit files have already been reencoded +in the local encoding. Usually on Linux hosts, this means UTF-8 with +Unix-style line endings. Since Windows regedit files are often in +UTF-16LE with Windows-style line endings, you may need to reencode the +whole file before or after processing. + +To reencode a file from Windows format to Linux (before processing it +with the C<--merge> option), you would do something like this: + + iconv -f utf-16le -t utf-8 < win.reg | dos2unix > linux.reg + +To go in the opposite direction, after using C<--export> and before +sending the file to a Windows user, do something like this: + + unix2dos linux.reg | iconv -f utf-8 -t utf-16le > win.reg + +For more information about encoding, see L<Win::Hivex::Regedit(3)>. + +If you are unsure about the current encoding, use the L<file(1)> +command. Recent versions of Windows regedit.exe produce a UTF-16LE +file with Windows-style (CRLF) line endings, like this: + + $ file software.reg + software.reg: Little-endian UTF-16 Unicode text, with very long lines, + with CRLF line terminators + +This file would need conversion before you could C<--merge> it. + +=head2 SHELL QUOTING + +Be careful when passing parameters containing C<\> (backslash) in the +shell. Usually you will have to use 'single quotes' or double +backslashes (but not both) to protect them from the shell. + +=head2 CurrentControlSet etc. + +Registry keys like C<CurrentControlSet> don't really exist in the +Windows Registry at the level of the hive file, and therefore you +cannot modify these. Replace this with C<ControlSet001>, and +similarly for other C<Current...> keys. + +=head1 EXAMPLE + + $ virt-cat WindowsGuest /Windows/System32/config/software > software.hive + $ hivexregedit --export \ + --prefix 'HKEY_LOCAL_MACHINE\SOFTWARE' \ + software.hive '\Microsoft' > ms-keys.reg + + $ hivexregedit --merge system.hive \ + --prefix 'HKEY_LOCAL_MACHINE\SYSTEM' additions.reg + +=head1 OPTIONS + +=over 4 + +=cut + +my $help; + +=item B<--help> + +Display help. + +=cut + +my $debug; + +=item B<--debug> + +Enable debugging in the hivex library. This is useful for diagnosing +bugs and also malformed hive files. + +=cut + +my $merge; + +=item B<--merge> + + hivexregedit --merge [--prefix prefix] [--encoding enc] \ + hivefile [regfile] + +Merge C<regfile> (a regedit-format text file) into the hive +C<hivefile>. If C<regfile> is omitted, then the program reads from +standard input. (Also you can give multiple input files). + +C<--prefix> specifies the Windows Registry prefix. It is almost +always necessary to use this when dealing with real hive files. + +C<--encoding> specifies the encoding for unmarked strings in the +input. It defaults to C<UTF-16LE> which should work for recent +versions of Windows. Another possibility is to use C<ASCII>. + +=cut + +my $export; + +=item B<--export> + + hivexregedit --export [--prefix prefix] hivefile key > regfile + +C<key> is a path within the hive C<hivefile>. (The key should not +contain any prefix and should be quoted to defend backslashes from the +shell). The key is exported, recursively, to standard output in the +textual regedit format. + +C<--prefix> specifies the Windows Registry prefix. It is almost +always necessary to use this when dealing with real hive files. + +=cut + +my $prefix; + +=item B<--prefix> prefix + +Hive files and Windows Registry key names are indirectly related. For +example, inside the software hive, all keys are stored relative to +C<HKEY_LOCAL_MACHINE\SOFTWARE>. Thus +C<HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft> appears in the hive file as +C<\Microsoft>. + +The hive format itself does not store this prefix, so you have to +supply it based on outside knowledge. (L<virt-win-reg(1)>, amongst +other things, already knows about this). + +Usually it is sufficient to pass the parameter +C<--prefix 'HKEY_LOCAL_MACHINE\SOFTWARE'> or similar when doing +merges and exports. + +=cut + +my $encoding; + +=item B<--encoding> UTF-16LE|ASCII + +When merging (only), you may need to specify the encoding for strings +to be used in the hive file. This is explained in detail in +L<Win::Hivex::Regedit(3)/ENCODING STRINGS>. + +The default is to use UTF-16LE, which should work with recent versions +of Windows. + +=back + +=cut + +GetOptions ("help|?" => \$help, + "debug" => \$debug, + "merge|import" => \$merge, + "export" => \$export, + "prefix=s" => \$prefix, + "encoding=s" => \$encoding, + ) or pod2usage (2); +pod2usage (1) if $help; + +if ($merge && $export) { + die "hivexregedit: cannot use --merge and --export at the same time\n" +} + +unless ($merge || $export) { + die "hivexregedit: use --merge or --export, see the manpage for help\n" +} + +if ($export && defined $encoding) { + die "hivexregedit: --encoding has no effect when used with --export\n" +} + +if ($merge) { # --merge (reg_import) + if (@ARGV < 1) { + die "hivexregedit --merge hivefile [input.reg ...]\n" + } + + my $hivefile = shift @ARGV; + + my $h = Win::Hivex->open ($hivefile, write => 1, debug => $debug); + + # Read from stdin unless other files have been specified. + unshift (@ARGV, '-') unless @ARGV; + + foreach (@ARGV) { + open FILE, $_ or die "$_: $!"; + reg_import (\*FILE, sub { + local $_ = shift; + # Remove prefix from the start of the path, matching + # case insensitively. + if (defined $prefix) { + my $len = length $prefix; + if (length $_ >= $len && + lc (substr ($_, 0, $len)) eq lc ($prefix)) { + $_ = substr ($_, $len); + } + } + ($h, $_) + }); + } + + $h->commit (undef); +} else { # --export (reg_export) + if (@ARGV != 2) { + die "hivexregedit --export hivefile key\n" + } + + my $hivefile = shift @ARGV; + my $key = shift @ARGV; + + my $h = Win::Hivex->open ($hivefile, debug => $debug); + + print "Windows Registry Editor Version 5.00\n\n"; + + reg_export ($h, $key, \*STDOUT, prefix => $prefix); +} + +=head1 SEE ALSO + +L<virt-win-reg(1)>, +L<Win::Hivex::Regedit(3)>, +L<Win::Hivex(3)>, +L<hivexsh(1)>, +L<dos2unix(1)>, +L<unix2dos(1)>, +L<iconv(1)>, +L<http://libguestfs.org/>. + +=head1 AUTHOR + +Richard W.M. Jones L<http://people.redhat.com/~rjones/> + +=head1 COPYRIGHT + +Copyright (C) 2010 Red Hat Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. diff --git a/regedit/run-locally b/regedit/run-locally new file mode 100755 index 0000000..9d9cca9 --- /dev/null +++ b/regedit/run-locally @@ -0,0 +1,52 @@ +#!/usr/bin/perl +# Copyright (C) 2009 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +# This script sets up the environment so you can run hivexregedit in +# place without needing to do 'make install' first. +# +# Use it like this: +# ./run-locally [usual hivexregedit args ...] +# eg: +# ./run-locally --export software.hive '\Microsoft' + +use strict; +use warnings; + +use File::Basename qw(dirname); +use File::Spec; +use Cwd qw(abs_path); + +my $path = $0; + +# Follow symlinks until we get to the real file +while(-l $path) { + my $link = readlink($path) or die "readlink: $path: $!"; + if(File::Spec->file_name_is_absolute($link)) { + $path = $link; + } else { + $path = File::Spec->catfile(dirname($path), $link); + } +} + +# Get the absolute path of the parent directory +$path = abs_path(dirname($path).'/..'); + +$ENV{LD_LIBRARY_PATH} = $path.'/lib/.libs'; +$ENV{PERL5LIB} = $path.'/perl/blib/lib:'.$path.'/perl/blib/arch'; + +#print (join " ", ("$path/regedit/hivexregedit", @ARGV), "\n"); +exec('perl', "$path/regedit/hivexregedit", @ARGV); -- 1.6.6.1
Richard W.M. Jones
2010-Mar-29 13:40 UTC
[Libguestfs] [PATCH 3/3] Improved version of virt-win-reg, supporting exporting and merging.
-- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones libguestfs lets you edit virtual machines. Supports shell scripting, bindings from many languages. http://et.redhat.com/~rjones/libguestfs/ See what it can do: http://et.redhat.com/~rjones/libguestfs/recipes.html -------------- next part -------------->From 1c37fb4ba9ab413bf131de87b416105639918a93 Mon Sep 17 00:00:00 2001From: Richard Jones <rjones at redhat.com> Date: Fri, 26 Mar 2010 17:14:04 +0000 Subject: [PATCH] Improved version of virt-win-reg, supporting exporting and merging. --- perl/lib/Sys/Guestfs/Lib.pm | 6 +- tools/virt-win-reg | 420 ++++++++++++++++++++++++++++++------------- 2 files changed, 303 insertions(+), 123 deletions(-) diff --git a/perl/lib/Sys/Guestfs/Lib.pm b/perl/lib/Sys/Guestfs/Lib.pm index ade4a6f..9dbce2c 100644 --- a/perl/lib/Sys/Guestfs/Lib.pm +++ b/perl/lib/Sys/Guestfs/Lib.pm @@ -20,6 +20,8 @@ package Sys::Guestfs::Lib; use strict; use warnings; +use Carp qw(croak); + use Sys::Guestfs; use File::Temp qw/tempdir/; use Locale::TextDomain 'libguestfs'; @@ -140,14 +142,14 @@ sub open_guest } elsif (ref ($first) eq "SCALAR") { @images = ($first); } else { - die __"open_guest: first parameter must be a string or an arrayref" + croak __"open_guest: first parameter must be a string or an arrayref" } my ($conn, $dom); if (-e $images[0]) { foreach (@images) { - die __x("guest image {imagename} does not exist or is not readable", + croak __x("guest image {imagename} does not exist or is not readable", imagename => $_) unless -r $_; } diff --git a/tools/virt-win-reg b/tools/virt-win-reg index 8f248d7..bcd77e7 100755 --- a/tools/virt-win-reg +++ b/tools/virt-win-reg @@ -1,6 +1,6 @@ #!/usr/bin/perl -w # virt-win-reg -# Copyright (C) 2009 Red Hat Inc. +# Copyright (C) 2010 Red Hat Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -23,6 +23,9 @@ use Sys::Guestfs; use Sys::Guestfs::Lib qw(open_guest get_partitions resolve_windows_path inspect_all_partitions inspect_partition inspect_operating_systems mount_operating_system); +use Win::Hivex; +use Win::Hivex::Regedit qw(reg_import reg_export); + use Pod::Usage; use Getopt::Long; use File::Temp qw/tempdir/; @@ -32,84 +35,124 @@ use Locale::TextDomain 'libguestfs'; =head1 NAME -virt-win-reg - Display Windows Registry entries from a Windows guest +virt-win-reg - Export and merge Windows Registry entries from a Windows guest =head1 SYNOPSIS - virt-win-reg [--options] domname '\Path\To\Subkey' name ['\Path'...] + virt-win-reg domname 'HKLM\Path\To\Subkey' + + virt-win-reg domname 'HKLM\Path\To\Subkey' name + + virt-win-reg domname 'HKLM\Path\To\Subkey' @ + + virt-win-reg --merge domname [input.reg ...] + + virt-win-reg [--options] disk.img ... # instead of domname - virt-win-reg [--options] domname '\Path\To\Subkey' @ ['\Path'...] +=head1 WARNING - virt-win-reg [--options] domname '\Path\To\Subkey' ['\Path'...] +You must I<not> use C<virt-win-reg> with the C<--merge> option on live +virtual machines. If you do this, you I<will> get irreversible disk +corruption in the VM. C<virt-win-reg> tries to stop you from doing +this, but doesn't catch all cases. - virt-win-reg [--options] disk.img [...] '\Path\To\Subkey' (name|@) +Modifying the Windows Registry is an inherently risky operation. The format +is deliberately obscure and undocumented, and Registry changes +can leave the system unbootable. Therefore when using the C<--merge> +option, make sure you have a reliable backup first. =head1 DESCRIPTION -This program can display Windows Registry entries from a Windows -guest. +This program can export and merge Windows Registry entries from a +Windows guest. The first parameter is the libvirt guest name or the raw disk image of -the Windows guest. +a Windows guest. -Then follow one or more sets of path specifiers. The path must begin -with a C<\> (backslash) character, and may be something like -C<'\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion'>. +If C<--merge> is I<not> specified, then the chosen registry +key is displayed/exported (recursively). For example: -The next parameter after that is either a value name, the single -at-character C<@>, or missing. + $ virt-win-reg Windows7 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft' -If it's a value name, then we print the data associated with that -value. If it's C<@>, then we print the default data associated with -the subkey. If it's missing, then we print all the data associated -with the subkey. +You can also display single values from within registry keys, +for example: -If this is confusing, look at the L</EXAMPLES> section below. + $ cvkey='HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion' + $ virt-win-reg Windows7 $cvkey ProductName + Windows 7 Enterprise -Usually you should use single quotes to protect backslashes in the -path from the shell. +With C<--merge>, you can merge a textual regedit file into +the Windows Registry: -Paths and value names are case-insensitive. + $ virt-win-reg --merge Windows7 changes.reg =head2 SUPPORTED SYSTEMS The program currently supports Windows NT-derived guests starting with Windows XP through to at least Windows 7. -Registry support is done for C<\HKEY_LOCAL_MACHINE\SAM>, -C<\HKEY_LOCAL_MACHINE\SECURITY>, C<\HKEY_LOCAL_MACHINE\SOFTWARE>, -C<\HKEY_LOCAL_MACHINE\SYSTEM> and C<\HKEY_USERS\.DEFAULT>. +Registry support is done for C<HKEY_LOCAL_MACHINE\SAM>, +C<HKEY_LOCAL_MACHINE\SECURITY>, C<HKEY_LOCAL_MACHINE\SOFTWARE>, +C<HKEY_LOCAL_MACHINE\SYSTEM> and C<HKEY_USERS\.DEFAULT>. + +You can use C<HKLM> as a shorthand for C<HKEY_LOCAL_MACHINE>, and +C<HKU> for C<HKEY_USERS>. -C<\HKEY_USERS\$SID> and C<\HKEY_CURRENT_USER> are B<not> supported at +C<HKEY_USERS\$SID> and C<HKEY_CURRENT_USER> are B<not> supported at this time. -=head2 NOTES +=head2 NOTE This program is only meant for simple access to the registry. If you want to do complicated things with the registry, we suggest you -download the Registry hive files from the guest using C<libguestfs(3)> -or C<guestfish(1)> and access them locally, eg. using C<hivex(3)>, -C<hivexml(1)> or C<reged(1)>. +download the Registry hive files from the guest using L<libguestfs(3)> +or L<guestfish(1)> and access them locally, eg. using L<hivex(3)>, +L<hivexsh(1)> or L<hivexregedit(1)>. + +=head2 ENCODING + +C<virt-win-reg> expects that regedit files have already been reencoded +in the local encoding. Usually on Linux hosts, this means UTF-8 with +Unix-style line endings. Since Windows regedit files are often in +UTF-16LE with Windows-style line endings, you may need to reencode the +whole file before or after processing. + +To reencode a file from Windows format to Linux (before processing it +with the C<--merge> option), you would do something like this: -=head1 EXAMPLES + iconv -f utf-16le -t utf-8 < win.reg | dos2unix > linux.reg - $ virt-win-reg MyWinGuest \ - '\HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion' \ - ProductName - Microsoft Windows Server 2003 +To go in the opposite direction, after exporting and before sending +the file to a Windows user, do something like this: - $ virt-win-reg MyWinGuest \ - '\HKEY_LOCAL_MACHINE\System\ControlSet001\Control' SystemBootDevice - multi(0)disk(0)rdisk(0)partition(1) + unix2dos linux.reg | iconv -f utf-8 -t utf-16le > win.reg - $ virt-win-reg MyWinGuest \ - '\HKEY_LOCAL_MACHINE\System\ControlSet001\Control' - "CurrentUser"="USERNAME" - "WaitToKillServiceTimeout"="20000" - "SystemStartOptions"="NOEXECUTE=OPTOUT FASTDETECT" - "SystemBootDevice"="multi(0)disk(0)rdisk(0)partition(1)" +For more information about encoding, see L<Win::Hivex::Regedit(3)>. -(please suggest some more) +If you are unsure about the current encoding, use the L<file(1)> +command. Recent versions of Windows regedit.exe produce a UTF-16LE +file with Windows-style (CRLF) line endings, like this: + + $ file software.reg + software.reg: Little-endian UTF-16 Unicode text, with very long lines, + with CRLF line terminators + +This file would need conversion before you could C<--merge> it. + +=head2 SHELL QUOTING + +Be careful when passing parameters containing C<\> (backslash) in the +shell. Usually you will have to use 'single quotes' or double +backslashes (but not both) to protect them from the shell. + +Paths and value names are case-insensitive. + +=head2 CurrentControlSet etc. + +Registry keys like C<CurrentControlSet> don't really exist in the +Windows Registry at the level of the hive file, and therefore you +cannot modify these. Replace this with C<ControlSet001>, and +similarly for other C<Current...> keys. =head1 OPTIONS @@ -133,6 +176,14 @@ Display version number and exit. =cut +my $debug; + +=item B<--debug> + +Enable debugging messages. + +=cut + my $uri; =item B<--connect URI> | B<-c URI> @@ -143,6 +194,33 @@ connect to the default libvirt hypervisor. If you specify guest block devices directly, then libvirt is not used at all. +=cut + +my $merge; + +=item B<--merge> + +In merge mode, this merges a textual regedit file into the Windows +Registry of the virtual machine. If this flag is I<not> given then +virt-win-reg displays or exports Registry entries instead. + +Note that C<--merge> is I<unsafe> to use on live virtual machines, and +will result in disk corruption. However exporting (without this flag) +is always safe. + +=cut + +my $encoding; + +=item B<--encoding> UTF-16LE|ASCII + +When merging (only), you may need to specify the encoding for strings +to be used in the hive file. This is explained in detail in +L<Win::Hivex::Regedit(3)/ENCODING STRINGS>. + +The default is to use UTF-16LE, which should work with recent versions +of Windows. + =back =cut @@ -150,6 +228,9 @@ at all. GetOptions ("help|?" => \$help, "version" => \$version, "connect|c=s" => \$uri, + "debug|d" => \$debug, + "merge" => \$merge, + "encoding=s" => \$encoding, ) or pod2usage (2); pod2usage (1) if $help; if ($version) { @@ -159,31 +240,20 @@ if ($version) { exit } -# Split the command line at the first path. Paths begin with -# backslash so this is predictable. - -my @lib_args; -my $i; - -for ($i = 0; $i < @ARGV; ++$i) { - if (substr ($ARGV[$i], 0, 1) eq "\\") { - @lib_args = @ARGV[0 .. ($i-1)]; - @ARGV = @ARGV[$i .. $#ARGV]; - last; - } -} +# virt-win-reg only takes a single disk image ... +die __"no libvirt domain name or disk image given\n" if @ARGV == 0; +my $domname_or_image = shift @ARGV; -pod2usage (__"virt-win-reg: no VM name, disk images or Registry path given") if 0 == @lib_args; - -my $g; -if ($uri) { - $g = open_guest (\@lib_args, address => $uri); -} else { - $g = open_guest (\@lib_args); -} +warn "launching libguestfs ..." if $debug; +my @lib_args = ([$domname_or_image]); +push @lib_args, address => $uri if $uri; +push @lib_args, rw => 1 if $merge; +my $g = open_guest (@lib_args); $g->launch (); +warn "inspecting guest ..." if $debug; + # List of possible filesystems. my @partitions = get_partitions ($g); @@ -200,99 +270,207 @@ die __"multiboot operating systems are not supported by virt-win-reg" if @roots my $root_dev = $roots[0]; my $os = $oses->{$root_dev}; -mount_operating_system ($g, $os); +my $ro = $merge ? 0 : 1; +mount_operating_system ($g, $os, $ro); # Create a working directory to store the downloaded registry files. my $tmpdir = tempdir (CLEANUP => 1); -# Now process each request in turn. -my $winfile; -my $localhive; -my $path; +# Only used when merging to map downloaded hive names to hive handles. +my %hives; + +if (!$merge) { # Export mode. + die __"expecting 1 or 2 more parameters, subkey path and optionally the value to export\n" + if @ARGV < 1 || @ARGV > 2; + + my $path = shift @ARGV; + my $name = shift @ARGV; # or undef + + # Map this to the hive name. This function dies on failure. + my ($hivename, $prefix); + ($hivename, $path, $prefix) = map_path_to_hive ($path); + + # Download the chosen hive. + download_hive ($hivename); + + # Open it. + my $h = Win::Hivex->open ("$tmpdir/$hivename", debug => $debug); + + unless ($name) { + # Export it. + warn "exporting $path from $hivename with prefix $prefix ..." if $debug; + reg_export ($h, $path, \*STDOUT, prefix => $prefix); + } else { + # Export a single key using hivexget. + my @args = ("hivexget", "$tmpdir/$hivename", $path, $name); + warn "running ", join (" ", @args), " ..." if $debug; + system (@args) == 0 or die "hivexget failed: $?" + } +} +else { # Import mode. + if (@ARGV == 0) { + reg_import (\*STDIN, \&import_mapper, encoding => $encoding); + } else { + foreach (@ARGV) { + open my $fh, $_ or die "open: $_: $!"; + reg_import ($fh, \&import_mapper, encoding => $encoding); + } + } + + # Now we've done importing, commit all the hive handles and + # close them all. + $_->commit (undef) foreach values %hives; + %hives = (); + + # Look in the tmpdir for all the hive files which have been + #?downloaded / modified by the import mapper, and upload + # each one. + opendir my $dh, $tmpdir or die "$tmpdir: $!"; + foreach (readdir $dh) { + unless (/^\./) { + upload_hive ($_) + } + } + + # Sync everything. + $g->umount_all (); + $g->sync (); +} + +exit 0; + +# map function passed to reg_import. +sub import_mapper +{ + local $_ = shift; + + my ($hivename, $path, $prefix) = map_path_to_hive ($_); + + # Need to download this hive? + unless (-f "$tmpdir/$hivename") { + download_hive ($hivename); -for ($i = 0; $i < @ARGV; ++$i) { - $_ = $ARGV[$i]; + my $h = Win::Hivex->open ("$tmpdir/$hivename", + write => 1, debug => $debug); + $hives{$hivename} = $h; + } + + return ($hives{$hivename}, $path); +} - if (/^\\HKEY_LOCAL_MACHINE\\SAM(\\.*)/i) { - $winfile = "/windows/system32/config/sam"; - $localhive = "$tmpdir/sam"; - $path = $1; +# Given a path, map that to the name of the hive and the true path +# within that hive. +sub map_path_to_hive +{ + local $_ = shift; + my ($hivename, $prefix); + + if (/^\\?(?:HKEY_LOCAL_MACHINE|HKLM)\\SAM(\\.*)?$/i) { + $hivename = "sam"; + $_ = defined $1 ? $1 : "\\"; + $prefix = "HKEY_LOCAL_MACHINE\\SAM"; } - elsif (/^\\HKEY_LOCAL_MACHINE\\SECURITY(\\.*)/i) { - $winfile = "/windows/system32/config/security"; - $localhive = "$tmpdir/security"; - $path = $1; + elsif (/^\\?(?:HKEY_LOCAL_MACHINE|HKLM)\\SECURITY(\\.*)?$/i) { + $hivename = "security"; + $_ = defined $1 ? $1 : "\\"; + $prefix = "HKEY_LOCAL_MACHINE\\SECURITY"; } - elsif (/^\\HKEY_LOCAL_MACHINE\\SOFTWARE(\\.*)/i) { - $winfile = "/windows/system32/config/software"; - $localhive = "$tmpdir/software"; - $path = $1; + elsif (/^\\?(?:HKEY_LOCAL_MACHINE|HKLM)\\SOFTWARE(\\.*)?$/i) { + $hivename = "software"; + $_ = defined $1 ? $1 : "\\"; + $prefix = "HKEY_LOCAL_MACHINE\\SOFTWARE"; } - elsif (/^\\HKEY_LOCAL_MACHINE\\SYSTEM(\\.*)/i) { - $winfile = "/windows/system32/config/system"; - $localhive = "$tmpdir/system"; - $path = $1; + elsif (/^\\?(?:HKEY_LOCAL_MACHINE|HKLM)\\SYSTEM(\\.*)?$/i) { + $hivename = "system"; + $_ = defined $1 ? $1 : "\\"; + $prefix = "HKEY_LOCAL_MACHINE\\SYSTEM"; } - elsif (/^\\HKEY_USERS\\.DEFAULT(\\.*)/i) { - $winfile = "/windows/system32/config/default"; - $localhive = "$tmpdir/default"; - $path = $1; + elsif (/^\\?(?:HKEY_USERS|HKU)\\.DEFAULT(\\.*)?$/i) { + $hivename = "default"; + $_ = defined $1 ? $1 : "\\"; + $prefix = "HKEY_LOCAL_MACHINE\\.DEFAULT"; } else { - die "virt-win-reg: $_: not a supported Windows Registry path\n" + die __x("virt-win-reg: {p}: not a supported Windows Registry path\n", + p => $_) } - unless (-f $localhive) { - # Check the hive file exists and get the real name. - eval { - $winfile = $g->case_sensitive_path ($winfile); - $g->download ($winfile, $localhive); - }; - if ($@) { - die "virt-win-reg: $winfile: could not download registry file: $@\n" - } - } + return ($hivename, $_, $prefix); +} - # What sort of request is it? Peek at the next arg. - my $name; # will be: undefined, @ or a name - if ($i+1 < @ARGV) { - if (substr ($ARGV[$i+1], 0, 1) ne "\\") { - $name = $ARGV[$i+1]; - $i++; - } +# Download a named hive file. Die on failure. +sub download_hive +{ + local $_; + my $hivename = shift; + + eval { + my $winfile + $g->case_sensitive_path ("/windows/system32/config/$hivename"); + warn "downloading $winfile ..." if $debug; + $g->download ($winfile, "$tmpdir/$hivename"); + }; + if ($@) { + die __x("virt-win-reg: {h}: could not download registry file: {err}\n", + h => $hivename, + err => $@); } +} - my @cmd; - if (defined $name) { - @cmd = ("hivexget", $localhive, $path, $name); - } else { - @cmd = ("hivexget", $localhive, $path); +# Upload a named hive file. Die on failure. +sub upload_hive +{ + local $_; + my $hivename = shift; + + eval { + my $winfile + $g->case_sensitive_path ("/windows/system32/config/$hivename"); + # Back up the original file. + $g->cp ($winfile, "$winfile.bak"); + warn "uploading $winfile ..." if $debug; + $g->upload ("$tmpdir/$hivename", $winfile); + }; + if ($@) { + die __x("virt-win-reg: {h}: could not upload registry file: {err}\n", + h => $hivename, + err => $@); } - - system (@cmd) == 0 - or die "hivexget command failed: $?\n"; } =head1 SEE ALSO L<hivex(3)>, -L<hivexget(1)>, L<hivexsh(1)>, +L<hivexregedit(1)>, L<guestfs(3)>, L<guestfish(1)>, L<virt-cat(1)>, L<Sys::Guestfs(3)>, L<Sys::Guestfs::Lib(3)>, +L<Win::Hivex(3)>, +L<Win::Hivex::Regedit(3)>, L<Sys::Virt(3)>, L<http://libguestfs.org/>. +=head1 BUGS + +When reporting bugs, please enable debugging and capture the +I<complete> output: + + export LIBGUESTFS_DEBUG=1 + virt-win-reg --debug [... rest ...] > /tmp/virt-win-reg.log 2>&1 + +Attach /tmp/virt-win-reg.log to a new bug report at +L<https://bugzilla.redhat.com/> + =head1 AUTHOR Richard W.M. Jones L<http://et.redhat.com/~rjones/> =head1 COPYRIGHT -Copyright (C) 2009 Red Hat Inc. +Copyright (C) 2010 Red Hat Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by -- 1.6.6.1
Richard W.M. Jones
2010-Mar-30 15:26 UTC
[Libguestfs] [PATCH 0/3] Export and merge into Windows Registry
These were tested by Marko Myllynen and I have pushed them to the hivex and libguestfs repositories respectively. Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones libguestfs lets you edit virtual machines. Supports shell scripting, bindings from many languages. http://et.redhat.com/~rjones/libguestfs/ See what it can do: http://et.redhat.com/~rjones/libguestfs/recipes.html