[tac_plus] tac_plus AFL (Auth Fail Lock)

Mark Ellzey Thomas mark.thomas at corp.aol.com
Mon Jun 23 19:11:54 UTC 2008


Greetings all,

Recently we have had the need for tac_plus to temporarily disable user
accounts based on the number of authentication failures the user has had
in a defined window of time.

Attached is a patch against F4.0.4.15 with the previously submitted
acct+syslog patch (if this is a problem please inform me and I will
patch against the base F4.0.4.15 tree). 

The following global configuration parameter has been added:

auth-fail-lock $int1 $int2 $int3

Where $int1 is the number of authentication failures
Where $int2 is the window (in seconds) in which to watch for auth fails
Where $int3 is the number of seconds to disable the user. 

An example would be:
# Watch for 10 authentication failures within 60 seconds, if triggered
# disable user for 120 seconds.
auth-fail-lock 10 60 120

The tac_plus daemon will log when a trigger is hit, and when the account
has been re-enabled:
Jun 23 14:51:36 192.168.0.1 tac_plus[27731]: User mark has been disabled for 120 seconds
Jun 23 14:53:46 192.168.0.1 tac_plus[28244]: Re-enabling account: mark  

Unfortunately since tac_plus is a forked architecture, I had to achieve
persistence of data via IPC. I understand that some may be weary of this
mechanism so they can turn the feature off at compile time by passing
the --disable-afl flag to configure.


Thanks for the project!

-- 
Mark E. Thomas - AOL Network Security

-------------- next part --------------
diff -Naur ../tacacs_orig/authen.c ./authen.c
--- ../tacacs_orig/authen.c	2008-06-18 18:24:09.000000000 -0400
+++ ./authen.c	2008-06-19 18:24:01.000000000 -0400
@@ -326,10 +326,21 @@
 	    return;
 	}
 
