#
# $Id: Passive.pm 2234 2014-04-08 13:05:14Z gomor $
#
package Net::SinFP3::Search::Passive;
use strict;
use warnings;
use base qw(Net::SinFP3::Search);
our @AS = qw(
sp
);
__PACKAGE__->cgBuildIndices;
__PACKAGE__->cgBuildAccessorsScalar(\@AS);
use Net::SinFP3 qw(:matchMask);
use Net::SinFP3::Ext::SP;
use Net::SinFP3::Result::Passive;
use Net::SinFP3::Result::PortError;
use Net::SinFP3::Result::Unknown;
use Net::Frame::Layer::TCP qw(:consts);
sub take {
return [
'Net::SinFP3::Mode::Passive',
];
}
sub new {
my $self = shift->SUPER::new(
@_,
);
return $self;
}
sub _getPossibleSignaturePIds {
my $self = shift;
my ($sp) = @_;
my $global = $self->global;
my $log = $global->log;
my $db = $global->db;
my %patterns = (
PatternTcpFlags => 'F',
PatternTcpWindow => 'W',
PatternTcpOptions => 'O',
PatternTcpMss => 'M',
PatternTcpWScale => 'S',
PatternTcpOLength => 'L',
);
my %results = ();
for my $tPattern (keys %patterns) {
my $pId = "id$tPattern";
# $sp->{F} for instance
my $p = $sp->{$patterns{$tPattern}};
my $_table = "_$tPattern";
my %ids = ();
for my $h ('Heuristic0', 'Heuristic1', 'Heuristic2') {
for my $t ($db->$_table) {
(my $method = $tPattern.$h) =~ s/^(.)(.*)$/@{[lc($1)]}$2/;
# We match either using regexp from DB,
# or regexp built in passive mode
if ($p =~ /^@{[$t->{$method}]}$/ || $t->{$method} =~ /$p/) {
#print "DEBUG: [$p] against [".$t->{$method}."]\n";
my $id = $t->{$pId};
my $list = $db->searchSignaturePIds(
$pId => $id,
);
for (@$list) {
$ids{$h}->{$_}++;
#print "DEBUG: possibleId [$id]\n";
}
}
}
}
$results{$patterns{$tPattern}} = \%ids;
}
#use Data::Dumper;
#print "DEBUG: _getPossibleSignaturePIds: ",
#Dumper(\%results),"\n";
return \%results;
}
sub _searchSmallestHeuristicHash {
my $self = shift;
my ($heuristic, @patterns) = @_;
my @ids = ();
my $last = 10_000; # Invalid huge number
for my $pList (@patterns) {
my $count = keys %{$pList->{$heuristic}};
next if $count == 0; # We need to skip, otherwise there is a bug ;)
if ($count < $last) {
@ids = keys %{$pList->{$heuristic}};
$last = $count;
}
}
return \@ids;
}
sub _searchSmallestHashWithMask {
my $self = shift;
my @ids = ();
my $last = 10_000; # Invalid huge number
for my $pList (@_) {
my $count = keys %$pList;
next if $count == 0; # We need to skip, otherwise there is a bug ;)
if ($count < $last) {
@ids = keys %$pList;
$last = $count;
}
}
return \@ids;
}
sub _getIntersection {
my $self = shift;
my ($fList, $wList, $oList, $mList, $sList, $lList) = @_;
my $inter;
for my $h ('Heuristic0', 'Heuristic1', 'Heuristic2') {
my $smallest = $self->_searchSmallestHeuristicHash(
$h, $fList, $wList, $oList, $mList, $sList, $lList,
);
#print "[*] DEBUG: _getIntersection: _searchSmallestHeuristicHash[$h]: ",Dumper($smallest),"\n";
for my $id (@$smallest) {
if (($fList->{Heuristic0}->{$id} || $fList->{Heuristic1}->{$id} || $fList->{Heuristic2}->{$id})
&& ($wList->{Heuristic0}->{$id} || $wList->{Heuristic1}->{$id} || $wList->{Heuristic2}->{$id})
&& ($oList->{Heuristic0}->{$id} || $oList->{Heuristic1}->{$id} || $oList->{Heuristic2}->{$id})
&& ($mList->{Heuristic0}->{$id} || $mList->{Heuristic1}->{$id} || $mList->{Heuristic2}->{$id})
&& ($sList->{Heuristic0}->{$id} || $sList->{Heuristic1}->{$id} || $sList->{Heuristic2}->{$id})
&& ($lList->{Heuristic0}->{$id} || $lList->{Heuristic1}->{$id} || $lList->{Heuristic2}->{$id})) {
my $f = $fList->{Heuristic0}->{$id} && 'FH0'
|| $fList->{Heuristic1}->{$id} && 'FH1'
|| $fList->{Heuristic2}->{$id} && 'FH2';
my $w = $wList->{Heuristic0}->{$id} && 'WH0'
|| $wList->{Heuristic1}->{$id} && 'WH1'
|| $wList->{Heuristic2}->{$id} && 'WH2';
my $o = $oList->{Heuristic0}->{$id} && 'OH0'
|| $oList->{Heuristic1}->{$id} && 'OH1'
|| $oList->{Heuristic2}->{$id} && 'OH2';
my $m = $mList->{Heuristic0}->{$id} && 'MH0'
|| $mList->{Heuristic1}->{$id} && 'MH1'
|| $mList->{Heuristic2}->{$id} && 'MH2';
my $s = $sList->{Heuristic0}->{$id} && 'SH0'
|| $sList->{Heuristic1}->{$id} && 'SH1'
|| $sList->{Heuristic2}->{$id} && 'SH2';
my $l = $lList->{Heuristic0}->{$id} && 'LH0'
|| $lList->{Heuristic1}->{$id} && 'LH1'
|| $lList->{Heuristic2}->{$id} && 'LH2';
$inter->{"$f$w$o$m$s$l"}->{$id}++;
}
}
# Stop if we found matches with this smallest heuristic level
#last if keys %$inter > 0;
}
return $inter;
}
sub _getIntersectionWithMask {
my $self = shift;
my ($fList, $wList, $oList, $mList, $sList, $lList, $mask) = @_;
my $f = $mask =~ /FH0/ && 'Heuristic0'
|| $mask =~ /FH1/ && 'Heuristic1'
|| $mask =~ /FH2/ && 'Heuristic2';
my $w = $mask =~ /WH0/ && 'Heuristic0'
|| $mask =~ /WH1/ && 'Heuristic1'
|| $mask =~ /WH2/ && 'Heuristic2';
my $o = $mask =~ /OH0/ && 'Heuristic0'
|| $mask =~ /OH1/ && 'Heuristic1'
|| $mask =~ /OH2/ && 'Heuristic2';
my $m = $mask =~ /MH0/ && 'Heuristic0'
|| $mask =~ /MH1/ && 'Heuristic1'
|| $mask =~ /MH2/ && 'Heuristic2';
my $s = $mask =~ /SH0/ && 'Heuristic0'
|| $mask =~ /SH1/ && 'Heuristic1'
|| $mask =~ /SH2/ && 'Heuristic2';
my $l = $mask =~ /LH0/ && 'Heuristic0'
|| $mask =~ /LH1/ && 'Heuristic1'
|| $mask =~ /LH2/ && 'Heuristic2';
# We force a search using a very specific heuristic mask
my $smallest = $self->_searchSmallestHashWithMask(
$fList->{$f}, $wList->{$w}, $oList->{$o}, $mList->{$m},
$sList->{$s}, $lList->{$l},
);
my $inter;
for my $id (@$smallest) {
if ($fList->{$f}->{$id}
&& $wList->{$w}->{$id}
&& $oList->{$o}->{$id}
&& $mList->{$m}->{$id}
&& $sList->{$s}->{$id}
&& $lList->{$l}->{$id}) {
$inter->{$mask}->{$id}++;
}
}
return $inter;
}
sub _searchSmallestInterWithMask {
my $self = shift;
my @ids = ();
my $last = 10_000; # Invalid huge number
for my $inter (@_) {
my $count = keys %$inter;
next if $count == 0; # We need to skip, otherwise there is a bug ;)
if ($count < $last) {
@ids = keys %$inter;
$last = $count;
}
}
return \@ids;
}
sub _tohash {
my $self = shift;
my ($s) = @_;
return {
F => $self->$s->F,
W => $self->$s->W,
O => $self->$s->O,
M => $self->$s->M,
S => $self->$s->S,
L => $self->$s->L,
};
}
sub _countInter {
my $self = shift;
my ($ids) = @_;
$ids->{nInter} = keys %{$ids->{Inter}};
return $ids;
}
sub search {
my $self = shift;
my $global = $self->global;
my $log = $global->log;
# Convert it to optimize a bit
my $sp = $self->_tohash('sp');
my $ids = {};
my $res = $self->_getPossibleSignaturePIds($sp);
my $inter = $self->_getIntersection(
$res->{F}, $res->{W}, $res->{O}, $res->{M}, $res->{S}, $res->{L},
);
$ids->{Ids} = $res;
$ids->{Inter} = $inter;
# Make masks unique
my %maskList = map { $_ => 1 } (keys %{$ids->{Inter}});
# Update number of resulting intersection
$self->_countInter($ids);
# For all masks, expand possible Signature IDs to make
# all of them comparable
for my $mask (keys %maskList) {
#print "[*] ".Dumper($ids->{$p}{Ids})."\n";
if (!exists $ids->{Inter}->{$mask}) {
#print "[*] Running with [$mask] against [$p]\n";
my $interNew = $self->_getIntersectionWithMask(
$ids->{Ids}->{F}, $ids->{Ids}->{W}, $ids->{Ids}->{O},
$ids->{Ids}->{M}, $ids->{Ids}->{S}, $ids->{Ids}->{L},
$mask,
);
if ($interNew) {
use Data::Dumper;
$log->debug("interNew: ".Dumper($interNew));
$ids->{Inter}->{$mask} = $interNew->{$mask};
}
}
}
# Update number of resulting intersections
# after we have expanded mask list
$self->_countInter($ids);
my @resultList = ();
#print "DEBUG: NS_MATCH_TYPE_P2\n";
my $results = $self->_buildResultList($ids->{Inter});
push @resultList, @$results;
my $clean4 = $self->_cleanResults(\@resultList, 'IPv4');
my $clean6 = $self->_cleanResults(\@resultList, 'IPv6');
# We keep IPv4 signatures in IPv6 mode only if no IPv6 matchs
# Else in IPv4 mode, we only keep IPv4 matchs
my @clean = ();
if ($self->global->ipv6 && @$clean6 > 0) {
push @clean, @$clean6;
}
elsif (@$clean4 > 0) {
push @clean, @$clean4;
}
return \@clean;
}
sub _cleanResults {
my $self = shift;
my ($results, $ip) = @_;
my $global = $self->global;
# Sort to easily filter out
my $sorted = {};
for my $r (@$results) {
if ($r->ipVersion ne $ip) {
next;
}
if ($global->threshold != 0 && $r->matchScore < $global->threshold) {
next;
}
push @{$sorted->{$r->matchScore}}, $r;
}
my $p2 = $sorted;
my @sorted2 = ();
if (keys %$p2 > 0) {
# Sort results by IP version and score, keep only highest score for an ID
my %idList = ();
my $bestScore = 0;
for my $p (sort { $b <=> $a } keys %$sorted) {
for my $r (@{$sorted->{$p}}) {
if (! exists($idList{$r->idSignatureP})) {
if ($global->bestScore) {
if ($r->matchScore >= $bestScore) {
push @sorted2, $r;
$idList{$r->idSignatureP}++;
if (! $bestScore) {
$bestScore = $r->matchScore;
}
}
}
else {
push @sorted2, $r;
$idList{$r->idSignatureP}++;
}
}
}
}
}
else {
# Or no matchs at all
return [];
}
return \@sorted2;
}
sub _buildResultList {
my $self = shift;
my ($result) = @_;
my $global = $self->global;
my $log = $global->log;
my $db = $global->db;
my @resultList = ();
for my $mask (keys %$result) {
for my $id (keys %{$result->{$mask}}) {
my %args = ();
my $sig = $db->retrieveSignatureP($id);
my $result = Net::SinFP3::Result::Passive->new(
global => $self->global,
sp => $self->sp,
trusted => $sig->{trusted},
idSignatureP => $sig->{idSignatureP},
ipVersion => $sig->{ipVersion},
systemClass => $sig->{systemClass},
vendor => $sig->{vendor},
os => $sig->{os},
osVersion => $sig->{osVersion},
osVersionFamily => $sig->{osVersionFamily},
matchType => 'P2',
matchMask => $mask,
osVersionChildrenList => $db->getOsVersionChildrenPList(
$id,
),
);
$result->updateMatchScore;
push @resultList, $result;
}
}
return \@resultList;
}
sub run {
my $self = shift->SUPER::run(@_) or return;
my $global = $self->global;
my $log = $global->log;
my $mode = $global->mode;
if (! defined($mode->sp)) {
$log->error("Nothing to search");
return;
}
$self->sp($mode->sp);
my $result = $self->search;
if (@$result == 0) {
my $r = Net::SinFP3::Result::Unknown->new(
global => $self->global,
sp => $self->sp,
);
$result = [ $r ];
}
# Fill frame attribute if available
if ($mode->frame) {
my $f = $mode->frame;
my $ip = $f->ref->{IPv4} || $f->ref->{IPv6};
my $tcp = $f->ref->{TCP};
for my $r (@$result) {
$r->frame($mode->frame);
$r->ip($ip->dst);
$r->port($tcp->dst);
if ($global->dnsReverse) {
$r->reverse($global->getAddrReverse(addr => $r->ip) || 'unknown');
}
}
}
return $result;
}
1;
__END__
=head1 NAME
Net::SinFP3::Search::Passive - matching passive signatures search engine
=head1 DESCRIPTION
Go to http://www.networecon.com/tools/sinfp/ to know more.
=head1 AUTHOR
Patrice E<lt>GomoRE<gt> Auffret
=head1 COPYRIGHT AND LICENSE
Copyright (c) 2011-2014, Patrice E<lt>GomoRE<gt> Auffret
You may distribute this module under the terms of the Artistic license.
See LICENSE.Artistic file in the source distribution archive.
=cut