#!/usr/local/bin/perl -w
##############################################################################
#
# Network Leak Finder Client - netleak
# 2005 © Jonas Hansen <jonas.v.hansen@gmail.com>
#
# $Id: netleak,v 1.1.1.1 2005/01/16 21:42:22 cvs Exp $
#
##############################################################################
#    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
##############################################################################

use Getopt::Long;
use IO::Socket;
use IO::Socket::INET;
use IO::Interface qw(:flags);
use Net::RawIP;
use NetAddr::IP;

# Autoflush buffers
$|++;

print "[~] netleak v.0.1a - Network Leak Finder Client\n";

# we need to be root
if ($< != 0) {
    die "[-] You need to be root to run netleak";
    <>;
}

# Set up the signal handlers
BEGIN {
    $SIG{'INT'}  = sub { print "\n[+] Exiting\n"; exit; };
    $SIG{'QUIT'} = sub { print "\n[+] Exiting\n"; exit; };
}


$d_args{'spoof=s'}     = 1; # IP-address where netleakd is running
$d_args{'protocol=s'}  = 1; # IP, UDP, ICMP or both
$d_args{'fast'}        = 1; # Fast scan mode (network & broadcast)
$d_args{'verbose'}     = 1; 
$d_args{'testing'}     = 1; # Enabling dummy mode (no-action)
$d_args{'signature=s'} = 1; # The string inside each packet
$d_args{'cfile=s'}     = 1; # Configuration file
$d_args{'interface=s'} = 1; # Network interface
$d_args{'policy=s'}    = 1; # politeness policy
$d_args{'tfile=s'}     = 1; # target file
$d_args{'help'}        = 1;

my $target;

# Load command line arguments
load_args();

if ($Args{'tfile'}) {
    @targets = read_target_file($Args{'tfile'}) if $Args{'tfile'};	
} else {
    @targets = @ARGV;    
}

die "[-] You must specify some targets!\n" if !@targets;

foreach $target ( @targets ) {
    my $net = new NetAddr::IP $target;
    die "[-] Error: Invalid target $target\n" if !$net;
    my $network   = $net->network();
    my $broadcast = $net->broadcast();
    #print "[*] Warning: The block size is unknown! Use CIDR notation\n" if ($network eq $broadcast);
    print "[+] Network address is ".$network->addr."\n" if $Args{'verbose'};
    print "[+] Broadcast address is ".$broadcast->addr."\n" if $Args{'verbose'};

    if ($Args{'fast'}) {
	# Fast scan mode
	print "[+] Leak testing ".$net." by broadcast & network addresses\n";
	do_broadcasts($net);
    } else {
	# prints also net & broadcast - fix!
	print "[+] Leak testing ".$net->range()." (".($net->masklen()<31?$net->num()-1:1)." host(s)) - external ip is ".$Args{'spoof'}."\n";
	do_range($net);
    }    
}



sub do_broadcasts {
    # leaktest a single host (in case user mixed flags up)
    my $net = shift(@_) or die "[-] Error: invalid target!\n";
    my $orig_net = $net;
    if ($net->masklen() > 30) {
	leaktest($net->addr(), $Args{'spoof'});
    } else {
	# give both network and broadcast addresses a go
	# Net::RawIP won't send packets to some network & broadcast addresses!
	# Happens when client & target are on the same subnet - FIX       
	leaktest($net->network()->addr(), $Args{'spoof'});
	print "[+] DONE\t".$orig_net->network()->addr()."\t(network)\n";	
	leaktest($net->broadcast()->addr(), $Args{'spoof'});	
	print "[+] DONE\t".$orig_net->broadcast()->addr()."\t(broadcast)\n";
    }

}

sub do_range {
    # leaktest a single host
    my $net = shift(@_) or die "[-] Error: invalid taget!\n";
    my $orig_net = $net;
    if ($net->masklen() > 30) {
	leaktest($net->addr(), $Args{'spoof'});
    }

    # leaktest a range
    my $sum = 0;
    my $total = $net->num()-1;
    
    # first give the broadcasts a go
    do_broadcasts($net) if ($net->masklen < 30);

    # start at the network address
    $net = $net->network;
    while ($net && $net < $net->broadcast && !$Args{'fast'}) {
	# skip the network address :) 
	if ($net->addr() =~ $net->network()->addr()) {
	    $net ++;
	    $sum ++;
	    next;
	}
	my $ip = $net->addr();
	leaktest($ip, $Args{'spoof'});
	print "\r[+] ", sprintf('%.0f', 100 * ($sum / $total)), '% ' if !$Args{'verbose'};
	$net ++;
	$sum ++;
	next;
    }
    print "\r[+] DONE\t".$orig_net."\n";
}



