# adsscan 1.0
# Alternate Data Streams scanner
#########################################################################################
# adsscan.pl 1.0									#
# by Floydman  floydian_99@yahoo.com							#
# Copyright 2003 SecurIT Informatique Inc.  http://securit.iquebec.com			#
# Based on previous work from Harlan Carvey and Dave Roth.				#
# This program will scan all local, fixed NTFS partitions for the presence of alternate #
# data stream.  If found, adsscan will report it at the same dest as LogAgent.		#
#########################################################################################
use Win32::API::Prototype;
use Win32::AdminMisc;
use File::Recurse;
use Socket;
use Sys::Hostname;

# Creation of machine ID table
@id = getid();

# Creation of config table
@config = getconfig();

my $OPEN_EXISTING = 3;
my $GENERIC_READ  = 0x80000000;                                     
my $BACKUP_DATA   = 0x00000001;
my $BACKUP_ALTERNATE_DATA = 0x00000004;
my $FILE_SHARE_READ = 0x00000001;
my $FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
 
ApiLink( 'kernel32.dll', 
         'HANDLE CreateFile( LPCTSTR pszPath, 
                             DWORD dwAccess,
                             DWORD dwShareMode, 
                             PVOID SecurityAttributes,
                             DWORD dwCreationDist, 
                             DWORD dwFlags,
                             HANDLE hTemplate )' ) 
    || die "Can not locate CreateFile()";

ApiLink( 'kernel32.dll', 
         'BOOL CloseHandle( HANDLE hFile )' ) 
    || die "Can not locate CloseHandle()";

ApiLink( 'kernel32.dll', 
         'BOOL BackupRead( HANDLE hFile, 
                           LPBYTE pBuffer, 
                           DWORD dwBytesToRead, 
                           LPDWORD pdwBytesRead, 
                           BOOL bAbort, 
                           BOOL bProcessSecurity, 
                           LPVOID *ppContext)' ) 
    || die "Can not locate BackupRead()";

ApiLink( 'kernel32.dll', 
         'BOOL BackupSeek( HANDLE hFile, 
                           DWORD dwLowBytesToSeek,
                           DWORD dwHighBytesToSeek,
                           LPDWORD pdwLowByteSeeked,
                           LPDWORD pdwHighByteSeeked,
                           LPVOID *pContext )' ) 
    || die "Can not create BackupSeek()";


print "ADS scanner 1.0, brought to you by Floydman\nBased on previous work from Harlan Carvey and Dave Roth\n";
print "Copyright 2003 SecurIT Informatique Inc.\n";
print "http://securit.iquebec.com\n";
print "\nPress CTRL-C to quit\n";

my @drives = Win32::AdminMisc::GetDrives(DRIVE_FIXED);
my $drive = "";
my $timer = 0;

