/* * Copyright (c) 2015 The DragonFly Project. All rights reserved. * * This code is derived from software contributed to The DragonFly Project * by Matthew Dillon * by Venkatesh Srinivas * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * 3. Neither the name of The DragonFly Project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific, prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ /* * Use: pipe syslog auth output to this program. * * Detects failed ssh login attempts and maps out the originating IP and * issues, in case of a PF firewall, adds to a PF table using * 'pfctl -tlockout -Tadd' commands. * * /etc/syslog.conf line example: * auth.info;authpriv.info |exec /usr/sbin/sshlockout -pf lockout * * Also suggest a cron entry to clean out the PF table at least once a day. * 3 3 * * * pfctl -tlockout -Tflush * * Alternatively there is an ipfw(8) mode (-ipfw ). */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct iphist { struct iphist *next; struct iphist *hnext; char *ips; time_t t; int hv; } iphist_t; struct args { int fw_type; char *arg1; int arg2; }; #define FW_IS_PF 1 #define FW_IS_IPFW 2 #define FW_IS_IPFWTBL 3 #define HSIZE 1024 #define HMASK (HSIZE - 1) #define MAXHIST 100 #define SSHLIMIT 5 /* per hour */ #define MAX_TABLE_NAME 20 /* PF table name limit */ static iphist_t *hist_base; static iphist_t **hist_tail = &hist_base; static iphist_t *hist_hash[HSIZE]; static int hist_count = 0; static int ipfw_sock = -1; static struct args args; static void init_iphist(void); static void checkline(char *buf); static int insert_iph(const char *ips, time_t t); static void delete_iph(iphist_t *ip); static void block_ip(const char *ips) { struct ipfw_ioc_tblcont ent; struct ipfw_ioc_tblent *te; char buf[128]; int r = 0; switch (args.fw_type) { case FW_IS_PF: r = snprintf(buf, sizeof(buf), "pfctl -t%s -Tadd %s", args.arg1, ips); break; case FW_IS_IPFW: r = snprintf(buf, sizeof(buf), "ipfw add %s deny tcp from %s to me 22", args.arg1, ips); break; case FW_IS_IPFWTBL: memset(&ent, 0, sizeof(ent)); ent.tableid = args.arg2; ent.entcnt = 1; te = &ent.ent[0]; r = inet_pton(AF_INET, ips, &te->key.sin_addr); if (r <= 0) break; te->key.sin_family = AF_INET; te->key.sin_len = sizeof(struct sockaddr_in); if (setsockopt(ipfw_sock, IPPROTO_IP, IP_FW_TBL_ADD, &ent, sizeof(ent)) < 0) { r = -1; break; } /* Done */ return; } if (r > 0 && (int)strlen(buf) == r) { system(buf); } else { syslog(LOG_ERR, "sshlockout: invalid command"); } } /* * Stupid simple string hash */ static __inline int iphash(const char *str) { int hv = 0xA1B3569D; while (*str) { hv = (hv << 5) ^ *str ^ (hv >> 23); ++str; } return hv; } static bool parse_args(int ac, char **av) { if (ac >= 2) { if (strcmp(av[1], "-pf") == 0 && ac == 3) { /* -pf */ char *tablename = av[2]; if (strlen(tablename) > 0 && strlen(tablename) < MAX_TABLE_NAME) { args.fw_type = FW_IS_PF; args.arg1 = tablename; return true; } } if (strcmp(av[1], "-ipfw") == 0 && ac == 3) { /* -ipfw */ char *rule = av[2]; if (strlen(rule) > 0 && strlen(rule) <= 5) { for (char *s = rule; *s; ++s) { if (!isdigit(*s)) return false; } if (atoi(rule) < 1) return false; if (atoi(rule) > 65535) return false; args.fw_type = FW_IS_IPFW; args.arg1 = rule; return true; } } if (strcmp(av[1], "-ipfwtbl") == 0 && ac == 3) { /* -ipfwtbl */ int tableid; char *eptr; tableid = strtoul(av[2], &eptr, 0); if (*eptr != '\0') return false; ipfw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (ipfw_sock < 0) return false; args.fw_type = FW_IS_IPFWTBL; args.arg2 = tableid; return true; } } return false; } int main(int ac, char **av) { char buf[1024]; args.fw_type = 0; args.arg1 = NULL; args.arg2 = 0; if (!parse_args(ac, av)) { syslog(LOG_ERR, "sshlockout: invalid argument"); return(1); } init_iphist(); openlog("sshlockout", LOG_PID|LOG_CONS, LOG_AUTH); syslog(LOG_ERR, "sshlockout starting up"); freopen("/dev/null", "w", stdout); freopen("/dev/null", "w", stderr); while (fgets(buf, sizeof(buf), stdin) != NULL) { if (strstr(buf, "sshd") == NULL) continue; checkline(buf); } syslog(LOG_ERR, "sshlockout exiting"); return(0); } static void checkip(const char *str, const char *reason1, const char *reason2) { char ips[128]; int n1; int n2; int n3; int n4; time_t t = time(NULL); ips[0] = '\0'; if (sscanf(str, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) { snprintf(ips, sizeof(ips), "%d.%d.%d.%d", n1, n2, n3, n4); } else { /* * Check for IPv6 address (primitive way) */ int cnt = 0; while (str[cnt] == ':' || isxdigit(str[cnt])) { ++cnt; } if (cnt > 0 && cnt < (int)sizeof(ips)) { memcpy(ips, str, cnt); ips[cnt] = '\0'; } } /* * We do not block localhost as is makes no sense. */ if (strcmp(ips, "127.0.0.1") == 0) return; if (strcmp(ips, "::1") == 0) return; if (strlen(ips) > 0) { /* * Check for DoS attack. When connections from too many * IP addresses come in at the same time, our hash table * would overflow, so we delete the oldest entries AND * block it's IP when they are younger than 10 seconds. * This prevents massive attacks from arbitrary IPs. */ if (hist_count > MAXHIST + 16) { while (hist_count > MAXHIST) { iphist_t *iph = hist_base; int dt = (int)(t - iph->t); if (dt < 10) { syslog(LOG_ERR, "Detected overflow attack, " "locking out %s\n", iph->ips); block_ip(iph->ips); } delete_iph(iph); } } if (insert_iph(ips, t)) { syslog(LOG_ERR, "Detected ssh %s attempt " "for %s, locking out %s\n", reason1, reason2, ips); block_ip(ips); } } } static void checkline(char *buf) { char *str; /* * ssh login attempt with password (only hit if ssh allows * password entry). Root or admin. */ if ((str = strstr(buf, "Failed password for root from")) != NULL || (str = strstr(buf, "Failed password for admin from")) != NULL) { while (*str && (*str < '0' || *str > '9')) ++str; checkip(str, "password login", "root or admin"); return; } /* * ssh login attempt with password (only hit if ssh allows password * entry). Non-existant user. */ if ((str = strstr(buf, "Failed password for invalid user")) != NULL) { str += 32; while (*str == ' ') ++str; while (*str && *str != ' ') ++str; if (strncmp(str, " from", 5) == 0) { checkip(str + 5, "password login", "an invalid user"); } return; } /* * ssh login attempt for non-existant user. */ if ((str = strstr(buf, "Invalid user")) != NULL) { str += 12; while (*str == ' ') ++str; while (*str && *str != ' ') ++str; if (strncmp(str, " from", 5) == 0) { checkip(str + 5, "login", "an invalid user"); } return; } /* * Premature disconnect in pre-authorization phase, typically an * attack but require 5 attempts in an hour before cleaning it out. */ if ((str = strstr(buf, "Received disconnect from ")) != NULL && strstr(buf, "[preauth]") != NULL) { checkip(str + 25, "preauth", "an invalid user"); return; } /* * Maximum authentication attempts exceeded */ if ((str = strstr(buf, "maximum authentication " "attempts exceeded for ")) != NULL && strstr(buf, "[preauth]") != NULL) { str += 45; while (*str == ' ') ++str; while (*str && *str != ' ') ++str; if (strncmp(str, " from", 5) == 0) { checkip(str + 5, "login", "many attempts"); } return; } } /* * Insert IP record */ static int insert_iph(const char *ips, time_t t) { iphist_t *ip = malloc(sizeof(*ip)); iphist_t *scan; int found; ip->hv = iphash(ips); ip->ips = strdup(ips); ip->t = t; ip->hnext = hist_hash[ip->hv & HMASK]; hist_hash[ip->hv & HMASK] = ip; ip->next = NULL; *hist_tail = ip; hist_tail = &ip->next; ++hist_count; /* * hysteresis */ if (hist_count > MAXHIST + 16) { while (hist_count > MAXHIST) delete_iph(hist_base); } /* * Check limit */ found = 0; for (scan = hist_hash[ip->hv & HMASK]; scan; scan = scan->hnext) { if (scan->hv == ip->hv && strcmp(scan->ips, ip->ips) == 0) { int dt = (int)(t - ip->t); if (dt < 60 * 60) { ++found; if (found > SSHLIMIT) break; } } } return (found > SSHLIMIT); } /* * Delete an ip record. Note that we always delete from the head of the * list, but we will still wind up scanning hash chains. */ static void delete_iph(iphist_t *ip) { iphist_t **scanp; iphist_t *scan; scanp = &hist_base; while ((scan = *scanp) != ip) { scanp = &scan->next; } *scanp = ip->next; if (hist_tail == &ip->next) hist_tail = scanp; scanp = &hist_hash[ip->hv & HMASK]; while ((scan = *scanp) != ip) { scanp = &scan->hnext; } *scanp = ip->hnext; --hist_count; free(ip); } static void init_iphist(void) { hist_base = NULL; hist_tail = &hist_base; for (int i = 0; i < HSIZE; i++) { hist_hash[i] = NULL; } hist_count = 0; }