#!/usr/bin/perl sub usage { print STDERR $_[0], "\n" if $_[0] ne ""; die "usage: xenidump.pl {[-r path] partition_option image}+\n" . "where\n" . "-r path directory path to extract to\n" . " default is image name with partition offset ext\n" . "and partition_option is one of\n" . "-p hexoffset offset to start of partition in the image\n" . "-h assume image is hard disk === -p 2400\n" . "-b1 assume image is boot floppy boot === -p 1800\n" . "-b2 assume image is boot floppy ram === -p 2f800\n". "-f assume image is a floppy === -p 0\n" . "Note image must be de-interleaved with bytes in standard order\n"; } my $INODE = 64; # size of an inode my $BLOCK = 1024; # block size my $SUPER = 64; # size of super block my $CYLINFO = 12; # size of each Cylinder Group Info my $fileSize; # will be used for file size to check for past EOF my $root; # user's name for root directory or filename with suffix of partition offset my $partOff; # offset to start of partition my $readError; # read beyond eof my @cylGrp; # location of the cylinder inode starts my $maxInode; # highest inode my %linked; # linked files # convert a xenix time to human readable form # dstr($time) sub dstr { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($_[0]); return sprintf "%04d-%02d-%02d %02d:%02d", $year + 1900, $mon + 1, $mday, $hour, $min; } # readBlk($n) read the nth block from the partition. sub readBlk { my $data; my $loc = $partOff + $_[0] * $BLOCK; # location in image if ($loc >= $fileSize) { # does it exist? print "Block $_[0] beyond end of image\n"; $readError++; return ""; } seek $in, $loc, 0 or die $!; read $in, $data, $BLOCK; return $data; } # return the file content specified by the passed in inode addr block # getFile(@addrbytes) # changed to use array of blocks to avoid large number of concat of strings # Warning this version does not support sparse files sub getFile { my @bytes = unpack("C*", $_[0]); my @file = (); # no file content to start $readError = 0; # clear error flag # process inode blocks for (my $i = 0; $i < 13 ;$i++) { # convert the 3 bytes to a block number my $blk = $bytes[$i*3] + ($bytes[$i*3 + 1] << 8) + ($bytes[$i*3 + 2] << 16); return join('', @file) if $blk == 0 || $readError; # all done? if ($i < 10) { # add simple block push @file, readBlk($blk); } elsif ($i == 10) { # one level indirect foreach (unpack("V*", readBlk($blk))) { return join('', @file) if $_ == 0 || $readError; # all done? push @file, readBlk($_); # add the block } } elsif ($i == 11) { # double indirect foreach (unpack("V*", readBlk($blk))) { return join('', @file) if $_ == 0 || $readError; foreach (unpack("V*", readBlk($_))) { return join('', @file) if $_ == 0 || $readError; push @file, readBlk($_); } } } else { # triple indirect foreach (unpack("V*", readBlk($blk))) { return join('', @file) if $_ == 0 || $readError; foreach (unpack("V*", readBlk($_))) { return join('', @file) if $_ == 0 || $readError; foreach (unpack("V*", readBlk($_))) { return join('', @file) if $_ == 0 || $readError; push @file, readBlk($_); } } } } } return join('', @file); # if we get here we have a massive file!! } # traverse the directory file that is passed in # directory($path, $dircontent) sub directory { my ($path, $dfile) = @_; while ($dfile ne "") { # keep going until no more entries my ($inode, $name) = unpack("vZ14", $dfile); $dfile = substr($dfile, 16); # prune this entry from the dircontent if ($name ne "." && $name ne ".." && $inode != 0) { # process only sensible entries inode($inode, "$path/$name"); # get process the inode } } } # convert the inode mode word to human readable format sub expandMode { my $mode = $_[0]; my $perm = "-rwxrwxrwx"; substr($perm, 3, 1) = 's' if ($mode & 04000); # set special bits substr($perm, 6, 1) = 's' if ($mode & 02000); substr($perm, 0, 1) = 'd' if ($mode & 0170000) == 040000; substr($perm, 0, 1) = 'c' if ($mode & 0170000) == 020000; substr($perm, 0, 1) = 'b' if ($mode & 0170000) == 060000; for (my $i = 0; $i < 9; $i++) { # clear no access bytes substr($perm, 9 - $i, 1) = '-' if (($mode & (1 << $i)) == 0); } return $perm; } # process an inode # inode($ino, $currentPath) sub inode { my ($ino, $path) = @_; if ($ino > $maxInode) { print "inode $ino > max $maxInode\n"; return; } # seek to location of inode information by indexing into the appropriate cylinder group # inode list seek $in, $cylGrp[int(($ino - 1) / $inodesPerCg)] + (($ino - 1) % $inodesPerCg) * $INODE, 0; # get the base inode info and unpack it # $addr is the packed information on disk locations my $raw; read $in, $raw, $INODE; my($mode, $nlink, $uid, $gid, $size, $addr, $atime, $mtime, $ctime) = unpack("vsvvla40lll", $raw); my $type = $mode >> 12; my $fpath = "$root$path"; # full path including user's root path if ($type != 4 && $nlink > 1) { push @{$linked{$ino}}, $path; } if ($type == 4) { # directory entry # emit the file info in ls format with leading inode printf "%04d: %s %-7s %11d %s %s\n", $ino, expandMode($mode), "$uid/$gid", $size, dstr($mtime), $path; mkdir $fpath or print "can't create $path\n" if ! -d $fpath; # make new dir directory($path, substr(getFile($addr), 0, $size)); # process the directory utime $atime, $mtime, $fpath; # set its times chown $uid, $gid, $fpath; # ownership chmod ($mode & 07777), $fpath; # permissions } elsif ($type == 010) { # regular file # emit the file info in ls format with leading inode printf "%04d: %s %-7s %11d %s %s\n", $ino, expandMode($mode), "$uid/$gid", $size, dstr($mtime), $path; unlink("$fpath"); # remove it currently exists open($out, ">:raw", "$fpath") or die "can't create $path\n"; print $out substr(getFile($addr), 0, $size); # write the file close $out; utime $atime, $mtime, $fpath; # set its times chown $uid, $gid, $fpath; # ownership chmod $mode, $fpath; # permissions } elsif ($type == 2 || $type == 6 || $type == 3 || $type == 7) { my ($major, $minor) = unpack("CC", $addr); printf "%04d: %s %-7s %6d,%4d %s %s - ***\n", $ino, expandMode($mode), "$uid/$gid", $major, $minor, dstr($mtime), $path; } else { printf "%04d: mode %o not currently supported for file %s\n", $mode, $path; } } sub printLinked { print "hard linked files\n"; print "inode: files\n"; for my $i (sort {$a <=> $b} keys %linked) { print "$i: ", join(", ", (sort @{$linked{$i}})), "\n"; } } sub getCylinderGroups { my $super, $cylinder; # decode the super block at block 1 # note currently only fs_cginodes and fs_cgnum are used my $super = readBlk(1); my ($fs_name, $fs_pack, $fs_fsize, $fs_cgblocks, $fs_maxblock, $fs_cginodes, $fs_maxino, $fs_time, $fs_fmod, $fs_ronly, $fs_clean, $fs_type, $fs_fnewcg, $fs_snewcg, $fs_ffree, $fs_ifree, $fs_dirs, $fs_extentsize, $fs_cgnum, $fs_cgrotor, $fs_reserved) = unpack("a6 a6 V v V v v V C C C C v v V v v C C C C15", $super); $inodesPerCg = $fs_cginodes; # save the number of inodes per cylinder group $maxInode = $fs_maxino; # now decode in the cylinder group info for (my $i = 0; $i < $fs_cgnum; $i++) { my ($fs_cgincore, $fs_cgblk, $fs_cgffree, $fs_gcifree, $fs_cgdirs) = unpack("v V v v v", substr($super, $SUPER + $i * $CYLINFO, $CYLINFO)); # record where on the disk the cylinder group header is $cylHdr[$i] = $partOff + $fs_cgblk * $BLOCK; } # now read in each header and save where the inodes for each group are on the disk @cylGrp = (); # clear any existing data for (my $i = 0; $i < $fs_cgnum; $i++) { die "Corrupt image cylinder group $i missing\n" if $cylHdr[$i] > $fileSize; seek $in, $cylHdr[$i] + 4, 0; # Cylinder Group inode offset is @ offset 4 in cylinder header read $in, $cylinder, 4; my $cg_ioffset = unpack("V", $cylinder); $cylGrp[$i] = $cg_ioffset * $BLOCK + $partOff; } } # define the options for short hand -p offset # easy to add to these my %opts = ( -h => 0x2400, -f => 0, -b1 => 0x1800, -b2 => 0x2f800 ); $partOff = -1; my $fcnt = 0; usage() if $#ARGV < 0; while (my $opt = shift @ARGV) { if ($opt eq "-r") { usage("root directory multiply defined") if $root ne ""; $root = shift @ARGV; usage("-r directory missing or is file") if $root eq "" || -f $root; } elsif (defined($opts{$opt})) { usage("partition offset multiply defined") if $partOff >= 0; $partOff = $opts{$opt}; } elsif ($opt eq "-p") { usage("partition offset multiply defined") if $partOff >= 0; $partOff = hex(shift @ARGV); } else { usage("unknown option") if substr($opt, 0, 1) eq '-'; usage("no partition offset") if $partOff < 0; usage("$opt not a file") if $partOff < 0 || ! -f $opt; if ($root eq "") { $root = $opt; $root =~ s/\.[^\.]*$//; $root .= sprintf "-%X", $partOff; } open ($in, "<:raw", $opt) or die "$opt $!"; # open the image $fileSize = -s $opt; # get its size to check out of range getCylinderGroups(); # get the cylinder / inode group info print "Processing image $opt\n"; inode(2, ""); # start processing at the root dir node. printLinked(); $root = ""; # reset for next file $partOff = -1; $fcnt++; # flag we have done a file } } usage("no file to process") if $fcnt == 0;