| #!/usr/bin/perl -wT |
| # |
| # Author: Jefferson Ogata (JO317) <jogata@pobox.com> |
| # Date: 2000/04/22 |
| # Version: 0.10 |
| # |
| # Please feel free to use or redistribute this program if you find it useful. |
| # If you have suggestions, or even better, bits of new code, send them to me |
| # and I will add them when I have time. The current version of this script |
| # can always be found at the URL: |
| # |
| # http://www.antibozo.net/ogata/webtools/plog.pl |
| # http://pobox.com/~ogata/webtools/plog.txt |
| # |
| # Parse ipmon output into a coherent form. This program only handles the |
| # lines regarding filter actions. It does not parse nat and state lines. |
| # |
| # Present lines from ipmon to this program on standard input. |
| # |
| # EXAMPLES |
| # |
| # plog -AF block,log < /var/log/ipf |
| # |
| # Generate source and destination reports of all packets logged with |
| # block or log actions, and report TCP flags and keep state actions. |
| # |
| # plog -S -s ./services www.example.com < /var/log/ipf |
| # |
| # Generate a source report of traffic to or from www.example.com using |
| # the additional services defined in ./services. |
| # |
| # plog -nSA block < /var/log/ipf |
| # |
| # Generate a source report of all blocked packets with no hostname |
| # lookups. This is handy for an initial pass to identify portscans or |
| # other aggressive traffic. |
| # |
| # plog -SFp 192.168.0.0/24 www.example.com/24 < /var/log/ipf |
| # |
| # Generate a source report of all packets whose source or destination |
| # address is either in 192.168.0.0/24 or an address associated with |
| # the host www.example.com, report packet flags and perform paranoid |
| # hostname lookups. This is a handy usage for examining traffic more |
| # closely after identifying a potential attack. |
| # |
| # TODO |
| # |
| # - Handle output from ipmon -v. |
| # - Handle timestamps from other locales. Anyone with a timestamp problem |
| # please email me the format of your timestamps. |
| # - It looks as though short TCP or UDP packets will break things, but I |
| # haven't seen any yet. |
| # |
| # CHANGES |
| # |
| # 2000/04/22 (0.10): |
| # - Restructured host name and address caches. Hosts are now cached using |
| # packed addresses as keys. Conversion to IPv6 should be simple now. |
| # - Added paranoid hostname lookups. |
| # - Added netmask qualifications for address arguments. |
| # - Tweaked usage info. |
| # 2000/04/20: |
| # - Added parsing and tracking of TCP and state flags. |
| # 2000/04/12 (0.9): |
| # - Wasn't handling underscore in hostname,servicename fields; these may be |
| # logged using ipmon -n. Observation by <ark@eltex.ru>. |
| # - Hadn't properly attributed observation and fix for repetition counter in |
| # 0.8 change log. Added John Ladwig to attribution. Thanks, John. |
| # |
| # 2000/04/10 (0.8): |
| # - Service names can also have hyphens, dummy. I wasn't allowing these |
| # either. Observation and fix thanks to Taso N. Devetzis |
| # <devetzis@snet.net>. |
| # - IP Filter now logs a repetition counter. Observation and fixes (changed |
| # slightly) from Andy Kreiling <Andy@ntcs-inc.com> and John Ladwig |
| # <jladwig@nts.umn.edu>. |
| # - Added fix to handle new Solaris log format, e.g.: |
| # Nov 30 04:49:37 raoul ipmon[121]: [ID 702911 local0.warning] 04:49:36.420541 hme0 @0:34 b 205.152.16.6,58596 -> 204.60.220.24,113 PR tcp len 20 44 |
| # Fix thanks to Taso N. Devetzis <devetzis@SNET.Net>. |
| # - Added services map option. |
| # - Added options for generating only source/destination tables. |
| # - Added verbosity option. |
| # - Added option for reporting traffic for specific hosts. |
| # - Added some more ICMP unreachable codes, and made code and type names |
| # match the ones in IP Filter parse.c. |
| # - Condensed output format somewhat. |
| # - Various minor improvements, perhaps slight speed improvements. |
| # - Documented new options in usage() and tried to improve wording. |
| # |
| # 1999/08/02 (0.7): |
| # - Hostnames can have hyphens, dummy. I wasn't allowing them in the syslog |
| # line. Fix from Antoine Verheijen <antoine.verheijen@ualberta.ca>. |
| # |
| # 1999/05/05 (0.6): |
| # - IRIX syslog prefixes the hostname with a severity code. Handle it. Fix |
| # from John Ladwig <jladwig@nts.umn.edu>. |
| # |
| # 1999/05/05 (0.5): |
| # - Protocols other than TCP, UDP, or ICMP have packet lengths reported in |
| # parentheses for some reason. The script now handles this. Thanks to |
| # Dispatcher <dispatch@blackhelicopters.org>. |
| # - I had mixed up info-request and info-reply ICMP codes, and omitted the |
| # traceroute code. Sorted this out. I had also missed code 0 for type 6 |
| # (alternate address for host). Thanks to John Ladwig <jladwig@nts.umn.edu>. |
| # |
| # 1999/05/03: |
| # - Now accepts hostnames in the source and destination address fields, as |
| # well as port names in the port fields. This allows the people who are |
| # using ipmon -n to still use plog. Note that if you are logging |
| # hostnames, you are vulnerable to forgery of DNS information, modified |
| # DNS information, and your log files will be larger also. If you are |
| # using this program you can have it look up the names for you (still |
| # vulnerable to forgery) and keep your logged addresses all in numeric |
| # format, so that packets from the same source will always show the same |
| # source address regardless of what's up with DNS. Obviously, I don't |
| # favor using ipmon -n. Nevertheless, some people wanted this, so here it |
| # is. |
| # - Added S and n flags to %acts hash. Thanks to Stephen J. Roznowski |
| # <sjr@home.net>. |
| # - Stopped reporting host IPs twice when numeric output was requested. |
| # Thanks, yet again, to Stephen J. Roznowski <sjr@home.net>. |
| # - Number of minor tweaks that might speed it up a bit, and some comments. |
| # - Put the script back up on the web site. I had moved the site and |
| # forgotten to move the tool. |
| # |
| # 1999/02/04: |
| # - Changed log line parser to accept fully-qualified name in the logging |
| # host field. Thanks to Stephen J. Roznowski <sjr@home.net>. |
| # |
| # 1999/01/22: |
| # - Changed high port strategy to use 65536 for unknown high ports so that |
| # they are sorted last. |
| # |
| # 1999/01/21: |
| # - Moved icmp parsing to output loop. |
| # - Added parsing of icmp codes, and more types. |
| # - Changed packet sort routine to sort by port number rather than service |
| # name. |
| # |
| # 1999/01/20: |
| # - Fixed problem matching ipmon log lines. Sometimes they have "/ipmon" in |
| # them, sometimes just "ipmon". |
| # - Added numeric parse option to turn off hostname lookups. |
| # - Moved summary to usage() sub. |
| |
| use strict; |
| use Socket; |
| use IO::File; |
| |
| select STDOUT; $| = 1; |
| |
| my %hosts; |
| |
| my $me = $0; |
| $me =~ s/^.*\///; |
| |
| # Map of log codes for various actions. Not all of these can occur, but |
| # I've included everything in print_ipflog() from ipmon.c. |
| my %acts = ( |
| 'p' => 'pass', |
| 'P' => 'pass', |
| 'b' => 'block', |
| 'B' => 'block', |
| 'L' => 'log', |
| 'S' => 'short', |
| 'n' => 'nomatch', |
| ); |
| |
| # Map of ICMP types and their relevant codes. |
| my %icmpTypeMap = ( |
| 0 => +{ |
| name => 'echorep', |
| codes => +{0 => undef}, |
| }, |
| 3 => +{ |
| name => 'unreach', |
| codes => +{ |
| 0 => 'net-unr', |
| 1 => 'host-unr', |
| 2 => 'proto-unr', |
| 3 => 'port-unr', |
| 4 => 'needfrag', |
| 5 => 'srcfail', |
| 6 => 'net-unk', |
| 7 => 'host-unk', |
| 8 => 'isolate', |
| 9 => 'net-prohib', |
| 10 => 'host-prohib', |
| 11 => 'net-tos', |
| 12 => 'host-tos', |
| 13 => 'filter-prohib', |
| 14 => 'host-preced', |
| 15 => 'preced-cutoff', |
| }, |
| }, |
| 4 => +{ |
| name => 'squench', |
| codes => +{0 => undef}, |
| }, |
| 5 => +{ |
| name => 'redir', |
| codes => +{ |
| 0 => 'net', |
| 1 => 'host', |
| 2 => 'tos', |
| 3 => 'tos-host', |
| }, |
| }, |
| 6 => +{ |
| name => 'alt-host-addr', |
| codes => +{ |
| 0 => 'alt-addr' |
| }, |
| }, |
| 8 => +{ |
| name => 'echo', |
| codes => +{0 => undef}, |
| }, |
| 9 => +{ |
| name => 'routerad', |
| codes => +{0 => undef}, |
| }, |
| 10 => +{ |
| name => 'routersol', |
| codes => +{0 => undef}, |
| }, |
| 11 => +{ |
| name => 'timex', |
| codes => +{ |
| 0 => 'in-transit', |
| 1 => 'frag-assy', |
| }, |
| }, |
| 12 => +{ |
| name => 'paramprob', |
| codes => +{ |
| 0 => 'ptr-err', |
| 1 => 'miss-opt', |
| 2 => 'bad-len', |
| }, |
| }, |
| 13 => +{ |
| name => 'timest', |
| codes => +{0 => undef}, |
| }, |
| 14 => +{ |
| name => 'timestrep', |
| codes => +{0 => undef}, |
| }, |
| 15 => +{ |
| name => 'inforeq', |
| codes => +{0 => undef}, |
| }, |
| 16 => +{ |
| name => 'inforep', |
| codes => +{0 => undef}, |
| }, |
| 17 => +{ |
| name => 'maskreq', |
| codes => +{0 => undef}, |
| }, |
| 18 => +{ |
| name => 'maskrep', |
| codes => +{0 => undef}, |
| }, |
| 30 => +{ |
| name => 'tracert', |
| codes => +{ }, |
| }, |
| 31 => +{ |
| name => 'dgram-conv-err', |
| codes => +{ }, |
| }, |
| 32 => +{ |
| name => 'mbl-host-redir', |
| codes => +{ }, |
| }, |
| 33 => +{ |
| name => 'ipv6-whereru?', |
| codes => +{ }, |
| }, |
| 34 => +{ |
| name => 'ipv6-iamhere', |
| codes => +{ }, |
| }, |
| 35 => +{ |
| name => 'mbl-reg-req', |
| codes => +{ }, |
| }, |
| 36 => +{ |
| name => 'mbl-reg-rep', |
| codes => +{ }, |
| }, |
| ); |
| |
| # Arguments we will parse from argument list. |
| my $numeric = 0; # Don't lookup hostnames. |
| my $paranoid = 0; # Do paranoid hostname lookups. |
| my $verbosity = 0; # Bla' bla' bla'. |
| my $sTable = 0; # Generate source table. |
| my $dTable = 0; # Generate destination table. |
| my @services = (); # Preload services tables. |
| my $showFlags = 0; # Show TCP flag combinations. |
| my %selectAddrs; # Limit report to these hosts. |
| my %selectActs; # Limit report to these actions. |
| |
| # Parse argument list. |
| while (defined ($_ = shift)) |
| { |
| if (s/^-//) |
| { |
| while (s/^([vnpSD\?hsAF])//) |
| { |
| my $flag = $1; |
| if ($flag eq 'v') |
| { |
| ++$verbosity; |
| } |
| elsif ($flag eq 'n') |
| { |
| $numeric = 1; |
| } |
| elsif ($flag eq 'p') |
| { |
| $paranoid = 1; |
| } |
| elsif ($flag eq 'S') |
| { |
| $sTable = 1; |
| } |
| elsif ($flag eq 'D') |
| { |
| $dTable = 1; |
| } |
| elsif ($flag eq 'F') |
| { |
| $showFlags = 1; |
| } |
| elsif (($flag eq '?') || ($flag eq 'h')) |
| { |
| &usage (0); |
| } |
| else |
| { |
| my $arg = shift; |
| defined ($arg) || &usage (1, qq{-$flag requires an argument}); |
| if ($flag eq 's') |
| { |
| push (@services, $arg); |
| } |
| elsif ($flag eq 'A') |
| { |
| my @acts = split (/,/, $arg); |
| my $a; |
| foreach $a (@acts) |
| { |
| my $aa; |
| my $match = 0; |
| foreach $aa (keys (%acts)) |
| { |
| if ($acts{$aa} eq $a) |
| { |
| ++$match; |
| $selectActs{$aa} = $a; |
| } |
| } |
| $match || &usage (1, qq{unknown action $a}); |
| } |
| } |
| } |
| } |
| |
| &usage (1, qq{unknown option: -$_}) if (length); |
| |
| next; |
| } |
| |
| # Add host to hash of hosts we're interested in. |
| (/^(.+)\/([\d+\.]+)$/) || (/^(.+)$/) || &usage (1, qq{invalid CIDR address $_}); |
| my ($addr, $mask) = ($1, $2); |
| my @addr = &hostAddrs ($addr); |
| (scalar (@addr)) || &usage (1, qq{cannot resolve hostname $_}); |
| if (!defined ($mask)) |
| { |
| $mask = (2 ** 32) - 1; |
| } |
| elsif (($mask =~ /^\d+$/) && ($mask <= 32)) |
| { |
| $mask = (2 ** 32) - 1 - ((2 ** (32 - $mask)) - 1); |
| } |
| elsif (defined ($mask = &isDottedAddr ($mask))) |
| { |
| $mask = &integerAddr ($mask); |
| } |
| else |
| { |
| &usage (1, qq{invalid CIDR address $_}); |
| } |
| foreach $addr (@addr) |
| { |
| # Save mask unless we already have a less specific one for this address. |
| my $a = &integerAddr ($addr) & $mask; |
| $selectAddrs{$a} = $mask unless (exists ($selectAddrs{$a}) && ($selectAddrs{$a} < $mask)); |
| } |
| } |
| |
| # Which tables will we generate? |
| $dTable = $sTable = 1 unless ($dTable || $sTable); |
| my @dirs; |
| push (@dirs, 'd') if ($dTable); |
| push (@dirs, 's') if ($sTable); |
| |
| # Are we interested in specific hosts? |
| my $selectAddrs = scalar (keys (%selectAddrs)); |
| |
| # Are we interested in specific actions? |
| if (scalar (keys (%selectActs)) == 0) |
| { |
| %selectActs = %acts; |
| } |
| |
| # We use this hash to cache port name -> number and number -> name mappings. |
| # Isn't it cool that we can use the same hash for both? |
| my %pn; |
| |
| # Preload any services maps. |
| my $sm; |
| foreach $sm (@services) |
| { |
| my $sf = new IO::File ($sm, "r"); |
| defined ($sf) || &quit (1, qq{cannot open services file $sm}); |
| |
| while (defined ($_ = $sf->getline ())) |
| { |
| my $text = $_; |
| chomp; |
| s/#.*$//; |
| s/\s+$//; |
| next unless (length); |
| my ($name, $spec, @aliases) = split (/\s+/); |
| ($spec =~ /^([\w\-]+)\/([\w\-]+)$/) |
| || &quit (1, qq{$sm:$.: invalid definition: $text}); |
| my ($pnum, $proto) = ($1, $2); |
| |
| # Enter service definition in pn hash both forwards and backwards. |
| my $port; |
| my $pname; |
| foreach $port ($name, @aliases) |
| { |
| $pname = "$pnum/$proto"; |
| $pn{$pname} = $port; |
| } |
| $pname = "$name/$proto"; |
| $pn{$pname} = $pnum; |
| } |
| |
| $sf->close (); |
| } |
| |
| # Cache for host name -> addr mappings. |
| my %ipAddr; |
| |
| # Cache for host addr -> name mappings. |
| my %ipName; |
| |
| # Hash for protocol number <--> name mappings. |
| my %pr; |
| |
| # Under IPv4 port numbers are unsigned shorts. The value below is higher |
| # than the maximum value of an unsigned short, and is used in place of |
| # high port numbers that don't correspond to known services. This makes |
| # high ports get sorted behind all others. |
| my $highPort = 0x10000; |
| |
| while (<STDIN>) |
| { |
| chomp; |
| |
| # For ipmon output that came through syslog, we'll have an asctime |
| # timestamp, an optional severity code (IRIX), the hostname, |
| # "ipmon"[process id]: prefixed to the line. For output that was |
| # written directly to a file by ipmon, we'll have a date prefix as |
| # dd/mm/yyyy (no y2k problem here!). Both formats then have a packet |
| # timestamp and the log info. |
| my ($log); |
| if (s/^\w+\s+\d+\s+\d+:\d+:\d+\s+(?:\d\w:)?[\w\.\-]+\s+\S*ipmon\[\d+\]:\s+(?:\[ID\s+\d+\s+[\w\.]+\]\s+)?\d+:\d+:\d+\.\d+\s+//) |
| { |
| $log = $_; |
| } |
| elsif (s/^(?:\d+\/\d+\/\d+)\s+(?:\d+:\d+:\d+\.\d+)\s+//) |
| { |
| $log = $_; |
| } |
| else |
| { |
| # It don't look like no ipmon output to me, baby. |
| next; |
| } |
| next unless (defined ($log)); |
| |
| print STDERR "$log\n" if ($verbosity); |
| |
| # Parse the log line. We're expecting interface name, rule group and |
| # number, an action code, a source host name or IP with possible port |
| # name or number, a destination host name or IP with possible port |
| # number, "PR", a protocol name or number, "len", a header length, a |
| # packet length (which will be in parentheses for protocols other than |
| # TCP, UDP, or ICMP), and maybe some additional info. |
| my @fields = ($log =~ /^(?:(\d+)x)?\s*(\w+)\s+@(\d+):(\d+)\s+(\w)\s+([\w\-\.,]+)\s+->\s+([\w\-\.,]+)\s+PR\s+(\w+)\s+len\s+(\d+)\s+\(?(\d+)\)?\s*(.*)$/ox); |
| unless (scalar (@fields)) |
| { |
| print STDERR "$me:$.: cannot parse: $_\n"; |
| next; |
| } |
| my ($count, $if, $group, $rule, $act, $src, $dest, $proto, $hlen, $len, $more) = @fields; |
| |
| # Skip actions we're not interested in. |
| next unless (exists ($selectActs{$act})); |
| |
| # Packet count defaults to 1. |
| $count = 1 unless (defined ($count)); |
| |
| my ($sport, $dport, @flags); |
| |
| if ($proto eq 'icmp') |
| { |
| if ($more =~ s/^icmp (\d+)\/(\d+)\s*//) |
| { |
| # We save icmp type and code in both sport and dport. This |
| # allows us to sort icmp packets using the normal port-sorting |
| # code. |
| $dport = $sport = "$1.$2"; |
| } |
| else |
| { |
| $sport = ''; |
| $dport = ''; |
| } |
| } |
| else |
| { |
| if ($showFlags) |
| { |
| if (($proto eq 'tcp') && ($more =~ s/^\-([A-Z]+)\s*//)) |
| { |
| push (@flags, $1); |
| } |
| if ($more =~ s/^K\-S\s*//) |
| { |
| push (@flags, 'state'); |
| } |
| } |
| if ($src =~ s/,([\-\w]+)$//) |
| { |
| $sport = &portSimplify ($1, $proto); |
| } |
| else |
| { |
| $sport = ''; |
| } |
| if ($dest =~ s/,([\-\w]+)$//) |
| { |
| $dport = &portSimplify ($1, $proto); |
| } |
| else |
| { |
| $dport = ''; |
| } |
| } |
| |
| # Make sure addresses are numeric at this point. We want to sort by |
| # IP address later. If the hostname doesn't resolve, punt. If you |
| # must use ipmon -n, be ready for weirdness. Use only the first |
| # address returned. |
| my $x; |
| $x = (&hostAddrs ($src))[0]; |
| unless (defined ($x)) |
| { |
| print STDERR "$me:$.: cannot resolve hostname $src\n"; |
| next; |
| } |
| $src = $x; |
| $x = (&hostAddrs ($dest))[0]; |
| unless (defined ($x)) |
| { |
| print STDERR "$me:$.: cannot resolve hostname $dest\n"; |
| next; |
| } |
| $dest = $x; |
| |
| # Skip hosts we're not interested in. |
| if ($selectAddrs) |
| { |
| my ($a, $m); |
| my $s = &integerAddr ($src); |
| my $d = &integerAddr ($dest); |
| my $cute = 0; |
| while (($a, $m) = each (%selectAddrs)) |
| { |
| if ((($s & $m) == $a) || (($d & $m) == $a)) |
| { |
| $cute = 1; |
| last; |
| } |
| } |
| next unless ($cute); |
| } |
| |
| # Convert proto to proto number. |
| $proto = &protoNumber ($proto); |
| |
| sub countPacket |
| { |
| my ($host, $dir, $peer, $proto, $count, $packet, @flags) = @_; |
| |
| # Make sure host is in the hosts hash. |
| $hosts{$host} = |
| +{ |
| 'd' => +{ }, |
| 's' => +{ }, |
| } unless (exists ($hosts{$host})); |
| |
| # Get the source/destination traffic hash for the host in question. |
| my $trafficHash = $hosts{$host}->{$dir}; |
| |
| # Make sure there's a hash for the peer. |
| $trafficHash->{$peer} = +{ } unless (exists ($trafficHash->{$peer})); |
| |
| # Make sure the peer hash has a hash for the protocol number. |
| my $peerHash = $trafficHash->{$peer}; |
| $peerHash->{$proto} = +{ } unless (exists ($peerHash->{$proto})); |
| |
| # Make sure there's a counter for this packet type in the proto hash. |
| my $protoHash = $peerHash->{$proto}; |
| $protoHash->{$packet} = +{ '' => 0 } unless (exists ($protoHash->{$packet})); |
| |
| # Increment the counter and mark flags. |
| my $packetHash = $protoHash->{$packet}; |
| $packetHash->{''} += $count; |
| map { $packetHash->{$_} = undef; } (@flags); |
| } |
| |
| # Count the packet as outgoing traffic from the source address. |
| &countPacket ($src, 's', $dest, $proto, $count, "$sport:$dport:$if:$act", @flags) if ($sTable); |
| |
| # Count the packet as incoming traffic to the destination address. |
| &countPacket ($dest, 'd', $src, $proto, $count, "$dport:$sport:$if:$act", @flags) if ($dTable); |
| } |
| |
| my $dir; |
| foreach $dir (@dirs) |
| { |
| my $order = ($dir eq 's' ? 'source' : 'destination'); |
| my $arrow = ($dir eq 's' ? '->' : '<-'); |
| |
| print "###\n"; |
| print "### Traffic by $order address:\n"; |
| print "###\n"; |
| |
| sub ipSort |
| { |
| &integerAddr ($a) <=> &integerAddr ($b); |
| } |
| |
| sub packetSort |
| { |
| my ($asport, $adport, $aif, $aact) = split (/:/, $a); |
| my ($bsport, $bdport, $bif, $bact) = split (/:/, $b); |
| $bact cmp $aact || $aif cmp $bif || $asport <=> $bsport || $adport <=> $bdport; |
| } |
| |
| my $host; |
| foreach $host (sort ipSort (keys %hosts)) |
| { |
| my $traffic = $hosts{$host}->{$dir}; |
| |
| # Skip hosts with no traffic. |
| next unless (scalar (keys (%{$traffic}))); |
| |
| if ($numeric) |
| { |
| print &dottedAddr ($host), "\n"; |
| } |
| else |
| { |
| print &hostName ($host), " \[", &dottedAddr ($host), "\]\n"; |
| } |
| |
| my $peer; |
| foreach $peer (sort ipSort (keys %{$traffic})) |
| { |
| my $peerHash = $traffic->{$peer}; |
| my $peerName = ($numeric ? &dottedAddr ($peer) : &hostName ($peer)); |
| my $proto; |
| foreach $proto (sort (keys (%{$peerHash}))) |
| { |
| my $protoHash = $peerHash->{$proto}; |
| my $protoName = &protoName ($proto); |
| |
| my $packet; |
| foreach $packet (sort packetSort (keys %{$protoHash})) |
| { |
| my ($sport, $dport, $if, $act) = split (/:/, $packet); |
| my $packetHash = $protoHash->{$packet}; |
| my $count = $packetHash->{''}; |
| $act = '?' unless (defined ($act = $acts{$act})); |
| if (($protoName eq 'tcp') || ($protoName eq 'udp')) |
| { |
| printf (" %-6s %7s %4d %4s %16s %2s %s.%s", $if, $act, $count, $protoName, &portName ($sport, $protoName), $arrow, $peerName, &portName ($dport, $protoName)); |
| } |
| elsif ($protoName eq 'icmp') |
| { |
| printf (" %-6s %7s %4d %4s %16s %2s %s", $if, $act, $count, $protoName, &icmpType ($sport), $arrow, $peerName); |
| } |
| else |
| { |
| printf (" %-6s %7s %4d %4s %16s %2s %s", $if, $act, $count, $protoName, '', $arrow, $peerName); |
| } |
| if ($showFlags) |
| { |
| my @flags = sort (keys (%{$packetHash})); |
| if (scalar (@flags)) |
| { |
| shift (@flags); |
| print ' (', join (',', @flags), ')' if (scalar (@flags)); |
| } |
| } |
| print "\n"; |
| } |
| } |
| } |
| } |
| |
| print "\n"; |
| } |
| |
| exit (0); |
| |
| # Translates a numeric port/named protocol to a port name. Reserved ports |
| # that do not have an entry in the services database are left numeric. High |
| # ports that do not have an entry in the services database are mapped |
| # to '<high>'. |
| sub portName |
| { |
| my $port = shift; |
| my $proto = shift; |
| my $pname = "$port/$proto"; |
| unless (exists ($pn{$pname})) |
| { |
| my $name = getservbyport ($port, $proto); |
| $pn{$pname} = (defined ($name) ? $name : ($port <= 1023 ? $port : '<high>')); |
| } |
| return $pn{$pname}; |
| } |
| |
| # Translates a named port/protocol to a port number. |
| sub portNumber |
| { |
| my $port = shift; |
| my $proto = shift; |
| my $pname = "$port/$proto"; |
| unless (exists ($pn{$pname})) |
| { |
| my $number = getservbyname ($port, $proto); |
| unless (defined ($number)) |
| { |
| # I don't think we need to recover from this. How did the port |
| # name get into the log file if we can't find it? Log file from |
| # a different machine? Fix /etc/services on this one if that's |
| # your problem. |
| die ("Unrecognized port name \"$port\" at $."); |
| } |
| $pn{$pname} = $number; |
| } |
| return $pn{$pname}; |
| } |
| |
| # Convert all unrecognized high ports to the same value so they are treated |
| # identically. The protocol should be by name. |
| sub portSimplify |
| { |
| my $port = shift; |
| my $proto = shift; |
| |
| # Make sure port is numeric. |
| $port = &portNumber ($port, $proto) |
| unless ($port =~ /^\d+$/); |
| |
| # Look up port name. |
| my $portName = &portName ($port, $proto); |
| |
| # Port is an unknown high port. Return a value that is too high for a |
| # port number, so that high ports get sorted last. |
| return $highPort if ($portName eq '<high>'); |
| |
| # Return original port number. |
| return $port; |
| } |
| |
| # Translates a numeric address into a hostname. Pass only packed numeric |
| # addresses to this routine. |
| sub hostName |
| { |
| my $ip = shift; |
| return $ipName{$ip} if (exists ($ipName{$ip})); |
| |
| # Do an inverse lookup on the address. |
| my $name = gethostbyaddr ($ip, AF_INET); |
| unless (defined ($name)) |
| { |
| # Inverse lookup failed, so map the IP address to its dotted |
| # representation and cache that. |
| $ipName{$ip} = &dottedAddr ($ip); |
| return $ipName{$ip}; |
| } |
| |
| # For paranoid hostname lookups. |
| if ($paranoid) |
| { |
| # If this address already matches, we're happy. |
| unless (exists ($ipName{$ip}) && (lc ($ipName{$ip}) eq lc ($name))) |
| { |
| # Do a forward lookup on the resulting name. |
| my @addr = &hostAddrs ($name); |
| my $match = 0; |
| |
| # Cache the forward lookup results for future inverse lookups, |
| # but don't stomp on inverses we've already cached, even if they |
| # are questionable. We want to generate consistent output, and |
| # the cache is growing incrementally. |
| foreach (@addr) |
| { |
| $ipName{$_} = $name unless (exists ($ipName{$_})); |
| $match = 1 if ($_ eq $ip); |
| } |
| |
| # Was this one of the addresses? If not, tack on a ?. |
| $name .= '?' unless ($match); |
| } |
| } |
| else |
| { |
| # Just believe it and cache it. |
| $ipName{$ip} = $name; |
| } |
| |
| return $name; |
| } |
| |
| # Translates a hostname or dotted address into a list of packed numeric |
| # addresses. |
| sub hostAddrs |
| { |
| my $name = shift; |
| my $ip; |
| |
| # Check if it's a dotted representation. |
| return ($ip) if (defined ($ip = &isDottedAddr ($name))); |
| |
| # Return result from cache. |
| $name = lc ($name); |
| return @{$ipAddr{$name}} if (exists ($ipAddr{$name})); |
| |
| # Look up the addresses. |
| my @addr = gethostbyname ($name); |
| splice (@addr, 0, 4); |
| |
| unless (scalar (@addr)) |
| { |
| # Again, I don't think we need to recover from this gracefully. |
| # If we can't resolve a hostname that ended up in the log file, |
| # punt. We want to be able to sort hosts by IP address later, |
| # and letting hostnames through will snarl up that code. Users |
| # of ipmon -n will have to grin and bear it for now. The |
| # functions that get undef back should treat it as an error or |
| # as some default address, e.g. 0 just to make things work. |
| return (); |
| } |
| |
| $ipAddr{$name} = [ @addr ]; |
| return @{$ipAddr{$name}}; |
| } |
| |
| # If the argument is a valid dotted address, returns the corresponding |
| # packed numeric address, otherwise returns undef. |
| sub isDottedAddr |
| { |
| my $addr = shift; |
| if ($addr =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) |
| { |
| my @a = (int ($1), int ($2), int ($3), int ($4)); |
| foreach (@a) |
| { |
| return undef if ($_ >= 256); |
| } |
| return pack ('C*', @a); |
| } |
| return undef; |
| } |
| |
| # Unpacks a packed numeric address and returns an integer representation. |
| sub integerAddr |
| { |
| my $addr = shift; |
| return unpack ('N', $addr); |
| |
| # The following is for generalized IPv4/IPv6 stuff. For now, it's a |
| # lot faster to assume IPv4. |
| my @a = unpack ('C*', $addr); |
| my $a = 0; |
| while (scalar (@a)) |
| { |
| $a = ($a << 8) | shift (@a); |
| } |
| return $a; |
| } |
| |
| # Unpacks a packed numeric address into a dotted representation. |
| sub dottedAddr |
| { |
| my $addr = shift; |
| my @a = unpack ('C*', $addr); |
| return join ('.', @a); |
| } |
| |
| # Translates a protocol number into a protocol name, or a number if no name |
| # is found in the protocol database. |
| sub protoName |
| { |
| my $code = shift; |
| return $code if ($code !~ /^\d+$/); |
| unless (exists ($pr{$code})) |
| { |
| my $name = scalar (getprotobynumber ($code)); |
| if (defined ($name)) |
| { |
| $pr{$code} = $name; |
| } |
| else |
| { |
| $pr{$code} = $code; |
| } |
| } |
| return $pr{$code}; |
| } |
| |
| # Translates a protocol name or number into a protocol number. |
| sub protoNumber |
| { |
| my $name = shift; |
| return $name if ($name =~ /^\d+$/); |
| unless (exists ($pr{$name})) |
| { |
| my $code = scalar (getprotobyname ($name)); |
| if (defined ($code)) |
| { |
| $pr{$name} = $code; |
| } |
| else |
| { |
| $pr{$name} = $name; |
| } |
| } |
| return $pr{$name}; |
| } |
| |
| sub icmpType |
| { |
| my $typeCode = shift; |
| my ($type, $code) = split ('\.', $typeCode); |
| |
| return "?" unless (defined ($code)); |
| |
| my $info = $icmpTypeMap{$type}; |
| |
| return "\(type=$type/$code?\)" unless (defined ($info)); |
| |
| my $typeName = $info->{name}; |
| my $codeName; |
| if (exists ($info->{codes}->{$code})) |
| { |
| $codeName = $info->{codes}->{$code}; |
| $codeName = (defined ($codeName) ? "/$codeName" : ''); |
| } |
| else |
| { |
| $codeName = "/$code"; |
| } |
| return "$typeName$codeName"; |
| } |
| |
| sub quit |
| { |
| my $ec = shift; |
| my $msg = shift; |
| |
| print STDERR "$me: $msg\n"; |
| exit ($ec); |
| } |
| |
| sub usage |
| { |
| my $ec = shift; |
| my @msg = @_; |
| |
| if (scalar (@msg)) |
| { |
| print STDERR "$me: ", join ("\n", @msg), "\n\n"; |
| } |
| |
| print <<EOT; |
| usage: $me [-nSDF] [-s servicemap] [-A act1,...] [address...] |
| |
| Parses logging from ipmon and presents it in a comprehensible format. This |
| program generates two reports: one organized by source address and another |
| organized by destination address. For the first report, source addresses are |
| sorted by IP address. For each address, all packets originating at the address |
| are presented in a tabular form, where all packets with the same source and |
| destination address and port are counted as a single entry. Any port number |
| greater than 1023 that does not match an entry in the services table is treated |
| as a "high" port; all high ports are coalesced into the same entry. The fields |
| for the source address report are: |
| iface action packet-count proto src-port dest-host.dest-port \[\(flags\)\] |
| The fields for the destination address report are: |
| iface action packet-count proto dest-port src-host.src-port \[\(flags\)\] |
| |
| Options are: |
| -n Disable hostname lookups, and report only IP addresses. |
| -p Perform paranoid hostname lookups. |
| -S Generate a source address report. |
| -D Generate a destination address report. |
| -F Show all flag combinations associated with packets. |
| -s map Supply an alternate services map to be preloaded. The map should |
| be in the same format as /etc/services. Any service name not found |
| in the map will be looked for in the system services file. |
| -A act1,... Limit the report to the specified actions. The possible actions |
| are pass, block, log, short, and nomatch. |
| |
| If any addresses are supplied on the command line, the report is limited to |
| these hosts. Addresses may be given as dotted IP addresses or hostnames, and |
| may be qualified with netmasks in CIDR \(/24\) or dotted \(/255.255.255.0\) format. |
| If a hostname resolves to multiple addresses, all addresses are used. |
| |
| If neither -S nor -D is given, both reports are generated. |
| |
| Note: if you are logging traffic with ipmon -n, ipmon will already have looked |
| up and logged addresses as hostnames where possible. This has an important side |
| effect: this program will translate the hostnames back into IP addresses which |
| may not match the original addresses of the logged packets because of numerous |
| DNS issues. If you care about where packets are really coming from, you simply |
| cannot rely on ipmon -n. An attacker with control of his reverse DNS can map |
| the reverse lookup to anything he likes. If you haven't logged the numeric IP |
| address, there's no way to discover the source of an attack reliably. For this |
| reason, I strongly recommend that you run ipmon without the -n option, and use |
| this or a similar script to do reverse lookups during analysis, rather than |
| during logging. |
| EOT |
| |
| exit ($ec); |
| } |
| |