#!/usr/bin/perl

# Unpack Instyler installers
#
# Author: PinkFreud@Nightstar
#
my $version = 0.2.2;	# I still can't code my way out of a paper bag...
#
#
# Changelog:
#
# 0.0.1:	Initial release
# 0.0.2:	Runs on big endian systems now
# 0.0.3:	Decodes DOS timestamps, converts to UNIX-style timestamps,
#       	and uses utime() to set timestamps on the extracted files
# 0.0.3.1:	Added $winfolder
# 0.1.0:	Merged changes from satmd (automatic search for fr_offset,
#               fd_offset) into 0.0.3.1.
# 0.2.0:	- Now accepts commandline options: --winfolder, --appfolder,
#		  --yes (as well as their shortened forms: -w, -a, -y).
#		  Explanation of options:
#			winfolder: specifies the path to %winfolder%
#			appfolder: specifies the path to %appfolder%
#			yes: Assume 'yes' to all confirmation questions
#		- Also adds path creation, if paths for extracted files don't
#		  exist yet, as well as file overwrite confirmation.
#		- Error checking has been added to this release - this util
#		  will now let the user know how many errors occured at the
#		  end of it's run.
#		- Included the offset of each file being inflated at the
#		  beginning of the file's info line.
#		- find_fr_offsets() filename matching code is now a bit
#		  more robust.
#		- Counts files extracted.
# 0.2.1:	Fixed how $numerrors was assigned to in main() - thanks
#		go to satmd for the fix!
# 0.2.2:	Fixed a bug that could cause process_fr_offsets() to try
#		to unpack the same files twice under some circumstances.
#
# Bugs: Many.  Only written to work with an installer for a spambot.  As I
#       come across more packages, I'll try to update this unpacker.
#
# Disclaimer: Use at your own risk.  I accept no responsibility for anything
#             this script might do.
#
# Authors:	PinkFreud: Original idea, reverse engineering of
#                          Instyler-packaged bots, initial author.
#         	zchrist:   Assisted with the bit math in dosts2unixts().
#		satmd:     Wrote the offset autosearch code.

use Compress::Zlib;
use Time::Local;
use Getopt::Long;

my $appfolder = ".";
my $winfolder = "./windows";
my $result;

$result = GetOptions ("winfolder=s"	=> \$winfolder,
		      "appfolder=s"	=> \$appfolder,
		      "yes"		=> \$yes);
usage() unless $result;

my $exe = shift or usage();
my (@files, $file);
my $numerrors = 0;

select STDERR; $| = 1;
select STDOUT; $| = 1;


sub usage {
  my $verstring = sprintf "uninstyler.pl version %vd by PinkFreud\n", $version;
  my ($me) = $0 =~ m|.*/(.*)|;
  print STDERR $verstring;
  print STDERR <<_EOF_;
Usage:
$me [--winfolder <dir>] [--appfolder <dir>] [--yes]
_EOF_

  exit 1;
}

sub inflate {
  # Most of this was gratuitously stolen from the man page for Compress::Zlib
  my $x = inflateInit()
     or die "Cannot create an inflation stream\n";
  my $data = $_[0];
  
  my ($output, $status);
  ($output, $status) = $x->inflate(\$data) ;
  return $output;
}

sub dosts2unixts {
  # Convert a DOS timestamp to a UNIX timestamp
  # Thanks to zchrist for help with the bit math
  my $timestamp = $_[0];
  my $secmask  = unpack ("N", (pack (B32, "00000000000000000000000000011111")));
  my $minmask  = unpack ("N", (pack (B32, "00000000000000000000011111100000")));
  my $hourmask = unpack ("N", (pack (B32, "00000000000000001111100000000000")));
  my $daymask  = unpack ("N", (pack (B32, "00000000000111110000000000000000")));
  my $monmask  = unpack ("N", (pack (B32, "00000001111000000000000000000000")));
  my $yearmask = unpack ("N", (pack (B32, "11111110000000000000000000000000")));
  my $secoffset = 0;
  my $minoffset = 5;
  my $houroffset = 11;
  my $dayoffset = 16;
  my $monoffset = 21;
  my $yearoffset = 25;

  my $second = (($timestamp & $secmask ) >> $secoffset) * 2;
  my $minute =  ($timestamp & $minmask ) >> $minoffset;
  my $hour   =  ($timestamp & $hourmask) >> $houroffset;
  my $day    =  ($timestamp & $daymask ) >> $dayoffset;
  my $month  = (($timestamp & $monmask ) >> $monoffset) - 1;
  my $year   = (($timestamp & $yearmask) >> $yearoffset) + 1980;

  # I'm assuming the DOS timestamp is in GMT.  Is this right?
  my $unixts = timegm ($second, $minute, $hour, $day, $month, $year);

  return ($unixts);
}