-	if ((*func) (datap)) {
+#ifdef AFL
+	if ((session.afl_cfg) && 
+		cfg_is_user_disabled(datap->NAS_id->username) == 1)
+	    datap->status = TAC_PLUS_AUTHEN_STATUS_FAIL; 
+	else if ((*func) (datap)) {
 	    send_authen_error("Unexpected authentication function failure");
 	    return;
 	}
+#else 
+	if((*func) (datap)) 
+	{
+	    send_authen_error("Unexpected authentication function failure");
+	    return;
+	}
+#endif
 
 	switch (datap->status) {
 
@@ -359,6 +370,10 @@
 
 	case TAC_PLUS_AUTHEN_STATUS_FAIL:
 	    /* An invalid user/password combination */
+#ifdef AFL
+	    if(session.afl_cfg)
+		cfg_increment_failure(datap->NAS_id->username);
+#endif
 	    send_authen_reply(TAC_PLUS_AUTHEN_STATUS_FAIL,
 			      datap->server_msg,
 			      datap->server_msg ? strlen(datap->server_msg) : 0,
diff -Naur ../tacacs_orig/config.c ./config.c
--- ../tacacs_orig/config.c	2008-06-18 18:24:09.000000000 -0400
+++ ./config.c	2008-06-22 13:38:15.000000000 -0400
@@ -113,6 +113,11 @@
 static int no_user_dflt = 0;		/* default if user doesn't exist */
 static char *authen_default = NULL;	/* top level authentication default */
 static char *nopasswd_str = "nopassword";
+#ifdef AFL
+static unsigned int user_count = 0;     /* the number of users in the config */
+static key_t failed_key;		/* the shm key for AFL */
+#endif
+
 
 /*
  * A host definition structure.
@@ -176,6 +181,9 @@
 #ifdef MAXSESS
     int maxsess;		/* Max sessions/user */
 #endif /* MAXSESS */
+#ifdef AFL
+    int shm_offset;             /* where the user is stored in shared memory */
+#endif
 };
 
 typedef struct user USER;
@@ -214,6 +222,16 @@
 
 typedef union hash HASH;
 
+#ifdef AFL
+struct failed_node {
+    char         username[65];
+    unsigned int failures;
+    time_t       first_failure;
+    time_t       locked_time; /* the time the lock was set */
+    char         disabled;
+};
+#endif
+
 void *grouptable[HASH_TAB_SIZE];/* Table of group declarations */
 void *usertable[HASH_TAB_SIZE];	/* Table of user declarations */
 #ifdef ACLS
@@ -521,6 +539,14 @@
 	session.acctfile = NULL;
     }
 
+#ifdef AFL
+    if (session.afl_cfg) {
+	cfg_destroy_failure_shm();
+	free(session.afl_cfg);
+	session.afl_cfg = NULL;
+    }
+#endif
+
 #ifdef ACLS
     /* clean the acltable */
     for (i = 0; i < HASH_TAB_SIZE; i++) {
@@ -746,7 +772,23 @@
 	switch (sym_code) {
 	case S_eof:
 	    return(0);
-
+#ifdef AFL
+	case S_auth_fail_lock:
+	    /* faillock a b c 
+	     * a = number of failures
+	     * b = in this many seconds
+	     * c = lock for this many seconds */ 
+	    session.afl_cfg = (struct afl_cfg *)tac_malloc(sizeof(struct afl_cfg));
+	    bzero(session.afl_cfg, sizeof(struct afl_cfg));
+	    sym_get();
+	    session.afl_cfg->num_failures = atoi(sym_buf);
+	    sym_get();
+	    session.afl_cfg->seconds = atoi(sym_buf);
+	    sym_get();
+	    session.afl_cfg->lock_time = atoi(sym_buf); 
+	    sym_get();
+	    break;
+#endif
 	case S_accounting:
 	    sym_get();
 
@@ -2388,6 +2430,306 @@
     return(authen_default);
 }
 
+#ifdef AFL
+static unsigned int fetch_user_count(void)
+{
+    int i;
+    unsigned int count;
+    USER *entry;
+
+    count = 0;
+
+    for (i=0; i < HASH_TAB_SIZE; i++)
+    {
+	entry = (USER *)usertable[i];
+	while (entry)
+	{
+	    count++;
+	    entry = entry->hash;
+	}
+    }
+    return count;
+}
+
+/* 
+ * Create a user-table in shared memory for AFL.
+ */
+void cfg_create_failure_shm(const char *path, int id)
+{
+    unsigned int shm_sz;
+    int offset;
+    int i, shmid;
+    char *shm = NULL;
+
+    user_count = shm_sz = offset = 0;
+
+    user_count = fetch_user_count();
+    shm_sz = user_count * sizeof(struct failed_node);
+    
+    if((failed_key = ftok(path, id))<0)
+    {
+	report(LOG_ERR, "ftok unable to create key: %s",
+		strerror(errno));
+	tac_exit(1);
+    }
+
+    if ((shmid = shmget(failed_key, shm_sz, IPC_CREAT|0666)) < 0)
+    {
+	report(LOG_ERR, "shmget unable to get memory: %s",
+		strerror(errno));
+	tac_exit(1);
+    }
+
+    if ((shm = (char *)shmat(shmid, NULL, 0)) == (char *)-1)
+    {
+	report(LOG_ERR, "shmat: %s", strerror(errno));
+	tac_exit(1);
+    }
+
+    /* iterate over all the users and add them a failed_node 
+     * structure to the shared memory */
+    for (i=0; i < HASH_TAB_SIZE; i++)
+    {
+	struct failed_node *failed_node;
+	USER *entry = (USER *)usertable[i];
+
+	while(entry)
+	{
+	    entry->shm_offset = offset;
+	    failed_node = (struct failed_node *)&shm[offset];
+	    bzero(failed_node, sizeof(struct failed_node));
+	
+	    if (strlen(entry->name) > 64)
+	    {
+		report(LOG_ERR, "username %s > length of 64", 
+		    entry->name);
+		tac_exit(1);
+	    }
+	
+	    strncpy(failed_node->username, entry->name, 64); 
+	    offset += sizeof(struct failed_node);
+
+	    if (offset > user_count * sizeof(struct failed_node))
+	    {
+		report(LOG_ERR, "More users than previously allocated");
+		tac_exit(1);
+	    }
+
+	    entry = entry->hash;
+	}
+    }
+
+    /* now create a semaphore for locking */
+    if(semget(failed_key, 1, 0666 | IPC_CREAT) < 0)
+    {
+	report(LOG_ERR, "Unable to create semaphore: %s", strerror(errno));
+	tac_exit(1);
+    }
+}
+
+char *shm_fetch_and_lock(key_t key, unsigned int sz)
+{
+    int shmid, semid;
+    struct sembuf sem[2];
+    char *shm = NULL;
+
+    if((semid = semget(key, 1, 0666)) < 0)
+    {
+	report(LOG_ERR, "unable to fetch semaphore: %s", strerror(errno));
+	tac_exit(1);
+    }
+
+    sem[0].sem_num = 0;
+    sem[1].sem_num = 0;
+    sem[0].sem_flg = SEM_UNDO;
+    sem[1].sem_flg = SEM_UNDO;
+    sem[0].sem_op = 0;
+    sem[1].sem_op = 1;
+
+    semop(semid, sem, 2);
+
+    if((shmid = shmget(key, sz, 0666)) < 0)
+    {
+ 	report(LOG_ERR, "shm_fetchnlock unable to get shmid: %s",
+		strerror(errno));
+	tac_exit(1);
+    }
+
+    if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) 
+    {
+	report(LOG_ERR, "shm_fetchnlock Unable to find shm segment: %s",
+		strerror(errno));
+	tac_exit(1);
+    }
+
+    return shm;
+}
+
+static void shm_unlock(key_t key)
+{
+    int semid;
+    struct sembuf sem;
+
+    if ((semid = semget(key, 1, 0666)) < 0)
+	tac_exit(1);
+
+    sem.sem_num = 0;
+    sem.sem_flg = SEM_UNDO;
+    sem.sem_op  = -1;
+
+    semop(semid, &sem, 1);
+}
+
+static void destroy_semaphore(key_t key)
+{
+    int semid;
+    semid = semget(key, 0, 0666);
+    semctl(semid, 0, IPC_RMID, NULL);
+}
+
+static void destroy_shm(key_t key)
+{
+    int shmid;
+    shmid = shmget(key, 1, 0666);
+    shmctl(shmid, IPC_RMID, NULL);
+}
+   
+
+void cfg_destroy_failure_shm(void)
+{
+    destroy_semaphore(failed_key);
+    destroy_shm(failed_key);
+}
+
+static int shm_failed_offset(char *username, void *arg)
+{
+    USER *user;
+
+    if (arg == NULL)
+	user = (USER *)hash_lookup(usertable, username);
+    else
+	user = (USER *)arg;
+
+    return  (user ? user->shm_offset:-1);
+}
+
+void cfg_increment_failure(char *username)
+{
+    USER *user;
+    int offset;
+    char *data;
+    struct failed_node *node;
+    time_t now;
+
+    user = hash_lookup(usertable, username);
+
+    if (user == NULL)
+	return;
+
+    if ((offset = shm_failed_offset(username, user)) < 0)
+	return;
+
+    data = shm_fetch_and_lock(failed_key,
+	    user_count * sizeof(struct failed_node));
+
+    node = (struct failed_node *)&data[user->shm_offset];
+
+    if (strcmp(node->username, username) != 0)
+    {   
+        report(LOG_ERR, "Shared memory has something amiss (%s!=%s)",
+                node->username, username);
+        shm_unlock(failed_key);
+        return;
+    }
+
+    time(&now);
+
+    if (!node->first_failure) 
+	node->first_failure = now;
+
+    /* determine if this fail has exceeded the number of failures within
+     * the time window. If it has then lock the account.
+     */
+    if((!node->disabled) && ++node->failures >= session.afl_cfg->num_failures)
+    {
+	report(LOG_WARNING, "User %s has been disabled for %d seconds", 
+		username, session.afl_cfg->lock_time); 
+	node->locked_time = now;
+	node->disabled = 1;
+    }
+
+    shm_unlock(failed_key);
+}
+
+/* 
+ * Attempt to determine whether a user is locked out or not,
+ * this function also does timer expiration.
+ */
+int cfg_is_user_disabled(char *username)
+{
+    USER *user;
+    int offset;
+    char *data;
+    struct failed_node *node;
+    int ret = 0;
+    time_t now;
+
+    user = hash_lookup(usertable, username);
+
+    if (user == NULL)
+	return -1;
+
+    if ((offset = shm_failed_offset(username, user))<0)
+	return -1;
+
+    data = shm_fetch_and_lock(failed_key, 
+	    user_count * sizeof(struct failed_node)); 
+
+    node = (struct failed_node *)&data[user->shm_offset]; 
+
+    /* check to make sure what we have is true */
+    if (strcmp(node->username, username) != 0)
+    {
+	report(LOG_ERR, "Shared memory has something amiss (%s!=%s)",
+		node->username, username);
+	shm_unlock(failed_key);
+	return -1;
+    }
+
+    ret = node->disabled?1:0;
+    time(&now);
+
+    if (ret)
+    {
+	/* Check locked account expiration. Unlock if expired. */
+	if (difftime(now, node->locked_time) > session.afl_cfg->lock_time)
+	{
+	    report(LOG_WARNING, "Re-enabling account: %s", username);
+	    node->first_failure = 0;
+	    node->disabled      = 0;
+	    node->failures      = 0;
+	    node->locked_time   = 0;
+	    ret                 = 0;
+	}
+    }
+    else {
+	/* Check to see if the auth-fail window has expired. */
+	if ((node->first_failure) && 
+		difftime(now, node->first_failure) > session.afl_cfg->seconds)
+	{   
+	    report(LOG_INFO,"Resetting failure clock for user: %s\n", username);
+	    node->first_failure = 0;
+	    node->disabled      = 0;
+	    node->failures      = 0;
+	    node->locked_time   = 0;
+	}
+    }
+
+    shm_unlock(failed_key);
+    return ret;
+}
+#endif /* AFL */
+
 /*
  * Return 1 if this user has any ppp services configured. Used for
  * authorizing ppp/lcp requests
diff -Naur ../tacacs_orig/config.h.in ./config.h.in
--- ../tacacs_orig/config.h.in	2008-06-18 18:24:09.000000000 -0400
+++ ./config.h.in	2008-06-19 18:05:43.000000000 -0400
@@ -210,5 +210,7 @@
 # define socklen_t	int
 #endif
 
+#undef AFL
+
 #endif /* CONFIG_H */
 
diff -Naur ../tacacs_orig/configure ./configure
--- ../tacacs_orig/configure	2008-06-18 18:24:09.000000000 -0400
+++ ./configure	2008-06-19 18:29:11.000000000 -0400
@@ -722,6 +722,7 @@
 TACPLUS_LOGFILE
 PROFLAGS
 PROFLIBS
+AFL
 TAR
 INST_PROGS
 PERLV_PATH
@@ -1327,6 +1328,7 @@
   --enable-uenable        tacacs config per-user enable support (default)
   --enable-maxsess        Enforce a limit on maximum sessions per user
   --enable-finger         finger NAS for number of sessions a user is using
+  --enable-afl            Enable AFL support (default)
 
 Optional Packages:
   --with-PACKAGE[=ARG]    use PACKAGE [ARG=yes]
@@ -6112,6 +6114,52 @@
 
 
 
+{ echo "$as_me:$LINENO: checking whether to enable AFL support" >&5
+echo $ECHO_N "checking whether to enable AFL support... $ECHO_C" >&6; }
+
+
+# Check whether --enable-afl was given.
+if test "${enable_afl+set}" = set; then
+  enableval=$enable_afl;  case "$enable_afl" in
+  no)
+		{ echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+		use_afl=0
+		;;
+	yes)
+		{ echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+		cat >>confdefs.h <<\_ACEOF
+#define AFL 1
+_ACEOF
+
+		use_afl=1
+		;;
+	*)
+		{ echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+		cat >>confdefs.h <<\_ACEOF
+#define AFL 1
+_ACEOF
+
+		use_afl=1
+		;;
+	esac
+else
+  { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+	cat >>confdefs.h <<\_ACEOF
+#define AFL 1
+_ACEOF
+
+	use_afl=1
+
+fi
+
+
+
+
+
 # look for PAM
 
 
@@ -7953,6 +8001,7 @@
 TACPLUS_LOGFILE!$TACPLUS_LOGFILE$ac_delim
 PROFLAGS!$PROFLAGS$ac_delim
 PROFLIBS!$PROFLIBS$ac_delim
+AFL!$AFL$ac_delim
 TAR!$TAR$ac_delim
 INST_PROGS!$INST_PROGS$ac_delim
 PERLV_PATH!$PERLV_PATH$ac_delim
@@ -7961,7 +8010,7 @@
 LTLIBOBJS!$LTLIBOBJS$ac_delim
 _ACEOF
 
-  if test `sed -n "s/.*$ac_delim\$/X/p" conf$$subs.sed | grep -c X` = 17; then
+  if test `sed -n "s/.*$ac_delim\$/X/p" conf$$subs.sed | grep -c X` = 18; then
     break
   elif $ac_last_try; then
     { { echo "$as_me:$LINENO: error: could not make $CONFIG_STATUS" >&5
diff -Naur ../tacacs_orig/configure.in ./configure.in
--- ../tacacs_orig/configure.in	2008-06-18 18:24:09.000000000 -0400
+++ ./configure.in	2008-06-19 18:29:07.000000000 -0400
@@ -557,6 +557,37 @@
 AC_SUBST(PROFLAGS)
 AC_SUBST(PROFLIBS)
 
+dnl 
+dnl DISABLE AFL Support (Authentication Failure Locking)
+dnl
+AC_MSG_CHECKING(whether to enable AFL support)
+AH_TEMPLATE(AFL, [define this to disable Authentication Failure Locking support])
+AC_ARG_ENABLE(afl,
+[  --enable-afl            Enable AFL support (default)],
+[ case "$enable_afl" in
+  no)
+		AC_MSG_RESULT(no)
+		use_afl=0
+		;;
+	yes)
+		AC_MSG_RESULT(yes)
+		AC_DEFINE(AFL)
+		use_afl=1
+		;;
+	*)
+		AC_MSG_RESULT(yes)
+		AC_DEFINE(AFL)
+		use_afl=1
+		;;
+	esac ],
+	AC_MSG_RESULT(yes)
+	AC_DEFINE(AFL)
+	use_afl=1
+)
+AC_SUBST(AFL)
+
+
+
 # look for PAM
 AH_TEMPLATE(HAVE_PAM, [define if your system has libpam])
 AC_CHECK_LIB([pam], [pam_start],
diff -Naur ../tacacs_orig/parse.c ./parse.c
--- ../tacacs_orig/parse.c	2008-06-18 18:24:10.000000000 -0400
+++ ./parse.c	2008-06-19 18:19:00.000000000 -0400
@@ -118,6 +118,9 @@
     declare("PAM", S_pam);
 #endif
     declare("syslog", S_syslog);
+#ifdef AFL
+    declare("auth-fail-lock", S_auth_fail_lock);
+#endif
 }
 
 /* Return a keyword code if a keyword is recognized. 0 otherwise */
diff -Naur ../tacacs_orig/parse.h ./parse.h
--- ../tacacs_orig/parse.h	2008-06-18 18:24:10.000000000 -0400
+++ ./parse.h	2008-06-19 18:19:13.000000000 -0400
@@ -87,3 +87,6 @@
 # define S_pam		49
 #endif
 #define S_syslog  50
+#ifdef AFL
+#define S_auth_fail_lock 51
+#endif
diff -Naur ../tacacs_orig/tac_plus.c ./tac_plus.c
--- ../tacacs_orig/tac_plus.c	2008-06-18 18:24:10.000000000 -0400
+++ ./tac_plus.c	2008-06-23 14:39:14.000000000 -0400
@@ -90,6 +90,10 @@
     report(LOG_NOTICE, "Received signal %d, shutting down", signum);
     if (childpid > 0)
 	unlink(pidfilebuf);
+
+#ifdef AFL
+    cfg_destroy_failure_shm();
+#endif
     tac_exit(0);
 }
 
