#  DetectRandomHTTP.pm - a FlowScan report class to detect "Code Red" hosts
#  Copyright (C) 2001  Dave Plonka
#
#  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., 675 Mass Ave, Cambridge, MA 02139, USA.

# $Id: DetectRandomHTTP.pm,v 1.11 2001/08/07 09:21:59 dplonka Exp $
# Dave Plonka <plonka@doit.wisc.edu>

use strict;

package DetectRandomHTTP;

require 5;
require Exporter;

@DetectRandomHTTP::ISA=qw(FlowScan Exporter);
# convert the RCS revision to a reasonable Exporter VERSION:
'$Revision: 1.11 $' =~ m/(\d+)\.(\d+)/ && (( $DetectRandomHTTP::VERSION ) = sprintf("%d.%03d", $1, $2));

use ConfigReader::DirectiveStyle;
use POSIX;
use Socket; # for inet_ntoa
use Net::Patricia;
use FindBin;
use Cflow qw(:flowvars 1.024); # for use in wanted sub
use FlowScan qw(1.005);
use IO::File;
use File::Basename;

# { BEGIN CONFIGURATION SECTION ################################################
   
# whether or not to consider HTTP flows to dot-zero address as suspicious:
$DetectRandomHTTP::do_zeroes = 0; # 0=false, 1=true

# Do not report the host unless it has talked to at least n ".0" addresses:
$DetectRandomHTTP::MIN_VICTIMS = 5;
# purge suspects that have not talked to a ".0" in this many minutes:
$DetectRandomHTTP::MINUTES = 60;
# email report (at most once per raw flow file) to this address:
$DetectRandomHTTP::MAILTO = ''; # e.g. 'you@your.domain', '' to supress email

# The following is based on a proposed method of detecting Code Red which
# was suggested by Stefan Savage <savage@cs.ucsd.edu>.  Roughly, the
# suggestion was to privately route traffic to unallocated blocks of
# the IPv4 address space to a particular destination in the local network.
# Then use a router ACL or stand-alone packet filter to report which hosts
# are probing those addresses (presumably systematically).
#
# Consider HTTP flows to the follow "unused" networks to be suspicious:
@DetectRandomHTTP::UNALLOCATED=qw(

   58.0.0.0/8
   59.0.0.0/8 
   60.0.0.0/8
   96.0.0.0/6 

);

# } END CONFIGURATION SECTION ##################################################

# { "global" data objects:

$DetectRandomHTTP::unallocated = new Net::Patricia;
die unless ref($DetectRandomHTTP::unallocated);

$DetectRandomHTTP::net = new Net::Patricia;
die unless ref($DetectRandomHTTP::net);

$DetectRandomHTTP::suspect = new Net::Patricia;
die unless ref($DetectRandomHTTP::suspect);

# }{ initialize the "unallocated" Patricia Tree:

foreach (@DetectRandomHTTP::UNALLOCATED) {
   if (!$DetectRandomHTTP::unallocated->add_string($_, 1)) {
      warn "$_ add failed!\n";
      next
   }
}

# }{ initialize the "net" Patricia Tree:

my $c = new ConfigReader::DirectiveStyle;
$c->directive('NextHops');
$c->directive('OutputIfIndexes');
$c->required('OutputDir');
$c->directive('TCPServices');
$c->directive('UDPServices');
$c->directive('Protocols');
$c->directive('ASPairs');
$c->directive('LocalNextHops');
$c->directive('LocalSubnetFiles');
$c->directive('Rateup');
$c->directive('Verbose');
$c->directive('TopN');
$c->directive('ReportPrefixFormat');
$c->directive('NapsterSubnetFiles');
$c->directive('NapsterSeconds');
$c->directive('NapsterPorts');
$c->directive('BGPDumpFile');
$c->directive('ASNFile');
$c->directive('WebProxyIfIndex');
$c->directive('SamplingRatio');
$c->load("${FindBin::Bin}/CampusIO.cf"); # cheat - use CampusIO.cf

@DetectRandomHTTP::subnet_files = split(m/\s*,\s*/,
                                        $c->value('LocalSubnetFiles'));

