How to Make a Basic Intrusion Detection System with Bash

Introduction

For most of us WEP encryption has become a joke. WPA is quickly going the same way thanks to many tools such as Aircrack-ng. On top of this, wired networks are no strangers to unwanted guests as well. Anyone serious about security should have a good Intrusion Detection system in their toolbox.

There are already some very good IDS’s (Intrusion Detection Systems) available. Why would anyone want to re-invent the wheel in Bash??? There are a couple of reasons for this. Obviously Bash scripts can be very light weight. Especially compared to some of the GUI programs that are out there. While programs like Etherape suck us in with pretty colors, they require constant monitoring to know when the network has changed. If you are like most of us, you only use the computer for two things, work and play. By using the system bell to alert for new clients online you can leave this script running and not have to have a constant watch. If you do decide you want to inspect what a suspicious client is doing more closely, you can always open up etherape, wireshark, or your tool of choice. But until you have a problem you can play or work on other things.

Another bonus to this program is that it will only show ip addresses on the networks connected to your computer. If you were hosting a busy server or perhaps downloading the latest Linux distro though a torrent client, an IDS may be flooded with connections. Looking for a new malicious client can be like looking for a needle in a hay stack. While this script may seem simple compared to other IDS’s, simplicity can have its perks too.

What you will need

Nmap is required for this script to work. We will not be doing any port scanning. However, to make this script fast we needed something better than a regular ping. Nmap’s -sP parameter will only use a ping scan to check if a clients up. There were some variations in how Nmap outputs information between versions. So far this script has only been tested using Nmap 5.00 (Debian Squeeze) and 5.21 (Debian Sid). You may have luck with other distros and versions of Nmap. However, with all the possibilities I could only support a couple at this time.

You will also need to be sure you are using Bash version 4.0 or above. You should find this in any distro that is stable or newer. But any versions of Bash below that will not support arrays which are used in this script. Root access is also required or the arp command will not be found to block any clients.

NOTE: This script does not work well with virtual network interfaces such as VMware, VirtualBox and etc.

Executing the Script

To run this script simply run:

# chmod +x leecher.sh; ./leecher.sh

At this time there are no parameters to set.

How this script works

Skip past all the beginning functions for now so we can see the actual flow of the script. The first thing we do is check that the user is root and nmap is installed on the current system. If it is not the script will explain that root privledges are required or that nmap is a dependency here and exit. If these requirements are met the script will skip on with a greeting to the user and explain some features. I used setterm to turn off the cursor. It was definitely an aesthetic eye-sore.

I set a trap control-C to make the script stop. While you might think ‘Wait, Control-C usually stops command-line program anyway!’ While this is normally true, I found the forever loop we use later to cause problems stopping the script with Control-C. By using a trap with SIGINT we were able to make this work. We set some variables in the following if statement to check which supported version of nmap we are using here. This is important as the output is completely different between these versions. The first thing we did here was to make a loop that will first get the version of nmap we are using here. This is important as the output is completely different between these versions. The next thing we did here was to make a loop that will first get the ip addresses of all our the interfaces that are currently online. We are also using awk here to filter out 127.0.0.1 as there is no need to scan the loopback address. Plus we are using awk to cut out the ending octet in these ip addresses. For instance, if the interface eth0 has an ip of 192.168.1.12 we do not need the ending 12. A normal scan of a subnet like this would be “nmap -sP 192.168.1.0/24” So for now this loop will strip out any of the ip’s on any active interfaces and pass them on at a time to nmap until we’re done. Inside the loop we receive the value for ip an interface and append “.0/24” to scan the entire network in that range.(Or 0-255) We will pass the correct variables for the version of nmap so awk knows where to get the ip’s returned from each scan. All of the values returned from each scan will be plugged into an array. After the first scan of all of your interfaces networks we will simply use another loop to display the initial results to the user.

I should point out here what the new following message to the user is saying. If you want to hear the system bell it must be enabled in your desktop settings. The location of this will vary depending of what version of KDE, Gnome, Xface, or whatever desktop you are using. However, you may think that just because you’ve heard a bell before that it is enabled. I noticed my OS had a similar bell to let me know my laptop battery was about to die. Please check how to enable the system bell on your distro if you experience any problems.

Next is the forever loop to keep the scanning and monitoring of this script constant. If you are new to Bash, or forever loops, this might have you questioning why we would use something that is an infinite loop. Many of you have no doubt been warned about the danger of infinite loops and how they can crash a machine. As you may have noticed, we used a sleep statement after the first scan. We will use this again inside our forever loop and some of the functions it includes. Sleep will allow execution to pause and temporarily give back resources to the computer. I’ve tested this script on a pretty modest processor and experienced no problems at all. But if you are on a very old machine, or one tapped for resources already, you can change the number of seconds that sleep is being used here.