@@ -102,7 +106,9 @@
     report(LOG_NOTICE, "Reading config");
 
     session.acctfile = NULL;
-    //session.acctfile = tac_strdup(TACPLUS_ACCTFILE);
+#ifdef AFL
+    session.afl_cfg  = NULL;
+#endif
 
     if (!session.cfgfile) {
 	report(LOG_ERR, "no config file specified");
@@ -115,6 +121,11 @@
 	tac_exit(1);
     }
 
+#ifdef AFL
+    if(session.afl_cfg)
+	cfg_create_failure_shm(session.cfgfile, 'A'); 
+#endif
+
     initialised++;
 
     report(LOG_NOTICE, "Version %s Initialized %d", version, initialised);
@@ -326,6 +337,9 @@
     signal(SIGUSR1, handler);
     signal(SIGHUP, handler);
     signal(SIGTERM, die);
+#ifdef AFL
+    signal(SIGINT, die);
+#endif
     signal(SIGPIPE, SIG_IGN);
 
     if (parse_only)
@@ -863,6 +877,9 @@
 #if __STDC__
     fprintf(stdout, "__STDC__\n");
 #endif
+#if AFL
+   fprintf(stdout, "AFL\n"); 
+#endif
 
     return;
 }
diff -Naur ../tacacs_orig/tac_plus.h ./tac_plus.h
--- ../tacacs_orig/tac_plus.h	2008-06-18 18:24:10.000000000 -0400
+++ ./tac_plus.h	2008-06-19 18:23:13.000000000 -0400
@@ -177,6 +177,12 @@
 # include <fcntl.h>
 #endif
 
