#!/usr/local/bin/perl
#

$version = "1.02";
# print banner
print "-- ISNprober / $version / Tom Vandepoel (Tom.Vandepoel\@ubizen.com) --\n\n";

# 
# ftp://ftp.ubizen.com/tools/isnprober-1.02.tgz
#
# ISNprober is a tool that samples TCP Initial Sequence Numbers or IP ID's
# and can use that information to determine if a set of IP addresses belong
# to the same TCP/IP stack (machine) or not.
#
# This tool has been written for professional penetration testing. 
# If you're a kiddie, you're not going to be interested if two vulnerable
# IIS's are on one and the same box, so don't waste your time on this tool.
#
# Usage of this tool is completely at your own risk and the author
# will not be held liable for any damage.
#
# This script requires Net::RawIP which can be obtained from:
# http://quake.skif.net/RawIP/ or http://www.cpan.org
#
# Thanks to Zillion (Zillion@safemode.org) for supplying a skeleton script
# to base my code on.
#
# Thanks to HDMoore (hdm@secureaustin.com) for the excellent suggestion of 
# using IP ID's instead of TCP ISN's.
#
# The following paper my Michal Zalewski has been very helpful in 
# writing this tool:
# http://razor.bindview.com/publish/papers/tcpseq.html
#
# Don't hesitate to contact me if you've got suggestions/additions.

# HISTORY
#
# 1.0:  20010827: initial version
# 1.01: 20010829: fork/wait bug fix
#                 reliability/retry for group mode
#                 output cosmetics
# 1.02: 20011005: added --ipid switch
#       20011108: better reporting of no response
#                 responses with seq = 0 are now distinguished from no response
#       20011211: writeup README for release
#                 added case for constant IPID=0 (or TCPISN=0) 

use Net::RawIP; 

# defaults
$response_timeout = 1; 
$mode = 1;
$iterations = 3;
$output_disabled = false;
$variate_source_port = false;
$ipid_mode = false;
$quiet = false;
$source_port = int rand(255) + 1024;

while($_ = $ARGV[0], /^-/) {
    shift;
    last if $arg =~ /^--$/;
    /^-c/ && do { $mode = 2; };
    /^-g/ && do { $mode = 3; };
    /^-q/ && do { $quiet = true; };
    /^-i/ && do { $dev = shift; }; 
    /^-n/ && do { $iterations = shift; };
    /^-p/ && do { $default_port = shift; };
    /^-w/ && do { $response_timeout = shift; };
    /^--variate-source-port/ && do { $variate_source_port = true; };
    /^--ipid/ && do {$ipid_mode = true; };
    /^-v/ && do { exit 0; };
    /^-h/ && do { &usage; exit 0; };
    /^-\?/ && do { &usage; exit 0; };
}


sub usage {
print <<"ENDEND";
Usage:
 Single host mode:
 $0 [options] <ip>|<ip:port>

 Compare mode:
 $0 [options] -c <ip1>|<ip1:port1> <ip2>|<ip2:port2>

 Group mode:
 $0 [options] -g <filename>  
   
-v prints version number and exit 
-n <iterations>: number of probe iterations [default = 3]
-i <interface>: network interface 
-p <default port>: default port to use if port not specified [default = 80]
-q: suppress raw output, only display results
-w: timeout to wait for response packet (s) [default = 1]
--ipid: use IP ID's instead of TCP ISN's
--variate-source-port: use a different source port for each packet sent
(default is to use the same source port for all probes) 

ENDEND
}


$a = ifaddrlist;
foreach $d (keys %$a) {
    if (!$dev) {
	    if ($d !~ /^lo/) {
		$dev = $d;
		$myip = $a->{$d};
	    }
	} else {
	    if ($dev eq $d) {
		$myip = $a->{$d};
	    }
	}
}


if (!$myip) { &usage; die "Not a valid device name";}

print "Using $dev:$myip\n";