The first thing that our forever loop will do is jump up to the function named engine(). What we are doing here is exactly the same as our first scan except we are putting it into a different array. After that function is ran we now go back to our forever loop where an if statement will compare if these two arrays are the same. If they are the same the array from the second scan will be emptied to prevent duplicate values on the next iteration of the loop. However, if the value is a difference in these two arrays we will jump down to the else clause which redirects us to our interrupt function.

The interrupt function will stop and announce to the user that the list of clients has changed. From here we will call a function named “twice” where we display to the user the contents of ip addresses in the second array. We will now ask the user if they want to block an ip address. It can be any ip, not just the ones displayed. If the user answers “y” for yes they will asked to enter an ip address. If the ip entered is not null we will ping this ip to add its mac address to our arp cache. For whatever reason when nmap pings the network it does not do this. Then we use arp to give us the mac address of the client. Since ip’s can be reassigned by a router we do not want to block by ip addresses. Once this is done we use a nested if statement to check if the mac address we now have stored in $mac is null. This is good for error checking in case the user enters a string of garbage. If the mac address does not exist we tell the user the client does exist or has left the network and resume our monitoring in the forever loop. If the mac address does exist we add it to an iptables rule that will block that user from any connection to our computer. I should note here that this does not block you from sending packets to that machine, only incoming traffic to you. However, this does not protect your entire network. Only the machine you are using until your iptables rules are flushed. If you accidentally block a client that find you do need to connect to you can release this rule with a few simple iptables commands. The if statement continues by telling user that the mac address of the ip entered is now blocked and shows the current clients online. The blocked client will still appear on this list as we have only blocked it from us, not the network. If the user had chose to not block a client we would simply display the change in the network and go back to our forever loop.

Regardless of what the user did in the interrupt function, we now need to update the values of our arrays. Since the second array currently holds the new values of our network we need to feed that to the other array before the engine function fills it again. We first clear that array to prevent any duplicate values and then copy the contents of the second array to the first array. Now use empty the second array and we are ready to start the loop over with the engine function.

Of course there was one function that I skipped until now. You may have noticed that our first message to the user said to hit Control-C at anytime to block additional clients or exit. Our trap calls the first function named control_c(). All I did here was ask the user in an if statement if they want to block a user in almost the same way as before. You will notice if the user answers yes to the if statement there is a new line here. “bash leecher.sh” is used to restart this script. If you have named this script something different you must supply that here. We re-execute our script because the trap wants to still send SIGINT and kill the script. Creating a new instance prevents the script from unwantedly dying. However, creating the new instance does not let SIGINT complete.

You may have also noticed that we used sleep a little longer too. This is only to give the user time to read what’s happening before switching to our new instance of the script that will take over this terminal. If the user had chosen “no” instead of “yes” the else clause would just allow the script to exit. We will also be sure to use setterm to return our cursor or we will not have one in this terminal even though the script has exited.

The purpose of having on-the-fly blocking is easy. You may have more than one client to block if there are multiple aggressive clients. You might decide later after skipping the chance block a client in the interrupt function that you need to. Or perhaps you know something is wrong as soon as you start the script. If no new clients came or left on the network in question we would not have a chance to block anything until they did.

How this Script can be improved

Obviously hearing the system bell constantly go off for false positives can be annoying. Making this script able to whitelist clients you trust would probably cut down on this. The system bell can definitely be bothersome if one person is having trouble staying connected for long periods of time.
At times you may notice some of the clients switching from ip’s to hostnames. Many programs, such as Etherape, do the same thing. If your router is acting as your DNS will probably show the host name continuously. I don’t think any of you will want to block connections with your router. However, offering a parameter to switch to ip’s only might be nice for some of you.
There is also a small problem with the script forking when a user blocks a client with Control-C. This presents no danger unless a user decides to block thousands of clients with Control-C. However all instances of the script are killed upon exit. But since we’re going for basic here, this should be fine.

An Intrusion Detection System with BASH

#!/bin/bash

# Interupt and Exit Function
control_c()
{
   clear
   echo -e "Would you like to block connections with a client?\n"
   echo -e "Enter y or n: "
   read yn

    if [ "$yn" == "y" ]; then

      echo -e "\nEnter Ip Address to Block: \n"
      read ip

                if [ -n $ip ]; then

                        echo -e "\nNow retrieving mac address to block...\n"
                        ping -c 1 $ip > /dev/null
                        mac=`arp $ip | grep ether | awk '{ print $3 }'`

                        if [ -z $mac ]; then
                                clear
                                echo -e "\n***Client does not exist or is no longer\
                                         on this network***"
                                echo -e "\nSkipping action and resuming monitoring.\n\n"
                                sleep 2
                                bash leecher.sh
                                exit 0

                        else
                                iptables -A INPUT -m mac --mac-source $mac -j DROP
                                clear
                                echo -e "\nClient with mac address $mac is now\
                                          blocked.\n"
                                echo -e "We will continue monitoring for changes\
                                         in clients\n\n"
                                sleep 2
                                bash leecher.sh
                                exit 0
                        fi
                fi


    else
          clear
          echo -e "\n\nLeecher has exited\n\n"
          setterm -cursor on
          rm -f $pid
          exit 0
    fi
}

