5.5. Writing an Operating System Fingerprinting Module for MSF
Assuming an exploit works, the key factors for
successful exploitation are the PAYLOAD and
TARGET settings. If the
target host
is behind a well-configured firewall, a bind socket payload
won't allow you to access the host. Also, if you
don't know the remote operating system, using an
OS-specific target is useless; a return address for Windows NT
typically won't work against a Windows XP machine.
Usually the application level can aid in the targeting process. For
instance, if an HTTP request returns Apache/1.3.22
(Win32), you probably aren't
using FreeBSD targets. But what if the service yields no obvious clue
regarding its underlying operating system? In this case we would use
a technique called operating system
fingerprinting to narrow the scope of possible targets and
increase the likelihood of success. This is vital for so-called
"one-shot" exploits in which the
service crashes or becomes unexploitable after failed attempts.
5.5.1. Operating System Fingerprinting and p0f
When we talk about operating system fingerprinting
we're really talking about identifying a remote
operating system based on the characteristics of its
TCP/IP network
stack. Due to differences in the way developers implement networking
stacks, typically, unique identifiers within the packets transmitted
by a host will allow for comparison based on known signatures.
Two general techniques are used to profile a networking stack for known
signature comparison: active and passive. Active
fingerprinting requires network interaction with the
target host by sending out a probe and then looking for packet
settings or flags that differ in the response. For example, if I were
to send a packet to an open port with the ACK TCP
flag set, and then I received a response packet from the host with a
window size of 4,000, the DF bit set, the RST flag
set, and the sequence number unsynced with the one sent, I could
search through a list of known behaviors from this type of probe and
establish that this likely is the Mac OS 8.6 operating system. Of
course, this requires that a huge database of known stack signatures
be compiled beforehand.
Nmap by Fyodor (http://www.insecure.org/nmap/) is one of the
best active fingerprinting tools in the field; it uses a variety of
probes and has a signature database with thousands of entries.
Passive
fingerprinting doesn't require
special probes. Passive fingerprinting observes normal network
traffic, but uses the same difference analysis techniques as active
fingerprinting to take a best guess at what the OS is. Passive
fingerprinting is useful in situations where stealth is a high
priority or where we have access to all network traffic, such as a
compromised router, a wireless network, or a hubbed network.
p0f by Michal Zalewski (http://lcamtuf.coredump.cx/p0f.shtml) is a
program that implements passive signature-matching techniques very
effectively. p0f uses 15 different analysis
techniques to determine the operating system and other valuable
information, such as uptime, network link type, and firewall/NAT
presence.
This described functionality fills our need for better targeting and
payload settings, so let's write an MSF module for
it. One way to do this is to launch p0f and
process its text output; however, this would provide inconsistent
results, as the p0f display format varies based
on the command-line options used. Fortunately, p0f
provides an interface for querying its connection cache
via traditional Unix sockets.
This simple interface is outlined in the
p0f-query.c file that comes in the latest version of
the p0f source. The examples in this chapter use
p0f Version 2.0.4.
The Microsoft family of operating systems does not fully support Unix
sockets, so this functionality in p0f will not
work on Windows operating systems.
5.5.2. Setting Up and Modifying p0f
When setting up
p0f, you should use options that set up
the Unix socket and specific SYN/ACK mode. The A
option places the program in SYN/ACK mode, the O
option indicates that the Unix socket interface will be used, and
~/socket is given as the name of the socket. This
mode will fingerprint systems we connect to, as opposed to the
default, which fingerprints systems that connect to us. After
launching p0f, do a basic HTTP request so that
p0f has some packets to fingerprint:
$p0f -qlAQ ~/socket
192.168.0.100:80 - Linux recent 2.4 (1) (up: 210 hrs) ->
192.168.0.109:9818 (distance 1, link: pppoe (DSL))
Leave that process running in a shell and then, in a separate shell,
use the p0fq example tool to query the socket
for the specific connection:
$./p0fq ../sock 192.168.0.100 80 192.168.0.109 9818
Genre : Linux
Details : recent 2.4 (1)
Distance : 1 hops
Link : pppoe (DSL)
Uptime : 210 hrs
This appears to be working, but specifying source and destination
ports is too cumbersome. Let's write a small patch
to p0f to make it easier on the user. The
following patch is against p0f Version 2.0.4.
You can apply it with the patch
-p0 <
p0f-2.0.4-msf.patch command:
--- p0f-query.org.c Fri Jan 3 18:19:58 2004
+++ p0f-query.c Fri Jan 3 19:09:46 2004
@@ -122,6 +122,14 @@
send(sock,n,sizeof(struct p0f_response),MSG_NOSIGNAL);
return;
+ }else if((cur->sad == q->src_ad) && (cur->dad == q->dst_ad) &&
+ (q->src_port == NULL) && (q->dst_port == NULL)){
+ struct p0f_response* n = &cur->s;
+ n->magic = QUERY_MAGIC;
+ n->type = RESP_OK;
+ n->id = q->id;
+ send(sock,n,sizeof(struct p0f_response),MSG_NOSIGNAL);
+ return;
}
}
This patch adds a "search mode"
that allows us to search the cache by the source and destination IP
addresses only (both of the ports will be NULL).
This selects the first hit in the cache for interaction between the
source and destination IP addresses.
5.5.3. Writing the p0f_socket Module
Now, to write the module itself, first determine
what MSF options a user would need to set. The query needs the host
to fingerprint and the source IP address that makes the
connectionthat is, our IP address. For the target IP address,
use RHOST as a user option. The source IP address
can be autodetected via a method from Pex::Utils,
but we'll leave it as an advanced option named
SourceIP just in case a user wants to specify it.
After p0f is launched with the
-Q option, it creates a socket file on the
filesystem. The SOCK user option allows a user to
specify the path to the socket file. A nice feature would be an
"active" mode in which the module
initiates a remote connection to an open port. To enable this, add an
ACTIVE Boolean user option
that will toggle the functionality, as well as an
RPORT user option that should be a known open
port. Now, if a user chooses passive mode, the module will have to
wait for a connection to appear in the cache. In that case
we'll assume the connection will appear close to the
time the user executes the module, so we'll use an
advanced option named Timeout with a default value
of 30 seconds to wait for the connection to appear in the cache.
Our Exploit( ) method's logic
flow is pretty simple. First, it determines whether a user wants
active or passive mode. In active mode it makes a connection, and
then it makes a query to the p0f socket. If it
doesn't get a response, it will wait in the hope
that a connection will exist in the cache before the timeout. You can
implement this as shown here using the previously discussed user
options:
sub Exploit {
my $self = shift;
my $target = $self->GetVar('RHOST');
my $port = $self->GetVar('RPORT');
my $active_mode = $self->GetVar('ACTIVE');
my $timeout = int($self->GetLocal('Timeout'));
After loading some MSF user options the method checks for
Active mode, which simply initiates a TCP
connection so that p0f can fingerprint the
SYN/ACK from the target and add the connection to its cache:
if($active_mode){ # "Active" mode
my $s = Msf::Socket::Tcp->new
(
'PeerAddr' => $target,
'PeerPort' => $port
);
if ($s->IsError){
$self->PrintLine('[*] Error creating TCP socket in active mode: '
. $s->GetError);
return;
}else{
#the connection is made, a cache entry should have been added
goto doQuery;
}
$s->Close( );
}
At this point a connection should exist in the p0f
cache, so the method tries to query the socket using Query(
), and if it encounters errors it waits until the timeout
to try again before giving up:
doQuery:
if($self->Query($target) < 0){
$self->PrintLine("[*] Inital p0f query unsuccessful, sleeping ".
$timeout ." seconds");
for(1 .. $timeout){ print "."; sleep(1);}print "\n";
if($self->Query($target) < 0){
$self->PrintLine("[*] All p0f queries unsuccessful.".
"Make sure that:\n".
"-p0f is setup correctly(-Q and -A, binding interface, etc.)\n".
"-if using passive mode(default) it is up to you to get a connection\n".
" entry into the p0f cache, use active mode if you want to make things
easier\n".
"-if using active mode make sure RPORT is set to an open TCP port on the
target\n");
}
}
return;
}
The final piece to complete this module is the Query() method. To correctly format the p0f
query and parse the response, use the format of the structure defined
in p0f-query.h. If errors crop up while making
and parsing the query, display an appropriate error message and
return a negative error code so that Exploit( )
can act accordingly:
sub Query {
my $self = shift;
my $target = shift;
my $unixsock = $self->GetVar('SOCK');
my $QUERY_MAGIC = 0x0defaced;
my $qid = int rand(0xffffffff);
my $src = my $dst;
After loading up some variables and parameters set up the query and
send it to the socket. Note that this code uses the source and
destination port values of 0 due to our patch to
p0f:
unless($src){
$self->PrintLine("Cannot resolve $target");
return -1;
}
if($self->GetLocal('SourceIP') eq "auto-detect"){
$dst = inet_aton(Pex::Utils::SourceIP( ));
}else{
$dst = inet_aton($self->GetLocal('SourceIP'));
}
my $query = pack("L L", $QUERY_MAGIC, $qid) .
$src . $dst . pack("S", 0)x2;
my $sock = new IO::Socket::UNIX (Peer => $unixsock,
Type => SOCK_STREAM);
unless($sock){
$self->PrintLine("Could not create UNIX socket: $!");
return -2;
}
# Send the request, receive response stucture
print $sock $query;
my $response = <$sock>;
close $sock;
Assuming a response was received from the socket, unpack the response
and do some error checking to make sure everything is satisfactory
before it is displayed:
# Break out the response vars
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);
# Error checking
if($magic != $QUERY_MAGIC){
$self->PrintLine("Bad response magic");
return -3;
}elsif(int($id) != int($qid)){
$self->PrintLine(sprintf("Wrong query id: 0x%08x != 0x%08x", $id, $qid));
return -4;
}elsif($type == 1){
$self->PrintLine("P0f did not honor our query.");
return -5;
}elsif($type == 2){
$self->PrintLine("This connection is not (no longer?) in p0f's cache.");
return -6;
}
# Display result
if( !$genre ){
$self->PrintLine("Genre and details unknown");
}else{
$self->PrintLine("Genre : " . $genre . "\n".
"Details : " . $detail);
$self->PrintLine("Distance : " . $dist) unless ($dist == -1);
}
$self->PrintLine("Link : " . $link . "\n".
"Service : " . $tos . ");
$self->PrintLine("Uptime : " . $uptime . " hrs") unless ($uptime == -1);
$self->PrintLine("The host appears to be behind a NAT") if $nat;
$self->PrintLine("The host appears to be behind a Firewall") if $fw;
return 0;
}
When we are done we'll put the module into the
~/.msf/exploits/ directory, populate the
%info and %advanced hashes with
some metadata, and launch msfconsole to test
it
out.
Figure 5-7 shows an example of how the
p0f_socket module works.