foreach $drive (sort @drives)
{print "Scanning drive $drive\n";
 Win32::SetCwd ("$drive");
 $type = Win32::FsType ();

 if ($type eq 'NTFS')
  {
	my %files = Recurse(['/'],{match => '\.'});
	foreach (sort keys %files) 
	{$dir = $_; $dir = $dir."/" unless ($dir =~ m/\/$/);
	 foreach (@{ $files{$_} }) 
		{
		 if ($timer > 100) {sleep 1; $timer = 0;} else {$timer++;} # remove this line to obtain full performance
		 $File = $dir.$_;  
		    $hFile = CreateFile( $File, 
		                       $GENERIC_READ, 
		                       $FILE_SHARE_READ, 
		                       undef, 
		                       $OPEN_EXISTING, 
		                       $FILE_FLAG_BACKUP_SEMANTICS, 
		                       0 ) || die "Can not open the file '$File'\n";

		  # If CreateFile() failed $hFile is a negative value
		  if($hFile > 0) {
		    my $iStreamCount = 0;
		    my $pBytesRead = pack( "L", 0 );
		    my $pContext = pack( "L", 0 );
		    my $pStreamIDStruct = pack( "L5", 0,0,0,0,0,0 );
    
		    while( BackupRead( $hFile, 
		                       $pStreamIDStruct, 
		                       length($pStreamIDStruct), 
		                       $pBytesRead, 
		                       0, 
		                       0, 
		                       $pContext)) 
		     {
		      my $BytesRead = unpack("L",$pBytesRead);
		      my $Context = unpack("L",$pContext);
		      my %Stream;
		      my($pSeekBytesLow,$pSeekBytesHigh) = (pack("L",0),pack("L",0));
		      my $StreamName = "";
		      my $StreamSize;
      
		      # No more data to read
		      last if($BytesRead == 0);

		      @Stream{ id, attributes, 
		               size_low, size_high, 
		               name_size } = unpack("L5",$pStreamIDStruct);

			# This indicates that an ADS has been detected.      
		      if($BACKUP_ALTERNATE_DATA == $Stream{id}) 
			{ 
		      	$StreamName = NewString($Stream{name_size});
		        if( BackupRead( $hFile, 
		                        $StreamName, 
		                        $Stream{name_size}, 
		                        $pBytesRead, 
		                        0, 
		                        0, 
		                        $pContext)) {
		          my $String = CleanString($StreamName,1);
		          $String =~ s/^:(.*?):.*$/$1/;
		          $StreamName = ":".$String;
		        }
		        my $FileName;
        
		        if (-f $File) {
		        	$FileName = $File;
		        }
			elsif (-d $File) {
		        	if ($File =~ m/\.$/) {
		        		$FileName = substr($File,0,length($File)-1);
		        	}
		        	elsif (!($File =~ m/\\$/)) {
		        		$FileName = $File."\\";
		        	}
		        }
		                
		        $StreamSize = MakeLargeInt($Stream{size_low},$Stream{size_high});
			$drive=~s/\\//;$Filename=~s/\//\\/;
			sendoutput ('adsscan.log', "$drive$FileName$StreamName,($StreamSize bytes)", @config) if($StreamName ne "");
		      }
      
			# Move to next stream...
		      last if(!BackupSeek($hFile, 
		                        $Stream{size_low}, 
		                        $Stream{size_high}, 
		                        $pSeekBytesLow, 
		                        $pSeekBytesHigh, 
		                        $pContext));
		      $pBytesRead = pack("L2",0);
		      $pStreamIDStruct = pack("L5",0,0,0,0,0);
		    } 
		    # Abort the backup reading. Win32 API claims we MUST do this.
		    BackupRead( $hFile,undef,0,0,1,0,$pContext );
		    CloseHandle( $hFile );
		  }

		} 
	}
  }
}

sub FormatNumber {
  my( $Num ) = @_;
  {} while( $Num =~ s/^(-?\d+)(\d{3})/$1,$2/ );
  return( $Num );
}

sub MakeLargeInt {
  my( $Low, $High ) = @_;
  return( $High * ( 1 + 0xFFFFFFFF ) + $Low );
}


#########################################################################################
# procedure getconfig() 								#
# This procedure gets the configuration file config.txt.				#
#########################################################################################
sub getconfig
{
my @configtable;
my @dirtable;
$j = 0;
$numarg = 0;
open(CONFIGFILE,"<config.txt") || die "Can't open config.txt";
$logip = <CONFIGFILE> || die "Can't read  logip from config.txt";
($logip=~m/LOGIP/i) || die "LOGIP entry missing in config.txt";

$loghost = <CONFIGFILE> || die "Can't read loghost from config.txt";
($loghost=~m/LOGHOST/i) || die "LOGHOST entry missing in config.txt";

$loguser = <CONFIGFILE> || die "Can't read loguser from config.txt";
($loguser=~m/LOGUSER/i) || die "LOGUSER entry missing in config.txt";

$logdate = <CONFIGFILE> || die "Can't read logdate from config.txt";
($logdate=~m/LOGDATE/i) || die "LOGDATE entry missing in config.txt";

$logtime = <CONFIGFILE> || die "Can't read logtime from config.txt";
($logtime=~m/LOGTIME/i) || die "LOGTIME entry missing in config.txt";

$showconsole = <CONFIGFILE> || die "Can't showconsole read from config.txt";
($showconsole=~m/SHOWCONSOLE/i) || die "SHOWCONSOLE entry missing in config.txt";

$lp = <CONFIGFILE> || die "Can't read lineprint from config.txt";
($lp=~m/LINEPRINT/i) || die "LINEPRINT entry missing in config.txt";

$lpbuffer = <CONFIGFILE> || die "Can't read lineprint buffer from config.txt";
($lpbuffer=~m/LINEPRINT BUFFER/i) || die "LINEPRINT BUFFER entry missing in config.txt";

while (defined($dir = <CONFIGFILE>))
	{
	 $dirtable[$j]=$dir;
	 $j++;
	}
($j==0) && die "No destination directory specifed in config.txt.";
close (CONFIGFILE) || die "Can't close config.txt";

@configtable = ($logip, $loghost, $loguser, $logdate, $logtime, $showconsole, $lp, $lpbuffer, @dirtable);
@configtable = parse(@configtable);

(($numarg=@configtable)<9) && die "Not enough parameters in config.txt.  Check file for errors.";

# Tranformation of the first 7 lines of configtable to boolean value
$configtable[0]=$configtable[0]=~m/Y/i;
$configtable[1]=$configtable[1]=~m/Y/i;
$configtable[2]=$configtable[2]=~m/Y/i;
$configtable[3]=$configtable[3]=~m/Y/i;
$configtable[4]=$configtable[4]=~m/Y/i;
$configtable[5]=$configtable[5]=~m/Y/i;
$configtable[6]=$configtable[6]=~m/Y/i;

# Get the lineprint buffer
@element=split(/=/, $configtable[7]);
$configtable[7]=$element[1]; ($configtable[7]>=0) || die "LINEPRINT BUFFER entry must be 0 or a positive number in config.txt";
return (@configtable);
}


