# 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 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 . 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