sub initfilelist {
  my (@files);
  my $fr_offset = $_[0];
  seek (EXE, $fr_offset, 0);
  while (read (EXE, $data, 287)) {
    my ($len, undef, undef, undef, $fn, $timestamp, $unknown, undef,
      $filesize, $packedsize, undef) = unpack ("vvvCZ255VVZ8VVC", $data);

    last unless $len == 257;
    
    my $unixts = dosts2unixts ($timestamp);
    ($fn) =~ s|\\|/|g;
    ($fn) =~ s|%appfolder%|$appfolder|g;
    ($fn) =~ s|%windows%|$winfolder|g;
    push (@files, {
      filename => $fn, size => $filesize, psize => $packedsize, ts => $unixts
    });
  }
  return @files;
}

sub find_fr_offsets {
  my(@fr_offsets);
  my $fr_offset = 0;
  my $oldoffset = 0;
  my $offset = 0;
  while (!eof(EXE) && ($fr_offset == 0)) {
    $offset++;
    seek (EXE, $offset, 0);
    read (EXE, $data, 287);
    my ($len, undef, undef, undef, $fn, $timestamp, $unknown, undef, $filesize, $packedsize, undef) = unpack ("vvvCZ255VVZ8VVC", $data);
#    ($fn) =~ s|\\|/|g;
#    ($fn) =~ s|%appfolder%|$appfolder|g;
#    ($fn) =~ s|%windows%|$winfolder|g;
    #if (($len == 257) && ( $fn =~ /^\.\/[\032-\255]+$/ )) { 
    if (($len == 257) && ( $fn =~ /^(%(appfolder|windows)%|\\)/ )) { 
      if ( $offset - $oldoffset != 287 ) {
        $fr_offset = $offset;
        push(@fr_offsets,$offset);
      }
      $oldoffset = $offset;
    }
  }
  return @fr_offsets;
}

sub process_fr_offset {
  my $fr_offset = $_[0];
  my $yes = $_[1];
  my(@files) = initfilelist($fr_offset);
  my $offset = $fr_offset;
  my $answer;
  my $extractedcount = 0;
  my $lastoffset = 0;
  my $numerrors;

  while (!eof(EXE)) {
    $offset = $offset + 1 + $lastoffset;
    seek (EXE, $offset, 0);
    read (EXE, $header, 4);
    seek (EXE, $offset, 0);
    if ($header eq "zlb" . chr(0x1a)) {
      my $fd_offset = $offset-4;
      printf " fd_offset = 0x%08x\n", $fd_offset;
      for $file (@files) {
        my ($f, $s, $ps, $ts) = @{$file}{filename, size, psize, ts};
        my $current_offset = tell (EXE);
        my $header;
        read (EXE, $header, 4);
        last unless $header eq "zlb" . chr(0x1a);
        my $br = read (EXE, $data, $ps);
        print "   Requested $ps bytes, got $br instead..." unless $br == $ps;
        if (-e $f && $answer ne 'a' && ! $yes) {
	  print "$f already exists - extract anyway?  [Y]es [N]o [A]lways: ";
	  chomp ($answer = lc <>);
	  if ($answer !~ /^[ayn]$/) {
	    $answer = 'n';
	  }
	  next if $answer eq 'n';
	}
        $data = inflate $data;
        printf "   [%08x] Inflating $f ($ps -> $s bytes) ... ",
          $current_offset;
        if ($f =~ m|/|) {
	  my ($path, $filename) = $f =~ m|(.*)/(.*)|;
	  my $p;
	  my $tree;
	  foreach $p (split /\//, $path) {
	    next if $p eq '';
	    $tree .= "$p/";
	    if (! (-d $tree)) {
	      if (! mkdir $tree) {
		warn "Failed to create $path: $!\n";
		$numerrors++;
	      }
	    }
	  }
	}
        if (open (OUT, ">$f")) {
          binmode (OUT);
          print OUT $data;
          close (OUT);
          utime $ts, $ts, $f;
          print "done\n";
          $extractedcount++;
	  $lastoffset = tell (EXE);
        } else {
	  warn "Failed to write to $f: $!\n";
	  $numerrors++;
	}
      }
    }
  }
  print "Extracted $extractedcount file(s).\n";
  return $numerrors;
}

sub main {
  my $yes = $_[0];
  my $numerrors = 0;
  print "Locating fr_offset ...\n";
  my (@fr_offsets) = find_fr_offsets();
  print "Locating fd_offset ...\n";
  for $fr_offset (@fr_offsets) {
    printf " fr_offset = 0x%08x\n", $fr_offset;
    $numerrors += process_fr_offset $fr_offset, $yes;
  }
  return $numerrors;
}


printf "uninstyler.pl version %vd by PinkFreud\n", $version;
open(EXE, $exe);
binmode(EXE);
$numerrors = main ($yes);
close(EXE);

if ($numerrors > 0) {
  my $error = $numerrors == 1 ? "error" : "errors";
  warn "Encountered $numerrors $error!\n";
  exit 1;
}
