From: Richard Jones Date: Fri, 26 Mar 2010 17:14:04 +0000 (+0000) Subject: Improved version of virt-win-reg, supporting exporting and merging. X-Git-Tag: 1.0.89~4 X-Git-Url: http://git.annexia.org/?p=libguestfs.git;a=commitdiff_plain;h=4b48f93ce6407923ea23451cfe39ac63811d1f42 Improved version of virt-win-reg, supporting exporting and merging. --- diff --git a/README b/README index c00ebd3..e309126 100644 --- a/README +++ b/README @@ -52,7 +52,7 @@ Requirements - genisoimage / mkisofs -- (Optional) hivex to build Windows Registry support +- (Optional) hivex >= 1.2.1 to build Windows Registry support - (Optional) FUSE to build the FUSE module diff --git a/configure.ac b/configure.ac index 9b97010..b5b2026 100644 --- a/configure.ac +++ b/configure.ac @@ -718,7 +718,7 @@ AM_CONDITIONAL([HAVE_HASKELL], dnl Check for Perl modules needed by virt-df, inspector, etc. missing_perl_modules=no -for pm in Pod::Usage Getopt::Long Sys::Virt Data::Dumper XML::Writer Locale::TextDomain; do +for pm in Pod::Usage Getopt::Long Sys::Virt Data::Dumper XML::Writer Locale::TextDomain Win::Hivex Win::Hivex::Regedit; 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/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..b4bb1f0 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 use C with the C<--merge> option on live +virtual machines. If you do this, you I get irreversible disk +corruption in the VM. C 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 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 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, +C, C, +C and C. + +You can use C as a shorthand for C, and +C for C. -C<\HKEY_USERS\$SID> and C<\HKEY_CURRENT_USER> are B supported at +C and C are B 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 -or C and access them locally, eg. using C, -C or C. +download the Registry hive files from the guest using L +or L and access them locally, eg. using L, +L or L. + +=head2 ENCODING + +C 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. -(please suggest some more) +If you are unsure about the current encoding, use the L +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 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, and +similarly for other C 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 given then +virt-win-reg displays or exports Registry entries instead. + +Note that C<--merge> is I 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. + +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,213 @@ 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); +} + +# Download a named hive file. Die on failure. +sub download_hive +{ + local $_; + my $hivename = shift; + + my $systemroot = $os->{root}->{systemroot} || "/windows"; + my $winfile_before = "$systemroot/system32/config/$hivename"; + my $winfile; + eval { $winfile = $g->case_sensitive_path ($winfile_before); }; + if ($@) { + die __x("virt-win-reg: {p}: file not found in guest: {err}\n", + p => $winfile_before, err => $@); } - # 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++; - } + warn "downloading $winfile ..." if $debug; + eval { $g->download ($winfile, "$tmpdir/$hivename"); }; + if ($@) { + die __x("virt-win-reg: {p}: could not download registry file: {err}\n", + p => $winfile, 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; + + my $systemroot = $os->{root}->{systemroot} || "/windows"; + my $winfile_before = "$systemroot/system32/config/$hivename"; + my $winfile; + eval { $winfile = $g->case_sensitive_path ($winfile_before); }; + if ($@) { + die __x("virt-win-reg: {p}: file not found in guest: {err}\n", + p => $winfile_before, err => $@); } - system (@cmd) == 0 - or die "hivexget command failed: $?\n"; + warn "uploading $winfile ..." if $debug; + eval { $g->upload ("$tmpdir/$hivename", $winfile); }; + if ($@) { + die __x("virt-win-reg: {p}: could not upload registry file: {err}\n", + p => $winfile, err => $@); + } } =head1 SEE ALSO L, -L, L, +L, L, L, L, L, L, +L, +L, L, L. +=head1 BUGS + +When reporting bugs, please enable debugging and capture the +I 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 + =head1 AUTHOR Richard W.M. Jones L =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