@DetectRandomHTTP::subnets_files = <@DetectRandomHTTP::subnet_files>;
$DetectRandomHTTP::net = new Net::Patricia;
die unless ref($DetectRandomHTTP::net);
my($subnets_file, $stream, $cargo);
foreach $subnets_file (@DetectRandomHTTP::subnets_files) {
   print(STDERR "Loading \"$subnets_file\" ...\n") if -t;
   my $fh = new IO::File "<$subnets_file";
   $fh || die "open \"$subnets_file\", \"r\": $!\n";
   $stream = new Boulder::Stream $fh;
   while ($cargo = $stream->read_record) {
      my $subnet = $cargo->get('SUBNET');
      my $hr = { SUBNET => $subnet };
      my $collision;
      if ($collision = $DetectRandomHTTP::net->match_string($subnet)) {
         warn "$subnet skipped.  It collided with $collision->{SUBNET}\n";
	 next
      }
      if ($DetectRandomHTTP::net->add_string($subnet, $hr)) {
         push(@DetectRandomHTTP::subnets, $hr);
      } else {
         warn "$subnet add failed!\n";
	 next
      }
   }
   undef $fh
}

# }

sub new {
   my $self = {};
   my $class = shift;
   return bless _init($self), $class
}

sub _init {
   my $self = shift;
   return $self
}

sub wanted {
   my $node;
   if (80 == $dstport &&
       $DetectRandomHTTP::net->match_integer($srcaddr) &&
       ($DetectRandomHTTP::unallocated->match_integer($dstaddr) or
        $DetectRandomHTTP::do_zeroes && 0x0 == ($dstaddr & 0xff))) {
     if (!($node = $DetectRandomHTTP::suspect->match_exact_integer($srcaddr))) {
        $node = { whence => $endtime,
                  addr   => $srcaddr,
                  other  => new Net::Patricia };
        $DetectRandomHTTP::suspect->add_string($srcip, $node)
     }
     $node->{other}->add_string($dstip, 1);
     $node->{whence} = $endtime
   }
   return 0 # always return zero so as not to mess up CampusIO hit ratio 
}

sub perfile {
   my $self = shift;
   my $file = shift;

   $self->SUPER::perfile($file);

   $DetectRandomHTTP::basename = basename $file
}

sub report {
   my $self = shift;
   my $whence = $self->{filetime} - $DetectRandomHTTP::MINUTES*60; # * seconds
   my @host = ();
   my @lines = ();
   my @Cisco = ();
   my @Juniper = ();
   my @purge = ();
   my @suspect = ();

   my $n = $DetectRandomHTTP::suspect->climb(
      sub {
         if ($_[0]->{whence} < $whence) {
            # this entry is old... we'll remember to remove it (below)
            push(@purge, $_[0]->{addr});
            return 1
         }

         # count the number of suspicious HTTP destinations to which this
         # suspect has talked:
         my $victims = $_[0]->{other}->climb(sub { 1 });

         my $host = inet_ntoa(pack("N", $_[0]->{addr}));

         if ($victims >= $DetectRandomHTTP::MIN_VICTIMS) {
            push(@host, $host);
            push(@Cisco,
                 "! DetectRandomHTTP: " .
                 "$host talked to $victims suspicious HTTP destinations:\n" .
                 "ip route $host 255.255.255.255 Null0");
            push(@Juniper,
                 "set routing-options static route $host/32 discard; " .
                 "/* DetectRandomHTTP: " .
                 "$host - talked to $victims suspicious HTTP destinations */")
         } else {
            push(@suspect, $host)
         }

         return 1
      });

   # purge the suspects that have not talked to suspicious addresses "recently":
   foreach (@purge) {
      $DetectRandomHTTP::suspect->remove_string(inet_ntoa(pack("N", $_)));
   }

   printf(STDERR "%s DetectRandomHTTP reporting %d hosts (@host) of %d suspects, leaving: @suspect\n", strftime("%Y/%m/%d %H:%M:%S", localtime), scalar(@host), $n) if (1);

   if (@host && '' ne ${DetectRandomHTTP::MAILTO}) {
      @lines =
      ("The following are suspected sources of HTTP traffic" .
          " to random destinations:\n");
      foreach my $host (@host) {
         push(@lines, $host)
      }

      open(MAIL, "|/usr/lib/sendmail -t") || warn "spawn sendmail failed\n";
      print MAIL "From: DetectRandomHTTP\n";
      print MAIL "To: ${DetectRandomHTTP::MAILTO}\n";
      print MAIL "Subject: Random HTTP detected (\"$DetectRandomHTTP::basename\")\n";
      print MAIL join("\n", @lines), "\n\n",
                 join("\n", @Cisco), "\n\n",
                 join("\n", @Juniper), "\n";
      close(MAIL);
      my $val = $?/256;
      if (0 == $val/256) {
         # purge the reported suspects (to restart detection of them):
         foreach my $host (@host) {
            $DetectRandomHTTP::suspect->remove_string($host)
         }
      } else {
         warn "sendmail failed: $val\n"
      }
   }
}

1