sub check_interface {
    my $s = IO::Socket::INET->new(Proto => 'udp');
    my @interfaces = $s->if_list;
    my $found = 0;
    my $iface = shift(@_);
    for my $if (@interfaces) {
        if($if eq $iface) {
	    $found=1;
	    last;
        }
    }
    die "[-] Interface $iface not found!" if(!$found);
    my $flags = $s->if_flags($iface);
    print "[*] Warning: interface $iface is loopback!\n" if $flags & IFF_LOOPBACK;
    die "[-] Interface $iface not up!" if !($flags & IFF_RUNNING);
    print "[+] Using network interface $iface\n" if $Args{'verbose'};
}


sub parse_cf {
    my $file = shift(@_);
    open F, "< $file" or die "[-] Can't open configuration-file $file : $!";
    print "[+] Using config file $file\n";
    my @settings = <F>;
    close F;

    %CArgs = ();    
    foreach my $s ( @settings ) {
	# ignore blank lines
	next if $s =~ /^(\s)*$/;
	# ignore comments
	next if $s =~ /^\#.*/;
	chomp $s;
	my ($key,$value) = split(/\=/,$s,2);
	# ignore trash
	next if !$key || !$value;
	# load everything - even garbage config!! FIXME
	$CArgs{$key} = $value;
	# load configuration into Args if it werent specified on the command line
	$Args{$key} = $value if !defined($Args{$key});
    }
    
}

sub load_args {
    die "Invalid commandline option(s)!\n" unless GetOptions(\%Args, keys %d_args);
    if (defined($Args{'help'})) {
	show_usage();
	exit 0;
    }

    print "[+] Enabling verbose mode\n" if defined($Args{'verbose'});
    
    if ($Args{'cfile'}) {
	parse_cf($Args{'cfile'});
    } else {
	# search for default config files
	(-e '/usr/local/etc/netleak.conf') && parse_cf('/usr/local/etc/netleak.conf') &&
	    print "[+] Using config-file /etc/netleak.conf\n";
	(-e '/etc/netleak.conf') && parse_cf('/etc/netleak.conf') &&
	    print "[+] Using config-file /etc/netleak.conf\n";
	(-e '~/.netleak') && parse_cf('~/.netleak') &&
	    print "[+] Using config-file ~/.netleak\n";
    }

    $Args{'interface'} = 'eth0' if !$Args{'interface'};
    check_interface($Args{'interface'});
    die "[-] No network interface specified!\n" if !$Args{'interface'};
    
    $Args{'signature'} = 'IP:' if !$Args{'signature'};
    print "[+] Using signature prefix \"".$Args{'signature'}."\"\n" if $Args{'verbose'};
    
    if (defined($Args{'fast'})) {
	$Args{'fast'} = '1';
    } else {
	$Args{'fast'} = '0';
    }
    
    #defined($Args{'fast'}) ? $Args{'fast'}='1': $Args{'fast'}='0';  # huh?
    print "[+] Using fast scan mode\n" if $Args{'fast'} && $Args{'verbose'};
    
    die "[-] Unspecified spoof address!\n" if !$Args{'spoof'};
    print "[+] Using spoofed host ".$Args{'spoof'}."\n" if $Args{'verbose'};

    # derive used protocol. Commandline -> config files -> default
    $Args{'protocol'} = 'icmp' if !$Args{'protocol'};
    die "[-] Error: Unknown protocol $Args{'protocol'}\n" 
	unless ( $Args{'protocol'} =~ m/^icmp$/i ||
		 $Args{'protocol'} =~ m/^udp$/i  ||
		 $Args{'protocol'} =~ m/^ip$/i   ||
		 $Args{'protocol'} =~ m/^all$/i
		 );    
    die "[-] You must specify a protocol!\n" if !defined($Args{'protocol'});
    print "[+] Using ".$Args{'protocol'}." protocol\n" if $Args{'verbose'};
    
    $Args{'policy'} = 'normal' if !defined($Args{'policy'});
    die "[-] Invalid policy $Args{'policy'} specified!\n"
	unless ( $Args{'policy'} =~ m/^slow$/i ||
		 $Args{'policy'} =~ m/^normal$/i ||
		 $Args{'policy'} =~ m/^fast$/i
		 );
    die "[-] You must specify a policy!\n" if !defined($Args{'policy'});	
    print "[+] Using \"".$Args{'policy'}."\" policy\n" if $Args{'verbose'};

    print "[+] Reading targets from \"$Args{'tfile'}\"\n" if $Args{'tfile'} && $Args{'verbose'};    
    
}


sub leaktest {
    my ($ip, $spoof) = @_;
    leaktest_icmp($ip, $spoof) if $Args{'protocol'} =~ m/^icmp$/i;
    leaktest_udp($ip, $spoof) if $Args{'protocol'} =~ m/^udp$/i;
    leaktest_ip($ip, $spoof) if $Args{'protocol'} =~ m/^ip$/i;
    
    if ($Args{'protocol'} =~ 'all' ) {
	leaktest_icmp($ip, $spoof);
	policy_sleep();
	leaktest_udp($ip, $spoof);
	policy_sleep();
	leaktest_ip($ip,$spoof);
    }
    policy_sleep();
}

