#!/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