6 use Getopt
::Long qw
/:config auto_version auto_help/;
11 use constant ETH_TYPE_ARP
=> 0x0806;
12 use constant ARP_OPCODE_REPLY
=> 2;
16 # list of NetPacket:: modules
17 my @modules = map { "NetPacket::$_" } qw
/Ethernet IP ARP ICMP TCP UDP/;
21 for my $module (@modules) {
23 # try to use installed version first
25 next MODULE
unless($@
);
28 warn "$module is not installed. Using bundled version instead\n";
31 # use bundled version instead
32 local @INC = ("$FindBin::Bin/../lib");
38 if($ENV{'DEBUG'} and $bundle) {
39 warn "Run this command to install missing modules:\n";
40 warn "\$ perl -MCPAN -e'install NetPacket'\n";
46 prads.pl - inspired by passive.sourceforge.net and http://lcamtuf.coredump.cx/p0f.shtml
58 --dev|-d : network device (default: eth0)
59 --config|-c : path to prads configfile
60 --service-signatures|-s : path to service-signatures file (default: /etc/prads/tcp-service.sig)
61 --os-fingerprints|-o : path to os-fingerprints file (default: /etc/prads/os.fp
62 --debug : enable debug messages 0-255 (default: disabled(0))
63 --dump : Dumps all signatures and fingerprints then exits
64 --arp : Enables ARP discover check
65 --service : Enables Service detection
66 --os : Enables OS detection
67 --db : DBI string (default: dbi:SQLite:dbname=prads.db)
68 --help : this help message
69 --version : show prads.pl version
80 our $DATABASE = q
(dbi
:SQLite
:dbname
=prads
.db
);
85 #my $DEVICE = q(eth0);
87 my $CONFIG = q
(/etc/prads
/prads
.conf
);
88 my $S_SIGNATURE_FILE = q
(/etc/prads
/tcp
-service
.sig
);
89 my $OS_SYN_FINGERPRINT_FILE = q
(/etc/prads
/os
.fp
);
90 my $OS_SYNACK_FINGERPRINT_FILE = q
(/etc/prads
/osa
.fp
);
91 my $MAC_SIGNATURE_FILE = q
(/etc/prads
/mac
.sig
);
94 init_dev
=> q
(Unable to determine network device
for monitoring
- %s),
95 lookup_net
=> q
(Unable to look up device information
for %s - %s),
96 create_object
=> q
(Unable to create packet capture on device
%s - %s),
97 compile_object_compile
=> q
(Unable to compile packet capture filter
),
98 compile_object_setfilter
=> q
(Unable to set packet capture filter
),
99 loop => q
(Unable to perform packet capture
),
102 # extract & load config before parsing rest of commandline
103 for my $i (0..@ARGV-1){
104 if($ARGV[$i] =~ /^--?(config|c)$/){
105 $CONFIG = splice @ARGV, $i, $i+1;
106 print "Loading config $CONFIG\n";
107 last; # we've modified @ARGV
110 # loads default config if none specified
111 my $conf = load_config
("$CONFIG");
112 my $C_INIT = $CONFIG;
114 $DATABASE = $conf->{'db'} if $conf->{'db'};
115 $DB_USERNAME = $conf->{'db'} if $conf->{'db'};
116 $DB_PASSWORD = $conf->{'db_username'};
117 $DEVICE = $conf->{interface
};
119 $SERVICE = $conf->{service
};
120 $DEBUG = $conf->{debug
};
121 $OS = $conf->{os_fingerprint
};
122 $OS = $conf->{os_synack_fingerprint
};
123 $OS = $conf->{os_syn_fingerprint
};
124 $BPF = $conf->{bpfilter
};
127 # commandline overrides config
129 'config|c=s' => \
$CONFIG,
130 'dev|d=s' => \
$DEVICE,
131 'service-signatures|s=s' => \
$S_SIGNATURE_FILE,
132 'os-fingerprints|o=s' => \
$OS_SYN_FINGERPRINT_FILE,
133 'debug=s' => \
$DEBUG,
136 'service' => \
$SERVICE,
141 # if 2nd config file specified, load that one too
142 if ($C_INIT ne $CONFIG){
143 load_config
("$CONFIG");
146 #my @array = split(/\s+/, $conf->{array-param});
147 #my $variable = $conf->{variable};
148 #$DEBUG = $conf->{debug};
150 my $PRADS_HOSTNAME = $conf->{hostname
};
151 $PRADS_HOSTNAME ||= `hostname`;
152 chomp $PRADS_HOSTNAME;
154 my $PRADS_START = time;
157 print "\n ##### Dumps all signatures and fingerprints then exits ##### \n";
159 warn "Loading MAC fingerprints\n" if ($DEBUG>0);
160 my $MAC_SIGS = load_mac
($MAC_SIGNATURE_FILE);
161 print Dumper
$MAC_SIGS;
163 print "\n *** Loading OS fingerprints *** \n\n";
164 my $OS_SYN_SIGS = load_os_syn_fingerprints
($OS_SYN_FINGERPRINT_FILE, $OS_SYNACK_FINGERPRINT_FILE);
165 print Dumper
$OS_SYN_SIGS;
167 print "\n *** Loading Service signatures *** \n\n";
168 my @TCP_SERVICE_SIGNATURES = load_signatures
($S_SIGNATURE_FILE);
169 print Dumper
@TCP_SERVICE_SIGNATURES;
171 print "\n *** Loading MTU signatures *** \n\n";
172 my $MTU_SIGNATURES = load_mtu
("/etc/prads/mtu.sig");
173 print Dumper
$MTU_SIGNATURES;
178 warn "Starting prads.pl...\n";
180 warn "Loading OS fingerprints\n" if ($DEBUG>0);
181 my $OS_SYN_SIGS = load_os_syn_fingerprints
($OS_SYN_FINGERPRINT_FILE, $OS_SYNACK_FINGERPRINT_FILE)
182 or Getopt
::Long
::HelpMessage
();
185 warn "Loading MAC fingerprints\n" if ($DEBUG>0);
186 my $MAC_SIGS = load_mac
($MAC_SIGNATURE_FILE);
188 warn "Loading MTU fingerprints\n" if ($DEBUG>0);
189 my $MTU_SIGNATURES = load_mtu
("/etc/prads/mtu.sig")
190 or Getopt
::Long
::HelpMessage
();
192 warn "Initializing device\n" if ($DEBUG>0);
193 warn "Using $DEVICE\n" if $DEVICE;
194 $DEVICE = init_dev
($DEVICE)
195 or Getopt
::Long
::HelpMessage
();
197 warn "Loading TCP Service signatures\n" if ($DEBUG>0);
198 my @TCP_SERVICE_SIGNATURES = load_signatures
($S_SIGNATURE_FILE)
199 or Getopt
::Long
::HelpMessage
();
201 warn "Loading UDP Service signatures\n" if ($DEBUG>0);
202 # Currently loading the wrong sig file :)
203 my @UDP_SERVICE_SIGNATURES = load_signatures
($S_SIGNATURE_FILE)
204 or Getopt
::Long
::HelpMessage
();
206 warn "Loading persistent database ". $DATABASE ."\n" if ($DEBUG > 0);
207 $OS_SYN_DB = load_persistent
($DATABASE,$DB_USERNAME,$DB_PASSWORD);
209 warn "Creating object\n" if ($DEBUG>0);
210 my $PCAP = create_object
($DEVICE);
212 warn "Compiling Berkeley Packet Filter\n" if ($DEBUG>0);
213 filter_object
($PCAP);
215 warn "Looping over object\n" if ($DEBUG>0);
216 Net
::Pcap
::loop($PCAP, -1, \
&packets
, '') or die $ERROR{'loop'};
218 warn "Closing device\n" if ($DEBUG>0);
219 Net
::Pcap
::close($PCAP);
225 =head2 load_persistent
227 Load persistent database
231 sub load_persistent
{
232 my ($db,$user,$password) = @_;
233 my $dbh = DBI
->connect($db,$user,$password);
237 my $ms = $SIG{__WARN__
};
238 $SIG{'__WARN__'} = sub { };
239 #my $sql = "DROP TABLE asset";
240 #my $sth = $dbh->prepare($sql);
242 $sql = "CREATE TABLE asset (ip TEXT, service TEXT, time TEXT, fingerprint TEXT,".
243 "mac TEXT, os TEXT, details TEXT, link TEXT, distance TEXT, reporting TEXT)";
244 $sth = $dbh->prepare($sql);
245 #$dbh->{PrintError} = 0;
246 #$dbh->{RaiseError} = 0;
247 #$dbh->{PrintWarn} = 0;
252 $SIG{'__WARN__'} = $ms;
255 #$sql = "DELETE FROM asset WHERE service = 'ARP'";
256 #$sth = $dbh->prepare($sql) or die "foo $!";
257 #$sth->execute or die "$!";
258 $sql = "SELECT * from asset";
259 $sth = $dbh->prepare($sql) or die "foo $!";
260 $sth->execute or die "$!";
263 #$dbh->{'RaiseError'} = 1;
271 Callback function for C<Net::Pcap::loop>.
273 * Strip ethernet encapsulation of captured packet
274 * pass to protocol handlers
277 my ($user_data, $header, $packet) = @_;
278 $pradshosts{"tstamp"} = time;
279 warn "Packet received - processing...\n" if($DEBUG>50);
281 #setup the storage hash.. could also let adding to DB be caller's job
282 my $ethernet = NetPacket
::Ethernet
::strip
($packet);
283 my $eth = NetPacket
::Ethernet
->decode($packet);
285 # Check if arp - get mac and register...
286 if ($ARP == 1 && $eth->{type
} == ETH_TYPE_ARP
) {
287 arp_check
($eth, $pradshosts{"tstamp"});
291 unless(NetPacket
::IP
->decode($ethernet)) {
292 warn "Not an IP packet..\n" if($DEBUG>50);
293 warn "Done...\n\n" if($DEBUG>50);
297 # We should now have us an IP packet... good!
298 my $ip = NetPacket
::IP
->decode($ethernet);
301 # Collect necessary info from IP packet; if
302 my $ttl = $ip->{'ttl'};
303 my $ipopts = $ip->{'options'}; # Not used in p0f
304 my $len = $ip->{'len'}; # total length of packet
305 my $id = $ip->{'id'};
307 my $ipflags = $ip->{'flags'}; # 2=dont fragment/1=more fragments, 0=nothing set
310 $df = 1; # Dont fragment
312 $df = 0; # Fragment or more fragments
315 # Check if this is a TCP packet
316 if($ip->{proto
} == 6) {
317 warn "Packet is of type TCP...\n" if($DEBUG>50);
318 packet_tcp
($ip, $ttl, $ipopts, $len, $id, $ipflags, $df);
320 }elsif ($ip->{proto
} == 17) {
321 # Can one do UDP OS detection !??!
322 warn "Packet is of type UDP...\n" if($DEBUG>30);
323 my $udp = NetPacket
::UDP
->decode($ip->{'data'});
324 if ($udp->{'data'} && $SERVICE == 1) {
325 udp_service_check
($udp->{'data'},$ip->{'src_ip'},$udp->{'src_port'},$pradshosts{"tstamp"});
331 warn "Done...\n\n" if($DEBUG>50);
338 * Decode contents of TCP/IP packet contained within captured ethernet packet
339 * Search through the signatures, print dst host, dst port, and ID String.
340 * Collect pOSf data : ttl,tot,orig_df,op,ocnt,mss,wss,wsc,tstamp,quirks
341 # Fingerprint entry format:
343 # wwww:ttt:D:ss:OOO...:QQ:OS:Details
345 # wwww - window size (can be * or %nnn or Sxx or Txx)
346 # "Snn" (multiple of MSS) and "Tnn" (multiple of MTU) are allowed.
348 # D - don't fragment bit (0 - not set, 1 - set)
349 # ss - overall SYN packet size (* has a special meaning)
350 # OOO - option value and order specification (see below)
351 # QQ - quirks list (see below)
352 # OS - OS genre (Linux, Solaris, Windows)
353 # details - OS description (2.0.27 on x86, etc)
357 my ($ip, $ttl, $ipopts, $len, $id, $ipflags, $df) = @_;
358 # Collect necessary info from TCP packet; if
359 my $tcp = NetPacket
::TCP
->decode($ip->{'data'});
360 my $winsize = $tcp->{'winsize'};
361 my $tcpflags= $tcp->{'flags'};
362 my $tcpopts = $tcp->{'options'};
363 my $seq = $tcp->{'seqnum'};
364 my $ack = $tcp->{'acknum'};
365 my $urg = $tcp->{'urg'};
366 my $data = $tcp->{'data'};
367 my $reserved= $tcp->{'reserved'};
368 my $src_port= $tcp->{'src_port'};
369 my $dst_port= $tcp->{'dst_port'};
371 # Check if SYN is set (both SYN and SYN+ACK)
372 if ($OS == 1 && ($tcpflags & SYN
)){
373 warn "Initial connection... Detecting OS...\n" if($DEBUG>20);
374 my ($optcnt, $scale, $mss, $sackok, $ts, $optstr, @quirks) = check_tcp_options
($tcpopts);
376 # big packets are packets of size > 100
377 my $tot = ($len < 100)?
$len : 0;
379 # do we have an all-zero timestamp?
380 my $t0 = (not defined $ts or $ts != 0)?
0:1;
382 # parse rest of quirks
383 push @quirks, check_quirks
($id,$ipopts,$urg,$reserved,$ack,$tcpflags,$data);
384 my $quirkstring = quirks_tostring
(@quirks);
386 my $src_ip = $ip->{'src_ip'};
389 my $packet = "ip:$src_ip size=$len ttl=$ttl, DF=$df, ipflags=$ipflags, winsize=$winsize, tcpflags=$tcpflags, OC:$optcnt, WSC:$scale, MSS:$mss, SO:$sackok,T0:$t0, Q:$quirkstring O: $optstr ($seq/$ack) tstamp=" . $pradshosts{"tstamp"};
390 print "OS: $packet\n" if($DEBUG > 2);
392 # We need to guess initial TTL
393 my $gttl = normalize_ttl
($ttl);
394 my $dist = $gttl - $ttl;
396 my $wss = normalize_wss
($winsize, $mss);
397 my $fpstring = "$wss:$gttl:$df:$tot:$optstr:$quirkstring";
399 # TODO: make a list of previously matched OS'es (NAT ips) and
400 # check on $db->{$ip}->{$fingerprint}
402 my ($os, $details, @more) = os_find_match
(
403 $tot, $optcnt, $t0, $df,\
@quirks, $mss, $scale,
404 $winsize, $gttl, $optstr, $src_ip, $fpstring);
407 my $link = get_mtu_link
($mss);
409 # asset database: want to know the following intel:
410 # src ip, {OS,DETAILS}, service (port), timestamp, fingerprint
411 # maybe also add binary IP packet for audit?
412 if ($tcpflags & ACK
){
413 add_asset
('SYNACK', $src_ip, $fpstring, $dist, $link, $os, $details, @more);
415 add_asset
('SYN', $src_ip, $fpstring, $dist, $link, $os, $details, @more);
418 ### SERVICE: DETECTION
419 ### Can also do src/dst_port
420 if ($tcp->{'data'} && $SERVICE == 1) {
421 # Check content(TCP data) against signatures
422 tcp_service_check
($tcp->{'data'},$ip->{'src_ip'},$tcp->{'src_port'},$pradshosts{"tstamp"});
429 Function to match options
435 my @o1 = split /,/,$o1;
436 my @o2 = split /,/,$o2;
438 print "$_:$o2[0]\n" if $DEBUG & 8;
439 if(/([MW])(\d*|\*)/){
440 if(not $o2[0] =~ /$1($2|\*)/){
441 print "$o2[0] != $1$2\n" if $DEBUG > 1;
444 }elsif($_ ne $o2[0]){
454 port of p0f find_match()
455 # WindowSize : InitialTTL : DontFragmentBit : Overall Syn Packet Size : Ordered Options Values : Quirks : OS : Details
457 returns: ($os, $details, [...])
460 for each signature in db:
461 match packet size (0 means >= PACKET_BIG (=200))
462 match tcp option count
464 match don't fragment bit (ip->off&0x4000!= 0)
467 check MSS (mod or no)
470 -- do complex windowsize checks
475 == dump unknow packet
478 NAT checks, unknown packets, error handling, refactor
482 # Port of p0f matching code
483 my ($tot, $optcnt, $t0, $df, $qq, $mss, $scale, $winsize, $gttl, $optstr, $ip, $fp) = @_;
485 my $sigs = $OS_SYN_SIGS;
488 #warn "Matching $packet\n" if $DEBUG;
489 #sigs ($ss,$oc,$t0,$df,$qq,$mss,$wsc,$wss,$oo,$ttl)
492 my @ec = ('packet size', 'option count', 'zero timestamp', 'don\'t fragment bit');
493 for($tot, $optcnt, $t0, $df){
495 $matches = $matches->{$_};
496 #print "REDUCE: $j:$_: " . Dumper($matches). "\n";
500 print "ERR: $ip [$fp] Packet has no match for $ec[$j]:$_\n";
504 # we should have $matches now.
505 warn "ERR: $ip [$fp] No match in fp db, but should have a match.\n" and return if not $matches;
507 #print "INFO: p0f tot:oc:t0:frag match: " . Dumper($matches). "\n";
509 $matches = $matches->{'.'};
510 warn "ERR: $ip [$fp] No quirks match.\n" and return if not defined $matches;
515 next if @qq != @quirks;
524 $matches = $matches->{$_} and last if $i == @quirks;
526 warn "ERR: $ip [$fp] No quirks match\n" and return if not $i;
528 #print "INFO: p0f quirks match: " . Dumper( $matches). "\n";
530 # Maximum Segment Size
531 my @mssmatch = grep {
532 (/^\%(\d)*$/ and ($mss % $_) == 0) or
533 (/^(\d)*$/ and $mss eq $_) or
536 #print "INFO: p0f mss match: " . Dumper(@mssmatch). "\n";
537 warn "ERR: $ip [$fp] No mss match in fp db.\n" and return if not @mssmatch;
539 # WSCALE. There may be multiple simultaneous matches to search beyond this point.
540 my (@wmatch,@fuzmatch);
541 for my $s (@mssmatch){
542 for my $wsc ($scale, '*'){
543 my $t = $matches->{$s}->{$wsc};
547 for my $wss (keys %$t){
548 #print "INFO: wss:$winsize,$_, " . Dumper($t->{$_}) ."\n";
549 if( ($wss =~ /S(\d*)/ and $1*$mss == $winsize) or
550 ($wss =~ /M(\d*)/ and $1*($mss+40) == $winsize) or
551 ($wss =~ /%(\d*)/ and $winsize % $1 == 0) or
552 ($wss eq $winsize) or
555 push @wmatch, $t->{$wss};
557 push @fuzmatch, $t->{$wss};
562 if(not @wmatch and @fuzmatch){
567 print "$pradshosts{tstamp} $ip [$fp] Closest matches: \n" if $DEBUG > 0;
568 for my $s (@mssmatch){
569 print Data
::Dumper
->Dump([$matches->{$s}],["MSS$s"]) if $DEBUG >0;
574 #print "INFO: wmatch: " . Dumper(@wmatch) ."\n";
576 # TCP option sequence
580 #print "INFO: omatch:$optstr:$_ " .Dumper($h->{$_}) ."\n";
581 push @omatch, $h->{$_} and last if match_opts
($optstr,$_);
585 print "$pradshosts{tstamp} $ip [$fp] Closest matches: \n";
586 print Data
::Dumper
->Dump([@wmatch],["WSS"]);
592 my $match = $_->{$gttl};
593 if(not $match and $gttl < 255){
594 # re-normalize ttl, machine may be really distant
595 # (over ttl/2 hops away)
596 my $ttl = normalize_ttl
($gttl+1);
597 #print "Re-adjusted ttl from $gttl to $ttl\n" if $ttl != 64;
600 #print "INFO: omatch: " .Dumper($match) ."\n";
603 push @os, ($_, $match->{$_});
608 print "$pradshosts{tstamp} $ip [$fp] Closest matches: \n" if $DEBUG > 0;
609 print Data
::Dumper
->Dump([@omatch],["TTL"]) if $DEBUG > 0;
613 # if we have non-generic matches, filter out generics
617 # loop through to check for non-generics
618 my ($os, $details, @more) = @os;
624 ($os, $details, @more) = @more;
627 ($os, $details, @more) = @os;
629 if(not ($skip and $os =~ /^@/)){
630 push @filtered, ($os, $details);
632 ($os, $details, @more) = @more;
640 # quirk P (opts past EOL) and T(non-zero 2nd timestamp) are implemented in
641 # check_tcp_options, where it makes most sense.
642 # TODO: '!' : broken opts (?)
647 my ($id,$ipopts,$urg,$reserved,$ack,$tcpflags,$data) = @_;
650 push @quirks, 'Z' if not $id;
651 push @quirks, 'I' if $ipopts;
652 push @quirks, 'U' if $urg;
653 push @quirks, 'X' if $reserved;
654 push @quirks, 'A' if $ack;
655 push @quirks, 'F' if $tcpflags & ~(SYN
|ACK
);
656 push @quirks, 'D' if $data;
660 =head2 quirks_tostring
661 Function to make quirks into a string.
664 sub quirks_tostring
{
666 my $quirkstring = '';
670 $quirkstring = '.' if not @quirks;
675 =head2 check_tcp_options
677 Takes tcp options as input, and returns which args are set.
683 ($count, $scale, $mss, $sackok, $ts, $optstr, $quirks);
687 sub check_tcp_options
{
688 # NetPacket::IP->decode gives us binary opts
689 # so get the interesting bits here
691 my ($scale, $mss, $sackok, $ts, $t2) = (0,undef,0,undef,0);
692 print "opts: ". unpack("B*", $opts)."\n" if $DEBUG & 8;
693 my ($kind, $rest, $size, $data, $count) = (0,0,0,0,0);
697 ($kind, $rest) = unpack("C a*", $opts);
701 print "EOL\n" if $DEBUG & 8;
703 # quirk if opts past EOL
704 push @quirks, 'P' if $rest ne '';
708 print "NOP\n" if $DEBUG & 8;
711 ($size, $rest) = unpack("C a*", $rest);
712 #print "$kind # $size\n";
714 #($data, $rest) = unpack "C${size}a", $rest;
716 ($mss, $rest) = unpack("n a*", $rest);
718 print "$size MSS: $mss\n" if $DEBUG & 8;
720 ($scale, $rest) = unpack("C a*", $rest);
721 $optstr .= "W$scale,";
722 print "WSOPT$size: $scale\n" if $DEBUG & 8;
726 print "SACKOK\n" if $DEBUG & 8;
730 my ($c, $t, $tsize) = (0,0,$size);
732 ($c, $rest) = unpack("C a*", $rest);
733 # hack HACK: ts is 64bit and wraps our 32bit perl ints.
734 # it's ok tho: we don't care what the value is, as long as it's not 0
739 print "TS$size: $t\n" if $DEBUG & 8;
745 if(defined $ts and $t){
746 # non-zero second timestamp
753 $optstr .= "?$kind,";
754 ($rest) = unpack("x$size a*", $rest);
755 print "unknown $kind:$size:" if $DEBUG & 8;
757 print "rest: ". unpack("B*", $rest)."\n" if $DEBUG & 8;
760 last if not defined $opts;
763 $optstr = '.' if $optstr eq '';
765 # MSS may be undefined
766 $mss = '*' if not $mss;
768 return ($count, $scale, $mss, $sackok, $ts, $optstr, @quirks);
772 =head2 load_signatures
774 Loads signatures from file
777 <service>,<version info>,<signature>
780 www,v/Apache/$1/$2/,Server: Apache\/([\S]+)[\s]+([\S]+)
784 sub load_signatures
{
788 open(my $FH, "<", $file) or die "Could not open '$file': $!";
791 while (my $line = readline $FH) {
794 next LINE
unless($line); # empty line
795 # One should check for a more or less sane signature file.
796 my($service, $version, $signature) = split /,/, $line, 3;
800 $signatures{$signature} = [$service, qq("$version"), qr{$signature}];
803 return map { $signatures{$_} }
804 sort { length $b <=> length $a }
810 Loads MTU signatures from file
824 open(my $FH, "<", $file) or die "Could
not open '$file': $!";
827 while (my $line = readline $FH) {
830 next LINE unless($line); # empty line
831 # One should check for a more or less sane signature file.
832 my($mtu, $info) = split /,/, $line, 2;
833 $signatures->{$mtu} = $info;
840 Loads MAC signatures from file
843 AB:CD:EE Vendor # DETAILS
845 hash->{'byte'}->{'byte'}->...
847 on conflicts, if we have two sigs
849 00-E0-2B-00-00-01 Extreme-EEP
850 hash->{00}->{E0}->{2B}->{00}->{00}->{01} = Extreme-EEP
851 hash->{00}->{E0}->{2B}->{_} = Extreme
853 if you know of a more efficient way of looking up these things,
854 look me up and we'll discuss it.
862 open(my $FH, "<", $file) or die "Could
not open '$file': $!";
865 while (my $line = readline $FH) {
867 $line =~ s/^\s*\#.*//;
868 next LINE unless($line); # empty line
869 # One should check for a more or less sane signature file.
870 my($mac, $info, $details) = split /\s/, $line, 3;
872 $details =~ /\# (.*)$/;
875 #print "$mac : $info, $details\n";
876 my ($prefix, $mask) = split /\//, $mac, 2;
878 my ($max, $rem) = ($mask / 8, $mask % 8);
879 my @bytes = split /[:\.\-]/, $prefix, $max;
882 push(@bytes, sprintf "%s/%d", pop @bytes, $rem);
884 push @bytes, sprintf "00/%d", $rem;
887 my $ptr = $signatures;
888 for my $i (0..@bytes-1){
889 my $byte = lc $bytes[$i];
890 $ptr->{$byte} ||= {};
891 if(not ref $ptr->{$byte}){
892 $ptr->{$byte} = { _ => $ptr->{$byte} };
895 if($ptr->{$byte}->{_}){
896 print "XXX
: $info $mac crashes with
".Dumper($ptr->{$byte}->{_});
899 $ptr->{$byte}->{_} = [$mac, $info, $details];
902 $ptr = $ptr->{$byte};
907 #print "$mac : $mask\n" if $mask;
909 print "!!11!!$mac $prefix\n" if $max != 6;
911 # handle mac bitmasks (in)sanely
912 my ($rest, $bits) = (0, 0);
913 my $ptr = $signatures;
914 for my $i (0..@bytes-1){
915 my $byte = lc $bytes[$i];
917 $rest = ($mask? $mask - $bits:0);
919 if((not $mask and $i == @bytes-1) or ($mask and $rest == 0)){
920 # ran out of bitmask or bytes
921 if(ref $ptr->{$byte}){
922 if($ptr->{$byte}->{_}){
923 print "ERRXXX
: Clash
$mac with
$ptr->{$byte}->{_
}\n";
926 $ptr->{$byte}->{_} = [$mac, $info, $details];
928 $ptr->{$byte} = [$mac, $info, $details];
931 }elsif($rest > 8 or not $mask){
933 $ptr->{$byte} ||= {};
934 if(not ref $ptr->{$byte}){
935 $ptr->{$byte} = { _ => $ptr->{$byte} };
937 $ptr = $ptr->{$byte};
940 # $mask and $rest < 8
942 $ptr->{"$byte/$rest"} = [$mac, $info, $details];
944 print "$byte/XXX/$rest/$mask;";
946 last; # because some mac strings don't terminate on the dollar.
949 if($mask and $rest > 0){
950 print "00/ZZZ/$rest";
951 $ptr->{"00/$rest"} = [$mac, $info, $details];
953 #print " $info, $details\n";
955 #print Data::Dumper->Dump([$ptr],["*m
"]) if $mask;
958 #print Data::Dumper->Dump([$signatures],["*m
"]);
962 =head2 mac_find_match
964 Match the MAC address with our vendor prefix hash.
971 my ($byte, $rest) = split /[:\.-]/, $mac,2;
972 #print "mac matching
: $byte, $rest ";
973 if(ref $ptr->{$byte}){
975 my $match = mac_find_match($rest,$ptr->{$byte}) || $ptr->{$byte}->{_};
977 #print "reduce
$ptr->{$byte}\n";
978 return $ptr->{$byte};
981 print ":$byte: match
\n";
982 $ptr = $ptr->{$byte};
984 # this is end-of-line
985 my @masks = grep { /([0-9a-fA-F][0-9a-fA-F])\/(\d*)/ } keys %$ptr;
986 print Data::Dumper->Dump([$ptr ],['*ptr']);
988 print Data::Dumper->Dump([$ptr->{$_} ],['@macmask']);
990 return $ptr; # hopefully (one or more) valid sig.
999 =head2 load_os_syn_fingerprints
1001 Loads SYN signatures from file
1002 optimize for lookup matching
1004 if you know of a more efficient way of looking up these things,
1005 look me up and we'll discuss it.
1008 sub load_os_syn_fingerprints {
1011 # Fingerprint entry format:
1012 # WindowSize : InitialTTL : DontFragmentBit : Overall Syn Packet Size : Ordered Options Values : Quirks : OS : Details
1013 #my $re = qr{^ ([0-9%*()ST]+) : (\d+) : (\d+) : ([0-9()*]+) : ([^:]+) : ([^\s]+) : ([^:]+) : ([^:]+) }x; # suuure, validate this!
1015 for my $file (@files) {
1016 open(my $FH, "<", $file) or die "Could
not open '$file': $!";
1019 while (my $line = readline $FH) {
1023 next unless($line); # empty line
1025 my @elements = split/:/,$line;
1026 unless(@elements == 8) {
1027 die "Error
: Not valid fingerprint format
in: '$file'";
1029 my ($wss,$ttl,$df,$ss,$oo,$qq,$os,$detail) = @elements;
1032 my ($mss, $wsc) = ('*','*');
1036 my @opt = split /[, ]/, $oo;
1039 if(/([MW])([\d%*]*)/){
1051 my($details, $human) = splice @elements, -2;
1054 for my $e ($ss,$oc,$t0,$df,$qq,$mss,$wsc,$wss,$oo,$ttl){
1058 if($tmp->{$details}){
1059 print "$file:$lineno:Conflicting signature
: '$line' overwrites earlier signature
'$details:$tmp->{$details}'\n\n" if ($DEBUG);
1061 $tmp->{$details} = $human;
1069 Use network device passed in program arguments or if no
1070 argument is passed, determine an appropriate network
1071 device for packet sniffing using the
1072 Net::Pcap::lookupdev method
1080 unless (defined $dev) {
1081 $dev = Net::Pcap::lookupdev(\$err);
1082 die sprintf $ERROR{'init_dev'}, $err if defined $err;
1090 Look up network address information about network
1091 device using Net::Pcap::lookupnet - This also acts as a
1092 check on bogus network device arguments that may be
1093 passed to the program as an argument
1099 my($err, $address, $netmask);
1101 Net::Pcap::lookupnet(
1102 $dev, \$address, \$netmask, \$err
1103 ) and die sprintf $ERROR{'lookup_net'}, $dev, $err;
1105 warn "lookup_net
: $address, $netmask\n" if($DEBUG>0);
1106 return $address, $netmask;
1109 =head2 create_object
1111 Create packet capture object on device
1120 $object = Net::Pcap::open_live($dev, 1500, $promisc, 0, \$err)
1121 or die sprintf $ERROR{'create_object'}, $dev, $err;
1122 warn "create_object
: $dev\n" if($DEBUG>0);
1126 =head2 filter_object
1128 Compile and set packet filter for packet capture
1129 object - For the capture of TCP packets with the SYN
1130 header flag set directed at the external interface of
1131 the local host, the packet filter of '(dst IP) && (tcp
1132 [13] & 2 != 0)' is used where IP is the IP address of
1133 the external interface of the machine. Here we use 'tcp'
1134 as a default BPF filter.
1140 # my($address, $netmask) = lookup_net($DEVICE);
1143 # my $BPF = q(tcp and src net 192.168.0.0 mask 255.255.255.0);
1144 # my $BPF = q(ip and src net 87.238.45.0/24);
1145 # my $BPF = q(src net 0.0.0.0 mask 0.0.0.0 or dst net 0.0.0.0 mask 0.0.0.0);
1148 $object, \$filter, $BPF, 0, $netmask
1149 ) and die $ERROR{'compile_object_compile'};
1151 Net::Pcap::setfilter($object, $filter)
1152 and die $ERROR{'compile_object_setfilter'};
1153 warn "filter_object
: $filter\n" if($DEBUG>0);
1156 =head2 normalize_wss
1158 Computes WSS respecive of MSS
1162 my ($winsize, $mss) = @_;
1164 if ($mss =~ /^[+-]?\d+$/) {
1165 if (not $winsize % $mss){
1166 $wss = $winsize / $mss;
1168 }elsif(not $winsize % ($mss +40)){
1169 $wss = $winsize / ($mss + 40);
1176 =head2 normalize_ttl
1178 Takes a ttl value as input, and guesses intial ttl
1185 # Only aiming for 255,128,64,60,32. But some strange ttls like
1186 # 200,30 exist, but are rare
1187 $gttl = 255 if (($ttl >= 128) && (255 > $ttl));
1188 $gttl = 128 if ((128 >= $ttl) && ($ttl > 64));
1189 $gttl = 64 if (( 64 >= $ttl) && ($ttl > 32));
1190 $gttl = 32 if (( 32 >= $ttl));
1194 =head2 tcp_service_check
1196 Takes input: $tcp->{'data'}, $ip->{'src_ip'}, $tcp->{'src_port'}, $pradshosts{"tstamp
"}
1197 Prints out service if found.
1201 sub tcp_service_check {
1202 my ($tcp_data, $src_ip, $src_port,$tstamp) = @_;
1204 # Check content(tcp_data) against signatures
1206 for my $s (@TCP_SERVICE_SIGNATURES) {
1209 if($tcp_data =~ /$re/) {
1210 my($vendor, $version, $info) = split m"/", eval $s->[1];
1211 add_asset('SERVICE', $src_ip, $src_port, $vendor, $version, $info);
1212 # printf("Service
: ip
=%s port
=%i -> \"%s %s %s\" timestamp
=%i\n",
1213 # $src_ip, $src_port,
1224 =head2 udp_service_check
1226 Takes input: $udp->{'data'}, $ip->{'src_ip'}, $udp->{'src_port'}, $pradshosts{"tstamp
"}
1227 Prints out service if found.
1231 sub udp_service_check {
1232 my ($udp_data, $src_ip, $src_port,$tstamp) = @_;
1234 # Make UDP asset detection here... PoC CODE at the moment.
1235 ### When ready - call udp_service_check ($udp->{'data'},$ip->{'src_ip'},$udp->{'src_port'},$pradshosts{"tstamp
"});
1236 #warn "Detecting UDP asset
...\n" if($DEBUG);
1237 if ($src_port == 53){
1238 add_asset('SERVICE', $src_ip, $src_port, "-","-","DNS
");
1239 # printf ("Service
: ip
=%s port
=%i -> \"DNS
\" timestamp
=%i\n",$src_ip, $src_port, $tstamp);
1241 elsif ($src_port == 1194){
1242 add_asset('SERVICE', $src_ip, $src_port, "OpenVPN
","-","-");
1243 # printf ("Service
: ip
=%s port
=%i -> \"OpenVPN
\" timestamp
=%i\n",$src_ip, $src_port, $tstamp);
1246 warn "UDP ASSET DETECTION IS NOT IMPLEMENTED YET
...\n" if($DEBUG>20);
1249 # # Check content(udp_data) against signatures
1251 # for my $s (@UDP_SERVICE_SIGNATURES) {
1254 # if($udp_data =~ /$re/) {
1255 # my($vendor, $version, $info) = split m"/", eval $s->[1];
1256 # printf("SERVICE
: ip
=%s port
=%i -> \"%s %s %s\" timestamp
=%i\n",
1257 # $src_ip, $src_port,
1270 Takes 'NetPacket::Ethernet->decode($packet)' and timestamp as input and prints out arp asset.
1275 my ($eth,$tstamp) = @_;
1277 my $arp = NetPacket::ARP->decode($eth->{data}, $eth);
1278 my $aip = $arp->{spa};
1279 my $h1 = hex(substr( $aip,0,2));
1280 my $h2 = hex(substr( $aip,2,2));
1281 my $h3 = hex(substr( $aip,4,2));
1282 my $h4 = hex(substr( $aip,6,2));
1283 my $ip = "$h1.$h2.$h3.$h4";
1285 my $ash = $arp->{sha};
1286 # more human readable
1287 # join(':', split(/([0-9a-fA-F][0-9a-fA-F])/, $ash);
1289 substr($ash,0,2) .':'.
1290 substr($ash,2,2) .':'.
1291 substr($ash,4,2) .':'.
1292 substr($ash,6,2) .':'.
1293 substr($ash,8,2) .':'.
1295 add_asset('ARP', $mac, $ip, @{mac_find_match($mac)});
1300 Takes MSS as input, and returns a guessed Link for that MTU.
1306 my $link = "UNKNOWN
";
1307 # if ($mss =~ m/^[0-9]+$/) {
1308 if ($mss =~ /^[+-]?\d+$/) {
1309 my $mtu = $mss + 40;
1310 if (my $link = $MTU_SIGNATURES->{ $mtu }) {return $link}
1317 Reads the configuration file and loads variables.
1318 Takes the config file as input, and returns a hash of config options.
1326 warn "Config
'$file' not readable
\n";
1329 open(my $FH, "<",$file) or die "Could
not open '$file': $!\n";
1330 while (my $line = <$FH>) {
1333 next unless($line); # empty line
1334 if (my ($key, $value) = ($line =~ m/(\w+)\s*=\s*(.*)$/)) {
1335 # my ($key, $value) = ($line =~ m/(\w+)\s*=\s*(.*)$/);
1336 warn "$key:$value\n";
1337 $config->{$key} = $value;
1339 die "Error
: Not valid configfile format
in: '$file'";
1348 Add an asset record to the asset table;
1357 my $db = $OS_SYN_DB;
1358 my ($dbh, $ip, $service, $time, $fp, $mac, $os, $details, $link, $dist, $host) = @_;
1360 my $sql = "SELECT ip
,fingerprint
,time FROM
$table WHERE ip
= ? AND fingerprint
= ?
";
1361 #print "$sql,$ip,$service,$time,$fp,$mac,$os,$details,$link,$dist,$host\n" if $service eq 'ARP';
1363 $h_select = $db->prepare_cached($sql) or die "Failed
:$!" if not $h_select;
1364 $h_select->execute($ip,$fp);
1365 my ($o_ip, $o_fp, $o_time) = $h_select->fetchrow_array();
1367 if($o_time < $PRADS_START){
1368 printf "%11d [%-10s
] ip
:%16s - %s - %s [%s] distance
:%d link:%s %s\n",
1369 $o_time, $service, $ip, $os, $details, $fp, $dist, $link, '[OLD]';
1370 #print "$o_time [$service] ip
:$ip - $os - $details [$fp] distance
:$dist link:$link [OLD
]\n";
1373 $h_update = $db->prepare_cached("UPDATE
$table SET
time=? WHERE ip
=? AND fingerprint
=?
") or die "$!" if not $h_update;
1374 $h_update->execute($time,$ip,$fp);
1376 $h_insert = $db->prepare_cached(
1377 "INSERT INTO
$table ".
1378 "(ip
, service
, time, fingerprint
, mac
, os
, details
,".
1379 "link, distance
, reporting
)".
1380 "VALUES
(?
,?
,?
,?
,?
,?
,?
,?
,?
,?
)") if not $h_insert;
1381 #('$ip', '$service', '$time', '$fp', '$mac', '$os', '$details', '$link', '$dist', '$host')") if not $h_insert;
1382 $h_insert->execute($ip,$service,$time,$fp,$mac,$os,$details,$link,$dist,$host);
1384 printf "%11d [%-10s] ip:%16s - %s - %s [%s] distance:%d link:%s %s\n",
1385 $time, $service, $ip, $os, $details, $fp, $dist, $link, '';
1392 Takes input: type, type-specific args, ...
1393 Adds the asset to the internal list of assets, or if it exists, just updates the timestamp.
1398 my $db = $OS_SYN_DB;
1399 my ($type, @rest) = @_;
1402 my ($src_ip, $fingerprint, $dist, $link, $os, $details, @more) = @rest;
1406 $details = 'UNKNOWN';
1408 add_db
($db, $src_ip, $type, $pradshosts{'tstamp'}, $fingerprint, '', $os, $details, $link, $dist, $PRADS_HOSTNAME);
1409 }elsif($type eq 'SYNACK'){
1410 my ($src_ip, $fingerprint, $dist, $link, $os, $details, @more) = @rest;
1414 $details = 'UNKNOWN';
1416 add_db
($db, $src_ip, $type, $pradshosts{'tstamp'}, $fingerprint, '', $os, $details, $link, $dist, $PRADS_HOSTNAME);
1417 }elsif($type eq 'ARP'){
1418 my ($mac, $ip, $prefix, $vendor, $details, @more) = @rest;
1420 add_db
($db, $ip, $type, $pradshosts{'tstamp'}, $prefix, $mac, $vendor, $details, 'ethernet', 1, $PRADS_HOSTNAME);
1423 # Service: ip=87.238.47.67 port=631 -> "CUPS 1.2 " timestamp=1242033096
1424 # add_asset('SERVICE', $ip, $port, $vendor, $version, $info, @more);
1425 elsif($type eq 'SERVICE'){
1426 my ($ip, $port, $vendor, $version, $info, @more) = @rest;
1428 add_db
($db, $ip, $type, $pradshosts{'tstamp'}, "$ip:$port", '', $vendor, "$info; $version","SERVICE", 1, $PRADS_HOSTNAME);
1442 This library is free software, you can redistribute it and/or modify
1443 it under the same terms as Perl itself.