Snacktime: A Perl Solution
|
Tod Beardsley, Plan B Security June, 2003 Austin, TX |
Franck Veysset, Olivier Courtay, and Olivier Heen of Intranode research noticed that one could fairly reliably detect a wide range of operating systems by timing the retransmission timeout lengths of the TCP handshake. Turns out, this is not only a surprisingly reliable, but has the potential to be extremely stealthy. Their proof-of-concept tool, RING, demonstrated this technique, and I reviewed their research for my GCIA Assignment #1, Ring out the old, RING in the new; see these papers for more in-depth analysis on how RTO timing works.
Being that I'm a chimp, I'm much better with Perl than I am with C, so I ported the concepts over, and added on some extra passive fingerprinting techniques. The result is Snacktime -- a half-open, half-passive OS Fingerprinting tool.
And yes, you need to be root in order to do anything useful.
Snacktime is available for download at http://www.planb-security.net/tools/snacktime.tgz, and includes:
LICENSE | GPL happiness |
CHANGELOG | What's changed between versions |
snacktime.html | This documentation |
snack.fp | Snacktime's fingerprint file |
snacktime.pl | Snacktime itself |
Snacktime is also available uncompressed at the end of this document, in case you just want to read through it.
Snacktime runs quite reliably on Linux kernels 2.2.x and 2.4.x (I use Debian) with libpcap, and requires the following CPAN Perl modules:
Net::RawIP | For crafting and sending packets | (.deb) |
Net::Pcap | For receiving packets | (.deb) |
Finally, you'll need some method of dropping your own RSTs so they don't spoil everything.
One of the drawbacks of RING is that when you're fingerprinting, there's not a lot else you can do, network-wise. Snacktime doesn't even try to shut up your stack, so you'll have to take care of that on your own. Two methods I've found work well:
The traditional method for dropping packets is using a firewall rule. There are a few ways to do this, depending on your existing firewall requirements, but either of these rules would work, presuming you have iptables installed:
iptables -A OUTPUT -p tcp --dport [target port] --tcp-flags RST RST -j DROP iptables -A INPUT -p tcp --sport [target port] --tcp-flags SYN SYN -j DROP
The former will work as long as it's on any device between you and your target, while the latter will only work if the iptables firewall is on your machine.
Another way to avoid RSTing the connection is to spoof both your source IP address (using Snacktime's -s option) and your MAC address (using Dug Song's arpspoof (.deb)). This is more fun, and can have the added effect of reducing the number of packets sent to the target with identifying information about your machine from one to zero. To achieve this, get and install dsniff, and run (in another console):
arpspoof -t [your gateway] [your UNUSED, local fake IP]
A couple caveats: ARP spoofing can be dangerous (some network devices react badly when you start mucking around with their ARP tables), and illegal if you do it with a publicly-addressable Internet address (some states specifically outlaw this sort of spoofing activity through brain dead Super-DMCA legislature).
Snacktime has a short usage blurb, gotten at by supplying no options, or -h. Here are some more details:
So, why would you want to use Snacktime? I can think of a few reasons:
As far as I can tell, measuring RTOs seems to be a pretty accurate and stealthy method for profiling networked devices, and Snacktime seems to be a pretty reasonable implementation. Furthermore, masking ones "normal" RTO can be difficult for some operating systems, and given this technique's relative newness, is much more uncommon than more traditional methods of defeating profiling (but see below).
Some comparisons to other fingerprinting techniques:
However, these applications both have at their disposal comprehensive fingerprint databases. Snacktime has only been tested (and lightly, I might add) against a couple dozen different devices. So, if you have any new fingerprints, please feel free to mail them in to snacktime@planb-security.net.
Finally, there are other fingerprinting applications (QueSO, Internet Scanner, Ettercap, etc.), and while they all have their particular interface niceties, they tend make use of Nmap's fingerprint database and techniques (many funny packets to multiple dest ports).
Snacktime packets look like this:
0.000000 192.168.1.21 -> 192.168.1.1 TCP 12676 > 666 [SYN] Seq=2730301395 Ack=0 Win=64240 Len=0 0000 00 04 5a f6 ed c0 00 10 a4 13 0f f5 08 00 45 00 0010 00 28 05 cd 00 00 40 06 f1 9c c0 a8 01 15 c0 a8 0020 01 01 31 84 02 9a a2 bd 17 d3 00 00 00 00 50 02 0030 fa f0 42 dc 00 00I haven't made any particular effort to make Snacktime packets look more or less like any other normal SYN packets, but that said, I'd be very surprised if there's an IDS system out there that could reliably detect Snacktime traffic -- after all, little lost SYNs are not all that odd on the Internet.
As for defeating Snacktime (and other RTO profilers), changing the behavior of a device's SYN-ACK retries can range from trivial to impossible. For Linux, Toby Miller's recent paper on passive OS fingerprinting discusses the/proc/sys/net/ip4 directory -- therein, you can edit the tcp_synack_retriesto change the number (but not the timing) of SYN-ACKs sent back to Snacktime.
For Windows NT/2k, Microsoft's KB article 120642 discusses a number of TCP/IP behavioral defaults one could change, including default TTL and WinSize, but the RTO is not one of them. This is the same situation for the BSDs, as far as I can tell -- again, BSD administrators can edit the TTL and WinSize values using sysctl, but it appears the RTOs are off-limits. These measures will certainly muck up evaluating Snacktime scores using the -m (more stuff) option, but Snacktime's core functionality remains unaffected.
In case you just want to read my lame source (or, more likely, you're Googling around for examples of Net::RawIP or Net::Pcap usage), the current Snacktime source is avaialable here.
#!/usr/bin/perl
##############################################################################
# snacktime.pl is a Perl implementation of RING for OS Fingerprinting.
# Usage: Snacktime.pl -t <target> -p <port> [-s <source address>]
#
# Requirements: Perl v5.6.1 (or later? probably) (Debian package perl)
# root access (for pcap stuff, and firewall/arp manipulation)
# write access to cwd (since my IPC structure is lame)
# an OS that supports fork() and alarm() (Like Debian GNU/Linux)
# libpcap by LBNL Research Group (Debian package libpcap0)
# Net::RawIP by Sergy Kolychev (Debian package: libnet-rawip-perl)
# Net::Pcap by Tim Potter (Debian package: libnet-pcap-perl)\
# Some method of ensuring you don't send RSTs, so either:
# Iptables by Rusty Russel (Debian package: iptables)
# Arpspoof by Dug Song (Debian package: dsniff)
# permission from your network security officer :)
#
#
#
# Shoutouts: Franck Veysset, Olivier Courtay, and Olivier Heen of the
# Intranode Research Team for their RING proof-of-concept
# <http://www.intranode.com/pdf/techno/ring-full-paper.pdf>
# Stanislav Shanlunov for netkill.pl's and all the syntax and
# structure I ripped off from it.
# <http://people.internet2.edu/~shalunov/netkill/>
# Dug Song for dsniff and Rusty Russel for iptables
# <http://naughty.monkey.org/~dugsong/dsniff/>
# <http://www.netfilter.org/>
# Mike Dausin for UAT and feature enhancements.
# <http://www.dausin.com/>
#
# Updates and documentation: http://www.planb-security.net/wp/snacktime.html
#
# Copyright (C) 2002-2003 Tod Beardsley
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# (Some day I'll convert this and everything else to pod.)
##############################################################################
use Net::RawIP; #For packet crafting.
use Net::Pcap; #For packet listening.
use Socket; #For Inet address packing.
use IO::Handle; #For autoflush;
use Getopt::Std; #For arguments;
use Math::BigFloat; #For sensible floating point math
use strict; #For masochism;
$| = 1 ; # Turn off IO buffering (it'll mess up timing)
# Filling in all the options.
my %options;
getopts('hvVmt:p:f:w:D:s:P:I:',\%options) || usage();
# Don't bother if you're not root. Really.
my $uid = `id -u`; chomp $uid;
die "You must be root to get a snack. Sorry.\n" if $uid > 0;
if ($options{h} || not $options{t}) {
print "Snacktime, alpha release v0.5, Jun/2003. (GNU GPL, version 2).\n";
usage();
}
shift and die "Missing some options. Try $0 -h for help.\n"; # Complain if there's anything unexpected.
my $alarmtimeout = $options{w} || 65; # Seconds to wait after the last syn-ack.
my $target = $options{t} || "192.168.1.1"; # Target we want to fingerprint.
my $dport = $options{p} || 80; # Target's listening port.
my $device = $options{D} || rdev($options{t}); # Ethernet device.
my $source = $options{s} || ${ifaddrlist()}{$device}; # Source address. Required for arpspoof method.
my $sport = $options{P} || int rand 30000 + 1024; # Your source port.
my $verbose = $options{v} || $options{V}; # See times, runner up scores.
my $veryverbose = $options {V}; # See scoring details and more packet info.
my $morestuff = $options{m}; # Check more stuff (winsize and ttl)
# Other variable declarations.
my $pid = $$; # My pid, for tempfiles.
my $parentpid = getppid(); # My parent pid, for child control.
my $filter = "tcp and src host $target and dst port $sport and dst host $source"; # BPF Filter for incoming packets from $target to my $sport.
# Interrupt catcher.
$SIG{INT} = sub {
unless (getppid() != $parentpid) { # I'm the parent.
die "Caught interrupt -- cleaning up.\n";
} else { # I'm the child.
snackcleanup(); # Clean up temp file.
die "\n";
}
} ;
# Main.
die "Can't resolve $target! Quitting!\n" unless inet_aton($target);
$target = inet_ntoa(inet_aton($target));
print "It's snacktime for $target on port $dport...\n\n";
if (fork) { # Gotta fork off a child process to listen.
sleep 1; # Take a little nap while the kid sets up.
getasnack($device, $target, $dport, $source, $sport);
wait;
} else {
snackcatcher($alarmtimeout, $device, $filter, $target);
}
snacktimes($target); # Calculate RTOs.
snackcleanup(); # Clean up temp file.
# Subroutines.
sub getasnack { # This is where we construct and fire off
# the initial syn packet to elicit some snacks.
my $a = shift; # Device
my $b = shift; # Destination target
my $c = shift; # Destination port
my $d = shift; # Source host
my $e = shift; # Source port
my $ipid = int rand 5000 + 100;
my $seq = int rand 4294967295;
my @target = split(/\./,$b); # A bunch of rigamarole to turn
my @source = split(/\./,$d); # strings into a network addresses.
my $dst_ip_bin = pack("C4",@target); # There's surely a better way to
my $src_ip_bin = pack("C4",@source); # go about this.
my $dst_ip = inet_ntoa($dst_ip_bin); #
my $src_ip = inet_ntoa($src_ip_bin); # Yeah, pretty annoying.
my $packet = new Net::RawIP({tcp => {}});
# For the future -- make this much more configurable
$packet->set(
{ip=> {saddr=>$src_ip, daddr=>$dst_ip, frag_off=>0,
tos=>0, id=>$ipid},
tcp=>{source=>$e, dest=>$c, syn=>1, window=>64240, seq=>$seq}}
);
fireprobe($packet);
}
sub fireprobe { #Fire the packet off.
my $a = shift;
$a->send;
}
sub snackcatcher {
my $a = shift; # Alarm timeout
my $b = shift; # Device
my $c = shift; # Pcap filter statement
my $d = shift; # Target host;
alarm $a;
# Net::Pcap listener. This is used for better accuracy when pulling the time.
my $err; #Error messages for Pcap setup stuff.
if (defined $err) {
die; #Be sure to die on an error.
}
# Pcap needs to provide its own source address. Which is good since we're faking it sometimes.
my ($address, $netmask);
if (Net::Pcap::lookupnet($b, \$address, \$netmask, \$err)) {
die "Can't figure out the local netmask. I need this.";
}
my $object;
$object = Net::Pcap::open_live($b, 1500, 0, 0, \$err);
die "Unable to create packet capture object on ", $b, " - ", $err unless $object;
my $filter_in;
my $filter_c;
my $targethost;
$targethost="$d";
$filter_in = "$c";
Net::Pcap::compile(
$object,
\$filter_c,
$filter_in,
0,
$netmask
) && die;
Net::Pcap::setfilter($object, $filter_c);
Net::Pcap::loop($object, 0, \&callback, '') || die ;
Net::Pcap::close($object);
sub callback {
unless (getppid() == $parentpid) { # I'm the child.
my ($user_data, $header, $packet) = @_;
my ($snacktime, $snacktime_sec, $snacktime_usec);
$snacktime_sec = $header->{'tv_sec'} ;
$snacktime_usec = sprintf("%06d", $header->{'tv_usec'} ); # Eek! This method drops leading zeros! Need the sprintf to hang on to them.
$snacktime = "$snacktime_sec.$snacktime_usec";
my $ip_packet = unpack("H*",substr($packet,14)); # Drop LLC headers -- this assumes we're on Ethernet -- though these will be useful in the future if we want to type NICs by MAC address.
my $ip_ttl = hex(substr($ip_packet,16,2)); # The TTL value in decimal.
my $ip_len = hex(substr($ip_packet,4,4)); # Reported IP packet length in decimal.
my $tcp_packet = substr($ip_packet,(substr($ip_packet,1,1)*4*2)); # Take the offset from the 2nd byte of the IP header. That *2 at the end is cause we're dealing with a unpacked hex string now.
my $tcp_flags = hex(substr($tcp_packet,26,2)); # TCP flags in decimal.
my $bin_tcp_flags = substr(unpack("B32",pack("N",$tcp_flags)),-8); # Convert flags back to binary.
my $tcp_win = hex(substr($tcp_packet,28,4));
if ( substr($bin_tcp_flags,-2,1) && substr($bin_tcp_flags,-5,1) ) { # Test the SYN and ACK bits directly.
print sprintf("%-39s", "\t$d gave me a snack! Yum!");
print "\[SYN-ACK Time: $snacktime]" if $verbose;
print "\n";
open ("SNACKS", ">>tempsnacks-$pid");
autoflush SNACKS;
if ($morestuff) {
print SNACKS "$ip_ttl $tcp_win\n";
}
print SNACKS "$snacktime\n";
alarm $a;
} elsif (substr($bin_tcp_flags,-3,1)) { # Test the RST bit.
print sprintf("%-39s", "\t$d reset the connection.");
print "Pick a different port.\n";
alarm 1;
} else {
print sprintf("%-39s", "\t$d sent something weird.");
print "I'm outta here.\n";
alarm 1;
}
if ($veryverbose) {
print " " x 46; print "[Flags: $bin_tcp_flags] [Length: $ip_len]\n"; # Display flags and packet length.
print " " x 46; print "[WinSize: $tcp_win] "; # Display WinSize.
print "[TTL: $ip_ttl]\n";
}
}
}
}
sub snacktimes {
my $a = shift; # Target host
open ("SNACKS" , "tempsnacks-$pid" );
my $ttl; my $winsize;
my @snacktimes;
while (<SNACKS>) {
chomp;
push @snacktimes, $_ if /\./;
if ($morestuff) {
unless (/\./) {
$ttl = "TTL:".(split)[0];
$winsize = "WinSize:".(split)[1];
}
}
}
if ($morestuff) {
snackmath(@snacktimes,$ttl,$winsize);
} else {
snackmath(@snacktimes);
}
}
sub snackmath {
my @datapoints = @_;
my $datapoint; my $seen_ttl ; my $seen_winsize; my @snacktimes;
if ($morestuff) {
foreach $datapoint (@datapoints) {
push (@snacktimes,$datapoint) unless $datapoint =~ /:/;
if ($datapoint =~ /\:/) {
$seen_winsize = $datapoint if substr($datapoint,0,1) eq "W";
$seen_ttl = (split(/\:/,$datapoint))[1] if substr($datapoint,0,1) eq "T";
}
}
} else {
@snacktimes = @datapoints;
}
my $thissnack;
my $lastsnack;
my @rttdeltas;
if ( $snacktimes[1]) { # At least two snacks.
print "\nFirst snack at:\t\t\t\t$snacktimes[0]\n" if $verbose;
for my $current (1..@snacktimes-1) {
$thissnack = new Math::BigFloat $snacktimes[$current];
$lastsnack = new Math::BigFloat $snacktimes[$current-1];
push @rttdeltas, $thissnack - $lastsnack;
if ($verbose) {
print "Retry $current delta: ";
print $thissnack - $lastsnack;
print " secs\t\t$thissnack\n";
}
}
} elsif ( $snacktimes[1] == 0 ) {
return;
} else {
print "Not enough snacks. I need at least two to compare.\n\n";
return;
}
my $fprints = $options{f} || "./snack.fp";
open("FINGERPRINTS",$fprints) || die "Fingerprint file missing. No analysis for you!\n";
my @fingerprints;
while (<FINGERPRINTS>) {
chomp;
push @fingerprints, $_;
}
my $stack;
my $osname;
my @knownrtts;
my $rtt;
my $known_winsize;
my $known_maxttl;
my %scores;
my $score;
foreach $stack (@fingerprints) {
if ($stack =~ m/^[\w+]/) {
if ($stack =~ /WinSize:/ && $stack =~ /MaxTTL:/) {
($osname,$known_winsize,$known_maxttl,@knownrtts) = split (/\s+/,$stack) ;
} else {
($osname,@knownrtts) = split (/\s+/,$stack) ;
}
if (@rttdeltas == @knownrtts) {
$score++; # Start with a point if it's the same number of retries.
print "\n\t$osname:\n\t--> Expected retry count for this OS. Start with a point.\n" if $veryverbose;
if ($morestuff) {
if ($seen_winsize eq $known_winsize) {
$score = $score+3;
print "\t--> Matching WinSize for this OS. Three extra points.\n" if $veryverbose;
}
$known_maxttl = (split(/:/,$known_maxttl))[1];
if ($seen_ttl <= $known_maxttl) {
$score++;
print "\t--> Possible TTL for this OS. Extra point.\n" if $veryverbose;
if ($seen_ttl >= ($known_maxttl-32) ) {
$score++;
print "\t--> Within 32 hops of Max TTL for this OS. Extra point.\n" if $veryverbose;
}
}
}
if ($osname =~ m/^Generic/) {
$score--; # But lose a point if you're "Generic."
print "\t--> (You're a \"Generic\" OS. Lose a point.)\n" if $veryverbose;
}
for $rtt (0..(@rttdeltas-1)) {
my $subsecond;
my @precision = qw(1 0.1 0.01 0.001 0.0001 0.00001 0.000001);
foreach $subsecond (@precision) {
if ( ( ($knownrtts[$rtt] - $subsecond) <= $rttdeltas[$rtt] )
and
( $rttdeltas[$rtt] <= ($knownrtts[$rtt] + $subsecond) ) ) {
$score++; # Get a point if you're within so many subseconds*2 on this retry.
if ($veryverbose) {
print "\t--> ";
print $knownrtts[$rtt]-$subsecond;
print " >= ";
print $rttdeltas[$rtt];
print " >= ";
print $knownrtts[$rtt]+$subsecond;
print ": Within ". $subsecond * 2 ." secs.\n";
}
}
}
}
$scores{$osname} = $score;
print "\t----> Total Points: $score\n" if $veryverbose;
undef $score;
}
}
}
unless (%scores) {
print "\nNo good guesses! ";
print "(Maybe try again with verbose (-v) enabled.)\n" unless $verbose;
print "\n";
} else {
print "\n";
}
my $possible; my $winner; my $lastscore; # Winner counters;
my @guesses = qw(Lame Weak Alright Good Educated Solid Elite Super-Leet);
foreach $possible (sort { $scores{$b} <=> $scores{$a} } keys %scores) {
$winner++;
if ( $winner == 1 ) {
$lastscore = $scores{$possible};
if ($scores{$possible} >= 24) {
$scores{$possible} = 24; # Max out the scores at 24.
}
print "@guesses[int($scores{$possible} / 3 )] guess: $target is $possible";
print " ($scores{$possible})" if $verbose;
print "\n";
} elsif ($winner == 2 ) {
if ($scores{$possible} > 24) {
$scores{$possible} = 24;
}
if ($verbose) {
print "Tied: $possible" if ($lastscore == $scores{$possible});
print "Runner up (@guesses[int($scores{$possible} / 3)] guess): $possible" unless ($lastscore == $scores{$possible});
print " ($scores{$possible})";
}
print "\n";
}
}
}
sub snackcleanup {
unlink "tempsnacks-$pid" if (-T "tempsnacks-$pid");
}
sub usage {
die <<EOF;
Usage: snacktime.pl -t target host [-p target port] [-w secs] [-v] [-V]
[-D dev] [-S source IP] [-P source port] [-I iptables] [-F fingerprints]
Common options:
-t : Target device (name or IP). No ranges, yet. *REQUIRED*
-p : Target port. It needs to be open. (default: 80)
-w : Wait seconds for last retry. Lower it for less accuracy. (default: 65)
-m : More stuff. Check win/ttl. Might be mangled by firewalls!
-v : Verbose. Its use is recommended to ID unknown OSes.
-s : Source IP. (default: real IP. Change if you're arpspoofing.)
Uncommon options:
-D : Ethernet device for Tx/Rx (default: best guess, usually eth0)
-P : Source port. (default: random > 1024)
-f : Fingerprint file. (default: ./snack.fp)
-V : Very verbose. Use to see the scoring and packet details.
-h : This help, version info, etc.
Examples:
snacktime.pl -t 68.163.90.12 -p 80 -v -m # Normal usage.
snacktime.pl -p 53 -S 53 -f fprints.txt # Using a source port.
snacktime.pl -t 146.82.174.12 -p 80 -s 192.168.1.10 # Using a spoofed IP.
| See README for details. | Copyright (c) Tod Beardsley, 2002-2003 |
EOF
}
Copyright © Tod Beardsley, 2002-2003