#!/usr/bin/env python
#
# $Id: arpalert.py 7 2009-03-02 12:50:26Z pwr $
#
# (C) 2008 by Philipp Winter
# Released under the GPLv3

# TODO:
# - better automatic detection of IP address

import os, sys, time, socket, struct, smtplib, getopt, re
import pcap
from time import strftime

# maps trusted IP to MAC addresses (gateways,...)
trustdb = {
	'0.0.0.0':'00:00:00:00:00:00', # gateway
	'1.1.1.1':'11:11:11:11:11:11'  # NAS
}

def loggedRecently():#{{{
	"""Returns 1 if an attack has been logged within the last 5 minutes."""

	global LASTATTACK
	now = int(time.time())
	diff = now - LASTATTACK
	LASTATTACK = now

	return (diff < 300)
#}}}

def sendMail(myip, mymac, packet):#{{{
	"""Sends the result of a possible attack to a specified E-Mail address."""

	msg = strftime("%d. %b %Y (%H:%M:%S)") + ':  Detected ARP poisoning attempt!\n'
	msg += 'Victim is host %s with MAC %s\n' % (myip, mymac)
	msg += 'Attacker with MAC %s pretends to be host %s\n' % (packet['srcmac'], \
		packet['srcip'])

	# change all this stuff to fit your configuration
	server = smtplib.SMTP('mailserver', 25) 
	server.set_debuglevel(1)
	server.starttls()
	server.ehlo('host.unix.local')
	server.login('username', 'password')
	server.sendmail(
		'src_email',
		'dst_email',
		'From: xxx\r\n' \
		'To: xxx\r\n' \
		'Subject: ARP Poisoning Alert!\r\n' + msg
	)
	server.quit()
#}}}

def getMACAddress():#{{{
	"""Returns the MAC address by consulting ifconfig(8)."""

	for line in os.popen("/sbin/ifconfig"):
		if line.find('Ether') > -1:
			mac = line.split()[4]
			break

	return mac.upper()
#}}}

def decodeARPPacket(data):#{{{
	"""Decodes the given packet - assuming, it has the format of ARP."""

	packet = {}
	packet['hwtype'] = socket.ntohs(struct.unpack('H', data[0:2])[0])
	packet['proto']  = socket.ntohs(struct.unpack('H', data[2:4])[0])
	packet['hwsize'] = ord(data[4])
	packet['protosize'] = ord(data[5])
	packet['opcode'] = socket.ntohs(struct.unpack('H', data[6:8])[0])
	packet['srcmac'] = data[8:14].encode('hex').upper()
	packet['srcmac'] = packet['srcmac'][0:2] + ':' + packet['srcmac'][2:4] + ':' + \
		packet['srcmac'][4:6] + ':' + packet['srcmac'][6:8] + ':' + packet['srcmac'][8:10] \
		+ ':' + packet['srcmac'][10:12]
	packet['srcip'] = pcap.ntoa(struct.unpack('i', data[14:18])[0])
	packet['dstmac'] = data[18:24].encode('hex').upper()
	packet['dstmac'] = packet['dstmac'][0:2] + ':' + packet['dstmac'][2:4] + ':' + \
		packet['dstmac'][4:6] + ':' + packet['dstmac'][6:8] + ':' + packet['dstmac'][8:10] \
		+ ':' + packet['dstmac'][10:12]
	packet['dstip'] = pcap.ntoa(struct.unpack('i', data[24:28])[0])

	return packet
#}}}

def printPacket(packet, mode=0):#{{{
	"""Prints the single fields of the given packet to stdout."""

	myprint(packet['srcmac'] + ' > ' + packet['dstmac'])
	if mode:
		myprint('Who has %s? Tell %s' % (packet['dstip'], packet['srcip']))
	else:
		myprint('%s is at %s' % (packet['srcip'], packet['srcmac']))

	for key in ['hwtype', 'proto', 'hwsize', 'protosize', 'opcode']:
		myprint('  %s: 0x%X' % (key, packet[key]))
#}}}

