X-Git-Url: http://git.annexia.org/?p=libguestfs.git;a=blobdiff_plain;f=tools%2Fvirt-win-reg;h=4c9308addec312c779285e17aa672a5d9438935f;hp=d4329d1c80d2af3b3f29c5f56aedc54f27ae0df3;hb=a0f85ba643990da25b6afe6a2bbd2407cf312c73;hpb=792c5283009ed6753239a14df9a6e9c71bea35fd diff --git a/tools/virt-win-reg b/tools/virt-win-reg index d4329d1..4c9308a 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 @@ -20,96 +20,153 @@ use warnings; use strict; 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 Sys::Guestfs::Lib qw(open_guest); +use Win::Hivex; +use Win::Hivex::Regedit qw(reg_import reg_export); + use Pod::Usage; use Getopt::Long; use File::Temp qw/tempdir/; +use File::Basename; use Locale::TextDomain 'libguestfs'; =encoding utf8 =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 [--options] domname '\Path\To\Subkey' @ ['\Path'...] + virt-win-reg --merge domname [input.reg ...] - virt-win-reg [--options] domname '\Path\To\Subkey' ['\Path'...] + virt-win-reg [--options] disk.img ... # instead of domname - virt-win-reg [--options] disk.img [...] '\Path\To\Subkey' (name|@) +=head1 WARNING + +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. + +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. -C<\HKEY_USERS\$SID> and C<\HKEY_CURRENT_USER> are B supported at +You can use C as a shorthand for C, and +C for C. + +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: + + iconv -f utf-16le -t utf-8 < win.reg | dos2unix > linux.reg + +To go in the opposite direction, after exporting 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. + +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. -=head1 EXAMPLES +=head2 CurrentControlSet etc. - $ virt-win-reg MyWinGuest \ - '\HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion' \ - ProductName - Microsoft Windows Server 2003 +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. - $ virt-win-reg MyWinGuest \ - '\HKEY_LOCAL_MACHINE\System\ControlSet001\Control' SystemBootDevice - multi(0)disk(0)rdisk(0)partition(1) +C is usually an alias for C. In +some circumstances it might refer to another control set. The way +to find out is to look at the C key: - $ 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)" + # virt-win-reg WindowsGuest 'HKLM\SYSTEM\Select' + [HKEY_LOCAL_MACHINE\SYSTEM\Select] + "Current"=dword:00000001 + "Default"=dword:00000001 + "Failed"=dword:00000000 + "LastKnownGood"=dword:00000002 -(please suggest some more) +"Default" is the one which Windows will choose when it boots. + +Similarly, other C keys in the path may need to +be replaced. =head1 OPTIONS @@ -133,6 +190,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 +208,50 @@ connect to the default libvirt hypervisor. If you specify guest block devices directly, then libvirt is not used at all. +=cut + +my $format; + +=item B<--format> raw + +Specify the format of disk images given on the command line. If this +is omitted then the format is autodetected from the content of the +disk image. + +If disk images are requested from libvirt, then this program asks +libvirt for this information. In this case, the value of the format +parameter is ignored. + +If working with untrusted raw-format guest disk images, you should +ensure the format is always specified. + +=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 +259,10 @@ at all. GetOptions ("help|?" => \$help, "version" => \$version, "connect|c=s" => \$uri, + "debug|d" => \$debug, + "format=s" => \$format, + "merge" => \$merge, + "encoding=s" => \$encoding, ) or pod2usage (2); pod2usage (1) if $help; if ($version) { @@ -159,139 +272,248 @@ if ($version) { exit } -# Split the command line at the first path. Paths begin with -# backslash so this is predictable. +# 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; -my @lib_args; -my $i; +warn "launching libguestfs ..." if $debug; -for ($i = 0; $i < @ARGV; ++$i) { - if (substr ($ARGV[$i], 0, 1) eq "\\") { - @lib_args = @ARGV[0 .. ($i-1)]; - @ARGV = @ARGV[$i .. $#ARGV]; - last; - } -} +my @lib_args = ([$domname_or_image]); +push @lib_args, address => $uri if $uri; +push @lib_args, rw => 1 if $merge; +push @lib_args, format => $format if defined $format; +my $g = open_guest (@lib_args); +$g->launch (); -pod2usage (__"virt-win-reg: no VM name, disk images or Registry path given") if 0 == @lib_args; +warn "inspecting guest ..." if $debug; -my $g; -if ($uri) { - $g = open_guest (\@lib_args, address => $uri); -} else { - $g = open_guest (\@lib_args); +my @roots = $g->inspect_os (); +if (@roots == 0) { + die __x("{prog}: No operating system could be detected inside this disk image.\n\nThis may be because the file is not a disk image, or is not a virtual machine\nimage, or because the OS type is not understood by libguestfs.\n\nIf you feel this is an error, please file a bug report including as much\ninformation about the disk image as possible.\n", + prog => basename ($0)); +} +if (@roots > 1) { + die __x("{prog}: multiboot operating systems are not supported.\n", + prog => basename ($0)) +} +my %fses = $g->inspect_get_mountpoints ($roots[0]); +my @fses = sort { length $a <=> length $b } keys %fses; +my $mountopts = $merge ? "" : "ro"; +foreach (@fses) { + $g->mount_options ($mountopts, $fses{$_}, $_); } -$g->launch (); +my $systemroot = $g->inspect_get_windows_systemroot ($roots[0]); -# List of possible filesystems. -my @partitions = get_partitions ($g); +# Create a working directory to store the downloaded registry files. +my $tmpdir = tempdir (CLEANUP => 1); -# Now query each one to build up a picture of what's in it. -my %fses = - inspect_all_partitions ($g, \@partitions, - use_windows_registry => 0); +# Only used when merging to map downloaded hive names to hive handles. +my %hives; -my $oses = inspect_operating_systems ($g, \%fses); +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 @roots = keys %$oses; -die __"no root device found in this operating system image" if @roots == 0; -die __"multiboot operating systems are not supported by virt-win-reg" if @roots > 1; -my $root_dev = $roots[0]; + my $path = shift @ARGV; + my $name = shift @ARGV; # or undef -my $os = $oses->{$root_dev}; -mount_operating_system ($g, $os); + # Map this to the hive name. This function dies on failure. + my ($hivename, $prefix); + ($hivename, $path, $prefix) = map_path_to_hive ($path); -# Create a working directory to store the downloaded registry files. -my $tmpdir = tempdir (CLEANUP => 1); + # 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 process each request in turn. -my $winfile; -my $localhive; -my $path; + # 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 ($_); -for ($i = 0; $i < @ARGV; ++$i) { - $_ = $ARGV[$i]; + # Need to download this hive? + unless (-f "$tmpdir/$hivename") { + download_hive ($hivename); - if (/^\\HKEY_LOCAL_MACHINE\\SAM(\\.*)/i) { - $winfile = "/windows/system32/config/sam"; - $localhive = "$tmpdir/sam"; - $path = $1; + my $h = Win::Hivex->open ("$tmpdir/$hivename", + write => 1, debug => $debug); + $hives{$hivename} = $h; } - elsif (/^\\HKEY_LOCAL_MACHINE\\SECURITY(\\.*)/i) { - $winfile = "/windows/system32/config/security"; - $localhive = "$tmpdir/security"; - $path = $1; + + return ($hives{$hivename}, $path); +} + +# 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\\SOFTWARE(\\.*)/i) { - $winfile = "/windows/system32/config/software"; - $localhive = "$tmpdir/software"; - $path = $1; + elsif (/^\\?(?:HKEY_LOCAL_MACHINE|HKLM)\\SECURITY(\\.*)?$/i) { + $hivename = "security"; + $_ = defined $1 ? $1 : "\\"; + $prefix = "HKEY_LOCAL_MACHINE\\SECURITY"; } - elsif (/^\\HKEY_LOCAL_MACHINE\\SYSTEM(\\.*)/i) { - $winfile = "/windows/system32/config/system"; - $localhive = "$tmpdir/system"; - $path = $1; + elsif (/^\\?(?:HKEY_LOCAL_MACHINE|HKLM)\\SOFTWARE(\\.*)?$/i) { + $hivename = "software"; + $_ = defined $1 ? $1 : "\\"; + $prefix = "HKEY_LOCAL_MACHINE\\SOFTWARE"; } - elsif (/^\\HKEY_USERS\\.DEFAULT(\\.*)/i) { - $winfile = "/windows/system32/config/default"; - $localhive = "$tmpdir/default"; - $path = $1; + elsif (/^\\?(?:HKEY_LOCAL_MACHINE|HKLM)\\SYSTEM(\\.*)?$/i) { + $hivename = "system"; + $_ = defined $1 ? $1 : "\\"; + $prefix = "HKEY_LOCAL_MACHINE\\SYSTEM"; + } + 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 $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 $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 SHELL QUOTING + +Libvirt guest names can contain arbitrary characters, some of which +have meaning to the shell such as C<#> and space. You may need to +quote or escape these characters on the command line. See the shell +manual page L for details. + =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 +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