#!/usr/bin/perl use warnings; use strict; use Net::RawIP; use vars qw($AUTOLOAD); use POSIX qw(strftime); use Net::SMTP; my %config_info = ( trusted_servers => ['X.X.X.X'], # add trusted servers here chaddr => 'XX:XX:XX:XX:XX:XX', # MAC Address of sending interface log_file => '/tmp/log.tmp', smtp_server => 'X.X.X.X', probe_frequency => 600, interface => 'eth0', email_notification => 'xxx@xxx.com', ); daemonize(); while (1) { # generate a unique transaction ID $config_info{transaction_id} = int(rand(99999999)); my $pid = fork(); if (not defined($pid)) { die "No resources available to fork.\n"; } elsif ($pid == 0) { my $p = pkt_capture(\%config_info); } elsif ($pid) { $SIG{CHLD} = 'IGNORE'; sleep 5; # wait for 5 secs to allow capture process to establish send_discover(\%config_info); sleep 10; # wait for 10 secs to allow capture kill(9, $pid); } sleep $config_info{probe_frequency}; } # end of contionuous while loop sub daemonize { my $pid = fork; exit if $pid; die "Resouces not available\n" unless (defined($pid)); POSIX::setsid() or die "Can't start a new session: $!\n"; } sub send_discover { my ($config_info) = @_; my ($k, $v); my $chaddr = $config_info->{chaddr}; my $xid = $config_info->{transaction_id}; my $int = $config_info->{interface}; my $op = 1; my $htype = 1; my $hlen = 6; my $hops = 0, my $secs = 0; my $flags = 0; my @ciaddr = ('0','0','0','0'); my @yiaddr = ('0','0','0','0'); my @siaddr = ('0','0','0','0'); my @giaddr = ('0','0','0','0'); my $sname = 0; my $file = 0; # Pad MAC address with 10 "0" and then convert Hex MAC address and padding to Dec my @macaddr = split(/:/, $chaddr); push(@macaddr, 0) for (1..10); my @chaddr = map { hex($_) } @macaddr; # Preparation of the Options field # The first four octets of the 'options' field of the DHCP message contain # the (decimal) values 99, 130, 83 and 99, respectively (this is the same # magic cookie as is defined in RFC 1497 [17]). my @options_magic = (99, 130, 83, 99); #DHCP Magic Cookie - RFC 1497 my @options = (53,1,1); # Discover packet my $options_end = 255; # 255 indicates the end of the options section my $data = pack "C4 H8 H4 H4 C4 C4 C4 C4 C16 H128 H256 C4 C3 C", $op, $htype, $hlen, $hops, $xid, $secs, $flags, @ciaddr, @yiaddr, @siaddr, @giaddr, @chaddr, $sname, $file, @options_magic, @options, $options_end; my $ip_pkt = Net::RawIP->new({udp => {}}); $ip_pkt->set({ ip => { saddr => '0.0.0.0', daddr => '255.255.255.255', }, udp => { source => 68, dest => 67, data => $data, }, }); $ip_pkt->ethnew("$int"); $ip_pkt->ethset(source => $chaddr, dest => 'ff:ff:ff:ff:ff:ff'); $ip_pkt->ethsend; } sub pkt_capture { my ($config_info) = @_; my $int = $config_info->{interface}; my $n = new Net::RawIP; my $p = $n->pcapinit("$int", "udp port 68 and ether dst $config_info->{chaddr}", 1500, 30); $p = loop($p, 10, \&process_pkt, $config_info); } sub process_pkt { my $config_info = $_[0]; my $raw_pkt = $_[2]; my (@dhcpsrv, @netmask, %packet, %opts); my @trusted_servers = @{$config_info->{trusted_servers}}; my $raw_data = unpack "H*", $raw_pkt; my $timestamp = strftime("%d %b %Y - %H:%M:%S\t",localtime(time())); %packet = ( dest_mac => join(':', unpack ("(a2)*" , substr($raw_data, 0, 12))), src_mac => join(':', unpack ("(a2)*" , substr($raw_data, 12, 12))), src_ip => join('.', map (hex($_), unpack("(a2)*", substr($raw_data, 52, 8)))), dest_ip => join('.', map (hex($_), unpack("(a2)*", substr($raw_data, 60, 8)))), src_udp => hex(substr($raw_data, 68, 4)), dest_udp => hex(substr($raw_data, 72, 4)), xid => substr($raw_data,92,8), ciaddr => join('.', (map hex($_), (unpack "(a2)*", substr($raw_data, 108, 8)))), yiaddr => join('.', (map hex($_), (unpack "(a2)*", substr($raw_data, 116, 8)))), siaddr => join('.', (map hex($_), (unpack "(a2)*", substr($raw_data, 124, 8)))), giaddr => join('.', (map hex($_), (unpack "(a2)*", substr($raw_data, 132, 8)))), ); my @options = unpack("(a2)*", substr($raw_data, (index($raw_data, "63825363") + 8))); # find options area for (my $i = 0; $i <= $#options; $i++) { my $option = hex($options[$i++]); last if ($option == 255); # end of DHCP Options my $length = $options[$i++]; # i = 0 my $offset = hex($length) + $i; for (my $k = $i; $k <= ($offset - 1); $k++ ) { push(@{$opts{$option}}, $options[$k]); } $i = $offset - 1; } # Format the most common options into their standard units while (my ($k, $v) = each %opts) { # Options relating to IP Addresses need to be changed to the dotted-decimal notation if (($k == 54) || ($k == 1)) { $opts{$k} = join('.', map hex($_), @{$opts{$k}}); } # Options relating to time are defined as an unsigned 32 bit integer if (($k == 51) || ($k == 59) || ($k ==58)) { $opts{$k} = unpack "N*", (pack "H*", join('', @{$opts{$k}})); } } $packet{options} = \%opts; open(TEMP, ">>$config_info->{log_file}"); select(TEMP); unless (grep(/$packet{siaddr}/, @trusted_servers)) { print "---------- $timestamp -----------\n"; my ($k, $v); print "$k\t$v\n" while (($k, $v) = each %packet); print "Client IP Address (ciaddr) $packet{ciaddr}\n"; print "Your IP Address (yiaddr) $packet{yiaddr}\n"; print "Server IP Address (siaddr) $packet{siaddr}\n"; print "Gateway IP Address (giaddr) $packet{giaddr}\n"; for my $key (keys %opts) { print $key, " ", dhcp_option($key),"\t"; if (ref($opts{$key}) =~ m/ARRAY/) { print $_, " " foreach (@{$opts{$key}}); } elsif (ref($opts{$key}) !~ /ARRAY/) { print $opts{$key}; } print "\n"; } print "---------------------------------\n"; email_alert(\%packet, $config_info); } elsif (grep(/$packet{siaddr}/, @trusted_servers)) { print "---------- $timestamp -----------\n"; print "Response received from valid DHCP Server\n"; print "-----------------------------------\n"; } select(STDOUT); close(TEMP); } sub dhcp_msg_type { my $opt_code = shift; my %types = ( "01" => "DHCPDISCOVER", "02" => "DHCPOFFER", "03" => "DHCPREQUEST", "04" => "DHCPDECLINE", "05" => "DHCPACK", "06" => "DHCPNAK", "07" => "DHCPRELEASE", "08" => "DHCPINFORM", ); return $types{$opt_code}; } sub dhcp_option { my $opt_num = shift; my %dhcp_options = ( 1 => "Subnet Mask ", 3 => "Router ", 4 => "Time Server ", 5 => "Name Server ", 6 => "Domain Name Server ", 11 => "Resource Location Server ", 12 => "Host Name ", 15 => "Domain Name ", 43 => "Vendor specific information ", 50 => "Requested IP Address ", 51 => "IP address lease time", 52 => "Option overload", 53 => "DHCP message type", 54 => "Server identifier", 55 => "Parameter request list", 56 => "Message ", 57 => "Maximum DHCP message size ", 58 => "Renew time value ", 59 => "Rebinding time value ", ); if ($dhcp_options{$opt_num}) { return $dhcp_options{$opt_num}; } else { return "Unknown Option"; } } sub email_alert { my ($packet_info, $config_info) = @_; my $smtp = Net::SMTP->new($config_info{smtp_server}); $smtp->mail($ENV{USER}); $smtp->to($config_info{email_notification}); $smtp->data(); $smtp->datasend("To: $config_info{email_notification}\n"); $smtp->datasend("\n"); $smtp->datasend("Untrusted DHCP Server identified $packet_info->{siaddr}\n\n"); $smtp->datasend("IP Address offered $packet_info->{yiaddr}\n\n"); $smtp->datasend("DHCP Message Type: ", dhcp_msg_type(@{$packet_info->{options}->{53}}), "\n"); $smtp->datasend("DHCP Options\n\n"); while (my ($k, $v) = each %{$packet_info->{options}}) { if (ref($v) =~ m/ARRAY/) { $smtp->datasend("$k\t", dhcp_option($k), "\t@$v\n"); } elsif (ref($v) !~ m/ARRAY/) { $smtp->datasend("$k\t", dhcp_option($k), "\t$v\n"); } } $smtp->dataend(); $smtp->quit; }