- #!/usr/local/bin/python
- #
- # abck - Examine and report on unauthorized intrusion attempts.
- # Copyright (c) 2001, TundraWare Inc., All Rights Reserved.
- # See the accompanying file called, abck-License.txt
- # for Licensing Terms
-
-
-
- ##########
-
- VERSION = "$Id: abck,v 1.98 2001/07/27 05:46:32 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
-
- ####################
- # 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, TundraWare Inc. All Rights Reserved.\n" + \
- " usage:\n" + \
- " abck [-d # days to look back]\n" + \
- " [-e except string]\n" + \
- " [-m match string]\n" + \
- " [-s Show, but do not process matching records]\n"
-
-
- ####################
- # Data Structures
- ####################
-
- # Dictionary of keywords indicating attack, and position of host address/name
- # in their log records
-
- AttackKeys = {
- "refused" : 8,
- "unauthorized" : 7
- }
-
- # Cache dictionary of all attacking hosts discovered this run of the program
-
- NameCache = {}
-
-
- ####################
- # 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 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 = ""
- for attackkey in AttackKeys.keys():
-
- logfield = logrecord.split()
- 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
-
- if SHOWONLY:
- print logrecord
- break
-
- # Different attack records put the hostquad in different places
- hostquad = logfield[AttackKeys[attackkey]]
- if hostquad[-1] == ',':
- hostquad = hostquad[:-1] # Strip trailing dots
-
- # 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)
-
- # Check if we've seen this abuser before
- 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:m:s')
- 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 == "-m":
- MATCH = val
- if opt == "-s":
- SHOWONLY = TRUE
-
-
- # 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):
- 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()
-
-