Tuesday, December 24, 2024

Sniffing with Net::Cap to stealthily managing iptables rules remotely, Part 1

Share

In our saga that began several weeks ago, we’re trying to create a firewall setup that allows no inbound access by default that can be modified remotely to allow a small window of inbound SSH connectivity. Remember that this machine must have no inbound TCP ports accessible to pass muster with the Windows-biased IT administrators, yet we want to allow inbound SSH dynamically when needed.[1]

So the trick was to find a way to dynamically allow inbound SSH access from ‘authorized’ machines. Since the machines he was going to be connecting from were Windows machines with almost no useful software[2] it was a bit of a trick to find something simple.

Using our 10 minute firewall setup, we had already effectively blocked inbound SSH because the initial SYN packet would always be discarded.[3] What we needed was a simple way to allow those inbound SYN packets for a short window.

I came up with the idea of using DNS queries to ‘authorise’ a client machine brief access to the SSH server. Recent Windows machines have a nslookup client by default, as do Mac OS X machines, so this seemed most portable. In order to open up the firewall for SSH access, you’d run request a specific DNS record from his machine. If the machine saw this packet, it would create a temporary rule allowing inbound SSH.

I didn’t want to write a full blown DNS-like server or anything for two reasons. First, it’s a lot of wasted time for such a simple need. Secondly, it would have shown another open port when the IT folks scanned the host, and that would have raised their suspicions – why would an end host be running a DNS server, after all?

Instead, the easiest plan seemed to be to run a sniffer on his machine which would see the DNS request and inform a second program of what it sees. This second program (to be covered next time) will handle opening the temporary inbound SSH access.

Because I’m extremely lazy, I wrote my sniffer using Perl’s Net::Pcap module. This module uses the standard libpcap code that is part of tcpdump and other packet sniffers. The way you use this library in Perl is extremely similar to the way you do it in C, naturally. The Net::Pcap man page can help if the code below isn’t sufficiently commented for your tastes.

The code does the following things:

  • Open up a capturing device
  • Drop root privileges for security reasons
  • Set up our filter to only snag DNS packets
  • Whenever a packet is received, call the process_pkt routine, which
    • Extracts the IP address that sent the request
    • Extracts the destination IP address
    • Extracts the DNS host name that was requested
    • Prints these values to STDOUT

This program doesn’t do anything to the firewall rules at all, it simply writes data to it’s standard output. The intent is that a second program, running as root, will analyse this output and create the appropriate rules. Separation of functionality is a good thing when it comes to security, of course.

Here’s the code:


#!/usr/bin/perl -w
#
# Copyright 2003, Brian Hatch, released under the GPL
#
# watch_dns:
#   A program to watch for inbound DNS queries, and print the
#   source, destination, and requested domain name of the queries.

# You'll need to fill this in with your actual IP address
# (If we didn't restrict the destination IP address, we'd
# catch all our outbound queries too.)
my $MY_IP_ADDRESS='10.1.1.1';

# The unprivileged uid/gid under which we should run.
my $UNPRIV="200";



# No changes required hereafter

use Net::Pcap;
use FileHandle;
use strict;
use English;	# for example purposes only - I prefer obfuscated code.

STDOUT->autoflush(1);

while ( 1 ) {

    my $pid = fork();
    if ( ! defined $pid ) { die "Unable to fork.  Yikes." };

    if ( $pid ) {
        # Parent process (running as root) will wait for
	# child.  If child exits, we'll create another one.
        wait();	
	sleep(1);  # To keep us from respawning too fast if necessary.
    } else {
    	print "Child startingn";

	# Child process will do actual sniffing.
	# First, create our packet capturing device
        my($pcap_t) = create_pcap();

        unless ( $pcap_t ) {
            die "Unable to create pcap";
        }

        # Let's stop running as root.  Since we already
	# have our pcap descriptor, we can still use it.
        $EGID="$UNPRIV $UNPRIV";	# setgid and setgroups()
        $GID=$UNPRIV;
        $UID=$UNPRIV; $EUID=$UNPRIV;

	# Capture packets forever.
        Net::Pcap::loop($pcap_t, -1, &process_pkt, 0);

        # Technically, we shouldn't get here since the loop
        # is infinite (-1), but just in case, close and exit.
        Net::Pcap::close($pcap_t);
        exit 1;
    }
}

