#!/usr/bin/env perl  
  
#####################################################  
#                                                   #  
#      Written by Jean L                            #  
#        Kudos:                                     #  
#        Sean Jenkins for Cscanner/Optimization     #  
#            Learned a lot from it                  #  
#        John Jones III for optimization suggestion #  
#                                                   #  
#        ToS: for future managing of the script.    #  
# $Id: trackback,v 1.1 2014/01/31 18:40:14 cade Exp $#  
#####################################################  

#http://perldoc.perl.org/PerlIO.html  
use strict;  
use warnings;  
use POSIX;  
use Fcntl qw( :flock :mode O_CREAT);  
use fadvise;  
use IO::Dirent qw(readdirent DT_DIR DT_REG DT_LNK);  
use Digest::MD5 qw(md5 md5_hex md5_base64);  
use File::HomeDir;  
use File::Path qw(make_path remove_tree);  
use Data::Dumper;  
use Getopt::Long;  
use Time::HiRes qw();  
use TOSconfig;
 
$| = 1;  
  
my (@files, @files_type_txt, @modify_find_results, @directories, $stats) = ();  
my $skipped_file_count = 0;  
  
#compiling regex at the start of a scrpt is faster then calling it within a subroutine more then once. Also looks cleaner.  
my $HOME = File::HomeDir->my_home;  
my $time = 0;  
our $config = TOSconfig::config_trackback();  
  
my $unresolved = 0;  
my ($verbose, $debug, $write_data_to_malware, $empty, $display_line, $depth) = (0,0,0,0,0,-1);  
my $self;  
my $path = $HOME."/public_html";  
GetOptions ("verbose" => \$verbose,  
            "bug"   =>\$debug,  
            "malware" =>\$write_data_to_malware,  
            "empty"   =>\$empty,  
            "help"    => \&help,  
            "line"    => \$display_line,  
            "depth=s" => \$depth,
            "path=s"  => \$path,  
            "<>"      => \&help,  
) || help();  
sub help {  
    print "\033[2J";  
    print "\033[0;0H"; #jump to 0,0  
    print "  
trackback(1)                           User Commands                           
  
NAME  
       General checker for hacked data - This scanning tool is designed to perform a full scan of all local files/folders on a server  
  
SYNOPSIS  
  
DESCRIPTION  
       This looks for the more common back door hacks. Most of these hackers are so vain that they put their 'signature' in their hack scripts  
              so this file looks for the more prominent hacker names.  
              Examples: Hackc0re, The Cyber Nuxbie, Scra3zy, PSYCODEZ, Indonesian Underground, and tons of others.  
  
       -h --help  
              This help page  
  
       -m, --malware    
              Writes data to a ~/malware.txt file via standard output appending the data '>>'    

       -e, --empty  
              Empties the ~/malware.txt file, if you don't want to append to ~/malware.txt call both -e -m  
  
       -l, --line  
              Prints line number  
  
       -p, --path  
              Choose a specific path  
  
       -d, --depth
              Sets a maximum directories it will scan recrusively, default is 15.





  
";  
  exit 0;  
}  
  
sub find_readdir {  
    my $dir = shift;  
    my $pattern = shift;  
    my $level = shift || 1;  
    my $exclude_pattern = shift || qr/^Unused$/;  
  
    my $max_depth;
    if ($depth > -1) {
        $max_depth = $depth;
    } else {
        $max_depth = $config->{max_depth}
    }
    if( $level > $max_depth ) { return }
  
    opendir my $dh, $dir  
    or do {  
        system("chmod 0755 $dir");
    };  
    opendir $dh, $dir  
    or do {  
        warn "Could not open $dir: $!\n";  
        return;  
    };  
  
    for my $entry ( readdirent($dh) ) {  
        next if $entry->{name} =~ /^\.\.?$/;  
        if( $entry->{type} == DT_DIR ) {  
             if ( $entry->{name} !~ $config->{skip_directories}) {  
                 find_readdir("$dir/" . $entry->{name}, $pattern, $level + 1, $exclude_pattern);  
                 push(@directories, "$dir/".$entry->{name});  
             } else {  
                 $skipped_file_count++;  
                 next;  
             }  
        } elsif ($entry->{type} == DT_REG) {  
            if ($entry->{name} =~ $config->{skip_files}) {  
                $skipped_file_count++;  
                next;  
            } elsif ($entry->{name} =~ $config->{skip_cache}) {  
                $skipped_file_count++;  
                next;  
            }  
  
        }  
        next if $entry->{name} =~ $exclude_pattern;  
        next unless $entry->{name} =~ $pattern;  
        next if $entry->{type} == DT_DIR;  
        next if $entry->{type} == DT_LNK;  
        push(@files, "$dir/" . $entry->{name});  
    }  
    closedir $dh;  
}  
  
  
  
