#! /usr/bin/perl # # squashfs dumper # # Copyright (C) 2004 Enrik Berkhan # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # use strict; use warnings; use Fcntl; use Getopt::Std; $Getopt::Std::STANDARD_HELP_VERSION = 1; $main::VERSION = "1.0"; sub main::HELP_MESSAGE { usage(); exit 1; } our %opt; getopts('C:htxuvf:', \%opt); sub usage { print STDERR < dumpsquashfs -x [-u] [-v] -C -f -h: show this help -t: list files containd in -x: extract files contained in -u: unlink files before writing -v: verbose operation -C : chdir() before extracting -f : squashfs image to read from, '-' is stdin EOF } if ($opt{h}) { usage(); if (scalar keys %opt == 1) { exit 0; } else { exit 1; } } unless ($opt{x} xor $opt{t}) { usage(); exit 1; } if ($opt{f} eq '') { usage(); exit(1); } elsif ($opt{f} eq '-') { open FS, "<&STDIN"; } else { open FS, "<$opt{f}" or die "can't read $opt{f}: $!"; } if ($opt{x} && $opt{C}) { chdir($opt{C}) or die "can't chdir($opt{C})"; } my $r = new SquashFS::Reader(\*FS); $opt{_reader} = $r; $r->directory_walk($r->{root_directory}, $r->{root_inode}, '.', \&walk_cb_early, \&walk_cb_late, \%opt); sub pretty_mode { my $type = shift; my $perms = shift; my $bits; if ($type == 1) { $bits = 'd'; } elsif ($type == 2) { $bits = '-'; } elsif ($type == 3) { $bits = 'l'; } elsif ($type == 4) { $bits = 'b'; } elsif ($type == 5) { $bits = 'c'; } elsif ($type == 6) { $bits = 'p'; } elsif ($type == 7) { $bits = 's'; } else { $bits = '?'; } $bits .= $perms & 0x100? "r" : "-"; $bits .= $perms & 0x080? "w" : "-"; $bits .= $perms & 0x040? "x" : "-"; $bits .= $perms & 0x020? "r" : "-"; $bits .= $perms & 0x010? "w" : "-"; $bits .= $perms & 0x008? "x" : "-"; $bits .= $perms & 0x004? "r" : "-"; $bits .= $perms & 0x002? "w" : "-"; $bits .= $perms & 0x001? "x" : "-"; return $bits; } sub walk_cb_early { my $inode = shift; my $pinode = shift; my $name = shift; my $opt = shift; my ($mtime, $size, $major, $minor); my $type = $inode->{type}; if ($type > 2) { $mtime = $pinode->{mtime}; } else { $mtime = $inode->{mtime}; } if ($type == 4 || $type == 5) { $major = $inode->{rdev}>>8; $minor = $inode->{rdev}&0xff; $size = sprintf(" %3d, %3d", $major, $minor); } else { $size = sprintf("%10d", $inode->{file_size}?$inode->{file_size}:0); } if ($opt->{t} && !$opt->{v} || ($opt->{x} && $opt->{v})) { print "$name\n"; } elsif ($opt{t} && $opt->{v}) { printf "%s %5d %5d %s %s %s%s\n", pretty_mode($type, $inode->{mode}), $inode->{uid}, $inode->{guid}, $size, scalar localtime($mtime), $name, $type==3?" -> $inode->{symlink}":""; } if ($opt->{x}) { if ($type == 1) { mkdir("$name"); } elsif ($type == 2) { if ($opt->{u}) { unlink $name; } open FILE, ">$name" or die "can't write $name"; $opt->{_reader}->dump_file($inode, \*FILE); close FILE; } elsif ($type == 3) { symlink($inode->{symlink}, "$name"); } elsif ($type == 4) { system("mknod $name b $major $minor") and warn("could not create block device $name ($major, $minor)"); } elsif ($type == 5) { system("mknod $name c $major $minor") and warn("could not create character device $name ($major, $minor)"); } elsif ($type == 6) { system("mkfifo $name") and warn("could not create name pipe $name"); } elsif ($type == 7) { use Socket; my $sun = sockaddr_un($name); socket SOCK, PF_UNIX, SOCK_STREAM, 0 and bind(SOCK, $sun) or warn("could not create socket $name"); } else { warn("type $type not yet supported in extraction"); } } # add: uid, gid return; } sub walk_cb_late { my $inode = shift; my $pinode = shift; my $name = shift; my $opt = shift; my ($mtime); my $type = $inode->{type}; if ($type > 2) { $mtime = $pinode->{mtime}; } else { $mtime = $inode->{mtime}; } if ($opt->{x}) { if ($type != 3) { chmod $inode->{mode}, "./$name"; utime $mtime, $mtime, "./$name"; } } } package SquashFS::Reader; use Compress::Zlib; use Compress::unLZMA; use Fcntl qw(SEEK_CUR); BEGIN { $SquashFS::Reader::debug = 0; } sub new { my $class = shift; my $input = shift; my $self = {}; $self->{in} = $input; my $buf; next_block: $self->{offset} = tell $input; my $l = read $input, $buf, 63; if ($l < 63) { warn("short read while reading SquashFS superblock"); return undef; } my ($s_magic, $inodes, $bytes_used, $uid_start, $guid_start, $inode_table_start, $directory_table_start, $s_major, $s_minor, $block_size_1, $block_log, $flags, $no_uids, $no_guids, $mkfs_time, $root_inode_offset, $root_inode_blk, $root_inode_hi, $block_size, $fragments, $fragment_table_start) = unpack("VVVVVVVvvvvCCCVvvVVVV", $buf); if ($s_magic == 0x68737173) { ($s_magic, $inodes, $bytes_used, $uid_start, $guid_start, $inode_table_start, $directory_table_start, $s_major, $s_minor, $block_size_1, $block_log, $flags, $no_uids, $no_guids, $mkfs_time, $root_inode_hi, $root_inode_blk, $root_inode_offset, $block_size, $fragments, $fragment_table_start) = unpack("NNNNNNNnnnnCCCNNnnNNN", $buf); $self->{be} = 1; } elsif ($s_magic == 0x73717368) { $self->{be} = 0; } else { goto next_block if read $input, $buf, 256-63; die("wrong superblock magic $s_magic"); } if ($s_major != 2) { die("wrong SquashFS major version $s_major"); } if ($s_minor == 76) { # 'L' -> LZMA $self->{lzma} = 1; } $self->{block_log} = $block_log; $self->{block_size} = $block_size; $self->{check} = 1 if $flags & 4; bless ($self, $class); if ($uid_start) { my $uids; seek $input, $uid_start+$self->{offset}, 0 or die "seek: $!"; my $l = read $input, $uids, $no_uids * 4; die "short read" if $l != $no_uids * 4; if ($self->{be}) { $self->{uids} = [ unpack("N$no_uids", $uids) ]; } else { $self->{uids} = [ unpack("V$no_uids", $uids) ]; } } else { $self->{uids} = [0]; } if ($guid_start) { my $guids; seek $input, $guid_start+$self->{offset}, 0 or die "seek: $!"; my $l = read $input, $guids, $no_guids * 4; die "short read" if $l != $no_guids * 4; if ($self->{be}) { $self->{gids} = [ unpack("N$no_guids", $guids) ]; } else { $self->{gids} = [ unpack("V$no_guids", $guids) ]; } } else { $self->{gids} = [0]; } $self->read_inode_table($inode_table_start, $directory_table_start); $self->read_directory_table($directory_table_start, $fragment_table_start); my $fragment_table_end; { use integer; $fragment_table_end = $fragment_table_start + 4*($fragments*8 + 8192 - 1)/8192 }; $self->read_fragment_index($fragment_table_start, $fragment_table_end); $self->{root_inode} = $self->read_inode($root_inode_blk, $root_inode_offset); $self->{root_directory} = $self->read_directory($self->{root_inode}); return $self; } sub directory_walk { my $self = shift; my $dir = shift; my $pinode = shift; my $pname = shift; my $callback1 = shift; my $callback2 = shift; my $cbparam = shift; foreach my $name (sort keys %$dir) { my $inode = $dir->{$name}; if ($callback1) { &$callback1($inode, $pinode, $pname.'/'.$name, $cbparam); } if ($inode->{type} == 1) { my $subdir = $self->read_directory($inode); $self->directory_walk($subdir, $inode, $pname.'/'.$name, $callback1, $callback2, $cbparam); } if ($callback2) { &$callback2($inode, $pinode, $pname.'/'.$name, $cbparam); } } } sub read_block { my ($self) = shift; my ($offset) = shift; my ($len, $buf); print STDERR "read_block: seek $$offset\n" if $SquashFS::Reader::debug; seek $self->{in}, $$offset+$self->{offset}, 0 or die "seek: $!"; my $l = read $self->{in}, $len, 2; die "short read: $!" if $l != 2; if ($self->{be}) { $len = unpack("n", $len); } else { $len = unpack("v", $len); } my $is_compressed = !($len & 0x8000); $len = $len & 0x7fff; $$offset += 2 + $len; if ($self->{check}) { $l = read $self->{in}, $buf, 1; die "short read: $!" if $l != 1; $$offset++; } $l = read $self->{in}, $buf, $len; die "short read: $!" if $l != $len; if ($is_compressed) { my $res; if ($self->{lzma}) { # pb = 2, lp = 0, lc = 3 my $buf2 = pack("cVVV", 2*9*5 + 0*9 + 3, 16384, $self->{block_size}, 0) . $buf; $res = Compress::unLZMA::uncompress($buf2); } else { $res = uncompress($buf); } die "uncompress failed" unless defined $res; return \$res; } else { return \$buf; } } sub read_inode_table { my ($self) = shift; my ($start) = shift; my ($end) = shift; my $inode_table_start = $start; $self->{inode_table} = ''; while ($start < $end) { $self->{inode_offsets}->{$start - $inode_table_start} = length($self->{inode_table}); $self->{inode_table} .= ${ $self->read_block(\$start) }; print STDERR "read_inode_table ", $start - $inode_table_start, "\n" if $SquashFS::Reader::debug; } } sub read_directory_table { my ($self) = shift; my ($start) = shift; my ($end) = shift; my $directory_table_start = $start; $self->{directory_table} = ''; while ($start < $end) { $self->{directory_offsets}->{$start - $directory_table_start} = length($self->{directory_table}); $self->{directory_table} .= ${ $self->read_block(\$start) }; print STDERR "read_directory_table ", $start - $directory_table_start, "\n" if $SquashFS::Reader::debug; } } sub read_fragment_index { my ($self) = shift; my ($start) = shift; my ($end) = shift; while ($start < $end) { my $offset; print STDERR "read_fragment_index: $start $end\n" if $SquashFS::Reader::debug; seek $self->{in}, $start+$self->{offset}, 0 or die "seek: $!"; my $l = read $self->{in}, $offset, 4; die "short read" if $l != 4; if ($self->{be}) { $offset = unpack("N", $offset); } else { $offset = unpack("V", $offset); } print STDERR "read_fragment_table: $offset\n" if $SquashFS::Reader::debug; $start += 4; if ($offset) { $self->{fragment_table} .= ${ $self->read_block(\$offset) }; } } } sub dump_file { my ($self) = shift; my ($inode) = shift; my ($out) = shift; die "not a plain file" unless $inode->{type} == 2; my ($offset) = $inode->{start_block}; my ($sizes) = $inode->{block_list}; my ($fragment) = $inode->{fragment}; my ($frag_offset) = $inode->{offset}; my ($filesize) = $inode->{file_size}; seek $self->{in}, $offset+$self->{offset}, 0 or die "seek: $!"; foreach my $len (@$sizes) { my ($buf); my $is_compressed = !($len & 0x1000000); my $len = $len & ~0x1000000; printf STDERR "read_block_list: %scompressed len %d\n", $is_compressed?"":"un", $len if $SquashFS::Reader::debug; my $l = read $self->{in}, $buf, $len; die "short read: $!" if $l != $len; if ($is_compressed) { my $res; if ($self->{lzma}) { # pb = 2, lp = 0, lc = 3 my $buf2 = pack("cVVV", 2*9*5 + 0*9 + 3, 16384, $self->{block_size}, 0) . $buf; $res = Compress::unLZMA::uncompress($buf2); } else { $res = uncompress($buf); } die "uncompress failed" unless defined $res; print $out $res; $filesize -= length($res); } else { print $out $buf; $filesize -= length($buf); } } printf STDERR "filesize $filesize fragment $fragment\n" if $SquashFS::Reader::debug; if ($filesize && $fragment > -1) { my ($buf); my ($fstart, $fsize); if ($self->{be}) { ($fstart, $fsize) = unpack("NN", substr($self->{fragment_table}, $fragment*8)); } else { ($fstart, $fsize) = unpack("VV", substr($self->{fragment_table}, $fragment*8)); } my $is_compressed = !($fsize & $self->{block_size}); $fsize = $fsize & ~$self->{block_size}; seek $self->{in}, $fstart+$self->{offset}, 0 or die "seek: $!"; my $l = read $self->{in}, $buf, $fsize; die "short read: $!" if $l != $fsize; if ($is_compressed) { my $res; if ($self->{lzma}) { # pb = 2, lp = 0, lc = 3 my $buf2 = pack("cVVV", 2*9*5 + 0*9 + 3, 16384, $self->{block_size}, 0) . $buf; $res = Compress::unLZMA::uncompress($buf2); } else { $res = uncompress($buf); } die "uncompress failed" unless defined $res; print STDERR "fragment $fragment len ", length($res), " offset $offset\n" if $SquashFS::Reader::debug; print $out substr($res, $frag_offset, $filesize); $filesize = 0; } else { print $out substr($buf, $frag_offset, $filesize); $filesize = 0; } } if ($filesize) { die "short file?"; } } sub read_inode { my $self = shift; my $c_offset = shift; my $offset = shift; print STDERR "reading inode $c_offset\n" if $SquashFS::Reader::debug; if (!defined $self->{inode_offsets}->{$c_offset}) { die "inode $c_offset not in cache?"; } my $inode = substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 4); print STDERR unpack("H*", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 32)), "\n" if $SquashFS::Reader::debug > 1; my ($type, $mode, $uid, $guid); if ($self->{be}) { ($mode, $uid, $guid) = unpack("nCC", $inode); $type = $mode >> 12; $mode = $mode & 0xfff } else { ($mode, $uid, $guid) = unpack("vCC", $inode); $type = $mode & 0xf; $mode = $mode >> 4; } $uid = $self->{uids}->[$uid]; if ($guid == 255) { $guid = $uid; } else { $guid = $self->{gids}->[$guid]; } printf STDERR "type %d mode %o uid %d guid %d\n", $type, $mode, $uid, $guid if $SquashFS::Reader::debug; if ($type == 1) { # DIR return $self->read_dir_inode($c_offset, $offset+4, $type, $mode, $uid, $guid); } elsif ($type == 2) { # FILE return $self->read_reg_inode($c_offset, $offset+4, $type, $mode, $uid, $guid); } elsif ($type == 3) { # SYMLINK return $self->read_symlink_inode($c_offset, $offset+4, $type, $mode, $uid, $guid); } elsif ($type == 4) { # BLK return $self->read_dev_inode($c_offset, $offset+4, $type, $mode, $uid, $guid); } elsif ($type == 5) { # CHR return $self->read_dev_inode($c_offset, $offset+4, $type, $mode, $uid, $guid); } elsif ($type == 6) { # FIFO return $self->read_reg_inode($c_offset, $offset+4, $type, $mode, $uid, $guid); } elsif ($type == 7) { # SOCKET return $self->read_reg_inode($c_offset, $offset+4, $type, $mode, $uid, $guid); } else { die "unknown inode type $type"; } } sub read_dev_inode { my $self = shift; my ($c_offset, $offset, $type, $mode, $uid, $guid) = @_; my $rdev; if ($self->{be}) { $rdev = unpack("n", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 2)); } else { $rdev = unpack("v", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 2)); } return {type => $type, mode => $mode, uid => $uid, guid => $guid, rdev => $rdev}; } sub read_symlink_inode { my $self = shift; my ($c_offset, $offset, $type, $mode, $uid, $guid) = @_; my $symlink; if ($self->{be}) { $symlink = unpack("n/a", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset)); } else { $symlink = unpack("v/a", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset)); } return {type => $type, mode => $mode, uid => $uid, guid => $guid, symlink => $symlink}; } sub read_reg_inode { my $self = shift; my ($c_offset, $offset, $type, $mode, $uid, $guid) = @_; my ($mtime, $start_block, $fragment, $off, $file_size, @block_list, $blocks); if ($self->{be}) { ($mtime, $start_block, $fragment, $off, $file_size) = unpack("NNNNN", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 20)); } else { ($mtime, $start_block, $fragment, $off, $file_size) = unpack("VVVVV", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 20)); } $fragment = unpack("l", pack("L", $fragment)); { use integer; $blocks = ($file_size + $self->{block_size} -1 ) / $self->{block_size}; } if ($fragment > -1) { $blocks-- } if ($type == 2) { print STDERR "reg_inode blocks $blocks\n" if $SquashFS::Reader::debug; if ($self->{be}) { @block_list = unpack("N$blocks", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset + 20)); } else { @block_list = unpack("V$blocks", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset + 20)); } } return {type => $type, mode => $mode, uid => $uid, guid => $guid, mtime => $mtime, start_block => $start_block, fragment => $fragment, offset => $off, file_size => $file_size, block_list => \@block_list }; } sub read_dir_inode { my $self = shift; my ($c_offset, $offset, $type, $mode, $uid, $guid) = @_; my ($file_size, $off, $mtime, $start_block, $t1, $t2, $t3); if ($self->{be}) { ($off, $mtime, $t1, $t2, $t3) = unpack("NNCCC", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 11)); $file_size = $off >> 13; $off = $off & 0x1fff } else { ($off, $mtime, $t1, $t2, $t3) = unpack("VVCCC", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 11)); $file_size = $off & 0x7ffff; $off = $off >> 19; } $start_block = ($t3<<16) | ($t2<<8) | $t1; return {type => $type, mode => $mode, uid => $uid, guid => $guid, file_size => $file_size, offset => $off, mtime => $mtime, start_block => $start_block}; } sub read_directory { my $self = shift; my $inode = shift; my $c_offset = $inode->{start_block}; my $offset = $inode->{offset}; my $size = $inode->{file_size}; print STDERR "reading dir $c_offset\n" if $SquashFS::Reader::debug; if (!defined $self->{directory_offsets}->{$c_offset}) { die "directory $c_offset not in cache?"; } my $dir = substr($self->{directory_table}, $self->{directory_offsets}->{$c_offset} + $offset, $size); print STDERR unpack("H*", substr($self->{directory_table}, $self->{directory_offsets}->{$c_offset} + $offset, 32)), "\n" if $SquashFS::Reader::debug > 1; my %dir; while (length($dir)) { my ($count, $t1, $t2, $t3) = unpack("CCCC", $dir); $dir = substr($dir, 4); $count++; my $start_block = $t1 | ($t2 << 8) | ($t3 << 16); while($count) { my ($type, $offset, $len, $name); if ($self->{be}) { ($offset, $len) = unpack("nC", $dir); $dir = substr($dir, 3); $type = $offset & 0x7; $offset = $offset >> 3; } else { ($offset, $len) = unpack("vC", $dir); $dir = substr($dir, 3); $type = $offset >> 13; $offset = $offset & 0x1fff; } $len++; $name = unpack("a$len", $dir); $dir = substr($dir, $len); $count--; $dir{$name} = $self->read_inode($start_block, $offset); if ($type != $dir{$name}->{type}) { die "read_directory: dir-type != inode-type"; } } } return \%dir; }