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 starting\n";
# 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/\00.*//g; # strip off everything after the domain
$data =~ s/[^-a-zA-Z0-9]/./g; # change the domain component separators
# back int to dots.
print "$source -> $destination: $data\n"
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
About the Author:
Brian Hatch is the author of Hacking
Linux Exposed, Building Linux
VPNs, and the weekly Linux
Security: Tips, Tricks, and Hackery newsletter.
In addition, he's the co-maintainer of the secure SSL wrapper Stunnel,
a frequent lecturer, security BOFH, and has this thing called a "Day Job" where
he masquerades as Chief Hacker at security consultancy Onsight,
Inc.
His root password is a 823 digit prime.
Read this newsletter at: http://www.linuxpronews.com/2003/0818.html |
|



|