Team LiB
Previous Section Next Section

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 = inet_aton($target);
    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.

Figure 5-7. The p0f_socket module in action


    Team LiB
    Previous Section Next Section