#########################################################################################
# procedure getid() 									#
# This procedure gets the IP address, the host name and the username of the machine.	#
#########################################################################################

sub getid
{
# Define username, IP address and hostname of the local machine
my $addr = inet_ntoa(scalar(gethostbyname($name)) || 'localhost');
my $host = hostname() || "hostname not defined";
my $login = getlogin || getpwuid($<) || "not logged";
my @id_table = ($addr, $host, $login);
return (@id_table);

}


#########################################################################################
# procedure sendoutput(filename, line, config)						#
# This procedure receives as arguments: the name of the modified file, the last line	#
# of the logfile, and then the config table (LOGIP, LOGHOST, LOGUSER, SHOWCONSOLE, and 	#
# the various destination directories).  The procedure checks the configuration to see	#
# if it has to append any information to the original line or not.  If SHOWCONSOLE in 	#
# enabled, then the line is printed on the screen, if not it simply passes to the next	#
# step which is to forward this line to all mentionned destinations in config.txt.	#
#########################################################################################

sub sendoutput
{ my ($filename, $lines, $logip, $loghost, $loguser, $logdate, $logtime, $showconsole, $lp, $lpbuffer, @dest) = @_;

my $line = '';
my @newlines = split (/\n/,$lines);
foreach $line (@newlines)
 {
 my $newline=""; 

 if ($logip) {$newline=$newline.$id[0].",";}
 if ($loghost) {$newline=$newline.$id[1].",";}
 if ($loguser) {$newline=$newline.$id[2].",";}
 if ($logdate || $logtime) {($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);} 
 if ($logdate) {$newline=$newline.($year+=1900)."/".$mon."/".$mday.",";}
 if ($logtime) {$newline=$newline.$hour.":".$min.":".$sec.",";}

 $newline=$newline.$line."\n";

 if ($showconsole) {print $newline;}

 if (lc($dest[0])ne"null") 
   {
   foreach $destdir (@dest)
      {
       $destination=$destdir.$filename;
       open (DEST, ">>".$destination) || die "Can't open master log file $destination";
       flock (DEST, 2) || die "Can't lock file for writing";
       print DEST $newline || die "Can't write to file";
       close (DEST) || die "Can't close master log file";
      }
   }


 if ($lp) { push @buffer, $newline; $buffer=@buffer;
	    if ($buffer>=$lpbuffer+1) { 
				       open(LINEPRINT,">LPT1") || die "Can't open pipe to LPT1";
				       foreach $line (@buffer){print LINEPRINT $line;}
				       close (LINEPRINT) || die "Can't close pipe to LPT1";
				       @buffer=();
				       } 
           }
 }
}

#########################################################################################
# procedure parse(table_file) 								#
# This procedure cleans the files from non-valid and blank characters that could be	#
# placed in the config files.  The procedure returns the file as a table.		#
#########################################################################################

sub parse
{ my (@table) = @_;

#check for invalid characters in table_file
chomp @table;

foreach $element (@table)
 {
  $element=~s%^\s+%%;
  @char = split (//, $element);

  foreach $char (@char)
    { $char=~s%\\%/%; }
  $element = join ('',@char);
 }

my @tabletemp;
my $x = 0;

foreach $element (@table)
   {
    if ($element ne '') {
	$tabletemp[$x]=$element;
	$x++;}
   }
@table = @tabletemp;
return (@table);
}
