Using Passive OS Fingerprinting (p0f)
Passive OS fingerprinting is the practice of trying to determine the operating system that a connecting network host is running, based on vendor-specific quirks in the TCP/IP stack. It's a technique that can let you know whether the machine sending you email is running Windows, Mac OS X, Linux, FreeBSD, Solaris, etc. More usefully, it can make a pretty educated guess about the version of the OS as well, so you can tell whether a Windows host is running a workstation OS like Windows 98 or server OS like Windows 2003. This can be handy for helping to identify botnet zombies.
To get started you'll need to download and install p0f on your mail gateway--the machine that outside hosts will be connecting to on port 25 to send you mail.
What we intend to do is run p0f as a daemon that monitors port 25 and caches information about all connecting hosts. Using a small Perl script, we'll then tell Postfix to consult that p0f cache when it receives a mail item, and then insert a special X-p0f-OS: header into the email with the pertinent OS information about the client. From there we can use a set of custom SpamAssassin rules that look for that header and adjust the score accordingly.
First we need a small policy script that Postfix can use to consult the p0f cache. Save this file as p0f-policy.pl somewhere (e.g. /var/amavisd/p0f-policy.pl), edit the handful of configurable settings (e.g. your mail server's IP address) and make it executable:
#!/usr/bin/perl
use Sys::Syslog qw(:DEFAULT setlogsock);
use IO::Socket;
use Net::IP;
# p0f-policy.pl is based on the os-greylist.pl script by
# Marmarek <marmarek@staszic.waw.pl>, which can be found at:
#
# http://marmarek.w.staszic.waw.pl/~marmarek/download/os-greylist.pl
#
# This script was modified by Robert LeBlanc <rjl@renaissoft.com> for
# use with Maia Mailguard as a tool for inserting p0f operating system
# information into a mail header called "X-p0f-OS:", which can then
# be used by SpamAssassin for scoring purposes with the rules in the
# associated p0f.cf file.
#
# The p0f-policy.pl script is intended to be used as a delegated
# Postfix smtpd policy server. The script should be installed in your
# amavis/maia user's home directory (e.g. /var/amavis), while the
# p0f.cf file should be installed in the same subdirectory as your
# SpamAssassin local.cf file.
#
# You should run p0f something like this:
#
# p0f -i any -u amavis -Q /var/amavis/p0f-socket -q -l -d -o /dev/null -0 'tcp dst port 25'
#
# In your /etc/postfix/master.cf add:
#
# policy unix - n n - - spawn
# user=amavis argv=/usr/bin/perl /var/amavis/p0f-policy.pl
#
# In your /etc/postfix/main.cf, make the 'check_policy_service' entry
# below the last smtpd_recipient_restriction to be tested:
#
# smtpd_recipient_restrictions =
# ...
# reject_unauth_destination
# check_policy_service unix:private/policy
#
# NOTE: specify check_policy_service AFTER reject_unauth_destination
# or else your system can become an open relay.
#
# To test this script by hand, execute:
#
# % perl p0f-policy.pl
#
# Each query is a bunch of attributes. Order does not matter, and
# the demo script uses only a few of all the attributes shown below:
#
# request=smtpd_access_policy
# protocol_state=RCPT
# protocol_name=SMTP
# client_address=1.2.3.4
# [empty line]
#
# The policy server script will answer in the same style, with an
# attribute list followed by a empty line, e.g.:
#
# action=prepend X-p0f-OS: Windows XP/2000
# [empty line]
######################################################################
# #
# CONFIGURABLE ITEMS (set these) #
# #
######################################################################
# Syslogging options for verbose mode and for fatal errors.
# NOTE: comment out the $syslog_socktype line if syslogging does not
# work on your system.
#
$syslog_socktype = 'unix'; # inet, unix, stream, console
$syslog_facility = 'mail';
$syslog_options = 'pid';
$syslog_priority = 'info';
# The full path to the socket file p0f is listening on.
$p0f_socket = "/var/amavisd/p0f-socket";
# The IP address of your Postfix host.
$mta_ipaddr = "10.0.0.2";
######################################################################
# #
# END OF CONFIGURABLE ITEMS (no need to change anything below here) #
# #
######################################################################
sub p0f_query {
my $QUERY_MAGIC = 0x0defaced;
my $QTYPE_FINGERPRINT = 1;
my $src = new Net::IP ($attr{"client_address"}) or die (Net::IP::Error());
my $dst = new Net::IP ($mta_ipaddr) or die (Net::IP::Error());
my $query = pack("L L L N N S S", $QUERY_MAGIC, $QTYPE_FINGERPRINT, 0x12345678, $src->intip(), $dst->intip(), 0, 25);
my $sock = new IO::Socket::UNIX (Peer => $p0f_socket, Type => SOCK_STREAM);
if (!$sock) {
syslog $syslog_priority, "Could not create p0f socket: $!\n";
return "unknown";
}
# Ask p0f
print $sock $query;
my $response = <$sock>;
close $sock;
# Extract the response from p0f
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);
if ($magic != $QUERY_MAGIC) {
syslog $syslog_priority, "bad p0f query magic";
return "unknown";
}
if ($type == 1) {
syslog $syslog_priority, "malformed p0f query";
return "unknown";
}
if ($type == 2) {
syslog $syslog_priority, "connection is not (no longer?) in the p0f cache" if $verbose;
return "unknown";
}
if ($genre eq "") {
$genre = "unknown";
}
if ($verbose) {
syslog $syslog_priority, "OS detected: %s %s", $genre, $detail;
}
return $genre, $detail;
}
# Demo SMTPD access policy routine. The result is an action just like
# it would be specified on the right-hand side of a Postfix access
# table. Request attributes are available via the %attr hash.
sub smtpd_access_policy {
my($key, $time_stamp, $now);
my ($genre, $detail) = p0f_query();
# The result can be any action that is allowed in a Postfix access(5) map.
#
# To label mail, return ``PREPEND'' headername: headertext
syslog $syslog_priority, "Client OS detected: %s %s", $genre, $detail;
return "prepend X-p0f-OS: $genre $detail";
}
# Log an error and abort.
sub fatal_exit {
my($first) = shift(@_);
syslog "err", "fatal: $first", @_;
exit 1;
}
# This process runs as a daemon, so it can't log to a terminal. Use
# syslog so that people can actually see our messages.
setlogsock $syslog_socktype;
openlog $0, $syslog_options, $syslog_facility;
# We don't need getopt() for now.
while ($option = shift(@ARGV)) {
if ($option eq "-v") {
$verbose = 1;
} else {
syslog $syslog_priority, "Invalid option: %s. Usage: %s [-v]",
$option, $0;
exit 1;
}
}
# Unbuffer standard output.
select((select(STDOUT), $| = 1)[0]);
# Receive a bunch of attributes, evaluate the policy, send the result.
while (<STDIN>) {
if (/([^=]+)=(.*)\n/) {
$attr{substr($1, 0, 512)} = substr($2, 0, 512);
} elsif ($_ eq "\n") {
if ($verbose) {
for (keys %attr) {
syslog $syslog_priority, "Attribute: %s=%s", $_, $attr{$_};
}
}
fatal_exit "unrecognized request type: '%s'", $attr{request}
unless $attr{"request"} eq "smtpd_access_policy";
$action = smtpd_access_policy();
syslog $syslog_priority, "Action: %s", $action if $verbose;
print STDOUT "action=$action\n\n";
%attr = ();
} else {
chop;
syslog $syslog_priority, "warning: ignoring garbage: %.100s", $_;
}
}
Next, tell Postfix where to find this script by adding this to your master.cf file:
...
policy unix - n n - - spawn
user=amavis argv=/usr/bin/perl /var/amavisd/p0f-policy.pl
...
In your main.cf file, add this policy service to your smtpd_recipient_restrictions, so that the p0f cache is consulted only after more basic recipient tests have been performed:
smtpd_recipient_restrictions = ... reject_unauth_destination check_policy_service unix:private/policy
NOTE: specify check_policy_service AFTER reject_unauth_destination or else your system can become an open relay.
Now that we've added the custom p0f header to incoming mail items, we need to assign some score values to different OS results. Save these SpamAssassin rules to a file called p0f.cf in the subdirectory where your local.cf file is located:
################################################################################### # p0f Rules for scoring client operating systems ################################################################################### header P0F_WIN311 X-p0f-OS =~ /^Windows 3.11/ score P0F_WIN311 3.0 describe P0F_WIN311 Client is running Windows 3.11 header P0F_WIN95 X-p0f-OS =~ /^Windows 95/ score P0F_WIN95 3.0 describe P0F_WIN95 Client is running Windows 95 header P0F_WIN98 X-p0f-OS =~ /^Windows 98/ score P0F_WIN98 3.0 describe P0F_WIN98 Client is running Windows 98 header P0F_WINME X-p0f-OS =~ /^Windows ME/ score P0F_WINME 3.0 describe P0F_WINME Client is running Windows ME header P0F_WINNT X-p0f-OS =~ /^Windows NT/ score P0F_WINNT 0.5 describe P0F_WINNT Client is running Windows NT header P0F_WIN2K X-p0f-OS =~ /^Windows 2000(?!.*XP)/ score P0F_WIN2K 1.5 describe P0F_WIN2K Client is running Windows 2000 header P0F_WINXP X-p0f-OS =~ /^Windows XP(?!.*2000)/ score P0F_WINXP 2.5 describe P0F_WINXP Client is running Windows XP header P0F_WINXP2K X-p0f-OS =~ /^Windows (XP.+2000|2000.+XP)/ score P0F_WINXP2K 1.5 describe P0F_WINXP2K Client is running Windows 2000 or XP header P0F_WIN2K3 X-p0f-OS =~ /^Windows 2003/ score P0F_WIN2K3 0.2 describe P0F_WIN2K3 Client is running Windows 2003 header P0F_WINNET X-p0f-OS =~ /^Windows \.NET/ score P0F_WINNET 0.2 describe P0F_WINNET Client is running Windows .NET Enterprise Server header P0F_WINCE X-p0f-OS =~ /^Windows CE/ score P0F_WINCE 0.1 describe P0F_WINCE Client is running Windows CE header P0F_WINVISTA X-p0f-OS =~ /^Windows Vista/ score P0F_WINVISTA 2.5 describe P0F_WINVISTA Client is running Windows Vista header P0F_MACOS X-p0f-OS =~ /^MacOS/ score P0F_MACOS 0.1 describe P0F_MACOS Client is running Mac OS header P0F_FREEBSD X-p0f-OS =~ /^FreeBSD/ score P0F_FREEBSD -0.1 describe P0F_FREEBSD Client is running FreeBSD header P0F_OPENBSD X-p0f-OS =~ /^OpenBSD/ score P0F_OPENBSD -1.0 describe P0F_OPENBSD Client is running OpenBSD header P0F_NETBSD X-p0f-OS =~ /^NetBSD/ score P0F_NETBSD -1.0 describe P0F_NETBSD Client is running NetBSD header P0F_SOLARIS X-p0f-OS =~ /^Solaris/ score P0F_SOLARIS -1.0 describe P0F_SOLARIS Client is running Solaris header P0F_HPUX X-p0f-OS =~ /^HP-UX/ score P0F_HPUX -1.0 describe P0F_HPUX Client is running HP-UX header P0F_TRU64 X-p0f-OS =~ /^Tru64/ score P0F_TRU64 -1.0 describe P0F_TRU64 Client is running Tru64 header P0F_AIX X-p0f-OS =~ /^AIX/ score P0F_AIX -1.0 describe P0F_AIX Client is running AIX header P0F_LINUX X-p0f-OS =~ /^Linux/ score P0F_LINUX -0.1 describe P0F_LINUX Client is running Linux header P0F_SUNOS X-p0f-OS =~ /^SunOS/ score P0F_SUNOS -1.0 describe P0F_SUNOS Client is running SunOS header P0F_IRIX X-p0f-OS =~ /^IRIX/ score P0F_IRIX -1.0 describe P0F_IRIX Client is running IRIX header P0F_OPENVMS X-p0f-OS =~ /^OpenVMS/ score P0F_OPENVMS -1.0 describe P0F_OPENVMS Client is running OpenVMS header P0F_RISCOS X-p0f-OS =~ /^RISC OS/ score P0F_RISCOS -1.0 describe P0F_RISCOS Client is running RISC OS header P0F_BSD X-p0f-OS =~ /^BSD/ score P0F_BSD -1.0 describe P0F_BSD Client is running BSD/OS header P0F_NEWTON X-p0f-OS =~ /^NewtonOS/ score P0F_NEWTON 0.1 describe P0F_NEWTON Client is running NewtonOS header P0F_NEXT X-p0f-OS =~ /^NeXTSTEP/ score P0F_NEXT -1.0 describe P0F_NEXT Client is running NeXTSTEP header P0F_BEOS X-p0f-OS =~ /^BeOS/ score P0F_BEOS -1.0 describe P0F_BEOS Client is running BeOS header P0F_OS400 X-p0f-OS =~ /^OS\/400/ score P0F_OS400 -1.0 describe P0F_OS400 Client is running OS/400 header P0F_ULTRIX X-p0f-OS =~ /^ULTRIX/ score P0F_ULTRIX -1.0 describe P0F_ULTRIX Client is running ULTRIX header P0F_QNX X-p0f-OS =~ /^QNX/ score P0F_QNX -1.0 describe P0F_QNX Client is running QNX header P0F_NETWARE X-p0f-OS =~ /^Novell NetWare/ score P0F_NETWARE 2.0 describe P0F_NETWARE Client is running NetWare header P0F_INTRANETWARE X-p0f-OS =~ /^Novell IntranetWare/ score P0F_INTRANETWARE 2.0 describe P0F_INTRANETWARE Client is running IntranetWare header P0F_BORDERMGR X-p0f-OS =~ /^Novell BorderManager/ score P0F_BORDERMGR 2.0 describe P0F_BORDERMGR Client is running BorderManager header P0F_SCO X-p0f-OS =~ /^SCO/ score P0F_SCO -1.0 describe P0F_SCO Client is running SCO header P0F_DOS X-p0f-OS =~ /^DOS/ score P0F_DOS 3.0 describe P0F_DOS Client is running DOS header P0F_OS2 X-p0f-OS =~ /^OS\/2/ score P0F_OS2 2.0 describe P0F_OS2 Client is running OS/2 header P0F_TOPS20 X-p0f-OS =~ /^TOPS-20/ score P0F_TOPS20 -1.0 describe P0F_TOPS20 Client is running TOPS-20 header P0F_AMIGA X-p0f-OS =~ /^AMIGA/ score P0F_AMIGA 1.0 describe P0F_AMIGA Client is running AMIGAOS header P0F_MINIX X-p0f-OS =~ /Minix/ score P0F_MINIX -1.0 describe P0F_MINIX Client is running Minix header P0F_PLAN9 X-p0f-OS =~ /^Plan9/ score P0F_PLAN9 -1.0 describe P0F_PLAN9 Client is running Plan9 header P0F_FREEMINT X-p0f-OS =~ /^FreeMiNT/ score P0F_FREEMINT 1.0 describe P0F_FREEMINT Client is running FreeMiNT header P0F_NETCACHE X-p0f-OS =~ /^NetCache/ score P0F_NETCACHE -0.1 describe P0F_NETCACHE Client is running NetCache header P0F_CACHEFLOW X-p0f-OS =~ /^CacheFlow/ score P0F_CACHEFLOW -0.1 describe P0F_CACHEFLOW Client is running CacheFlow header P0F_POWERAPP X-p0f-OS =~ /^Dell PowerApp/ score P0F_POWERAPP -0.1 describe P0F_POWERAPP Client is running PowerApp header P0F_PALMOS X-p0f-OS =~ /^PalmOS/ score P0F_PALMOS 0.1 describe P0F_PALMOS Client is running PalmOS header P0F_SYMBIANOS X-p0f-OS =~ /^SymbianOS/ score P0F_SYMBIANOS 0.1 describe P0F_SYMBIANOS Client is running SymbianOS header P0F_ZAURUS X-p0f-OS =~ /^Zaurus/ score P0F_ZAURUS 0.1 describe P0F_ZAURUS Client is running Zaurus header P0F_POCKETPC X-p0f-OS =~ /^PocketPC/ score P0F_POCKETPC 0.1 describe P0F_POCKETPC Client is running PocketPC header P0F_CONTIKI X-p0f-OS =~ /^Contiki/ score P0F_CONTIKI 0.1 describe P0F_CONTIKI Client is running Contiki header P0F_PLAYSTATION X-p0f-OS =~ /^Sony Playstation/ score P0F_PLAYSTATION 3.0 describe P0F_PLAYSTATION Client is running Sony Playstation header P0F_DREAMCAST X-p0f-OS =~ /^Sega Dreamcast/ score P0F_DREAMCAST 3.0 describe P0F_DREAMCAST Client is running Sega Dreamcast header P0F_UNKNOWN X-p0f-OS =~ /^UNKNOWN/ score P0F_UNKNOWN 0.8 describe P0F_UNKNOWN Client OS is unknown
Be sure to run the load-sa-rules.pl script to tell Maia about these new SpamAssassin rules, of course.
Finally, add a line to one of your startup scripts (e.g. rc.local) to make sure the p0f daemon is started at boot time:
... # Start the p0f daemon /usr/sbin/p0f -i any -u amavis -Q /var/amavisd/p0f-socket -q -l -d -o /var/amavisd/p0f.log -0 'tcp dst port 25' ...
Start the p0f daemon with that same command line manually, reload the Postfix daemon, restart amavisd-maia and you should be done. Verify that incoming email now contains the new mail header, and that a "P0F_" rule is being triggered.

