#!/usr/local/bin/python
#
# abck - Examine and report on unauthorized intrusion attempts.
# Copyright (c) 2001, 2002 TundraWare Inc., All Rights Reserved.
# See the accompanying file called, abck-License.txt
# for Licensing Terms
##########
VERSION = "$Id: abck,v 2.2 2002/09/04 21:24:27 tundra Exp $"
####################
# Imports
####################
import commands
import exceptions
import getopt
import os
import re
import sys
import socket
import time
####################
# Booleans
####################
FALSE = 0 == 1
TRUE = not FALSE
DONE = FALSE
IGNORE = TRUE
LISTIGNORED = FALSE
####################
# General Constants
####################
ANS = ";; ANSWER SECTION:"
AUTH = ";; AUTHORITY SECTION:"
DLEN = 24*60*60
DIG = "dig -t ptr -x "
HIST = ".abck_history"
HISTFILE = os.path.join(os.getenv("HOME"), HIST)
LOG = "/var/log/messages"
MOS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
WHO = "whois "
####################
# Constants Used In Outgoing eMail
####################
HOSTNAME = socket.gethostname()
HOSTADDR = socket.gethostbyname(HOSTNAME)
HOSTTZ = time.tzname
NOTIFYWHO = ("abuse", "root")
ORG = os.getenv("ORGANIZATION")
SUBJ = "\"Attempted Intrusion Attempt\""
MAILCMD = "mail -s %s" % (SUBJ)
MAILMSG = "An *unauthorized* attempt to access one of our computers\n" + \
"has been detected originating from your address space/domain.\n\n" + \
"Our machine, %s, has IP address,\n%s, and is located in the " + \
"%s Time Zone.\n\n" + \
"Our log entry documenting the attempted intrusion\n" + \
"from your address space/domain, follows:\n\n%s\n\n" + \
"Please take the necessary steps to remedy this situation.\n" + \
"Thank-You\n" + ORG + "\n"
####################
# Prompt And Message Strings
####################
PROMPT = "\nLog Record:\n%s\n\nWho Gets Message For: <%s>? %s[%s] "
USAGE = "abck " + VERSION.split()[2] + " " + \
"Copyright (c) 2001, 2002 TundraWare Inc. All Rights Reserved.\n" + \
" usage: abck [-hilsv] [-d num][ -e string][-m string] where,\n\n" + \
" -d # days to look back\n" + \
" -e except string\n" + \
" -h Display this help information\n" + \
" -i Do not ignore any addresses or names\n" + \
" -l Display records/IPs/hostnames being ignored\n" + \
" -m match string\n" + \
" -s Show, but do not process matching records\n" + \
" -v Show detailed version information\n"
####################
# Data Structures
####################
# Dictionary of keywords indicating attack, and position of host address/name
# in their log records
AttackKeys = {
"refused" : 8,
"unauthorized" : 7
}
# Associate IPs and Hostnames
DNSCache = {}
# Associate attacking hosts with who to notify
NameCache = {}
# List of IP addesses to ignore. Records with IP addresses or names
# found in this list will be ignored entirely. The addresses here may
# be partial IP quads. If IGNOREDFILE exists, its contents will
# be appended to the IGNORED data structure at program startup.
Ignored = []
IGNOREDFILE = os.path.join(os.getenv("HOME"), ".abck_ignored")
####################
# Globals
####################
Processed = []
####################
# Regular Expression Handlers
####################
# Regular Expression which describes a legit IP quad address
IPQuad = r"(\d{1,3}\.){3}\d{1,3}$"
####################
# Classes
####################
# Signify that the record under consideration is to be
# permanently forgotten
class ForgetRecord(exceptions.Exception):
def __init__(self, args=None):
self.args = args
# Signify we want to ignore a record
class IgnoreRecord(exceptions.Exception):
def __init__(self, args=None):
self.args = args
# Signify that the user want to quit the program
class QuitAbck(exceptions.Exception):
def __init__(self, args=None):
self.args = args
####################
# Function Definitions
####################
# Return the ending substring of a host name with 'depth' number of dots
# in it
def HostDepth(host, depth=2):
# Break the address down into components
components = host.split(".")
# And return the recombined pieces we want
return '.'.join(components[-depth:])
####################
# Check a name, see if it's an IP quad, and if so, return reverse.
# If not, return the original name.
#
# This is better than a socket.gethostbyaddr() call because
# this will return the authority information for an address
# if no explicit reverse resolution is found.
def CheckIPReverse(hostquad):
# If it's an IP address in quad form, reverse resolve it
if re.match(IPQuad, hostquad):
DIGResults = commands.getoutput(DIG + hostquad).splitlines()
ansname = ""
authname = hostquad
# Results must either have an Answer or Authority record
if ( DIGResults.count(ANS) + DIGResults.count(AUTH)):
i = 0
while i < len(DIGResults):
if DIGResults[i].startswith(ANS):
ansname = DIGResults[i+1].split()[4]
if DIGResults[i].startswith(AUTH):
authname = DIGResults[i+1].split()[-2]
i += 1
if ansname:
hostname = ansname
else:
hostname = authname
# Get rid of trailing dot, if any
if hostname[-1] == '.':
hostname = hostname[:-1]
else:
hostname = hostquad
return hostname
####################
# Notify the responsible authority about the attempted intrusion
def Notify(logrecord, domain):
dest=[]
logrecord = "\"" + logrecord + "\""
msg = (MAILMSG % (HOSTNAME, HOSTADDR, "/".join(HOSTTZ), logrecord))
for x in NOTIFYWHO:
dest.append(x + "@" + domain)
dest.append("root@" + HOSTNAME)
os.popen(MAILCMD + " " + " ".join(dest), "w").write(msg)
####################
# Paw through a log record, doing any reverse resolution needed,
# confirm with user, and return name of the host to notify about
# the instrusion attempt. A null return means the user want to
# skip this record.
def ProcessLogRecord(logrecord, NOMATCH, SHOWONLY):
# Check for each known attack keyword
sendto = ""
logfield = logrecord.split()
for attackkey in AttackKeys.keys():
if logrecord.count(attackkey):
# Even if it is a legitimate attack record,
# we do not process it if it contains text
# the user does not want matched.
if NOMATCH and logrecord.count(NOMATCH):
break
# Different attack records put the hostquad in different places
hostquad = logfield[AttackKeys[attackkey]]
if hostquad[-1] == ',':
hostquad = hostquad[:-1] # Strip trailing dots
# See if we've already done a reverse. If so, use it,
# otherwise do the lookup and store result in the cache
if DNSCache.has_key(hostquad):
hostname = DNSCache[hostquad]
else:
# Go do a reverse resolution if we need to
hostname = CheckIPReverse(hostquad)
# Check for the case of getting a PTR record back
hostname = ReversePTR(hostname)
DNSCache[hostquad] = hostname
# Check if record should be ignored
if IGNORE:
for ihost in Ignored:
if (hostquad.startswith(ihost)) or (hostname.endswith(ihost)):
if LISTIGNORED:
print "Ignoring record on match for: [%s]\n%s" % (ihost, logrecord)
raise IgnoreRecord
if SHOWONLY:
print logrecord
break
# Check if we've seen this abuser before
# i.e., Do we already know who to notify?
if NameCache.has_key(hostname):
sendto = NameCache[hostname]
# New one
else:
originalname = hostname
depth = 2
DONE=FALSE
while not DONE:
# Set depth of default response
default = HostDepth(hostname, depth)
# Ask the user about it
st = raw_input(PROMPT % (logrecord, originalname[-40:],
" " * (40 - len(originalname)),
default))
# Parse the response
if st.lower() == "f": # Forget this record forever
raise ForgetRecord # Raise error as the way back
elif st.lower() == "l": # More depth in recipient name
if depth < len(hostname.split('.')):
depth += 1
elif st.lower() == "q": # Quit the program
raise QuitAbck
elif st.lower() == "r": # Less depth in recipient name
if depth > 2:
depth -= 1
elif st.lower() == "s": # Skip this record
sendto = ""
DONE = TRUE
elif st.lower() == "w": # Run a 'whois' on 'em
print commands.getoutput(WHO + hostquad)
else:
if st: # User keyed in their own recipient
hostname = st
else: # User accepted the default
sendto = default
DONE = TRUE
NameCache[originalname] = sendto # Cache it
return sendto
####################
# Check the passed hostname and see if it looks like a PTR record.
# If so, strip out the address portion, reverse it, and trying
# doing another reverse lookup. If not, just return the original hostname.
def ReversePTR(hostname):
tmp = hostname.split("in-addr.arpa")
if len(tmp) > 1: # Looks like a PTR record
tmp = tmp[0].split('.') # Get addr components
tmp.reverse() # and reverse their order
# Take the 1st four quads (some PTR records have more)
# and see if we can dig out a reverse.
hostname = CheckIPReverse('.'.join(tmp[1:5]))
return hostname
#------------------------- Program Entry And Mail Loop -----------------------#
# Program entry and command line processing
try:
opts, args = getopt.getopt(sys.argv[1:], '-d:e:hilm:sv')
except getopt.GetoptError:
print USAGE
sys.exit(2)
OLDEST = 0
MATCH = ""
NOMATCH = ""
SHOWONLY = FALSE
for opt, val in opts:
if opt == "-d":
OLDEST = time.time() - (int(val) * DLEN)
if opt == "-e":
NOMATCH = val
if opt == "-h":
print USAGE
sys.exit(0)
if opt == "-i":
IGNORE = FALSE
LISTIGNORED = FALSE
if opt == "-l":
LISTIGNORED = TRUE
IGNORE = TRUE
if opt == "-m":
MATCH = val
if opt == "-s":
SHOWONLY = TRUE
if opt == "-v":
print VERSION
sys.exit(0)
# Process the ignored rc file, if any
if os.path.exists(IGNOREDFILE):
i = open(IGNOREDFILE)
for entry in i.read().splitlines():
Ignored.append(entry)
i.close()
# Read the log into a list
f = open(LOG, "r")
logfile = [x for x in f.read().splitlines()]
f.close()
# Remove any previously handled log events from further consideration
# unless all we're doing is showing records. In that case, show
# all records that match, even if we've already processed them.
if not SHOWONLY:
if os.path.exists(HISTFILE):
f = open(HISTFILE, "r")
for histrec in f.read().splitlines():
if logfile.count(histrec):
logfile.remove(histrec)
f.close()
# Examine, and possibly process, each record in the log
for logrecord in logfile:
# Check to see whether this record should even be
# processed.
DOIT = TRUE
# Did user limit how far back to look?
if OLDEST:
# Parse the record's time into into a list
logfields = logrecord.split()
logtime = logfields[2].split(":")
EventTime = [None, logfields[0], logfields[1],
logtime[0], logtime[1], logtime[2]]
# Figure out what year - not in the log explicitly
# We do this by comparing the Month in the log entry
# against today's month. We get away with this so long
# as the log never is allowed to get so big that it has
# entries over a year old in it. (Which should be the case
# for any reasonably administered system.
lt = time.localtime()
logyear = int(lt[0])
if MOS.index(EventTime[1]) > int(lt[1]): # Log shows a later month
logyear -= 1 # 'Must be last year
EventTime[0] = str(logyear)
# Don't process if older than the oldest allowed
if time.mktime(time.strptime("%s %s %s %s %s %s" % tuple(EventTime),
"%Y %b %d %H %M %S")) < OLDEST:
DOIT = FALSE
# Did user specify a selection matching string?
if not logrecord.count(MATCH):
DOIT = FALSE
# If we passed all those tests, it's time to process this record.
if DOIT:
try:
sendto = ProcessLogRecord(logrecord, NOMATCH, SHOWONLY)
except (ForgetRecord, IgnoreRecord):
Processed.append(logrecord)
except (QuitAbck):
sys.exit()
else:
# If we get a non-null string back, we need to let someone know
# about the attempted intrusion
if sendto:
Notify(logrecord, sendto)
Processed.append(logrecord)
if os.path.exists(HISTFILE):
f = open(HISTFILE, "a")
else:
f = open(HISTFILE, "w")
for x in Processed:
f.write(x + "\n")
f.close()
if LISTIGNORED:
print "\n\n--------------------------------------------------"
print "Records with the following IP Quads/Hostnames Were Ignored:\n"
for x in Ignored:
print x
syntax highlighted by Code2HTML, v. 0.9.1