Newer
Older
abck / abck
  1. #!/usr/local/bin/python
  2. #
  3. # abck - Examine and report on unauthorized intrusion attempts.
  4. # Copyright (c) 2001, TundraWare Inc., All Rights Reserved.
  5. # See the accompanying file called, abck-License.txt
  6. # for Licensing Terms
  7.  
  8.  
  9.  
  10. ##########
  11.  
  12. VERSION = "$Id: abck,v 1.98 2001/07/27 05:46:32 tundra Exp $"
  13.  
  14.  
  15.  
  16. ####################
  17. # Imports
  18. ####################
  19.  
  20. import commands
  21. import exceptions
  22. import getopt
  23. import os
  24. import re
  25. import sys
  26. import socket
  27. import time
  28.  
  29. ####################
  30. # Booleans
  31. ####################
  32.  
  33. FALSE = 0 == 1
  34. TRUE = not FALSE
  35.  
  36. DONE = FALSE
  37.  
  38. ####################
  39. # General Constants
  40. ####################
  41.  
  42. ANS = ";; ANSWER SECTION:"
  43. AUTH = ";; AUTHORITY SECTION:"
  44. DLEN = 24*60*60
  45. DIG = "dig -t ptr -x "
  46. HIST = ".abck_history"
  47. HISTFILE = os.path.join(os.getenv("HOME"), HIST)
  48. LOG = "/var/log/messages"
  49. MOS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  50. "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
  51. WHO = "whois "
  52.  
  53. ####################
  54. # Constants Used In Outgoing eMail
  55. ####################
  56.  
  57. HOSTNAME = socket.gethostname()
  58. HOSTADDR = socket.gethostbyname(HOSTNAME)
  59. HOSTTZ = time.tzname
  60. NOTIFYWHO = ("abuse", "root")
  61. ORG = os.getenv("ORGANIZATION")
  62. SUBJ = "\"Attempted Intrusion Attempt\""
  63.  
  64. MAILCMD = "mail -s %s" % (SUBJ)
  65.  
  66. MAILMSG = "An *unauthorized* attempt to access one of our computers\n" + \
  67. "has been detected originating from your address space/domain.\n\n" + \
  68. "Our machine, %s, has IP address,\n%s, and is located in the " + \
  69. "%s Time Zone.\n\n" + \
  70. "Our log entry documenting the attempted intrusion\n" + \
  71. "from your address space/domain, follows:\n\n%s\n\n" + \
  72. "Please take the necessary steps to remedy this situation.\n" + \
  73. "Thank-You\n" + ORG + "\n"
  74.  
  75.  
  76. ####################
  77. # Prompt And Message Strings
  78. ####################
  79.  
  80.  
  81. PROMPT = "\nLog Record:\n%s\n\nWho Gets Message For: <%s>? %s[%s] "
  82.  
  83. USAGE = "abck " + VERSION.split()[2] + " " + \
  84. "Copyright (c) 2001, TundraWare Inc. All Rights Reserved.\n" + \
  85. " usage:\n" + \
  86. " abck [-d # days to look back]\n" + \
  87. " [-e except string]\n" + \
  88. " [-m match string]\n" + \
  89. " [-s Show, but do not process matching records]\n"
  90.  
  91.  
  92. ####################
  93. # Data Structures
  94. ####################
  95.  
  96. # Dictionary of keywords indicating attack, and position of host address/name
  97. # in their log records
  98.  
  99. AttackKeys = {
  100. "refused" : 8,
  101. "unauthorized" : 7
  102. }
  103.  
  104. # Cache dictionary of all attacking hosts discovered this run of the program
  105.  
  106. NameCache = {}
  107.  
  108.  
  109. ####################
  110. # Globals
  111. ####################
  112.  
  113. Processed = []
  114.  
  115. ####################
  116. # Regular Expression Handlers
  117. ####################
  118.  
  119. # Regular Expression which describes a legit IP quad address
  120.  
  121. IPQuad = r"(\d{1,3}\.){3}\d{1,3}$"
  122.  
  123. ####################
  124. # Classes
  125. ####################
  126.  
  127. # Signify that the record under consideration is to be
  128. # permanently forgotten
  129.  
  130. class ForgetRecord(exceptions.Exception):
  131. def __init__(self, args=None):
  132. self.args = args
  133.  
  134.  
  135. # Signify that the user want to quit the program
  136.  
  137. class QuitAbck(exceptions.Exception):
  138. def __init__(self, args=None):
  139. self.args = args
  140.  
  141.  
  142.  
  143. ####################
  144. # Function Definitions
  145. ####################
  146.  
  147. # Return the ending substring of a host name with 'depth' number of dots
  148. # in it
  149.  
  150. def HostDepth(host, depth=2):
  151.  
  152. # Break the address down into components
  153. components = host.split(".")
  154.  
  155. # And return the recombined pieces we want
  156. return '.'.join(components[-depth:])
  157.  
  158.  
  159. ####################
  160.  
  161. # Check a name, see if it's an IP quad, and if so, return reverse.
  162. # If not, return the original name.
  163. #
  164. # This is better than a socket.gethostbyaddr() call because
  165. # this will return the authority information for an address
  166. # if no explicit reverse resolution is found.
  167.  
  168. def CheckIPReverse(hostquad):
  169. # If it's an IP address in quad form, reverse resolve it
  170. if re.match(IPQuad, hostquad):
  171.  
  172. DIGResults = commands.getoutput(DIG + hostquad).splitlines()
  173.  
  174. ansname = ""
  175. authname = hostquad
  176. # Results must either have an Answer or Authority record
  177. if ( DIGResults.count(ANS) + DIGResults.count(AUTH)):
  178.  
  179. i = 0
  180. while i < len(DIGResults):
  181.  
  182. if DIGResults[i].startswith(ANS):
  183. ansname = DIGResults[i+1].split()[4]
  184.  
  185. if DIGResults[i].startswith(AUTH):
  186. authname = DIGResults[i+1].split()[-2]
  187.  
  188. i += 1
  189.  
  190. if ansname:
  191. hostname = ansname
  192. else:
  193. hostname = authname
  194.  
  195. # Get rid of trailing dot, if any
  196. if hostname[-1] == '.':
  197. hostname = hostname[:-1]
  198.  
  199. else:
  200. hostname = hostquad
  201. return hostname
  202.  
  203.  
  204. ####################
  205.  
  206. # Notify the responsible authority about the attempted intrusion
  207.  
  208. def Notify(logrecord, domain):
  209. dest=[]
  210. logrecord = "\"" + logrecord + "\""
  211. msg = (MAILMSG % (HOSTNAME, HOSTADDR, "/".join(HOSTTZ), logrecord))
  212. for x in NOTIFYWHO:
  213. dest.append(x + "@" + domain)
  214. dest.append("root@" + HOSTNAME)
  215. os.popen(MAILCMD + " " + " ".join(dest), "w").write(msg)
  216.  
  217. ####################
  218.  
  219. # Paw through a log record, doing any reverse resolution needed,
  220. # confirm with user, and return name of the host to notify about
  221. # the instrusion attempt. A null return means the user want to
  222. # skip this record.
  223.  
  224.  
  225. def ProcessLogRecord(logrecord, NOMATCH, SHOWONLY):
  226.  
  227. # Check for each known attack keyword
  228.  
  229. sendto = ""
  230. for attackkey in AttackKeys.keys():
  231.  
  232. logfield = logrecord.split()
  233. if logrecord.count(attackkey):
  234.  
  235. # Even if it is a legitimate attack record,
  236. # we do not process it if it contains text
  237. # the user does not want matched.
  238.  
  239. if NOMATCH and logrecord.count(NOMATCH):
  240. break
  241.  
  242. if SHOWONLY:
  243. print logrecord
  244. break
  245. # Different attack records put the hostquad in different places
  246. hostquad = logfield[AttackKeys[attackkey]]
  247. if hostquad[-1] == ',':
  248. hostquad = hostquad[:-1] # Strip trailing dots
  249.  
  250. # Go do a reverse resolution if we need to
  251. hostname = CheckIPReverse(hostquad)
  252.  
  253. # Check for the case of getting a PTR record back
  254. hostname = ReversePTR(hostname)
  255.  
  256. # Check if we've seen this abuser before
  257. if NameCache.has_key(hostname):
  258. sendto = NameCache[hostname]
  259.  
  260. # New one
  261. else:
  262. originalname = hostname
  263. depth = 2
  264. DONE=FALSE
  265. while not DONE:
  266.  
  267. # Set depth of default response
  268. default = HostDepth(hostname, depth)
  269.  
  270. # Ask the user about it
  271. st = raw_input(PROMPT % (logrecord, originalname[-40:],
  272. " " * (40 - len(originalname)),
  273. default))
  274.  
  275. # Parse the response
  276.  
  277. if st.lower() == "f": # Forget this record forever
  278. raise ForgetRecord # Raise error as the way back
  279.  
  280. elif st.lower() == "l": # More depth in recipient name
  281. if depth < len(hostname.split('.')):
  282. depth += 1
  283.  
  284. elif st.lower() == "q": # Quit the program
  285. raise QuitAbck
  286.  
  287. elif st.lower() == "r": # Less depth in recipient name
  288. if depth > 2:
  289. depth -= 1
  290.  
  291. elif st.lower() == "s": # Skip this record
  292. sendto = ""
  293. DONE = TRUE
  294.  
  295. elif st.lower() == "w": # Run a 'whois' on 'em
  296. print commands.getoutput(WHO + hostquad)
  297.  
  298.  
  299. else:
  300. if st: # User keyed in their own recipient
  301. hostname = st
  302. else: # User accepted the default
  303. sendto = default
  304. DONE = TRUE
  305. NameCache[originalname] = sendto # Cache it
  306. return sendto
  307.  
  308. ####################
  309.  
  310. # Check the passed hostname and see if it looks like a PTR record.
  311. # If so, strip out the address portion, reverse it, and trying
  312. # doing another reverse lookup. If not, just return the original hostname.
  313.  
  314. def ReversePTR(hostname):
  315.  
  316. tmp = hostname.split("in-addr.arpa")
  317.  
  318. if len(tmp) > 1: # Looks like a PTR record
  319.  
  320. tmp = tmp[0].split('.') # Get addr components
  321. tmp.reverse() # and reverse their order
  322. # Take the 1st four quads (some PTR records have more)
  323. # and see if we can dig out a reverse.
  324. hostname = CheckIPReverse('.'.join(tmp[1:5]))
  325.  
  326. return hostname
  327.  
  328.  
  329. #------------------------- Program Entry And Mail Loop -----------------------#
  330.  
  331.  
  332.  
  333. # Program entry and command line processing
  334.  
  335. try:
  336. opts, args = getopt.getopt(sys.argv[1:], '-d:e:m:s')
  337. except getopt.GetoptError:
  338. print USAGE
  339. sys.exit(2)
  340. OLDEST = 0
  341. MATCH = ""
  342. NOMATCH = ""
  343. SHOWONLY = FALSE
  344.  
  345. for opt, val in opts:
  346. if opt == "-d":
  347. OLDEST = time.time() - (int(val) * DLEN)
  348. if opt == "-e":
  349. NOMATCH = val
  350. if opt == "-m":
  351. MATCH = val
  352. if opt == "-s":
  353. SHOWONLY = TRUE
  354.  
  355.  
  356. # Read the log into a list
  357.  
  358. f = open(LOG, "r")
  359. logfile = [x for x in f.read().splitlines()]
  360. f.close()
  361.  
  362. # Remove any previously handled log events from further consideration
  363. # unless all we're doing is showing records. In that case, show
  364. # all records that match, even if we've already processed them.
  365.  
  366. if not SHOWONLY:
  367. if os.path.exists(HISTFILE):
  368. f = open(HISTFILE, "r")
  369. for histrec in f.read().splitlines():
  370. if logfile.count(histrec):
  371. logfile.remove(histrec)
  372. f.close()
  373.  
  374.  
  375. # Examine, and possibly process, each record in the log
  376. for logrecord in logfile:
  377.  
  378. # Check to see whether this record should even be
  379. # processed.
  380.  
  381. DOIT = TRUE
  382. # Did user limit how far back to look?
  383. if OLDEST:
  384. # Parse the record's time into into a list
  385. logfields = logrecord.split()
  386. logtime = logfields[2].split(":")
  387. EventTime = [None, logfields[0], logfields[1],
  388. logtime[0], logtime[1], logtime[2]]
  389.  
  390. # Figure out what year - not in the log explicitly
  391. # We do this by comparing the Month in the log entry
  392. # against today's month. We get away with this so long
  393. # as the log never is allowed to get so big that it has
  394. # entries over a year old in it. (Which should be the case
  395. # for any reasonably administered system.
  396. lt = time.localtime()
  397. logyear = int(lt[0])
  398. if MOS.index(EventTime[1]) > int(lt[1]): # Log shows a later month
  399. logyear -= 1 # 'Must be last year
  400. EventTime[0] = str(logyear)
  401. # Don't process if older than the oldest allowed
  402. if time.mktime(time.strptime("%s %s %s %s %s %s" % tuple(EventTime),
  403. "%Y %b %d %H %M %S")) < OLDEST:
  404. DOIT = FALSE
  405.  
  406. # Did user specify a selection matching string?
  407. if not logrecord.count(MATCH):
  408. DOIT = FALSE
  409.  
  410. # If we passed all those tests, it's time to process this record.
  411. if DOIT:
  412. try:
  413. sendto = ProcessLogRecord(logrecord, NOMATCH, SHOWONLY)
  414. except (ForgetRecord):
  415. Processed.append(logrecord)
  416. except (QuitAbck):
  417. sys.exit()
  418. else:
  419. # If we get a non-null string back, we need to let someone know
  420. # about the attempted intrusion
  421. if sendto:
  422. Notify(logrecord, sendto)
  423. Processed.append(logrecord)
  424.  
  425. if os.path.exists(HISTFILE):
  426. f = open(HISTFILE, "a")
  427. else:
  428. f = open(HISTFILE, "w")
  429.  
  430. for x in Processed:
  431. f.write(x + "\n")
  432. f.close()
  433.  
  434.