# HostSentry - Login Anomaly Detector Main Processor # # Author: Craig H. Rowland # Created: 10-6-98 # # Send all changes/modifications/bugfixes to the above address. # # This software is Copyright(c) 1997-98 Craig H. Rowland # # Disclaimer: # # All software distributed by Craig H. Rowland ("the author") and # Psionic Systems is distributed AS IS and carries NO WARRANTY or # GUARANTEE OF ANY KIND. End users of the software acknowledge that # they will not hold the author, Psionic Systems, and any employer of # the author liable for failure or non-function of a software # product. YOU ARE USING THIS PRODUCT AT YOUR OWN RISK # # Licensing restrictions apply. See the license that came with this # file for more information or visit http://www.psionic.com for more # information. # # This software is NOT GPL NOR PUBLIC DOMAIN so please read the license # before modifying or distributing. Contact the above address if you have # any questions. # # $Id: hostsentry.py,v 1.2 1999/03/22 05:31:54 crowland Exp crowland $ # NOTES: # syslog module needs to be added. # - Go to python/Modules dir. # - Edit Setup and uncomment module. # - Compile and re-install from hostSentryCore import * import hostSentryConfig import hostSentryLog import hostSentryUser import hostSentryDB import hostSentryTTY import hostSentryTTYDB import hostSentryUtmp import sys import time import string import imp import os import select import socket # Number of seconds to wait betwee wtmp reads. POLL_DELAY = 1 # Duh. VERSION = '0.02' class hostSentry(hostSentryCore): def __init__(self): self.setLogLevel() self.actionFile = None self.ignoreFile = None self.moduleFile = None self.modulePath = None self.wtmpFile = None self.wtmpFormat = None self.dbFile = None self.ttydbFile = None ######################################################### # Run Method # # This method initializes the configuration file, reads # in the appropriate settings, and executes the main # monitoring loop. # ######################################################### def run(self): logLevel = self.getLogLevel() hostSentryLog.log('adminalert: HostSentry version %s is initializing.' % VERSION) hostSentryLog.log('adminalert: Send bug reports to ') # Safe umask for file operations os.umask(077) # Parse config settings. try: config = hostSentryConfig.hostSentryConfig() config.configInit() self.ignoreFile = config.parseToken('IGNORE_FILE') if self.ignoreFile == None: hostSentryLog.log('adminalert: IGNORE_FILE token not found in config. Aborting') self.exit() try: os.stat(self.ignoreFile) except: hostSentryLog.log('adminalert: Ignore file %s not found. Aborting' % self.ignoreFile) self.exit() self.actionFile = config.parseToken('ACTION_FILE') if self.actionFile == None: hostSentryLog.log('adminalert: ACTION_FILE token not found in config. Aborting') self.exit() try: os.stat(self.actionFile) except: hostSentryLog.log('adminalert: Action file %s not found. Aborting' % self.actionFile) self.exit() self.moduleFile = config.parseToken('MODULE_FILE') if self.moduleFile == None: hostSentryLog.log('adminalert: MODULE_FILE token not found in config. Aborting') self.exit() try: os.stat(self.moduleFile) except: hostSentryLog.log('adminalert: Module file %s not found. Aborting' % self.moduleFile) self.exit() self.modulePath = config.parseToken('MODULE_PATH') if self.modulePath == None: hostSentryLog.log('adminalert: MODULE_PATH token not found in config. Aborting') self.exit() try: os.stat(self.modulePath) sys.path.append(self.modulePath) except: hostSentryLog.log('adminalert: Module path %s not found. Aborting' % self.modulePath) self.exit() self.wtmpFile = config.parseToken('WTMP_FILE') if self.wtmpFile == None: hostSentryLog.log('adminalert: WTMP_FILE token not found in config. Aborting') self.exit() try: os.stat(self.wtmpFile) except: hostSentryLog.log('adminalert: wtmp/utmp file %s not found. Aborting' % self.wtmpFile) self.exit() self.wtmpFormat = config.parseToken('WTMP_FORMAT') if self.wtmpFormat == None: hostSentryLog.log('adminalert: WTMP_FORMAT token not found in config. Aborting') self.exit() self.dbFile = config.parseToken('DB_FILE') if self.dbFile == None: hostSentryLog.log('adminalert: DB_FILE token not found in config. Aborting') self.exit() self.ttydbFile = config.parseToken('DB_TTY_FILE') if self.ttydbFile == None: hostSentryLog.log('adminalert: DB_TTY_FILE token not found in config. Aborting') self.exit() # Delete old TTY database and make new one. else: try: os.unlink(self.ttydbFile) except: pass dbTTY = hostSentryTTYDB.hostSentryTTYDB(self.ttydbFile) dbTTY.close() config.close() except hostSentryError, errorMessage: config.close() hostSentryLog.log(errorMessage[0]) self.exit() # Go into main monitoring loop try: self.daemon() self.monitor() except hostSentryError, errorMessage: hostSentryLog.log(errorMessage) self.exit() ######################################################### # daemon Method # # This code makes us a daemon and is taken almost verbatim # from D'Arcy J.M. Cain post on # comp.lang.python on 09-16-98 # ######################################################### def daemon(nochdir = 0, noclose = 0): if os.fork(): os._exit(0) os.setsid() if nochdir == 0: os.chdir("/") if noclose == 0: fp = open("/dev/null", "rw") sys.stdin = sys.__stdin__ = fp sys.stdout = sys.__stdout__ = fp sys.stderr = sys.__stderr__ = fp del fp if os.fork(): os._exit(0) ######################################################### # monitor Method # # This method continuously watches wtmp for new additions # and sends them to either login or logout processing # methods. # ######################################################### def monitor(self): logLevel = self.getLogLevel() hostSentryLog.log('adminalert: HostSentry is active and monitoring logins.') # Wtmp object will handle the parsing. wtmp = hostSentryUtmp.hostSentryUtmp(self.wtmpFile) wtmp.setLogLevel(logLevel) # I need to use the low level functions to maintain # an open file descriptor. wtmpfd = os.open(self.wtmpFile, os.O_RDONLY) # Seek to the end of wtmp os.lseek(wtmpfd, 0, 2) # This figures out how much we read to get a complete wtmp entry wtmpEntrySize = string.atoi(string.split(self.wtmpFormat, "/")[0]) # Main monitoring loop while 1: try: data = None # The sleep ensures we don't tie up CPU with too many # calls. This can probably be done more efficiently # with select() and will be changed later. time.sleep(POLL_DELAY) data = os.read(wtmpfd, wtmpEntrySize) if len(data) >= wtmpEntrySize: wtmpEntry = wtmp.parse(data, self.wtmpFormat) # If the username is NULL it's a logout if wtmpEntry.getUsername() == '': # Form logout time stamp. logoutString = wtmpEntry.getTty() + '@%d' % time.time() self.processLogout(logoutString) else: # Try to resolve hostname if we can. try: ipAddr = socket.gethostbyname(wtmpEntry.getHostname()) except: ipAddr = wtmpEntry.getHostname() # Form login time stamp. loginString = wtmpEntry.getUsername() + '@' + ipAddr + '@' + \ wtmpEntry.getHostname() + '@' + wtmpEntry.getTty() + \ '@%d' % time.time() self.processLogin(loginString) except hostSentryError, errorMessage: raise hostSentryError(errorMessage) except: hostSentryLog.log('adminalert: Fatal error occurred while processing wtmp: %s' % sys.exc_value) raise hostSentryError('adminalert: Fatal error occurred while processing wtmp: %s' % sys.exc_value) ######################################################### # processLogin Method # # This method will take a login, enter the login into # the user DB, update the TTY state DB, and run the # extensions modules. # ######################################################### def processLogin(self, loginString): logLevel = self.getLogLevel() # Make our database object db = hostSentryDB.hostSentryDB(self.dbFile) dbTTY = hostSentryTTYDB.hostSentryTTYDB(self.ttydbFile) # This will break up the login stamp into useful parts. try: loginUsername, loginIP, loginHostname, loginTTY, loginTime = \ string.split(loginString, '@') loginStamp = loginIP + '@' + loginHostname + '@' + loginTTY + '@' + loginTime + '@' except: hostSentryLog.log('adminalert: ERROR: Error breaking down login stamp.') raise hostSentryError('adminalert: ERROR: Error breaking down login stamp.') # Look for the user. If they don't exist in the DB then # create them with empty hostSentryUser object. try: db.open() if db.exists(loginUsername): if logLevel > 0: hostSentryLog.log('debug: hostSentry: processLogin: Found username in DB: ' + loginUsername) userObj = db.get(loginUsername) userObj.cycleTrackLogins() userObj.insertTrackLogins(loginStamp) userObj.setTotalLogins(userObj.getTotalLogins() + 1) db.store(userObj) db.close() else: if logLevel > 0: hostSentryLog.log('debug: hostSentry: processLogin: Username NOT FOUND in DB: ' + loginUsername) userObj = hostSentryUser.hostSentryUser() userObj.setUsername(loginUsername) userObj.setRecordCreated(loginTime) userObj.setFirstLogin(loginStamp) userObj.setTotalLogins(1) userObj.insertTrackLogins(loginStamp) db.store(userObj) db.close() except: hostSentryLog.log('adminalert: Error reading/writing to USER database during login processing.') raise hostSentryError('adminalert: Error reading/writing to USER database during login processing: %s' % sys.exc_value[0]) # Write the user's currenty TTY to state DB try: dbTTY.open() if logLevel > 0: hostSentryLog.log('debug: hostSentry: processLogin: Adding user and TTY to state DB: %s (%s)' % (loginUsername, loginTTY)) ttyObj = hostSentryTTY.hostSentryTTY() ttyObj.setTty(loginTTY) ttyObj.setLoginStamp(loginStamp) ttyObj.setUsername(loginUsername) dbTTY.store(ttyObj) dbTTY.close() except: hostSentryLog.log('adminalert: Error reading/writing to TTY database during login processing.') raise hostSentryError('adminalert: Error reading/writing to TTY database during login processing: %s' % sys.exc_value[0]) # After adding the user to the userDB and TTYDB we will check if we should ignore # processing of this user. Note that ALL users get entries in the databases. if self.ignoreUser(userObj.getUsername()) != None: return None # Open the modules file and instantiate each object and run login() method. try: module = open(self.moduleFile) moduleData = None while moduleData != '': moduleData = module.readline()[:-1] if len(moduleData) > 0: try: # I don't like doing this and will likely improve # this in a later version. exec('import %s' % moduleData) exec('runModule = %s.%s()' % (moduleData, moduleData)) except: hostSentryLog.log('adminalert: ERROR: Module file: ' + moduleData + ' exec error: %s. Continuing with processing' % sys.exc_value[0]) try: runModule.setLogLevel(logLevel) runModule.login(userObj, loginStamp) except: hostSentryLog.log('adminalert: ERROR: Module file: ' + moduleData + ' exec error: %s. Continuing with processing' % sys.exc_value[0]) try: moduleResult = runModule.getResult() if moduleResult != None: self.action(moduleData, userObj) except: hostSentryLog.log('adminalert: ERROR: Module file: ' + moduleData + ' error during action processing: %s. Continuing with processing' % sys.exc_value[0]) except: hostSentryLog.log('adminalert: ERROR: Module listing file: ' + self.moduleFile + ' is corrupted or missing: %s' % sys.exc_value[0]) raise hostSentryError('adminalert: ERROR: Module listing file: ' + self.moduleFile + ' is corrupted or missing: %s' % sys.exc_value[0]) ######################################################### # processLogout Method # # This method takes the logout string and will look the # user up in the TTY DB. It will then purge them from # the TTY DB and update the UserDB with their logout # time for their active session. # ######################################################### def processLogout(self, logoutString): logLevel = self.getLogLevel() # Make our database objects db = hostSentryDB.hostSentryDB(self.dbFile) db.setLogLevel(logLevel) # TTY State DB dbTTY = hostSentryTTYDB.hostSentryTTYDB(self.ttydbFile) dbTTY.setLogLevel(logLevel) # Get the logout TTY and time try: logoutTTY, logoutTime = string.split(logoutString, '@') if logLevel > 0: hostSentryLog.log('debug: hostSentry: processLogout: LOGOUT: TTY: ' + logoutTTY) except: hostSentryLog.log('adminalert: ERROR: Error breaking down logout stamp.') raise hostSentryError('adminalert: ERROR: Error breaking down logout stamp.') # Search for active TTY in tty state DB to get username. try: dbTTY.open() if dbTTY.exists(logoutTTY): if logLevel > 0: hostSentryLog.log('debug: hostSentry: processLogout: Found TTY in state DB: ' + logoutTTY) ttyObj = dbTTY.get(logoutTTY) dbTTY.delete(logoutTTY) dbTTY.close() logoutUsername = ttyObj.getUsername() logoutStamp = ttyObj.getLoginStamp() else: hostSentryLog.log('securityalert: Login TTY: %s not found in TTY state DB.' % logoutTTY) dbTTY.close() return except: hostSentryLog.log('adminalert: Error reading/writing to TTY state database during logout processing.') raise hostSentryError('adminalert: Error reading/writing to TTY state database during logout processing: %s' % sys.exc_value[0]) # Now match up the TTY to the user and update their record with logout time. try: db.open() if db.exists(logoutUsername): if logLevel > 0: hostSentryLog.log('debug: hostSentry: processLogout: Found username in DB for logout: ' + logoutUsername) userObj = db.get(logoutUsername) db.close() else: hostSentryLog.log('attackalert: Corresponding user: %s does not exist in database to logout from TTY: %s. *** CHECK FOR TAMPERING ***' % (logoutUsername, logoutTTY)) return except: hostSentryLog.log('adminalert: Error reading/writing to USER database during logout processing.') raise hostSentryError('adminalert: Error reading/writing to USER database during logout processing: %s' % sys.exc_value[0]) # This reads in all the TrackLogin stamps and will search them # for this active session. It will then append on the logout time. stamps = userObj.getTrackLogins() for x in range(len(stamps)): if stamps[x] == logoutStamp: db.open() stamps[x] = stamps[x] + logoutTime logoutStamp = stamps[x] userObj.insertTrackLogins(stamps[x], x) userObj.delTrackLogins(x + 1) db.store(userObj) db.close() break # After adding the user to the userDB and TTYDB we will check if we should ignore # processing of this user. Note that ALL users get entries in the databases. if self.ignoreUser(userObj.getUsername()) != None: return None try: module = open(self.moduleFile) moduleData = None while moduleData != '': moduleData = module.readline()[:-1] if len(moduleData) > 0: try: # I don't like doing this and will likely improve # this in a later version. exec('import %s' % moduleData) exec('runModule = %s.%s()' % (moduleData, moduleData)) except: hostSentryLog.log('adminalert: ERROR: Module file: ' + moduleData + ' exec error: %s. Continuing with processing' % sys.exc_value[0]) try: runModule.setLogLevel(logLevel) runModule.logout(userObj, logoutStamp) except: hostSentryLog.log('adminalert: ERROR: Module file: ' + moduleData + ' exec error: %s. Continuing with processing' % sys.exc_value[0]) try: moduleResult = runModule.getResult() if moduleResult != None: self.action(moduleData, userObj) except: hostSentryLog.log('adminalert: ERROR: Module file: ' + moduleData + ' error during action processing: %s. Continuing with processing' % sys.exc_value[0]) except: hostSentryLog.log('adminalert: ERROR: Module listing file: ' + self.moduleFile + ' is corrupted or missing: %s' % sys.exc_value[0]) raise hostSentryError('adminalert: ERROR: Module listing file: ' + self.moduleFile + ' is corrupted or missing: %s ' % sys.exc_value[0]) ######################################################### # ignoreUser Method # # Takes a username and checks if they exist in the # hostSentry.ignore file. If they do it returns the found # username, otherwise None. # ######################################################### def ignoreUser(self, username): logLevel = self.getLogLevel() if self.getLogLevel() > 0: hostSentryLog.log('debug: hostSentry: ignoreUser: Checking whether to ignore user: %s' % username) # if the file doesn't exist then quietly continue and assume the user # should not be ignored. try: ignore = open(self.ignoreFile) except: hostSentryLog.log('adminalert: ERROR: Cannot open ignore file. Continuing processing: %s' % sys.exc_value[0]) return # Read file until end. try: while 1: ignoreData = ignore.readline()[:-1] if self.getLogLevel() > 0: hostSentryLog.log('debug: hostSentry: ignoreUser: parsing user from ignore file: %s' % ignoreData) if len(ignoreData) < 1: break elif ignoreData[0] == '#': pass elif ignoreData[:len(username)] == username: if self.getLogLevel() > 0: hostSentryLog.log('debug: hostSentry: ignoreUser: ignoring user: %s' % ignoreData) ignore.close() return ignoreData ignore.close() except: hostSentryLog.log('adminalert: ERROR: ignore file processing failed: %s' % sys.exc_value[0]) ######################################################### # action Method # # This module will take the resulting data from a # login/logout module and an active user object and # determine what actions should be done. # # NOT IMPLEMENTED YET. ######################################################### def action(self, moduleData, userObj): logLevel = self.getLogLevel() hostSentryLog.log('securityalert: Action being taken for user: ' + userObj.getUsername()) hostSentryLog.log('securityalert: Module requesting action is: ' + moduleData) try: action = open(self.actionFile) while 1: actionData = action.readline()[:-1] if self.getLogLevel() > 0: hostSentryLog.log('debug: hostSentry: action: parsing action from action file: %s' % actionData) if len(actionData) < 1: break elif actionData[0] == '#': pass action.close() except: hostSentryLog.log('adminalert: ERROR: Action file processing failed: %s' % sys.exc_value[0]) hostSentryLog.log('securityalert: Action complete for module: %s' % moduleData) ######################################################### # exit Method # # I hope I don't need to explain this to you. # ######################################################### def exit(self): hostSentryLog.log('adminalert: HostSentry is shutting down.') sys.exit() ######################################################### # main entry point # # The pure natural goodness starts here. # ######################################################### if __name__ == '__main__': main=hostSentry() # Turn on only if you like LOTS of syslog output. # main.setLogLevel(99) main.run()