sub create_pcap {
    my $promisc = 0;   # We're only looking for packets destined to us,
                       # so no need for promiscuous mode.
    my $snaplen = 135; # Allows a max of 80 characters in the domain name

    my $to_ms = 0;			# timeout
    my $opt=1;                          # Sure, optimisation is good...
    my($err,$net,$mask,$dev,$filter_t);

    my $filter = "udp dst port 53 and dst host $MY_IP_ADDRESS";

    # Look up an appropriate device (eth0 usually)
    $dev = Net::Pcap::lookupdev($err);
    $dev or die "Net::Pcap::lookupdev failed.  Error was $err";
    
    if ( (Net::Pcap::lookupnet($dev, $net, $mask, $err) ) == -1 ) {
        die "Net::Pcap::lookupnet failed.  Error was $err";
    }
    
    # Actually open up our descriptor
    my $pcap_t = Net::Pcap::open_live($dev, $snaplen, $promisc, $to_ms, $err);
    $pcap_t || die "Can't create packet descriptor.  Error was $err";
    
    if ( Net::Pcap::compile($pcap_t, $filter_t, $filter, $opt, $net) == -1 ) {
        die "Unable to compile filter string '$filter'n";
    }

    # Make sure our sniffer only captures those bytes we want in
    # our filter.
    Net::Pcap::setfilter($pcap_t, $filter_t);

    # Return our pcap descriptor
    $pcap_t;
}

# Routine to process the packet -- called by Net::Pcap::loop()
# every time an appropriate packet is snagged.
sub process_pkt {
    my($user_data, $hdr, $pkt) = @_;

    my($src_ip) = 26;           # start of the source IP in the packet
    my($dst_ip) = 30;           # start of the dest IP in the packet
    my($domain_start) = 55;     # start of the domain in the packet
    my($data);

    # extract the source IP addr into dotted quad form.
    my($source) = sprintf("%d.%d.%d.%d",
        ord( substr($pkt, $src_ip, 1) ),
        ord( substr($pkt, $src_ip+1, 1) ),
        ord( substr($pkt, $src_ip+2, 1) ),
        ord( substr($pkt, $src_ip+3, 1) ));

    # extract the destination IP addr into dotted quad form.
    my($destination) = sprintf("%d.%d.%d.%d",
        ord( substr($pkt, $dst_ip, 1) ),
        ord( substr($pkt, $dst_ip+1, 1) ),
        ord( substr($pkt, $dst_ip+2, 1) ),
        ord( substr($pkt, $dst_ip+3, 1) ));

    $data = substr($pkt, $domain_start);

    $data =~ s/0.*//g;             # strip off everything after the domain

                                     # back int to dots.

    print "$source -> $destination: $datan"
    	if ( $source and $destination and $data);  
}

As you can see, Net::Pcap is very easy to use. Since the actual packet capture code is written in C, it’s very fast. However the actual packet processing is in Perl, so if you have huge numbers of packets and your script can’t keep up, you may need to write your programs using libpcap in C natively. But for simple needs, Net::Pcap is your friend. The corresponding C code for the above program would probably take three times as many lines. God, I love Perl.

While I was writing this code back in May[4], Linux Journal published an article (available at ) by Martin Krzywinski, which describes a technique called Port Knocking. His method is much more robust, allowing actual encryption and authentication. It requires that you have the port knocking client software, which makes it less appealing for situations where you aren’t able to install software on the client machine, or you’re stuck on a client as unfriendly as a Windows box….

NOTES:

[1] Of course, this sort of trickery will piss off those network administrators if they figure it out…

[2] Putty was already installed on most machines, but beyond that, he was stuck with whatever was already available

[3] You could tell that an SSH server was running using Nmap, but you could not actually SSH to the machine. This was sufficient to the IT folks.

[4] Yes, sometimes I do write articles before the deadline

This article is part of an extended thread discussing how you can dynamically manage local firewall rulesets with stealth DNS packets. To start at the beginning, go to the following Linux Security: Tips, Tricks and Hackery article

Brian Hatch is Chief Hacker at Onsight, Inc and author of
Hacking Linux Exposed
and Building Linux VPNs.
Brian can be reached at brian@hackinglinuxexposed.com.

Table of contents

Read more

Local News