# Print the scan from the engine()
twice(){
  g=0
  len=${#second[@]}
  for (( g = 0; g < $len; g++ ));
  do
       echo -e "${second[$g]}\n"
  done
}

# If there's a change in the network, ask to block ips.
interupt(){
   clear
   echo -e "\nList of Clients has Changed!\n"
   twice
   echo -e '\a'
   echo -e "Would you like to block connections with a client?\n"
   echo -e "Enter y or n: "
   read yn

   if [ "$yn" == "y" ]; then

      echo -e "\nEnter Ip Address to Block: \n"
      read ip
                if [ -n $ip ]; then
                        ping -c 1 $ip > /dev/null
                        mac=`arp $ip | grep ether | awk '{ print $3 }'`

                        if [ -z $mac ]; then
                                clear
                                echo -e "\n***Client does not exist or is no longer on\
                                         this network***"
                                echo -e "\nSkipping action and resuming monitoring.\n\n"
                        else
                                iptables -A INPUT -m mac --mac-source $mac -j DROP
                                clear
                                echo -e "\nClient with mac address $mac is now blocked.\n"
                                echo -e "We will continue monitoring for changes\
                                         in clients\n\n"
                                echo -e "Current clients are: \n"
                                twice
                                echo -e "\nResuming monitoring..."
                        fi
                fi
    else
           clear
           echo -e "Current clients are: \n"
           twice
           echo -e "Resuming monitoring..."
    fi
}

# Function to keep monitoring for any changes
engine()
{
        # Scan networks again for comparison of changes.
        for subnet in $(/sbin/ifconfig | awk '/inet addr/ && !/127.0.0.1/ && !a[$2]++\
                        {print substr($2,6)}')
        do
                  second+=( "$(nmap -sP ${subnet%.*}.0/24 | awk 'index($0,t)\
                  { print $i }' t="$t" i="$i" )" )
                  sleep 1
        done
}

# Make sure user is logged in as root
if [[ $EUID -ne 0 ]]; then
   echo "This script must be run as root" 1>&2
   exit 1
fi

# Check if nmap is installed
ifnmap=`type -p nmap`
        if [ -z $ifnmap ]; then

                echo -e "\n\nNmap must be installed for this program to work\n"
                echo -e "Only Nmap 5.00 and 5.21 are supported at this time\n"
                echo -e "Please install and try again"
                exit 0
        fi

clear
echo -e "\nNow finding clients on your local network(s)"
echo -e "Press Control-C at any time to block additional clients or exit\n"


# Remove temp files on exit and allow Control-C to exit.
trap control_c SIGINT

# Turn off cursor
setterm -cursor off

# Make some arrays and variables
declare -a first
declare -a second
sid=5.21

# Check for which version of nmap
if [ 5.21 = $(nmap --version | awk '/Nmap/ { print $3 }') ]; then
    i=5  t=report
else
    i=2  t=Host
fi

# Get ip's from interfaces and run the first scan
for subnet in $(/sbin/ifconfig | awk '/inet addr/ && !/127.0.0.1/ && !a[$2]++ {print \
                substr($2,6)}')
do
          first+=( "$(nmap -sP ${subnet%.*}.0/24 | awk 'index($0,t) { print $i }' \
                  t="$t" i="$i" )" )
          sleep 1
done

                echo -e "Current clients are: \n"

                        #Display array elements and add new lines
                        e=0
                        len=${#first[@]}
                        for (( e = 0; e < $len; e++ ));
                        do
                                echo -e "${first[$e]}\n"
                        done

                echo -e "Leecher is now monitoring for new clients."
                echo -e "\nAny changes with clients will be reported by the system bell."
                echo -e "If bell is not enabled details will log to this console."

# Forever loop to keep monitoring constant
for ((  ;  ; ))
do
        engine

        if [[ ${first[@]} == ${second[@]} ]]; then

                second=( )
        else
                interupt
                sleep 1
                first=( )
                first=("${second[@]}")
                second=( )
        fi

done

Sample Output

Now finding clients on your local network(s)
Press Control-C at any time to block additional clients or exit

Current clients are: 

192.168.12.1
192.168.12.9
192.168.12.43

Mephistolist
10.0.0.121
10.0.0.137
10.0.0.140

Leecher is now monitoring for new clients.

Any changes with clients will be reported by the system bell.
If bell is not enabled details will log to this console.

==============================================================

List of Clients has Changed!

192.168.12.9
192.168.12.43

Mephistolist
10.0.0.140


Would you like to block connections with a client?

Enter y or n: 
y

Enter Ip Address to Block: 

192.168.12.9

============================================================

Client with mac address 7c:ed:8d:9c:93:8e is now blocked.

We will continue monitoring for changes in clients

============================================================


Comments and Discussions
Linux Forum