+#ifdef AFL
+#include <sys/ipc.h>
+#include <sys/shm.h>
+#include <sys/sem.h>
+#endif
+
 #if ! HAVE_STRCHR
 # define index strchr
 #endif
@@ -321,6 +327,14 @@
 
 #define NAS_PORT_MAX_LEN                255
 
+#ifdef AFL
+struct afl_cfg {
+	int num_failures;
+	int seconds;
+	int lock_time;
+};
+#endif
+
 struct session {
     int session_id;                /* host specific unique session id */
     int aborted;                   /* have we received an abort flag? */
@@ -336,6 +350,9 @@
     char port[NAS_PORT_MAX_LEN+1]; /* For error reporting */
     u_char version;                /* version of last packet read */
 		u_char acct_syslog;            /* syslog the accounting data */ 
+#ifdef AFL
+		struct afl_cfg *afl_cfg;       /* authentication failure lock cfg */
+#endif
 };
 
 extern struct session session;     /* the session */
@@ -689,6 +706,12 @@
 int	cfg_read_config(char *);
 int	cfg_user_exists(char *);
 int	cfg_user_svc_default_is_permit(char *);
+#ifdef AFL
+void cfg_create_failure_shm(const char *, int);
+void cfg_destroy_failure_shm(void);
+void cfg_increment_failure(char *);
+int cfg_is_user_disabled(char *);
+#endif
 
 /* default_fn.c */
 int	default_fn(struct authen_data *);


More information about the tac_plus mailing list