Newer
Older
abck / abck
#!/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