refactored p0f plugin, added p0f v3 support
This commit is contained in:
parent
368ce9401b
commit
0084986115
@ -43,15 +43,33 @@ from Windows computers.
|
|||||||
|
|
||||||
=head1 CONFIGURATION
|
=head1 CONFIGURATION
|
||||||
|
|
||||||
|
Configuration consists of two steps: starting p0f and configuring this plugin.
|
||||||
|
|
||||||
|
=head2 start p0f
|
||||||
|
|
||||||
Create a startup script for PF that creates a communication socket when your
|
Create a startup script for PF that creates a communication socket when your
|
||||||
server starts up.
|
server starts up.
|
||||||
|
|
||||||
p0f -u qpsmtpd -d -q -Q /tmp/.p0f_socket 'dst port 25' -o /dev/null
|
p0f v2 example:
|
||||||
chown qpsmtpd /tmp/.p0f_socket
|
|
||||||
|
p0f -u qpsmtpd -d -q -Q /tmp/.p0f_socket2 'dst port 25' -o /dev/null
|
||||||
|
chown qpsmtpd /tmp/.p0f_socket2
|
||||||
|
|
||||||
|
p0f v3 example:
|
||||||
|
|
||||||
|
p0f -u qpsmtpd -d -s /tmp/.p0f_socket3 'dst port 25'
|
||||||
|
chown qpsmtpd /tmp/.p0f_socket3
|
||||||
|
|
||||||
|
=head2 configure p0f plugin
|
||||||
|
|
||||||
add an entry to config/plugins to enable p0f:
|
add an entry to config/plugins to enable p0f:
|
||||||
|
|
||||||
ident/p0f /tmp/.p0f_socket
|
ident/p0f /tmp/.p0f_socket3
|
||||||
|
|
||||||
|
It's even possible to run both versions of p0f simultaneously:
|
||||||
|
|
||||||
|
ident/p0f:2 /tmp/.p0f_socket2 version 2
|
||||||
|
ident/p0f:3 /tmp/.p0f_socket3
|
||||||
|
|
||||||
=head2 local_ip
|
=head2 local_ip
|
||||||
|
|
||||||
@ -64,11 +82,20 @@ Example config/plugins entry with local_ip override:
|
|||||||
|
|
||||||
ident/p0f /tmp/.p0f_socket local_ip 208.75.177.101
|
ident/p0f /tmp/.p0f_socket local_ip 208.75.177.101
|
||||||
|
|
||||||
All code heavily based upon the p0fq.pl included with the p0f distribution.
|
|
||||||
|
=head2 version
|
||||||
|
|
||||||
|
The version settings specifies the version of p0f you are running. This plugin supports p0f versions 2 and 3. If version is not defined, version 3 is assumed.
|
||||||
|
|
||||||
|
Example entry specifying p0f version 2
|
||||||
|
|
||||||
|
ident/p0f /tmp/.p0f_socket version 2
|
||||||
|
|
||||||
=head1 Environment requirements
|
=head1 Environment requirements
|
||||||
|
|
||||||
p0f requires four pieces of information to look up the p0f fingerprint:
|
p0f v3 requires only the remote IP.
|
||||||
|
|
||||||
|
p0f v2 requires four pieces of information to look up the p0f fingerprint:
|
||||||
local_ip, local_port, remote_ip, and remote_port. TcpServer.pm has been
|
local_ip, local_port, remote_ip, and remote_port. TcpServer.pm has been
|
||||||
has been updated to provide that information when running under djb's
|
has been updated to provide that information when running under djb's
|
||||||
tcpserver. The async, forkserver, and prefork models will likely require
|
tcpserver. The async, forkserver, and prefork models will likely require
|
||||||
@ -76,19 +103,36 @@ some additional changes to make sure these fields are populated.
|
|||||||
|
|
||||||
=head1 ACKNOWLEDGEMENTS
|
=head1 ACKNOWLEDGEMENTS
|
||||||
|
|
||||||
Heavily based upon the p0fq.pl included with the p0f distribution.
|
Version 2 code heavily based upon the p0fq.pl included with the p0f distribution.
|
||||||
|
|
||||||
=head1 AUTHORS
|
=head1 AUTHORS
|
||||||
|
|
||||||
Matt Simerson <msimerson@cpan.org> - 5/1/2010
|
Robert Spier ( original author )
|
||||||
previous unnamed author
|
|
||||||
|
Matt Simerson
|
||||||
|
|
||||||
|
=head1 CHANGES
|
||||||
|
|
||||||
|
Added local_ip option - Matt Simerson (5/2010)
|
||||||
|
|
||||||
|
Refactored and added p0f v3 support - Matt Simerson (4/2012)
|
||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Qpsmtpd::Constants;
|
||||||
use IO::Socket;
|
use IO::Socket;
|
||||||
use Net::IP;
|
use Net::IP;
|
||||||
|
|
||||||
my $QUERY_MAGIC = 0x0defaced;
|
my $QUERY_MAGIC_V2 = 0x0defaced;
|
||||||
|
my $QUERY_MAGIC_V3 = 0x50304601;
|
||||||
|
my $RESP_MAGIC_V3 = 0x50304602;
|
||||||
|
|
||||||
|
my $P0F_STATUS_BADQUERY = 0x00;
|
||||||
|
my $P0F_STATUS_OK = 0x10;
|
||||||
|
my $P0F_STATUS_NOMATCH = 0x20;
|
||||||
|
|
||||||
sub register {
|
sub register {
|
||||||
my ($self, $qp, $p0f_socket, %args) = @_;
|
my ($self, $qp, $p0f_socket, %args) = @_;
|
||||||
@ -101,66 +145,206 @@ sub register {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sub hook_connect {
|
sub hook_connect {
|
||||||
my($self, $qp) = @_;
|
my($self, $qp) = @_;
|
||||||
|
|
||||||
my $p0f_socket = $self->{_args}->{p0f_socket};
|
my $p0f_version = $self->{_args}->{version} || 3;
|
||||||
my $local_ip = $self->{_args}{local_ip} || $self->qp->connection->local_ip;
|
if ( $p0f_version == 3 ) {
|
||||||
|
my $response = $self->query_p0f_v3() or return DECLINED;
|
||||||
|
$self->test_v3_response( $response ) or return DECLINED;
|
||||||
|
$self->store_v3_results( $response );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
my $response = $self->query_p0f_v2() or return DECLINED;
|
||||||
|
$self->test_v2_response( $response ) or return DECLINED;
|
||||||
|
$self->store_v2_results( $response );
|
||||||
|
}
|
||||||
|
|
||||||
my $src = new Net::IP ($self->qp->connection->remote_ip)
|
return DECLINED;
|
||||||
or $self->log(LOGERROR, "p0f: ".Net::IP::Error()), return (DECLINED);
|
}
|
||||||
my $dst = new Net::IP($local_ip)
|
|
||||||
or $self->log(LOGERROR, "p0f: ".NET::IP::Error()), return (DECLINED);
|
sub get_v2_query {
|
||||||
my $query = pack("L L L N N S S",
|
my $self = shift;
|
||||||
$QUERY_MAGIC,
|
|
||||||
|
my $local_ip = $self->{_args}{local_ip} || $self->qp->connection->local_ip;
|
||||||
|
|
||||||
|
my $src = new Net::IP ($self->qp->connection->remote_ip)
|
||||||
|
or $self->log(LOGERROR, "p0f: ".Net::IP::Error()), return;
|
||||||
|
|
||||||
|
my $dst = new Net::IP($local_ip)
|
||||||
|
or $self->log(LOGERROR, "p0f: ".NET::IP::Error()), return;
|
||||||
|
|
||||||
|
return pack("L L L N N S S",
|
||||||
|
$QUERY_MAGIC_V2,
|
||||||
1,
|
1,
|
||||||
rand ^ 42 ^ time,
|
rand ^ 42 ^ time,
|
||||||
$src->intip(),
|
$src->intip(),
|
||||||
$dst->intip(),
|
$dst->intip(),
|
||||||
$self->qp->connection->remote_port,
|
$self->qp->connection->remote_port,
|
||||||
$self->qp->connection->local_port);
|
$self->qp->connection->local_port);
|
||||||
|
};
|
||||||
|
|
||||||
# Open the connection to p0f
|
sub get_v3_query {
|
||||||
socket(SOCK, PF_UNIX, SOCK_STREAM, 0)
|
my $self = shift;
|
||||||
or $self->log(LOGERROR, "p0f: socket: $!"), return (DECLINED);
|
|
||||||
connect(SOCK, sockaddr_un($p0f_socket))
|
|
||||||
or $self->log(LOGERROR, "p0f: connect: $!"), return (DECLINED);
|
|
||||||
defined syswrite SOCK, $query
|
|
||||||
or $self->log(LOGERROR, "p0f: write: $!"), close SOCK, return (DECLINED);
|
|
||||||
|
|
||||||
my $response;
|
my $src_ip = $self->qp->connection->remote_ip;
|
||||||
defined sysread SOCK, $response, 1024
|
|
||||||
or $self->log(LOGERROR, "p0f: read: $!"), close SOCK, return (DECLINED);
|
|
||||||
close SOCK;
|
|
||||||
|
|
||||||
# Extract the response from p0f
|
if ( $src_ip =~ /:/ ) { # IPv6
|
||||||
my ($magic, $id, $type, $genre, $detail, $dist, $link, $tos, $fw,
|
my @bits = split(/\:/, $src_ip );
|
||||||
$nat, $real, $score, $mflags, $uptime) =
|
return pack( "L C C C C C C C C C C C C C C C C C", $QUERY_MAGIC_V3, 0x06, @bits );
|
||||||
unpack ("L L C Z20 Z40 c Z30 Z30 C C C s S N", $response);
|
};
|
||||||
|
|
||||||
if ($magic != $QUERY_MAGIC) {
|
my @octets = split(/\./, $src_ip);
|
||||||
$self->log(LOGERROR, "p0f: Bad response magic.");
|
return pack( "L C C16", $QUERY_MAGIC_V3, 0x04, @octets );
|
||||||
return (DECLINED);
|
};
|
||||||
}
|
|
||||||
if ($type == 1) {
|
|
||||||
$self->log(LOGERROR, "p0f: P0f did not honor our query");
|
|
||||||
return (DECLINED);
|
|
||||||
}
|
|
||||||
if ($type == 2) {
|
|
||||||
$self->log(LOGWARN, "p0f: This connection is no longer in the cache");
|
|
||||||
return (DECLINED);
|
|
||||||
}
|
|
||||||
|
|
||||||
my $p0f = {
|
sub query_p0f_v3 {
|
||||||
genre => $genre,
|
my $self = shift;
|
||||||
detail => $detail,
|
|
||||||
distance => $dist,
|
|
||||||
link => $link,
|
|
||||||
uptime => $uptime,
|
|
||||||
};
|
|
||||||
|
|
||||||
$self->qp->connection->notes('p0f', $p0f);
|
my $p0f_socket = $self->{_args}->{p0f_socket} or return;
|
||||||
$self->log(LOGINFO, "Results: ".$p0f->{genre}." (".$p0f->{detail}.")");
|
my $query = $self->get_v3_query();
|
||||||
$self->log(LOGERROR,"error: $@") if $@;
|
|
||||||
|
# Open the connection to p0f
|
||||||
|
my $sock;
|
||||||
|
eval {
|
||||||
|
$sock = IO::Socket::UNIX->new(Peer => $p0f_socket, Type => SOCK_STREAM );
|
||||||
|
};
|
||||||
|
if ( ! $sock ) {
|
||||||
|
$self->log(LOGERROR, "p0f: could not open socket: $@");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
$sock->autoflush(1); # paranoid redundancy
|
||||||
|
$sock->connected or do {
|
||||||
|
$self->log(LOGERROR, "p0f: socket not connected: $!");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
my $sent = $sock->send($query, 0) or do {
|
||||||
|
$self->log(LOGERROR, "p0f: send failed: $!");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
print $sock $query; # yes, this is redundant, but I get no response from p0f otherwise
|
||||||
|
|
||||||
|
$self->log(LOGDEBUG, "p0f: send $sent byte request");
|
||||||
|
|
||||||
|
my $response;
|
||||||
|
$sock->recv( $response, 232 );
|
||||||
|
my $length = length $response;
|
||||||
|
$self->log(LOGDEBUG, "p0f: received $length byte response");
|
||||||
|
close $sock;
|
||||||
|
return $response;
|
||||||
|
};
|
||||||
|
|
||||||
|
sub query_p0f_v2 {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
my $p0f_socket = $self->{_args}->{p0f_socket};
|
||||||
|
my $query = $self->get_v2_query() or return;
|
||||||
|
|
||||||
|
# Open the connection to p0f
|
||||||
|
socket(SOCK, PF_UNIX, SOCK_STREAM, 0)
|
||||||
|
or $self->log(LOGERROR, "p0f: socket: $!"), return;
|
||||||
|
connect(SOCK, sockaddr_un($p0f_socket))
|
||||||
|
or $self->log(LOGERROR, "p0f: connect: $!"), return;
|
||||||
|
defined syswrite SOCK, $query
|
||||||
|
or $self->log(LOGERROR, "p0f: write: $!"), close SOCK, return;
|
||||||
|
|
||||||
|
my $response;
|
||||||
|
defined sysread SOCK, $response, 1024
|
||||||
|
or $self->log(LOGERROR, "p0f: read: $!"), close SOCK, return;
|
||||||
|
close SOCK;
|
||||||
|
return $response;
|
||||||
|
};
|
||||||
|
|
||||||
|
sub test_v2_response {
|
||||||
|
my ($self, $response ) = @_;
|
||||||
|
|
||||||
|
# Extract part of the p0f response
|
||||||
|
my ($magic, $id, $type) = unpack ("L L C", $response);
|
||||||
|
|
||||||
|
# $self->log(LOGERROR, $response);
|
||||||
|
if ($magic != $QUERY_MAGIC_V2) {
|
||||||
|
$self->log(LOGERROR, "p0f: Bad response magic.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type == 1) {
|
||||||
|
$self->log(LOGERROR, "p0f: p0f did not honor our query");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elsif ($type == 2) {
|
||||||
|
$self->log(LOGWARN, "p0f: This connection is no longer in the cache");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
sub test_v3_response {
|
||||||
|
my ($self, $response ) = @_;
|
||||||
|
|
||||||
|
my ($magic,$status) = unpack ("L L", $response);
|
||||||
|
|
||||||
|
# check the magic response value (a p0f constant)
|
||||||
|
if ($magic != $RESP_MAGIC_V3 ) {
|
||||||
|
$self->log(LOGERROR, "p0f: Bad response magic.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
# check the response status
|
||||||
|
if ($status == $P0F_STATUS_BADQUERY ) {
|
||||||
|
$self->log(LOGERROR, "p0f: bad query");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elsif ($status == $P0F_STATUS_NOMATCH ) {
|
||||||
|
$self->log(LOGINFO, "p0f: no match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($status == $P0F_STATUS_OK ) {
|
||||||
|
$self->log(LOGDEBUG, "p0f: query ok");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
sub store_v2_results {
|
||||||
|
my ($self, $response ) = @_;
|
||||||
|
|
||||||
|
my ($magic, $id, $type, $genre, $detail, $dist, $link, $tos, $fw,
|
||||||
|
$nat, $real, $score, $mflags, $uptime) =
|
||||||
|
unpack ("L L C Z20 Z40 c Z30 Z30 C C C s S N", $response);
|
||||||
|
|
||||||
|
my $p0f = {
|
||||||
|
genre => $genre,
|
||||||
|
detail => $detail,
|
||||||
|
distance => $dist,
|
||||||
|
link => $link,
|
||||||
|
uptime => $uptime,
|
||||||
|
};
|
||||||
|
|
||||||
|
$self->qp->connection->notes('p0f', $p0f);
|
||||||
|
$self->log(LOGINFO, $genre." (".$detail.")");
|
||||||
|
$self->log(LOGERROR,"error: $@") if $@;
|
||||||
|
};
|
||||||
|
|
||||||
|
sub store_v3_results {
|
||||||
|
my ($self, $response ) = @_;
|
||||||
|
|
||||||
|
my @labels = qw/ magic status first_seen last_seen total_conn uptime_min
|
||||||
|
up_mod_days last_nat last_chg distance bad_sw os_match_q os_name os_flavor
|
||||||
|
http_name http_flavor link_type language /;
|
||||||
|
my @values = unpack ("L L L L L L L L L s C C A32 A32 A32 A32 A32 A32 A32", $response);
|
||||||
|
|
||||||
|
my %r;
|
||||||
|
foreach my $i ( 0 .. ( scalar @labels -1 ) ) {
|
||||||
|
next if ! defined $values[$i];
|
||||||
|
next if ! defined $values[$i];
|
||||||
|
$r{ $labels[$i] } = $values[$i];
|
||||||
|
};
|
||||||
|
|
||||||
|
$self->qp->connection->notes('p0f', \%r);
|
||||||
|
$self->log(LOGINFO, "$values[12] $values[13]");
|
||||||
|
$self->log(LOGDEBUG, join(' ', @values ));
|
||||||
|
$self->log(LOGERROR,"error: $@") if $@;
|
||||||
|
};
|
||||||
|
|
||||||
return DECLINED;
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user