# default TCP port to probe if none given
if (!$default_port) { $default_port = 80;}

# global variable to pass result
$globalseq = 0;
$globalvalid = false;

if ($mode==1)  {
    # single host (default) mode
    my $ip = shift;
    # quiet mode doesn't make sense here
    $quiet = false;
    # parse target syntax
    if ($ip !~ /^(\d+\.\d+\.\d+\.\d+)(\:(\d+))?$/) {
	&usage;
	die ("invalid ip:port syntax");
    };
    $ip = $1;
    if (!($port = $3)) {$port = $default_port; }
    print "Probing host: $ip on TCP port $port.\n\n";
    
    if ($ipid_mode eq false) {
	print pack("A20","Host:port").pack("A15","ISN").pack("A15","Delta")."\n";
    } else {
	print pack("A20","Host:port").pack("A15","IPID").pack("A15","Delta")."\n";
    }
    # probing loop
    for($times=0; $times < $iterations; $times++) {
	$prevseq = $seq;
	&isnprobe($ip,$port);
	$seq = $globalseq;
	$valid = $globalvalid;
	if ($first eq false) {
	    if ($valid eq false) {
		$delta = '!';
		$seq = '!';
	    } else {
		$delta = $seq - $prevseq; 
	    }
	} else { 
	    $first = false;
	    if ($valid eq false) {
		$seq = '!';
	    }
	}

	print pack("A20","$ip:$port").pack("A15",$seq).pack("A15",$delta)."\n";;
    }	
} elsif ($mode==2) {
    # compare mode
    my $ip1 = shift;
    my $ip2 = shift;
    my ($result,$rly1,$rly2) = &isncompare($ip1,$ip2);
    exit ($result);
} elsif ($mode == 3) {
    # read list of targets
    my $targetfile = shift;
    if (!open(TF, $targetfile)) { &usage; die "Can't open targetfile $targetfile"};

    # parse targets
    while (<TF>) {
	my $ip = $_;
	my $port = "";
	# check syntax
	if ($ip !~ /^(\d+\.\d+\.\d+\.\d+)(\:(\d+))?$/) {
	    print "Ignored invalid entry: $ip";
	    next;
	} else {
	    # store in array
	    $ip = $1;
	    if (!($port = $3)) {$port = $default_port; }
	    push @targets, "$ip:$port";
	}
    }
    close (TF);

    @targets2 = @targets;
 LOOP1:
    foreach $target1 (@targets) {
        # always shorten secondary loop first
	shift @targets2;
	# skip if listed as unreliable
	grep(/^$target1$/,@unreliable_targets) && next;
	# skip if already found as stack group
	grep(/(^|\/)$target1($|\/)/, values %stackgroups) && next; 
      LOOP2:
	foreach $target2 (@targets2) {
	    # skip if listed as unreliable
	    grep(/^$target2$/,@unreliable_targets) && next;
	    # skip if already listed in stackgroup
	    grep(/(^|\/)$target2($|\/)/, values %stackgroups) && next; 
	    my ($result,$rly1,$rly2) = &isncompare($target1,$target2);
	    # try once more if partial loss
	    if (($rly1 eq false) || ($rly2 eq false)) {
		($result,$rly1,$rly2) = &isncompare($target1,$target2);
	    } 
	    # if probe was reliable
	    if (($rly1 eq true) && ($rly2 eq true)) {
		# if result is positive create/modify stackgroups entry
		if ($result eq true) {
		    # check if $target1 already has a stackgroups entry
		    if ($stackgroups{$target1} != '') {
			$stackgroups{$target1} = $stackgroups{$target1}."/$target2";
		    } else {
			$stackgroups{$target1} = "$target1/$target2";
		    }
		}
	    # if probe is unreliable, list the guilty parties as unreliable
	    } else {
		if ($rly1 eq false) { 
		    if (!grep(/^$target1$/,@unreliable_targets)) {
			push @unreliable_targets, $target1;
			next LOOP1;
		    }
		}
		 
		if ($rly2 eq false) { 
		    if (!grep(/^$target2$/,@unreliable_targets)) {
			push @unreliable_targets, $target2;
			next LOOP2;
		    } 
		}
	    }
	}
 
	# if main iteration didn't find match it must be a sole ip
	if (!$stackgroups{$target1}) {
	    $stackgroups{$target1} = $target1;
	}
    }
    print "\nProbable stack groups:\n\n";
    foreach $group (values %stackgroups) {
	print (join " / ",(split (/\//,$group)));
	print "\n";
    }
    print "\n";
    if (@unreliable_targets) {
	print "Unreliable targets:\n\n";
	foreach $target (@unreliable_targets) {
	    print $target."\n";
	}
    }
}

sub isnprobe {
    my $ip = shift;
    my $port = shift;


    $b=0;

    # reset global variables for passing results
    $globalseq = 0;
    $globalvalid = false;

    my $filter = "src host $ip and src port $port";
    my $psize = 1500;

    if(fork()){
	# make sure random generator gets moved along
        # otherwise rand call in child will always produce the same
	rand;

	# main proc listens
	$b = new Net::RawIP;

        # temp redirect stdout/stderr to get rid of
        # annoying pcap output:
        # Kernel filter, protocol ALL, raw packet socket

	if ($quiet eq false) {&disable_output;}

	my $pcap = 0;
	if (!($pcap = $b->pcapinit($dev,$filter,$psize,10))) { 
	    &enable_output;
	    die "pcapinit failed\n";
	};

	# restore stdout/stderr
	if ($quiet eq false) { &enable_output;}

	$SIG{ALRM} = \&timed_out;
	eval {
	    # timeout for response packet
	    alarm($response_timeout);
	    loop $pcap,1,\&analyse,\@a;
	    alarm(0);           # Cancel the pending alarm if user responds.
	};
	# reap children or we'll create zombies
	wait;

	# cleanup pcap FD
        # normal close doesn't cut it: 
        # we run out of raw sockets after 1024 iterations
      
	Net::RawIP::close($pcap);

	sub timed_out {
	    die "Aarrgghhh cannot wait any longer....";
	}
     } else {
        # fork packet send
        # wait 100ms before sending packet
        select(undef, undef, undef, 0.10);
        &send_packet ($ip,$port);
        exit(0);
    }
}

sub send_packet {
    my $ip = shift;
    my $port = shift;
    my $source = 0;

    if ($variate_source_port eq true) {
	$source = int rand(255) + 1024;
    } else {
	$source = $source_port;
    };

    my $pkt = new Net::RawIP;
    
    my $data = "";
    
    $pkt->set({ ip => {saddr => $myip,
		       ttl => 66,
		       daddr => $ip
		       #,id => 6666
                 },
			   tcp=> {dest => $port,
                                  data => $data,
		 		   ack => 0,
			 	   fin => 0,
			        source => $source,
                                   syn => 1}
      });

    $pkt->send(0,1);    
}

sub analyse {
    $b->bset(substr( $_[2],14));
    my @fl;
    if ($ipid_mode eq false) {
	@fl = $b->get({tcp=>[qw(seq syn)]});
    } else {
	@fl = $b->get({ip=>[qw(id)]});
    }

    if($fl[0] ne '') {  
	# have to pass via global variable
	$globalseq = $fl[0];
	# for IPID any response is valid: all packets contain an id
	if ($ipid_mode eq true) {
	    $globalvalid = true;
        # for TCP ISN, it should be a SYN(ACK)
	# RST's always have seq = 0
	} elsif ($fl[1] eq 1) {
	    $globalvalid = true;
	}
    }   

}

sub isncompare {
    my $ip1 = shift;
    my $ip2 = shift;

    # parse target syntax
    if ($ip1 !~ /^(\d+\.\d+\.\d+\.\d+)(\:(\d+))?$/){
	&usage; 
	die ("invalid ip:port syntax")
	};
    $ip1 = $1;
    if (!($port1 = $3)) {$port1 = $default_port; }

    
    if ($ip2 !~ /^(\d+\.\d+\.\d+\.\d+)(\:(\d+))?$/) {
	&usage;
	die ("invalid ip:port syntax");
    };

    $ip2 = $1;
    if (!($port2 = $3)) {$port2 = $default_port; }

    if ($quiet eq true) { &disable_output; };
    print "\n";
    print "Probing host: $ip1 on TCP port $port1.\n";
    print "Probing host: $ip2 on TCP port $port2.\n";
    print "\n";
    
    my $first = true;
    my $same = true;
    my $reliable1 = true;
    my $reliable2 = true;
    my $delta = '';

    if ($ipid_mode eq false) {
	print pack("A20","Host:port").pack("A15","ISN").pack("A15","Delta")."\n";
    } else {
	print pack("A20","Host:port").pack("A15","IPID").pack("A15","Delta")."\n";
    }
    # compare loop
    for($times=0; $times < $iterations; $times++) {
	
	# probe target1
	$prevseq = $seq;
	&isnprobe($ip1,$port1);
	$seq = $globalseq;
	$valid = $globalvalid;
	if ($first eq false) { 
	    if ($valid eq false) {
		$reliable1 = false;
		$delta = '!';
		$seq = '!';
	    } else {
		$delta = $seq - $prevseq; 
		if ($delta < 0) { 
		    $same = false; 
		# case for ipid=0
		} elsif (($delta == 0) && ($seq == 0)) {
		    $same = false;
		};
	    };
	} else { 
	    $first = false;
	    if ($valid eq false) {
		$reliable1 = false;
		$seq = '!';
	    }
	}
	print pack("A20","$ip1:$port1").pack("A15",$seq).pack("A15",$delta)."\n";;
	# probe target2
	$prevseq = $seq;
	&isnprobe($ip2,$port2);
	$seq = $globalseq;
	$valid = $globalvalid;

	if ($valid eq false) {
	    $reliable2 = false;
	    #if (!grep(/^$ip2:$port2$/,@unreliable_targets)) {
	    #push @unreliable_targets, "$ip2:$port2";
	    #}
	    $delta = '!'; $seq = '!';
	} else {
	    $delta = $seq - $prevseq;	    
	    if ($delta < 0) { 
		$same = false;
	    # case for ipid=0
	    } elsif (($delta == 0) && ($seq == 0)) {
		$same = false;
	    };
	};
	print pack("A20","$ip2:$port2").pack("A15",$seq).pack("A15",$delta)."\n";;
	
    }

    print "\n";
    
    if ($quiet eq true) { &enable_output; };

    # output result line
    print pack("A20","$ip1:$port1");
    print " <> ";
    print pack ("A20","$ip2:$port2");
    print " == [";
    if ($reliable1 eq false || $reliable2 eq false) {
	print "!";
	$result = false;
    } elsif ($same eq true) {
	print "+";
	$result = true;
    } else {
	print "-";
	$result = false;
    }
    print "]\n";
    return ($result,$reliable1,$reliable2);
}

sub disable_output {
    if ($output_disabled eq false) {
	open(OLDOUT, ">&STDOUT");
	open(OLDERR, ">&STDERR");
	open(STDOUT, '>/dev/null') || print "Can't redirect stdout\n";
	open(STDERR, ">&STDOUT")     || print "Can't dup stdout\n";
	select(STDERR); $| = 1; # make unbuffered
	select(STDOUT); $| = 1; # make unbuffered
	$output_disabled = true;
	}
}

sub enable_output {
    if ($output_disabled eq true) {
	close(STDOUT);
	close(STDERR);
	open(STDOUT, ">&OLDOUT");
	open(STDERR, ">&OLDERR");
	$output_disabled = false;
    }
}
