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