refactored p0f plugin, added p0f v3 support

This commit is contained in:
Matt Simerson 2012-04-26 19:25:14 -04:00 committed by Ask Bjørn Hansen
parent 368ce9401b
commit 0084986115

View File

@ -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;
}