1 /* 2 * Copyright (c) 2015 The DragonFly Project. All rights reserved. 3 * 4 * This code is derived from software contributed to The DragonFly Project 5 * by Matthew Dillon <dillon@dragonflybsd.org> 6 * by Venkatesh Srinivas <vsrinivas@dragonflybsd.org> 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 12 * 1. Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 2. Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in 16 * the documentation and/or other materials provided with the 17 * distribution. 18 * 3. Neither the name of The DragonFly Project nor the names of its 19 * contributors may be used to endorse or promote products derived 20 * from this software without specific, prior written permission. 21 * 22 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 30 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 32 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 33 * SUCH DAMAGE. 34 */ 35 /* 36 * Use: pipe syslog auth output to this program. 37 * 38 * Detects failed ssh login attempts and maps out the originating IP and 39 * issues adds to a PF table <lockout> using 'pfctl -tlockout -Tadd' commands. 40 * 41 * /etc/syslog.conf line example: 42 * auth.info;authpriv.info |exec /usr/sbin/sshlockout lockout 43 * 44 * Also suggest a cron entry to clean out the PF table at least once a day. 45 * 3 3 * * * pfctl -tlockout -Tflush 46 */ 47 48 #include <sys/types.h> 49 #include <sys/time.h> 50 #include <stdio.h> 51 #include <stdlib.h> 52 #include <unistd.h> 53 #include <string.h> 54 #include <stdarg.h> 55 #include <syslog.h> 56 #include <ctype.h> 57 58 typedef struct iphist { 59 struct iphist *next; 60 struct iphist *hnext; 61 char *ips; 62 time_t t; 63 int hv; 64 } iphist_t; 65 66 #define HSIZE 1024 67 #define HMASK (HSIZE - 1) 68 #define MAXHIST 100 69 #define SSHLIMIT 5 /* per hour */ 70 #define MAX_TABLE_NAME 20 /* PF table name limit */ 71 72 static iphist_t *hist_base; 73 static iphist_t **hist_tail = &hist_base; 74 static iphist_t *hist_hash[HSIZE]; 75 static int hist_count = 0; 76 77 static char *pftable = NULL; 78 79 static void init_iphist(void); 80 static void checkline(char *buf); 81 static int insert_iph(const char *ips, time_t t); 82 static void delete_iph(iphist_t *ip); 83 84 static 85 void 86 block_ip(const char *ips) { 87 char buf[128]; 88 int r = snprintf(buf, sizeof(buf), 89 "pfctl -t%s -Tadd %s", pftable, ips); 90 if ((int)strlen(buf) == r) { 91 system(buf); 92 } 93 else { 94 syslog(LOG_ERR, "sshlockout: command size overflow"); 95 } 96 } 97 98 /* 99 * Stupid simple string hash 100 */ 101 static __inline 102 int 103 iphash(const char *str) 104 { 105 int hv = 0xA1B3569D; 106 while (*str) { 107 hv = (hv << 5) ^ *str ^ (hv >> 23); 108 ++str; 109 } 110 return hv; 111 } 112 113 int 114 main(int ac, char **av) 115 { 116 char buf[1024]; 117 118 init_iphist(); 119 120 if (ac == 2 && av[1] != NULL && 121 strlen(av[1]) > 0 && strlen(av[1]) < MAX_TABLE_NAME) { 122 pftable = av[1]; 123 } 124 else { 125 syslog(LOG_ERR, "sshlockout: invalid argument"); 126 return(1); 127 } 128 129 openlog("sshlockout", LOG_PID|LOG_CONS, LOG_AUTH); 130 syslog(LOG_ERR, "sshlockout starting up"); 131 freopen("/dev/null", "w", stdout); 132 freopen("/dev/null", "w", stderr); 133 134 while (fgets(buf, sizeof(buf), stdin) != NULL) { 135 if (strstr(buf, "sshd") == NULL) 136 continue; 137 checkline(buf); 138 } 139 syslog(LOG_ERR, "sshlockout exiting"); 140 return(0); 141 } 142 143 static 144 void 145 checkip(const char *str, const char *reason1, const char *reason2) { 146 char ips[128]; 147 int n1; 148 int n2; 149 int n3; 150 int n4; 151 time_t t = time(NULL); 152 153 ips[0] = '\0'; 154 155 if (sscanf(str, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) { 156 snprintf(ips, sizeof(ips), "%d.%d.%d.%d", n1, n2, n3, n4); 157 } 158 else { 159 /* 160 * Check for IPv6 address (primitive way) 161 */ 162 int cnt = 0; 163 while (str[cnt] == ':' || isxdigit(str[cnt])) { 164 ++cnt; 165 } 166 if (cnt > 0 && cnt < (int)sizeof(ips)) { 167 memcpy(ips, str, cnt); 168 ips[cnt] = '\0'; 169 } 170 } 171 172 /* 173 * We do not block localhost as is makes no sense. 174 */ 175 if (strcmp(ips, "127.0.0.1") == 0) 176 return; 177 if (strcmp(ips, "::1") == 0) 178 return; 179 180 if (strlen(ips) > 0) { 181 182 /* 183 * Check for DoS attack. When connections from too many 184 * IP addresses come in at the same time, our hash table 185 * would overflow, so we delete the oldest entries AND 186 * block it's IP when they are younger than 10 seconds. 187 * This prevents massive attacks from arbitrary IPs. 188 */ 189 if (hist_count > MAXHIST + 16) { 190 while (hist_count > MAXHIST) { 191 iphist_t *iph = hist_base; 192 int dt = (int)(t - iph->t); 193 if (dt < 10) { 194 syslog(LOG_ERR, 195 "Detected overflow attack, " 196 "locking out %s\n", 197 iph->ips); 198 block_ip(iph->ips); 199 } 200 delete_iph(iph); 201 } 202 } 203 204 if (insert_iph(ips, t)) { 205 syslog(LOG_ERR, 206 "Detected ssh %s attempt " 207 "for %s, locking out %s\n", 208 reason1, reason2, ips); 209 block_ip(ips); 210 } 211 } 212 } 213 214 static 215 void 216 checkline(char *buf) 217 { 218 char *str; 219 220 /* 221 * ssh login attempt with password (only hit if ssh allows 222 * password entry). Root or admin. 223 */ 224 if ((str = strstr(buf, "Failed password for root from")) != NULL || 225 (str = strstr(buf, "Failed password for admin from")) != NULL) { 226 while (*str && (*str < '0' || *str > '9')) 227 ++str; 228 checkip(str, "password login", "root or admin"); 229 return; 230 } 231 232 /* 233 * ssh login attempt with password (only hit if ssh allows password 234 * entry). Non-existant user. 235 */ 236 if ((str = strstr(buf, "Failed password for invalid user")) != NULL) { 237 str += 32; 238 while (*str == ' ') 239 ++str; 240 while (*str && *str != ' ') 241 ++str; 242 if (strncmp(str, " from", 5) == 0) { 243 checkip(str + 5, "password login", "an invalid user"); 244 } 245 return; 246 } 247 248 /* 249 * ssh login attempt for non-existant user. 250 */ 251 if ((str = strstr(buf, "Invalid user")) != NULL) { 252 str += 12; 253 while (*str == ' ') 254 ++str; 255 while (*str && *str != ' ') 256 ++str; 257 if (strncmp(str, " from", 5) == 0) { 258 checkip(str + 5, "login", "an invalid user"); 259 } 260 return; 261 } 262 263 /* 264 * Premature disconnect in pre-authorization phase, typically an 265 * attack but require 5 attempts in an hour before cleaning it out. 266 */ 267 if ((str = strstr(buf, "Received disconnect from ")) != NULL && 268 strstr(buf, "[preauth]") != NULL) { 269 checkip(str + 25, "preauth", "an invalid user"); 270 return; 271 } 272 } 273 274 /* 275 * Insert IP record 276 */ 277 static 278 int 279 insert_iph(const char *ips, time_t t) 280 { 281 iphist_t *ip = malloc(sizeof(*ip)); 282 iphist_t *scan; 283 int found; 284 285 ip->hv = iphash(ips); 286 ip->ips = strdup(ips); 287 ip->t = t; 288 289 ip->hnext = hist_hash[ip->hv & HMASK]; 290 hist_hash[ip->hv & HMASK] = ip; 291 ip->next = NULL; 292 *hist_tail = ip; 293 hist_tail = &ip->next; 294 ++hist_count; 295 296 /* 297 * hysteresis 298 */ 299 if (hist_count > MAXHIST + 16) { 300 while (hist_count > MAXHIST) 301 delete_iph(hist_base); 302 } 303 304 /* 305 * Check limit 306 */ 307 found = 0; 308 for (scan = hist_hash[ip->hv & HMASK]; scan; scan = scan->hnext) { 309 if (scan->hv == ip->hv && strcmp(scan->ips, ip->ips) == 0) { 310 int dt = (int)(t - ip->t); 311 if (dt < 60 * 60) { 312 ++found; 313 if (found > SSHLIMIT) 314 break; 315 } 316 } 317 } 318 return (found > SSHLIMIT); 319 } 320 321 /* 322 * Delete an ip record. Note that we always delete from the head of the 323 * list, but we will still wind up scanning hash chains. 324 */ 325 static 326 void 327 delete_iph(iphist_t *ip) 328 { 329 iphist_t **scanp; 330 iphist_t *scan; 331 332 scanp = &hist_base; 333 while ((scan = *scanp) != ip) { 334 scanp = &scan->next; 335 } 336 *scanp = ip->next; 337 if (hist_tail == &ip->next) 338 hist_tail = scanp; 339 340 scanp = &hist_hash[ip->hv & HMASK]; 341 while ((scan = *scanp) != ip) { 342 scanp = &scan->hnext; 343 } 344 *scanp = ip->hnext; 345 346 --hist_count; 347 free(ip); 348 } 349 350 static 351 void 352 init_iphist(void) { 353 hist_base = NULL; 354 hist_tail = &hist_base; 355 for (int i = 0; i < HSIZE; i++) { 356 hist_hash[i] = NULL; 357 } 358 hist_count = 0; 359 } 360