sub leaktest_icmp {
    my ($ip, $spoof) = @_;
    my $packet = new Net::RawIP({icmp =>{type=>'8',data=>'<'.$Args{'signature'}.$ip.'>'},
			         ip => { id => 16} # wont' survive
			     })
	or die "[-] Error crafting icmp-packet! $!\n";
    $packet->set({ip => {saddr => $spoof, daddr => $ip, tos=>0 }})
	or die "[-] Error crafting icmp-packet! $!\n";
    eval {
	$packet->send if(!$Args{'testing'});
    };
    print "[-] ".$@ if ($@);
    print "[+] [ICMP:1]\t".$ip."\n" if $Args{'verbose'};
}

sub leaktest_udp {
    my ($ip, $spoof) = @_;
    my $packet = new Net::RawIP({udp=>{data=>'<'.$Args{'signature'}.$ip.'>'}})
	or die "[-] Error crafting udp-packet! $!\n";
    $packet->set({ip => {saddr => $spoof, daddr => $ip, tos=>0 },
	          udp => { dest=>0 }
	      })
	or die "[-] Error crafting udp-packet! $!\n";
    eval {
	$packet->send() if !$Args{'testing'};
    };
    print "[-] ".$@ if ($@);
    print "[+] [UDP:1]\t".$ip."\n" if $Args{'verbose'};
}


#  Produces icmp paramter problem return message.
#  All well-behaving hosts should respond to this and all well-behaving routers should let it through

sub leaktest_ip {
    # set size to f.ex 8 (8x4=32 bytes) and leave 12 bytes for IP options field (which must contain garbage?)
    # freeze protocol to something that will make (non)sense with respect to IP options
    my ($ip,$spoof) = @_;
    my $packet = new Net::RawIP({
                                 ip => {
                                     ihl=>7,
                                     tot_len => 40,
                                     tos => 0,
                                     ttl => 64,
				     protocol=>17,
                                     saddr=>$spoof,
                                     daddr=>$ip
                                     },
					 generic => {data=> '<'.$Args{'signature'}.$ip.'>' }
                             }) or die "[-] Error crafting IP-packet! $!\n";

    # experiment some with ip-options here! (fragmentation?)
    eval {
	$packet->send if !$Args{'testing'};
    };
    print "[-] ".$@ if ($@);
    print "[+] [IP:1]\t".$ip."\n" if $Args{'verbose'};

}

sub read_target_file {
    my $file = shift(@_) or die "[-] Error: no file to parse!\n";
    open F, "< $file" or die "[-] Can't open target-file $file : $!";
    print "[+] Using targets from file \"$file\"\n";
    my @t = <F>;
    close F;
    
    my @targets;
    foreach my $target ( @t ) {
	chomp $target;
	next if $target =~ /^(\s)*$/;
	next if $target =~ /^\#.*/;
	if (!$target) {
	    print "[-] Error: undefined target $target\n";
	    next;
	}
	my $net = new NetAddr::IP($target) or die "[-] Error: you have a syntax error in your target file! ($target) $!\n";
	push @targets, $net->cidr();
	
    }
    return @targets;
}


sub policy_sleep {
    select ( undef, undef, undef, 30  / 1000 ) if $Args{'policy'} =~ m/^normal$/i;
    select ( undef, undef, undef, 150 / 1000 ) if $Args{'policy'} =~ m/^slow$/i;
}

sub show_usage {
    print "Usage: netleak [OPTION[( |=)VALUE]] <target>\n";
    print "Where target is an ip-address, a resolvable host or a CIDR block of ip-addresses\n";
    print " --cfile\t<file>\t\t\tSpecify the configuration file to use\n";
    print " --protocol\t<icmp|udp|all>\t\tProtocol to use\n";
    print " --spoof\t<IPv4 address>\t\tHost that is listening on the Internet\n";
    print " --signature\t<string>\t\tA string that should indicate an internal ip-address\n";
    print " --policy\t<slow|normal|fast>\tTells how fast packets should be sent\n";
    print " --tfile\t<file>\t\t\tRead target(s) from file\n";
    print " --help\t\t\t\t\tShow this\n";	
    print " --verbose\t\t\t\tEnable verbose mode\n";	
    print "\n";
    print "Examples: \n";
    print "\tLeaktest 10.0.0.1-10.0.0.254 using all protocols at normal speed: \n";
    print "\t\t\$# netleak --protocol all --policy normal --verbose 10.0.0.1/24\n";
    print "\tLeaktest \"server01.mycompany.foo\" using ICMP and with netleakd listening on the internet\n",
	  "\t at 208.239.76.34 for packets with a \"DIV03:\" signature in it: \n";
    print "\t\t\$# netleak --signature \"DIV03:\" --spoof 208.239.76.34 server01.mycompany.foo\n";
}
