1#!/usr/bin/perl -wT 2# 3# Author: Jefferson Ogata (JO317) <jogata@pobox.com> 4# Date: 2000/04/22 5# Version: 0.10 6# 7# Please feel free to use or redistribute this program if you find it useful. 8# If you have suggestions, or even better, bits of new code, send them to me 9# and I will add them when I have time. The current version of this script 10# can always be found at the URL: 11# 12# http://www.antibozo.net/ogata/webtools/plog.pl 13# http://pobox.com/~ogata/webtools/plog.txt 14# 15# Parse ipmon output into a coherent form. This program only handles the 16# lines regarding filter actions. It does not parse nat and state lines. 17# 18# Present lines from ipmon to this program on standard input. 19# 20# EXAMPLES 21# 22# plog -AF block,log < /var/log/ipf 23# 24# Generate source and destination reports of all packets logged with 25# block or log actions, and report TCP flags and keep state actions. 26# 27# plog -S -s ./services www.example.com < /var/log/ipf 28# 29# Generate a source report of traffic to or from www.example.com using 30# the additional services defined in ./services. 31# 32# plog -nSA block < /var/log/ipf 33# 34# Generate a source report of all blocked packets with no hostname 35# lookups. This is handy for an initial pass to identify portscans or 36# other aggressive traffic. 37# 38# plog -SFp 192.168.0.0/24 www.example.com/24 < /var/log/ipf 39# 40# Generate a source report of all packets whose source or destination 41# address is either in 192.168.0.0/24 or an address associated with 42# the host www.example.com, report packet flags and perform paranoid 43# hostname lookups. This is a handy usage for examining traffic more 44# closely after identifying a potential attack. 45# 46# TODO 47# 48# - Handle output from ipmon -v. 49# - Handle timestamps from other locales. Anyone with a timestamp problem 50# please email me the format of your timestamps. 51# - It looks as though short TCP or UDP packets will break things, but I 52# haven't seen any yet. 53# 54# CHANGES 55# 56# 2000/04/22 (0.10): 57# - Restructured host name and address caches. Hosts are now cached using 58# packed addresses as keys. Conversion to IPv6 should be simple now. 59# - Added paranoid hostname lookups. 60# - Added netmask qualifications for address arguments. 61# - Tweaked usage info. 62# 2000/04/20: 63# - Added parsing and tracking of TCP and state flags. 64# 2000/04/12 (0.9): 65# - Wasn't handling underscore in hostname,servicename fields; these may be 66# logged using ipmon -n. Observation by <ark@eltex.ru>. 67# - Hadn't properly attributed observation and fix for repetition counter in 68# 0.8 change log. Added John Ladwig to attribution. Thanks, John. 69# 70# 2000/04/10 (0.8): 71# - Service names can also have hyphens, dummy. I wasn't allowing these 72# either. Observation and fix thanks to Taso N. Devetzis 73# <devetzis@snet.net>. 74# - IP Filter now logs a repetition counter. Observation and fixes (changed 75# slightly) from Andy Kreiling <Andy@ntcs-inc.com> and John Ladwig 76# <jladwig@nts.umn.edu>. 77# - Added fix to handle new Solaris log format, e.g.: 78# 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 79# Fix thanks to Taso N. Devetzis <devetzis@SNET.Net>. 80# - Added services map option. 81# - Added options for generating only source/destination tables. 82# - Added verbosity option. 83# - Added option for reporting traffic for specific hosts. 84# - Added some more ICMP unreachable codes, and made code and type names 85# match the ones in IP Filter parse.c. 86# - Condensed output format somewhat. 87# - Various minor improvements, perhaps slight speed improvements. 88# - Documented new options in usage() and tried to improve wording. 89# 90# 1999/08/02 (0.7): 91# - Hostnames can have hyphens, dummy. I wasn't allowing them in the syslog 92# line. Fix from Antoine Verheijen <antoine.verheijen@ualberta.ca>. 93# 94# 1999/05/05 (0.6): 95# - IRIX syslog prefixes the hostname with a severity code. Handle it. Fix 96# from John Ladwig <jladwig@nts.umn.edu>. 97# 98# 1999/05/05 (0.5): 99# - Protocols other than TCP, UDP, or ICMP have packet lengths reported in 100# parentheses for some reason. The script now handles this. Thanks to 101# Dispatcher <dispatch@blackhelicopters.org>. 102# - I had mixed up info-request and info-reply ICMP codes, and omitted the 103# traceroute code. Sorted this out. I had also missed code 0 for type 6 104# (alternate address for host). Thanks to John Ladwig <jladwig@nts.umn.edu>. 105# 106# 1999/05/03: 107# - Now accepts hostnames in the source and destination address fields, as 108# well as port names in the port fields. This allows the people who are 109# using ipmon -n to still use plog. Note that if you are logging 110# hostnames, you are vulnerable to forgery of DNS information, modified 111# DNS information, and your log files will be larger also. If you are 112# using this program you can have it look up the names for you (still 113# vulnerable to forgery) and keep your logged addresses all in numeric 114# format, so that packets from the same source will always show the same 115# source address regardless of what's up with DNS. Obviously, I don't 116# favor using ipmon -n. Nevertheless, some people wanted this, so here it 117# is. 118# - Added S and n flags to %acts hash. Thanks to Stephen J. Roznowski 119# <sjr@home.net>. 120# - Stopped reporting host IPs twice when numeric output was requested. 121# Thanks, yet again, to Stephen J. Roznowski <sjr@home.net>. 122# - Number of minor tweaks that might speed it up a bit, and some comments. 123# - Put the script back up on the web site. I had moved the site and 124# forgotten to move the tool. 125# 126# 1999/02/04: 127# - Changed log line parser to accept fully-qualified name in the logging 128# host field. Thanks to Stephen J. Roznowski <sjr@home.net>. 129# 130# 1999/01/22: 131# - Changed high port strategy to use 65536 for unknown high ports so that 132# they are sorted last. 133# 134# 1999/01/21: 135# - Moved icmp parsing to output loop. 136# - Added parsing of icmp codes, and more types. 137# - Changed packet sort routine to sort by port number rather than service 138# name. 139# 140# 1999/01/20: 141# - Fixed problem matching ipmon log lines. Sometimes they have "/ipmon" in 142# them, sometimes just "ipmon". 143# - Added numeric parse option to turn off hostname lookups. 144# - Moved summary to usage() sub. 145 146use strict; 147use Socket; 148use IO::File; 149 150select STDOUT; $| = 1; 151 152my %hosts; 153 154my $me = $0; 155$me =~ s/^.*\///; 156 157# Map of log codes for various actions. Not all of these can occur, but 158# I've included everything in print_ipflog() from ipmon.c. 159my %acts = ( 160 'p' => 'pass', 161 'P' => 'pass', 162 'b' => 'block', 163 'B' => 'block', 164 'L' => 'log', 165 'S' => 'short', 166 'n' => 'nomatch', 167); 168 169# Map of ICMP types and their relevant codes. 170my %icmpTypeMap = ( 171 0 => +{ 172 name => 'echorep', 173 codes => +{0 => undef}, 174 }, 175 3 => +{ 176 name => 'unreach', 177 codes => +{ 178 0 => 'net-unr', 179 1 => 'host-unr', 180 2 => 'proto-unr', 181 3 => 'port-unr', 182 4 => 'needfrag', 183 5 => 'srcfail', 184 6 => 'net-unk', 185 7 => 'host-unk', 186 8 => 'isolate', 187 9 => 'net-prohib', 188 10 => 'host-prohib', 189 11 => 'net-tos', 190 12 => 'host-tos', 191 13 => 'filter-prohib', 192 14 => 'host-preced', 193 15 => 'preced-cutoff', 194 }, 195 }, 196 4 => +{ 197 name => 'squench', 198 codes => +{0 => undef}, 199 }, 200 5 => +{ 201 name => 'redir', 202 codes => +{ 203 0 => 'net', 204 1 => 'host', 205 2 => 'tos', 206 3 => 'tos-host', 207 }, 208 }, 209 6 => +{ 210 name => 'alt-host-addr', 211 codes => +{ 212 0 => 'alt-addr' 213 }, 214 }, 215 8 => +{ 216 name => 'echo', 217 codes => +{0 => undef}, 218 }, 219 9 => +{ 220 name => 'routerad', 221 codes => +{0 => undef}, 222 }, 223 10 => +{ 224 name => 'routersol', 225 codes => +{0 => undef}, 226 }, 227 11 => +{ 228 name => 'timex', 229 codes => +{ 230 0 => 'in-transit', 231 1 => 'frag-assy', 232 }, 233 }, 234 12 => +{ 235 name => 'paramprob', 236 codes => +{ 237 0 => 'ptr-err', 238 1 => 'miss-opt', 239 2 => 'bad-len', 240 }, 241 }, 242 13 => +{ 243 name => 'timest', 244 codes => +{0 => undef}, 245 }, 246 14 => +{ 247 name => 'timestrep', 248 codes => +{0 => undef}, 249 }, 250 15 => +{ 251 name => 'inforeq', 252 codes => +{0 => undef}, 253 }, 254 16 => +{ 255 name => 'inforep', 256 codes => +{0 => undef}, 257 }, 258 17 => +{ 259 name => 'maskreq', 260 codes => +{0 => undef}, 261 }, 262 18 => +{ 263 name => 'maskrep', 264 codes => +{0 => undef}, 265 }, 266 30 => +{ 267 name => 'tracert', 268 codes => +{ }, 269 }, 270 31 => +{ 271 name => 'dgram-conv-err', 272 codes => +{ }, 273 }, 274 32 => +{ 275 name => 'mbl-host-redir', 276 codes => +{ }, 277 }, 278 33 => +{ 279 name => 'ipv6-whereru?', 280 codes => +{ }, 281 }, 282 34 => +{ 283 name => 'ipv6-iamhere', 284 codes => +{ }, 285 }, 286 35 => +{ 287 name => 'mbl-reg-req', 288 codes => +{ }, 289 }, 290 36 => +{ 291 name => 'mbl-reg-rep', 292 codes => +{ }, 293 }, 294); 295 296# Arguments we will parse from argument list. 297my $numeric = 0; # Don't lookup hostnames. 298my $paranoid = 0; # Do paranoid hostname lookups. 299my $verbosity = 0; # Bla' bla' bla'. 300my $sTable = 0; # Generate source table. 301my $dTable = 0; # Generate destination table. 302my @services = (); # Preload services tables. 303my $showFlags = 0; # Show TCP flag combinations. 304my %selectAddrs; # Limit report to these hosts. 305my %selectActs; # Limit report to these actions. 306 307# Parse argument list. 308while (defined ($_ = shift)) 309{ 310 if (s/^-//) 311 { 312 while (s/^([vnpSD\?hsAF])//) 313 { 314 my $flag = $1; 315 if ($flag eq 'v') 316 { 317 ++$verbosity; 318 } 319 elsif ($flag eq 'n') 320 { 321 $numeric = 1; 322 } 323 elsif ($flag eq 'p') 324 { 325 $paranoid = 1; 326 } 327 elsif ($flag eq 'S') 328 { 329 $sTable = 1; 330 } 331 elsif ($flag eq 'D') 332 { 333 $dTable = 1; 334 } 335 elsif ($flag eq 'F') 336 { 337 $showFlags = 1; 338 } 339 elsif (($flag eq '?') || ($flag eq 'h')) 340 { 341 &usage (0); 342 } 343 else 344 { 345 my $arg = shift; 346 defined ($arg) || &usage (1, qq{-$flag requires an argument}); 347 if ($flag eq 's') 348 { 349 push (@services, $arg); 350 } 351 elsif ($flag eq 'A') 352 { 353 my @acts = split (/,/, $arg); 354 my $a; 355 foreach $a (@acts) 356 { 357 my $aa; 358 my $match = 0; 359 foreach $aa (keys (%acts)) 360 { 361 if ($acts{$aa} eq $a) 362 { 363 ++$match; 364 $selectActs{$aa} = $a; 365 } 366 } 367 $match || &usage (1, qq{unknown action $a}); 368 } 369 } 370 } 371 } 372 373 &usage (1, qq{unknown option: -$_}) if (length); 374 375 next; 376 } 377 378 # Add host to hash of hosts we're interested in. 379 (/^(.+)\/([\d+\.]+)$/) || (/^(.+)$/) || &usage (1, qq{invalid CIDR address $_}); 380 my ($addr, $mask) = ($1, $2); 381 my @addr = &hostAddrs ($addr); 382 (scalar (@addr)) || &usage (1, qq{cannot resolve hostname $_}); 383 if (!defined ($mask)) 384 { 385 $mask = (2 ** 32) - 1; 386 } 387 elsif (($mask =~ /^\d+$/) && ($mask <= 32)) 388 { 389 $mask = (2 ** 32) - 1 - ((2 ** (32 - $mask)) - 1); 390 } 391 elsif (defined ($mask = &isDottedAddr ($mask))) 392 { 393 $mask = &integerAddr ($mask); 394 } 395 else 396 { 397 &usage (1, qq{invalid CIDR address $_}); 398 } 399 foreach $addr (@addr) 400 { 401 # Save mask unless we already have a less specific one for this address. 402 my $a = &integerAddr ($addr) & $mask; 403 $selectAddrs{$a} = $mask unless (exists ($selectAddrs{$a}) && ($selectAddrs{$a} < $mask)); 404 } 405} 406 407# Which tables will we generate? 408$dTable = $sTable = 1 unless ($dTable || $sTable); 409my @dirs; 410push (@dirs, 'd') if ($dTable); 411push (@dirs, 's') if ($sTable); 412 413# Are we interested in specific hosts? 414my $selectAddrs = scalar (keys (%selectAddrs)); 415 416# Are we interested in specific actions? 417if (scalar (keys (%selectActs)) == 0) 418{ 419 %selectActs = %acts; 420} 421 422# We use this hash to cache port name -> number and number -> name mappings. 423# Isn't it cool that we can use the same hash for both? 424my %pn; 425 426# Preload any services maps. 427my $sm; 428foreach $sm (@services) 429{ 430 my $sf = new IO::File ($sm, "r"); 431 defined ($sf) || &quit (1, qq{cannot open services file $sm}); 432 433 while (defined ($_ = $sf->getline ())) 434 { 435 my $text = $_; 436 chomp; 437 s/#.*$//; 438 s/\s+$//; 439 next unless (length); 440 my ($name, $spec, @aliases) = split (/\s+/); 441 ($spec =~ /^([\w\-]+)\/([\w\-]+)$/) 442 || &quit (1, qq{$sm:$.: invalid definition: $text}); 443 my ($pnum, $proto) = ($1, $2); 444 445 # Enter service definition in pn hash both forwards and backwards. 446 my $port; 447 my $pname; 448 foreach $port ($name, @aliases) 449 { 450 $pname = "$pnum/$proto"; 451 $pn{$pname} = $port; 452 } 453 $pname = "$name/$proto"; 454 $pn{$pname} = $pnum; 455 } 456 457 $sf->close (); 458} 459 460# Cache for host name -> addr mappings. 461my %ipAddr; 462 463# Cache for host addr -> name mappings. 464my %ipName; 465 466# Hash for protocol number <--> name mappings. 467my %pr; 468 469# Under IPv4 port numbers are unsigned shorts. The value below is higher 470# than the maximum value of an unsigned short, and is used in place of 471# high port numbers that don't correspond to known services. This makes 472# high ports get sorted behind all others. 473my $highPort = 0x10000; 474 475while (<STDIN>) 476{ 477 chomp; 478 479 # For ipmon output that came through syslog, we'll have an asctime 480 # timestamp, an optional severity code (IRIX), the hostname, 481 # "ipmon"[process id]: prefixed to the line. For output that was 482 # written directly to a file by ipmon, we'll have a date prefix as 483 # dd/mm/yyyy (no y2k problem here!). Both formats then have a packet 484 # timestamp and the log info. 485 my ($log); 486 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+//) 487 { 488 $log = $_; 489 } 490 elsif (s/^(?:\d+\/\d+\/\d+)\s+(?:\d+:\d+:\d+\.\d+)\s+//) 491 { 492 $log = $_; 493 } 494 else 495 { 496 # It don't look like no ipmon output to me, baby. 497 next; 498 } 499 next unless (defined ($log)); 500 501 print STDERR "$log\n" if ($verbosity); 502 503 # Parse the log line. We're expecting interface name, rule group and 504 # number, an action code, a source host name or IP with possible port 505 # name or number, a destination host name or IP with possible port 506 # number, "PR", a protocol name or number, "len", a header length, a 507 # packet length (which will be in parentheses for protocols other than 508 # TCP, UDP, or ICMP), and maybe some additional info. 509 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); 510 unless (scalar (@fields)) 511 { 512 print STDERR "$me:$.: cannot parse: $_\n"; 513 next; 514 } 515 my ($count, $if, $group, $rule, $act, $src, $dest, $proto, $hlen, $len, $more) = @fields; 516 517 # Skip actions we're not interested in. 518 next unless (exists ($selectActs{$act})); 519 520 # Packet count defaults to 1. 521 $count = 1 unless (defined ($count)); 522 523 my ($sport, $dport, @flags); 524 525 if ($proto eq 'icmp') 526 { 527 if ($more =~ s/^icmp (\d+)\/(\d+)\s*//) 528 { 529 # We save icmp type and code in both sport and dport. This 530 # allows us to sort icmp packets using the normal port-sorting 531 # code. 532 $dport = $sport = "$1.$2"; 533 } 534 else 535 { 536 $sport = ''; 537 $dport = ''; 538 } 539 } 540 else 541 { 542 if ($showFlags) 543 { 544 if (($proto eq 'tcp') && ($more =~ s/^\-([A-Z]+)\s*//)) 545 { 546 push (@flags, $1); 547 } 548 if ($more =~ s/^K\-S\s*//) 549 { 550 push (@flags, 'state'); 551 } 552 } 553 if ($src =~ s/,([\-\w]+)$//) 554 { 555 $sport = &portSimplify ($1, $proto); 556 } 557 else 558 { 559 $sport = ''; 560 } 561 if ($dest =~ s/,([\-\w]+)$//) 562 { 563 $dport = &portSimplify ($1, $proto); 564 } 565 else 566 { 567 $dport = ''; 568 } 569 } 570 571 # Make sure addresses are numeric at this point. We want to sort by 572 # IP address later. If the hostname doesn't resolve, punt. If you 573 # must use ipmon -n, be ready for weirdness. Use only the first 574 # address returned. 575 my $x; 576 $x = (&hostAddrs ($src))[0]; 577 unless (defined ($x)) 578 { 579 print STDERR "$me:$.: cannot resolve hostname $src\n"; 580 next; 581 } 582 $src = $x; 583 $x = (&hostAddrs ($dest))[0]; 584 unless (defined ($x)) 585 { 586 print STDERR "$me:$.: cannot resolve hostname $dest\n"; 587 next; 588 } 589 $dest = $x; 590 591 # Skip hosts we're not interested in. 592 if ($selectAddrs) 593 { 594 my ($a, $m); 595 my $s = &integerAddr ($src); 596 my $d = &integerAddr ($dest); 597 my $cute = 0; 598 while (($a, $m) = each (%selectAddrs)) 599 { 600 if ((($s & $m) == $a) || (($d & $m) == $a)) 601 { 602 $cute = 1; 603 last; 604 } 605 } 606 next unless ($cute); 607 } 608 609 # Convert proto to proto number. 610 $proto = &protoNumber ($proto); 611 612 sub countPacket 613 { 614 my ($host, $dir, $peer, $proto, $count, $packet, @flags) = @_; 615 616 # Make sure host is in the hosts hash. 617 $hosts{$host} = 618 +{ 619 'd' => +{ }, 620 's' => +{ }, 621 } unless (exists ($hosts{$host})); 622 623 # Get the source/destination traffic hash for the host in question. 624 my $trafficHash = $hosts{$host}->{$dir}; 625 626 # Make sure there's a hash for the peer. 627 $trafficHash->{$peer} = +{ } unless (exists ($trafficHash->{$peer})); 628 629 # Make sure the peer hash has a hash for the protocol number. 630 my $peerHash = $trafficHash->{$peer}; 631 $peerHash->{$proto} = +{ } unless (exists ($peerHash->{$proto})); 632 633 # Make sure there's a counter for this packet type in the proto hash. 634 my $protoHash = $peerHash->{$proto}; 635 $protoHash->{$packet} = +{ '' => 0 } unless (exists ($protoHash->{$packet})); 636 637 # Increment the counter and mark flags. 638 my $packetHash = $protoHash->{$packet}; 639 $packetHash->{''} += $count; 640 map { $packetHash->{$_} = undef; } (@flags); 641 } 642 643 # Count the packet as outgoing traffic from the source address. 644 &countPacket ($src, 's', $dest, $proto, $count, "$sport:$dport:$if:$act", @flags) if ($sTable); 645 646 # Count the packet as incoming traffic to the destination address. 647 &countPacket ($dest, 'd', $src, $proto, $count, "$dport:$sport:$if:$act", @flags) if ($dTable); 648} 649 650my $dir; 651foreach $dir (@dirs) 652{ 653 my $order = ($dir eq 's' ? 'source' : 'destination'); 654 my $arrow = ($dir eq 's' ? '->' : '<-'); 655 656 print "###\n"; 657 print "### Traffic by $order address:\n"; 658 print "###\n"; 659 660 sub ipSort 661 { 662 &integerAddr ($a) <=> &integerAddr ($b); 663 } 664 665 sub packetSort 666 { 667 my ($asport, $adport, $aif, $aact) = split (/:/, $a); 668 my ($bsport, $bdport, $bif, $bact) = split (/:/, $b); 669 $bact cmp $aact || $aif cmp $bif || $asport <=> $bsport || $adport <=> $bdport; 670 } 671 672 my $host; 673 foreach $host (sort ipSort (keys %hosts)) 674 { 675 my $traffic = $hosts{$host}->{$dir}; 676 677 # Skip hosts with no traffic. 678 next unless (scalar (keys (%{$traffic}))); 679 680 if ($numeric) 681 { 682 print &dottedAddr ($host), "\n"; 683 } 684 else 685 { 686 print &hostName ($host), " \[", &dottedAddr ($host), "\]\n"; 687 } 688 689 my $peer; 690 foreach $peer (sort ipSort (keys %{$traffic})) 691 { 692 my $peerHash = $traffic->{$peer}; 693 my $peerName = ($numeric ? &dottedAddr ($peer) : &hostName ($peer)); 694 my $proto; 695 foreach $proto (sort (keys (%{$peerHash}))) 696 { 697 my $protoHash = $peerHash->{$proto}; 698 my $protoName = &protoName ($proto); 699 700 my $packet; 701 foreach $packet (sort packetSort (keys %{$protoHash})) 702 { 703 my ($sport, $dport, $if, $act) = split (/:/, $packet); 704 my $packetHash = $protoHash->{$packet}; 705 my $count = $packetHash->{''}; 706 $act = '?' unless (defined ($act = $acts{$act})); 707 if (($protoName eq 'tcp') || ($protoName eq 'udp')) 708 { 709 printf (" %-6s %7s %4d %4s %16s %2s %s.%s", $if, $act, $count, $protoName, &portName ($sport, $protoName), $arrow, $peerName, &portName ($dport, $protoName)); 710 } 711 elsif ($protoName eq 'icmp') 712 { 713 printf (" %-6s %7s %4d %4s %16s %2s %s", $if, $act, $count, $protoName, &icmpType ($sport), $arrow, $peerName); 714 } 715 else 716 { 717 printf (" %-6s %7s %4d %4s %16s %2s %s", $if, $act, $count, $protoName, '', $arrow, $peerName); 718 } 719 if ($showFlags) 720 { 721 my @flags = sort (keys (%{$packetHash})); 722 if (scalar (@flags)) 723 { 724 shift (@flags); 725 print ' (', join (',', @flags), ')' if (scalar (@flags)); 726 } 727 } 728 print "\n"; 729 } 730 } 731 } 732 } 733 734 print "\n"; 735} 736 737exit (0); 738 739# Translates a numeric port/named protocol to a port name. Reserved ports 740# that do not have an entry in the services database are left numeric. High 741# ports that do not have an entry in the services database are mapped 742# to '<high>'. 743sub portName 744{ 745 my $port = shift; 746 my $proto = shift; 747 my $pname = "$port/$proto"; 748 unless (exists ($pn{$pname})) 749 { 750 my $name = getservbyport ($port, $proto); 751 $pn{$pname} = (defined ($name) ? $name : ($port <= 1023 ? $port : '<high>')); 752 } 753 return $pn{$pname}; 754} 755 756# Translates a named port/protocol to a port number. 757sub portNumber 758{ 759 my $port = shift; 760 my $proto = shift; 761 my $pname = "$port/$proto"; 762 unless (exists ($pn{$pname})) 763 { 764 my $number = getservbyname ($port, $proto); 765 unless (defined ($number)) 766 { 767 # I don't think we need to recover from this. How did the port 768 # name get into the log file if we can't find it? Log file from 769 # a different machine? Fix /etc/services on this one if that's 770 # your problem. 771 die ("Unrecognized port name \"$port\" at $."); 772 } 773 $pn{$pname} = $number; 774 } 775 return $pn{$pname}; 776} 777 778# Convert all unrecognized high ports to the same value so they are treated 779# identically. The protocol should be by name. 780sub portSimplify 781{ 782 my $port = shift; 783 my $proto = shift; 784 785 # Make sure port is numeric. 786 $port = &portNumber ($port, $proto) 787 unless ($port =~ /^\d+$/); 788 789 # Look up port name. 790 my $portName = &portName ($port, $proto); 791 792 # Port is an unknown high port. Return a value that is too high for a 793 # port number, so that high ports get sorted last. 794 return $highPort if ($portName eq '<high>'); 795 796 # Return original port number. 797 return $port; 798} 799 800# Translates a numeric address into a hostname. Pass only packed numeric 801# addresses to this routine. 802sub hostName 803{ 804 my $ip = shift; 805 return $ipName{$ip} if (exists ($ipName{$ip})); 806 807 # Do an inverse lookup on the address. 808 my $name = gethostbyaddr ($ip, AF_INET); 809 unless (defined ($name)) 810 { 811 # Inverse lookup failed, so map the IP address to its dotted 812 # representation and cache that. 813 $ipName{$ip} = &dottedAddr ($ip); 814 return $ipName{$ip}; 815 } 816 817 # For paranoid hostname lookups. 818 if ($paranoid) 819 { 820 # If this address already matches, we're happy. 821 unless (exists ($ipName{$ip}) && (lc ($ipName{$ip}) eq lc ($name))) 822 { 823 # Do a forward lookup on the resulting name. 824 my @addr = &hostAddrs ($name); 825 my $match = 0; 826 827 # Cache the forward lookup results for future inverse lookups, 828 # but don't stomp on inverses we've already cached, even if they 829 # are questionable. We want to generate consistent output, and 830 # the cache is growing incrementally. 831 foreach (@addr) 832 { 833 $ipName{$_} = $name unless (exists ($ipName{$_})); 834 $match = 1 if ($_ eq $ip); 835 } 836 837 # Was this one of the addresses? If not, tack on a ?. 838 $name .= '?' unless ($match); 839 } 840 } 841 else 842 { 843 # Just believe it and cache it. 844 $ipName{$ip} = $name; 845 } 846 847 return $name; 848} 849 850# Translates a hostname or dotted address into a list of packed numeric 851# addresses. 852sub hostAddrs 853{ 854 my $name = shift; 855 my $ip; 856 857 # Check if it's a dotted representation. 858 return ($ip) if (defined ($ip = &isDottedAddr ($name))); 859 860 # Return result from cache. 861 $name = lc ($name); 862 return @{$ipAddr{$name}} if (exists ($ipAddr{$name})); 863 864 # Look up the addresses. 865 my @addr = gethostbyname ($name); 866 splice (@addr, 0, 4); 867 868 unless (scalar (@addr)) 869 { 870 # Again, I don't think we need to recover from this gracefully. 871 # If we can't resolve a hostname that ended up in the log file, 872 # punt. We want to be able to sort hosts by IP address later, 873 # and letting hostnames through will snarl up that code. Users 874 # of ipmon -n will have to grin and bear it for now. The 875 # functions that get undef back should treat it as an error or 876 # as some default address, e.g. 0 just to make things work. 877 return (); 878 } 879 880 $ipAddr{$name} = [ @addr ]; 881 return @{$ipAddr{$name}}; 882} 883 884# If the argument is a valid dotted address, returns the corresponding 885# packed numeric address, otherwise returns undef. 886sub isDottedAddr 887{ 888 my $addr = shift; 889 if ($addr =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) 890 { 891 my @a = (int ($1), int ($2), int ($3), int ($4)); 892 foreach (@a) 893 { 894 return undef if ($_ >= 256); 895 } 896 return pack ('C*', @a); 897 } 898 return undef; 899} 900 901# Unpacks a packed numeric address and returns an integer representation. 902sub integerAddr 903{ 904 my $addr = shift; 905 return unpack ('N', $addr); 906 907 # The following is for generalized IPv4/IPv6 stuff. For now, it's a 908 # lot faster to assume IPv4. 909 my @a = unpack ('C*', $addr); 910 my $a = 0; 911 while (scalar (@a)) 912 { 913 $a = ($a << 8) | shift (@a); 914 } 915 return $a; 916} 917 918# Unpacks a packed numeric address into a dotted representation. 919sub dottedAddr 920{ 921 my $addr = shift; 922 my @a = unpack ('C*', $addr); 923 return join ('.', @a); 924} 925 926# Translates a protocol number into a protocol name, or a number if no name 927# is found in the protocol database. 928sub protoName 929{ 930 my $code = shift; 931 return $code if ($code !~ /^\d+$/); 932 unless (exists ($pr{$code})) 933 { 934 my $name = scalar (getprotobynumber ($code)); 935 if (defined ($name)) 936 { 937 $pr{$code} = $name; 938 } 939 else 940 { 941 $pr{$code} = $code; 942 } 943 } 944 return $pr{$code}; 945} 946 947# Translates a protocol name or number into a protocol number. 948sub protoNumber 949{ 950 my $name = shift; 951 return $name if ($name =~ /^\d+$/); 952 unless (exists ($pr{$name})) 953 { 954 my $code = scalar (getprotobyname ($name)); 955 if (defined ($code)) 956 { 957 $pr{$name} = $code; 958 } 959 else 960 { 961 $pr{$name} = $name; 962 } 963 } 964 return $pr{$name}; 965} 966 967sub icmpType 968{ 969 my $typeCode = shift; 970 my ($type, $code) = split ('\.', $typeCode); 971 972 return "?" unless (defined ($code)); 973 974 my $info = $icmpTypeMap{$type}; 975 976 return "\(type=$type/$code?\)" unless (defined ($info)); 977 978 my $typeName = $info->{name}; 979 my $codeName; 980 if (exists ($info->{codes}->{$code})) 981 { 982 $codeName = $info->{codes}->{$code}; 983 $codeName = (defined ($codeName) ? "/$codeName" : ''); 984 } 985 else 986 { 987 $codeName = "/$code"; 988 } 989 return "$typeName$codeName"; 990} 991 992sub quit 993{ 994 my $ec = shift; 995 my $msg = shift; 996 997 print STDERR "$me: $msg\n"; 998 exit ($ec); 999} 1000 1001sub usage 1002{ 1003 my $ec = shift; 1004 my @msg = @_; 1005 1006 if (scalar (@msg)) 1007 { 1008 print STDERR "$me: ", join ("\n", @msg), "\n\n"; 1009 } 1010 1011 print <<EOT; 1012usage: $me [-nSDF] [-s servicemap] [-A act1,...] [address...] 1013 1014Parses logging from ipmon and presents it in a comprehensible format. This 1015program generates two reports: one organized by source address and another 1016organized by destination address. For the first report, source addresses are 1017sorted by IP address. For each address, all packets originating at the address 1018are presented in a tabular form, where all packets with the same source and 1019destination address and port are counted as a single entry. Any port number 1020greater than 1023 that does not match an entry in the services table is treated 1021as a "high" port; all high ports are coalesced into the same entry. The fields 1022for the source address report are: 1023 iface action packet-count proto src-port dest-host.dest-port \[\(flags\)\] 1024The fields for the destination address report are: 1025 iface action packet-count proto dest-port src-host.src-port \[\(flags\)\] 1026 1027Options are: 1028-n Disable hostname lookups, and report only IP addresses. 1029-p Perform paranoid hostname lookups. 1030-S Generate a source address report. 1031-D Generate a destination address report. 1032-F Show all flag combinations associated with packets. 1033-s map Supply an alternate services map to be preloaded. The map should 1034 be in the same format as /etc/services. Any service name not found 1035 in the map will be looked for in the system services file. 1036-A act1,... Limit the report to the specified actions. The possible actions 1037 are pass, block, log, short, and nomatch. 1038 1039If any addresses are supplied on the command line, the report is limited to 1040these hosts. Addresses may be given as dotted IP addresses or hostnames, and 1041may be qualified with netmasks in CIDR \(/24\) or dotted \(/255.255.255.0\) format. 1042If a hostname resolves to multiple addresses, all addresses are used. 1043 1044If neither -S nor -D is given, both reports are generated. 1045 1046Note: if you are logging traffic with ipmon -n, ipmon will already have looked 1047up and logged addresses as hostnames where possible. This has an important side 1048effect: this program will translate the hostnames back into IP addresses which 1049may not match the original addresses of the logged packets because of numerous 1050DNS issues. If you care about where packets are really coming from, you simply 1051cannot rely on ipmon -n. An attacker with control of his reverse DNS can map 1052the reverse lookup to anything he likes. If you haven't logged the numeric IP 1053address, there's no way to discover the source of an attack reliably. For this 1054reason, I strongly recommend that you run ipmon without the -n option, and use 1055this or a similar script to do reverse lookups during analysis, rather than 1056during logging. 1057EOT 1058 1059 exit ($ec); 1060} 1061 1062