my $pkg = bless {}, __PACKAGE__;  
  
#Max Run time in seconds  
$pkg->{'secs'} = 60*60; #60 minutes #temp  
$pkg->main();  
  
sub main {  
    $self = shift;  
    local $SIG{ALRM} = sub {   
        print "Error: Timeout!\n";  
        exit(1);   
    };  
    alarm $self->{'secs'};  
    #Start Multithreading, Maximum execution time will remain.  
    $self->start();  
    alarm 0;  
    return 0;  
}  
  
sub start {  
    my $start;  
    my $finish;  
    my $minute = 0;  
  
    if ($empty eq '1') {  
        open(MALWARE, ">", $HOME."/malware.txt");  
        fadvise(*MALWARE);  
        close(MALWARE);  
    }  
    if ($write_data_to_malware eq '1') {  
        open(MALWARE, ">>", $HOME."/malware.txt");  
    }  
  
    $start = Time::HiRes::time();  
    # Minor stuff  
    run_find();  
    refine_find();  
    fix_0777_0755_0644();  
    file_content_scan();
}  
sub run_find {
    my @files;
    find_readdir($path, qr//, 1, $config->{skip_files});
}
sub fix_0777_0755_0644 {
    if ( -e $HOME."/public_html") {
        foreach my $file (@files) {
            @$stats = stat($file);
            my $mode = sprintf "%04o", S_IMODE($stats->[2]);
            if (-d $file ) {
                chmod(0755,$file);
            } elsif (-f $file) {
                if ($file =~ $config->{extensions755}) {
                    if ($mode ne '0755') {
                        chmod(0755, $file);
                    }
                } elsif ($file =~ $config->{extensions644}) {
                    if ($mode ne '0644') {
                        chmod(0644, $file);
                    }
                }
            }
            if ($mode eq '0777') {
                if (-f $file) {
                    chmod(0644, $file);
                } elsif (-d $file) {
                    chmod(0755, $file);
                }
            }

        }
    } else {
        print "~/public_html doesn't exist";
    }
}

sub file_content_scan {  
    print "\nThese results are likely valid files that have had code added to them so they should be cleaned rather than removed:\n\n";
    if ($write_data_to_malware eq '1') {
        print MALWARE "\nThese results are likely valid files that have had code added to them so they should be cleaned rather than removed:\n\n";
    }

    foreach my $file (@files_type_txt) {  
        if (open(FH,"<:perlio","$file")){  
            flock(FH,LOCK_EX);  
            binmode(FH);  
            my $line_ = 0;  
            while (my $line = <FH>) {  
            $line_++;  
                chomp($line);  
                if ($line =~ $config->{file_content_regex}) {  
                    if ($display_line eq '1') {  
                        print "$file : line $line_\n";  
                    } else {  
                        print "$file\n";  
                    }  
                    if ($write_data_to_malware eq '1') {  
                        if ($display_line eq '1') {  
                            print MALWARE "$file : line $line_\n";  
                        } else {  
                            print MALWARE "$file\n";  
                        }  
                    }  
                    $unresolved++;  
                    last;  
                }  
            }  
            flock(FH,LOCK_UN);  
            fadvise(*FH);  
            close(FH);  
        } else {  
            print "Error:$file $!\n";  
        }  
    }  
}  
#Seems to take the longest to run.  
sub refine_find {  
    foreach my $file (@files) {  
        if (-T $file) {  
            @$stats = stat("$file");  
            #skip files larger then 1 MB  
            if ($stats->[7] > 1048576) {  
                $skipped_file_count++;  
                next;  
            }  
            next if ($file =~ $config->{exclude_content_regex});
            push(@files_type_txt, $file);  
        }  
    }  
    if  ($debug eq '1') {  
        print "skipped $skipped_file_count\n";  
    }  
  
    # sort by inode to make scan results faster.  
    sort { $$a{'inode'} <=> $$b{'inode'}} @files_type_txt;  
}  
  
sub Finished {  
    print "\nTotal number of files scanned: ".(scalar @files+scalar $skipped_file_count)."\n";  
    print "Number of files in need of review: ".$unresolved."\n";  
}  