def rcvPacket(len, data, time):#{{{
	"""Callback function which gets an incoming ARP packet"""

	global MYMAC, MYIP

	# not an ARP packet in ethernet frame
	if not data[12:14] == '\x08\x06':
		return

	myprint('\n')
	packet = decodeARPPacket(data[14:])
	printPacket(packet)
	trustedmac = ''

	# ARP reply for request of my machine
	if packet['dstip'] == MYIP and packet['dstmac'] == MYMAC:
		try:
			trustedmac = trustdb[packet['srcip']]
		except KeyError:
			pass

		if trustedmac != packet['srcmac']:
			myprint('########## ALERT: ARP POISONING DETECTED! ##########')
			if not loggedRecently():
				sendMail(MYIP, MYMAC, packet)

	# gratuitous ARP
	if packet['srcip'] == packet['dstip']:
		myprint('  Detected gratuitous ARP')
		try:
			trustedmac = trustdb[packet['srcip']]
		except KeyError:
			return
		if trustedmac != packet['srcmac']:
			myprint('########## ALERT: ARP POISONING DETECTED! ##########')
			if not loggedRecently():
				sendMail(MYIP, MYMAC, packet)
#}}}

def daemonize():#{{{
	"""Daemonizes the running script which contains the detachment from the controlling
	terminal and the closing of open file descriptors"""

	global WORKDIR, UMASK

	# detach from controlling terminal
	try:
		pid = os.fork()
	except OSError, err:
		print err

	if pid == 0:
		print 'INFO: Detaching from terminal (pid: %d)' % os.getpid()
		os.setsid()
		os.chdir(WORKDIR)
		os.umask(UMASK)
	else:
		sys.exit(0)

	# fetch maximum number of file descriptors
	import resource
	maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
	if (maxfd == resource.RLIM_INFINITY):
		maxfd = 1024

	# close open file descriptors
	for fd in range(0, maxfd):
		try:
			os.close(fd)
		except OSError, err:
			print err

	# redirect stdin, stdout, stderr
	os.open('/dev/null', os.O_RDWR)
	os.dup2(0, 1)
	os.dup2(0, 2)
#}}}

def usage():#{{{
	"""Prints the possible command line arguments and explains them"""

	print '\nUsage: %s [-d] [-h] [-i <if>]' % sys.argv[0]
	print 'Options:'
	print '\t-d\tDetach from terminal and run as daemon'
	print '\t-h\tPrint this help screen'
	print '\t-i\tSpecify listening interface manually\n'
#}}}

def myprint(string):#{{{
	global DAEMON

	if not DAEMON:
		print string
#}}}

def main():#{{{

	global LASTATTACK, MYIP, MYMAC, DAEMON, UMASK, WORKDIR
	dev = ''
	p = pcap.pcapObject()
	LASTATTACK = MYIP = DAEMON = None
	MYMAC = getMACAddress()
	UMASK = 0
	WORKDIR = '/tmp'

	try:
		opts, args = getopt.getopt(sys.argv[1:], "i:dh", ["help", "detach", "interface="])
	except getopt.GetoptError, err:
		print err
		return 1

	for (opt, arg) in opts:
		if opt in ('-d', '--detach'):
			DAEMON = True
		elif opt in ('-h', '--help'):
			usage()
			return 0
		elif opt in ('-i', '--interface'):
			dev = arg

	# try to guess interface if not given
	if not dev:
		try:
			dev = pcap.lookupdev()
		except:
			print 'ERROR: No device found. Specify it manually using "-i" or "--interface="'
			return 2
		print 'INFO: No interface specified, using "%s"' % dev

	# try to fetch IP address of interface
	ipflag = False
	for i in os.popen('/sbin/ifconfig'):
		if ipflag:
			m = re.search('^\s*inet addr:(.*)\s+Bcast:.*$', i)
			if m:
				MYIP = m.group(1)
				break
			ipflag = False
		if i.find(dev) != -1:
			ipflag = True

	if not MYIP:
		print 'ERROR: Was not able to get IP address. Does interface have an IP address?'
		return 3

	if DAEMON:
		daemonize()

	p.open_live(dev, 1500, 1, 0)
	p.setfilter("arp", 0, 0)

	try:
		while 1:
			p.dispatch(1, rcvPacket)
	# caught exception might take a while because of blocking socket
	except KeyboardInterrupt:
		myprint('Statistics: %d packets received, %d packets dropped, %d packets ' \
			'dropped by the interface.' % p.stats())

	return 0
#}}}

if __name__ == '__main__':
	sys.exit(main())

