#!/bin/bash # arpcheck # Copyright (c) 2005 by Stefan Behte # # 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # # Checks /proc/net/arp for MAC/IP combinations and compares them to a static or dynamic MAC list # If something does not fit, you'll get an alarm, you can also log them to a file. # # The MAC list is shorewall compatible, it's in the format: # [Interface] [MAC] {IP} # IP is optional! # # Example for a MAC list: # eth2 00:11:22:33:44:55 # you can add comments after "#" # eth2 00:de:ad:be:ef:00 192.168.13.37 # MAC/IP combo will be used # eth2 de:ad:be:ef:be:ee #192.168.69.69 IP commented out, only MAC will be used # #eth2 de:ad:73:50:ba:be # this line will be completely ignored # # # Features of ARP check: # - static mode (using a maclist) # - dynamic mode (learning network config) # - single mode (just run once against a maclist and show the results) # - logging functions (you can also turn this off, which would not be wise) # - detects lan scans (you can turn this on/off) # - blacklist support (you can turn this on/off) # - colored output (you can turn this on/off) # # # Changelog: # 19.06.2005: v1.0 - added colors # - corrected spelling mistakes # - fixed several errors in the code # - added logging functionality # 29.06.2005: v1.1 - uses /proc/net/arp directly (MUCH faster) # - changed output, added counter # 06.07.2005: v1.2 - changed the comments, added the header # - added "-single / -s" switch, just run the checks 1 time and do not run an endless loop # - now using multiple interfaces, specify them in INTERFACES (separate via space) # - added free line when sleeping # 13.07.2005: v1.3 - added script which gets called, if attack is detected # - added some comments # - added pidfile (/var/run/arpcheck.pid) # - added MAC blacklist, you can turn logging on/off for this via the variable LOGBLACK # - added -learn / -s mode (using temporary file ".learning") # - added lan scan detection # 14.07.2005: v1.4 - corrected many formating errors # - added SCANFACES # - added switch to turn colors on/off # - custum logging: you can log blacklist-attacks and lan scans to different files # - when new MAC/IP learned, we reduce the interval once # 18.07.2005: - fixed serious bug (when there was only interface+mac on one line an no comment->false detection as attacker) # 27.07.2005: v1.5 - separated the script into functions # - dynamic and static mode can now be run simoultanoiusly (some ifaces dynamic, some static, NOT both at once) # - added command line switches and help # - added -nc switch (no color) # - added -nd switch (no dynamic learning) # - added -ns switch (no static maclist checks) # - added -nb switch (no blacklist checks) # - added -nS switch (no scan detection) # - added -h / --help switch # - changed -single to -s only # - changed design, added more colors etc. # 29.08.2005: v1.6 - minor bugfixes # - checked performance, fixed bugs which slowed it down a lot # - added ".arp" hack: the whole script runs about 500% faster now :) # - minor bugfixes # 03.01.2005: v1.7 - releasing under GPL # # TODO: # - flood detection/protection (MAC in .flooddetect schreiben, nummer dahinter) # - fix the dirty checkmacs() hack! # - timeout bei den learned macs # # Verified to work on: # - Gentoo Linux Kernel 2.4 # - Gentoo Linux Kernel 2.6 # - SuSE Linux 9.1 # - it should work with any linux >= kernel-2.4 distro # # Runtime: # - of course, this script gets slower, the more entries you have in /proc/net/arp # - This script was written for networks with about 100 PCs and is perfomant enough to handle that # - Normal runtime for single check (all checks enabled): 0.2s on an Athlon XP 2400 # - test: every check enabled, static maclist with 77 entries, blacklist with 1 entry, massive LAN scanning to produce many arp entries # -> /proc/net/arp entries/time to run: 28248/4.6s, 26610/4.3s, 24877/3.7s, 13566/1.6s, 12033/1.5s, 9697/1s, 7498/0.8s, 3694/0.5s, 479/0.3s # - I think these runtimes are OK. On a normal lan you will only have about ~50 entries or so and the script will need 0.2 seconds for all checks # - You will only have more entries in /proc/net/arp if your box is router and someone scans on an other iface # # Written by Stefan Behte # ############# CONFIGURATION #################################################### ### ONLY CHANGE HERE !!! sINTERFACES="eth1 eth2" # which interfaces to check against static maclist, must be separated via space dINTERFACES="eth0 eth3" # interfaces which you want to check dynamically in "learning mode" # WARNING: do NOT specify the same interface in both !! # every interface can only be in static or dynamic (learning) mode. Choose one ;) sMACLIST="/etc/shorewall/maclist" # path to our static maclist dMACLIST=".learned" # path to our dynamic maclist (learning mode) LOGFILE="/var/log/arpcheck.log" # where to log BLACKLIST="./BLACK" # blacklist with MACS/reason, works on ANY interface (change to specific iface) IP eth0,eth1 LOGBLACK=$LOGFILE # leave empty to not log blacklisted MACs SCANFACES="eth0 eth1 eth2 eth3" # interfaces on which you want to detect scanners, leave empty if you don't want to use it SCANTHRES=10 # highest NR of 00:00:00:00:00:00 entries to ignore LOGSCAN=$LOGFILE # log the scan? leave empty if you do not want to log NORMALINTERVAL=30 # sleep interval when everything is ok ATTACKINTERVAL=10 # sleep interval, if attack detected SCRIPT="" # Script to run, when an attack is detected # event call parameters # MAC/IP do not fit "$INTERFACE/$MAC/$IP:$REAL" # MAC not in list "$INTERFACE/$MAC/$IP" # MAC is blacklisted "$INTERFACE/$EVILMAC/$IP:BLACK" COLOR=y # do you want colors? ### End of config ################################################################################ ## init, cleanup if [ "$1" = "-nc" ] || [ "$2" = "-nc" ] || [ "$3" = "-nc" ] || [ "$4" = "-nc" ] || [ "$5" = "-nc" ] || [ "$6" = "-nc" ] then COLOR=n fi if [ "$1" = "-ns" ] || [ "$2" = "-ns" ] || [ "$3" = "-ns" ] || [ "$4" = "-ns" ] || [ "$5" = "-ns" ] || [ "$6" = "-ns" ] then sINTERFACES="" sMACLIST="" fi if [ "$1" = "-nd" ] || [ "$2" = "-nd" ] || [ "$3" = "-nd" ] || [ "$4" = "-nd" ] || [ "$5" = "-nd" ] || [ "$6" = "-nd" ] then dINTERFACES="" dMACLIST="" fi if [ "$1" = "-nb" ] || [ "$2" = "-nb" ] || [ "$3" = "-nb" ] || [ "$4" = "-nb" ] || [ "$5" = "-nb" ] || [ "$6" = "-nb" ] then BLACKLIST="" fi if [ "$1" = "-nS" ] || [ "$2" = "-nS" ] || [ "$3" = "-nS" ] || [ "$4" = "-nS" ] || [ "$5" = "-nS" ] || [ "$6" = "-nS" ] then SCANFACES="" fi if [ "$COLOR" = "y" ] then BANNER="\033[1;34mARP Check v1.7 by Stefan Behte\033[0m" WARNING="\033[1;29m[\033[0m\033[1;31mWARNING\033[0m\033[1;29m]\033[0m" LEARNED="\033[1;34mLEARNED\033[0m" yOK="\033[1;33mOK\033[0m" gOK="\033[1;32mOK\033[0m" LAN="\033[1;34mLAN SCAN detection\033[0m" STATIC="\033[1;34mSTATIC MAClist\033[0m" DYNAMIC="\033[1;34mDYNAMIC learning\033[0m" BLACKLI="\033[1;34mBLACKLIST\033[0m" else BANNER="ARP Check v1.7 by Stefan Behte" WARNING="[WARNING]" LEARNED=LEARNED yOK=OK gOK=OK LAN="LAN SCAN detection" STATIC="STATIC MAClist" DYNAMIC="DYNAMIC learning" BLACKLI="BLACKLIST" fi if [ "$1" = "-h" ] || [ "$2" = "-h" ] || [ "$3" = "-h" ] || [ "$4" = "-h" ] || [ "$5" = "-h" ] || [ "$6" = "-h" ] || [ "$1" = "--help" ] || [ "$2" = "--help" ] || [ "$3" = "--help" ] || [ "$4" = "--help" ] || [ "$5" = "--help" ] || [ "$6" = "--help" ] then echo -e "\n$BANNER" cat </dev/null # check if we're already running (exclude single mode) if [ "$1" != "-s" ] && [ "$1" != "--single" ] then echo $$ > /var/run/arpcheck.pid fi if [ "$dINTERFACES" != "" ] then rm -f .learned &>/dev/null touch .learned fi if [ "$sINTERFACES" != "" ] then NRMACS=$[ `grep -v "^#" /etc/shorewall/maclist | sort | uniq | wc -l` - 1 ] fi if [ "$BLACKLIST" != "" ] then BLACKNR=`wc -l $BLACKLIST | awk '{print $1}'` fi checkmacs() { INTERFACES=`cat .ifaces` MACLIST=`cat .maclist` for INTERFACE in $INTERFACES do grep "$INTERFACE$" .arp | sort -n | while read line do IP=`echo $line | awk '{print $1}'` # save IP MAC=`echo $line | awk '{print $2}'` # save MAC # MAC in MAC list? # at the beginng -> commented out grep -v "^#" $MACLIST | grep "^$INTERFACE" | if grep $MAC &>/dev/null then # ja -> OK line=`grep -v "^#" $MACLIST | grep "^$INTERFACE" | grep $MAC` three=`echo $line | awk '{print $3}'` if [ "$three" = "${IP}" ] # does the MAC fit to IP? then printf "$INTERFACE $IP/$MAC\t[MAC+IP]\t[$gOK]\n" # ja -> OK else echo $three | if grep "^#" &>/dev/null # no -> test, if # in line IP commented out then printf "$INTERFACE $IP/$MAC\t[MAC]\t\t[$yOK]\n" # ja -> OK else # no -> MAC has a different IP -> someone is evil if [ "$three" = "" ] then printf "$INTERFACE $IP/$MAC\t[MAC]\t\t[$yOK]\n" else REAL=`grep -v "^#" $MACLIST | grep "^$INTERFACE" | grep "$MAC" | awk '{print $3}'` printf "$INTERFACE $IP/$MAC\t[MAC+IP]\t$WARNING\t$REAL/$MAC uses IP: $IP\n" echo "`date` $MAC/$REAL uses an other IP: $IP" >> $LOGFILE if [ "$SCRIPT" != "" ]; then ${SCRIPT} \""$INTERFACE/$MAC/$IP:$REAL\""; fi touch .attack fi fi fi else # MAC not in list printf "$INTERFACE $IP/$MAC\t[MAC]\t\t$WARNING\t$MAC not in MAC List!\n" printf "`date` $INTERFACE/$MAC not in MAC List!\n" >> $LOGFILE if [ "$SCRIPT" != "" ]; then ${SCRIPT} \""$INTERFACE/$MAC/$IP\""; fi touch .attack fi done # echo done } # checkmacs learnmacs() { if [ "$dINTERFACES" != "" ] then for INTERFACE in $dINTERFACES do grep $INTERFACE .arp | awk '{print $2}' | while read MAC do if ! grep $MAC $dMACLIST &>/dev/null then grep $MAC .arp | grep $INTERFACE | awk '{print $3 "\t" $2 "\t" $1}' >> $dMACLIST printf "`tail -n1 $dMACLIST | awk '{print $1 " " $3 "/" $2}'`\t\t\t[$LEARNED]\n" touch .attack fi done done fi } detectscan() { if [ "$SCANFACES" != "" ] then echo -e "$LAN enabled [$SCANFACES]:" grep "00:00:00:00:00:00" /proc/net/arp > .scandet ZEROMACS=0 for INTERFACE in $SCANFACES do ZEROMACS=`grep -c "$INTERFACE" .scandet` if [ $ZEROMACS -gt $SCANTHRES ] then NRZ=`grep -c $INTERFACE .scandet` printf "$INTERFACE 0.0.0.0/00:00:00:00:00:00\t\t\t\t$WARNING\tLAN scan detected on interface $INTERFACE, $NRZ entries\n" if [ "$LOGSCAN" != "" ] then echo "`date` LAN scan detected: $INTERFACE/$NRZ " >> $LOGSCAN fi touch .attack fi done fi } checkblacklist() { if [ -e "$BLACKLIST" ] then awk '{print $1}' $BLACKLIST | while read EVILMAC do if grep $EVILMAC .arp &>/dev/null then IP=`grep $EVILMAC .arp | awk '{print $1}'` INTERFACE=`grep $EVILMAC .arp | awk '{print $3}'` printf "$INTERFACE $IP/$EVILMAC\t[MAC]\t\t$WARNING\t$EVILMAC is BLACKLISTED!\n" if [ "$LOGBLACK" != "" ] then printf "`date` $INTERFACE/$EVILMAC/$IP is BLACKLISTED!\n" >> $LOGBLACK fi if [ "$SCRIPT" != "" ]; then ${SCRIPT} \""$INTERFACE/$EVILMAC/$IP:BLACK\""; fi touch .attack fi done fi } pause() { echo ##### choose correct sleeping interval (depends on state normal/attacked) if [ -e .attack ] then INTERVAL=$ATTACKINTERVAL rm .attack else INTERVAL=$NORMALINTERVAL fi ##### that cool counter ;) [seconds slept/seconds to sleep] echo -n "sleeping... [" for i in `seq 1 $INTERVAL` do echo -n "${i}/${INTERVAL}]" sleep 1 for j in `seq -1 $[${#i} + ${#INTERVAL}]` do echo -n -e '\x08' done done } while [ 1 ] do clear echo -e "$BANNER\n" detectscan if [ "$dINTERFACES" != "" ] || [ "$sINTERFACES" != "" ] || [ "$BLACKLIST" != "" ] then grep -v "00:00:00:00:00:00" /proc/net/arp | awk '{print $1 " " $4 " " $6}' > .arp fi if [ "$BLACKLIST" != "" ] then echo -e "\n$BLACKLI enabled ($BLACKNR entries)" checkblacklist fi if [ "$sINTERFACES" != "" ] then echo -e "\n$STATIC ($sMACLIST, $NRMACS entries) [$sINTERFACES]:" echo $sINTERFACES > .ifaces echo $sMACLIST > .maclist checkmacs fi if [ "$dINTERFACES" != "" ] then echo -e "\n$DYNAMIC (`wc -l .learned | awk '{print $1}'` found) [$dINTERFACES]:" learnmacs echo $dINTERFACES > .ifaces echo $dMACLIST > .maclist checkmacs fi if [ "$1" = "-s" ] || [ "$2" = "-s" ] || [ "$3" = "-s" ] || [ "$4" = "-s" ] || [ "$5" = "-s" ] || [ "$6" = "-s" ] then rm /var/run/arpcheck.pid .learned .scandet .ifaces .maclist .arp &>/dev/null echo exit -1 fi pause done rm /var/run/arpcheck.pid .learned .ifaces .maclist .scandet .arp &>/dev/null