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, in case of a PF firewall, adds to a PF table <lockout> using 40 * 'pfctl -tlockout -Tadd' commands. 41 * 42 * /etc/syslog.conf line example: 43 * auth.info;authpriv.info |exec /usr/sbin/sshlockout -pf lockout 44 * 45 * Also suggest a cron entry to clean out the PF table at least once a day. 46 * 3 3 * * * pfctl -tlockout -Tflush 47 * 48 * Alternatively there is an ipfw(8) mode (-ipfw <rulenum>). 49 */ 50 51 #include <sys/types.h> 52 #include <sys/time.h> 53 #include <stdio.h> 54 #include <stdlib.h> 55 #include <unistd.h> 56 #include <string.h> 57 #include <stdarg.h> 58 #include <syslog.h> 59 #include <ctype.h> 60 #include <stdbool.h> 61 62 typedef struct iphist { 63 struct iphist *next; 64 struct iphist *hnext; 65 char *ips; 66 time_t t; 67 int hv; 68 } iphist_t; 69 70 struct args { 71 int fw_type; 72 char *arg1; 73 char *arg2; 74 }; 75 76 #define FW_IS_PF 1 77 #define FW_IS_IPFW 2 78 79 #define HSIZE 1024 80 #define HMASK (HSIZE - 1) 81 #define MAXHIST 100 82 #define SSHLIMIT 5 /* per hour */ 83 #define MAX_TABLE_NAME 20 /* PF table name limit */ 84 85 static iphist_t *hist_base; 86 static iphist_t **hist_tail = &hist_base; 87 static iphist_t *hist_hash[HSIZE]; 88 static int hist_count = 0; 89 90 static struct args args; 91 92 static void init_iphist(void); 93 static void checkline(char *buf); 94 static int insert_iph(const char *ips, time_t t); 95 static void delete_iph(iphist_t *ip); 96 97 static 98 void 99 block_ip(const char *ips) { 100 char buf[128]; 101 int r = 0; 102 103 switch (args.fw_type) { 104 case FW_IS_PF: 105 r = snprintf(buf, sizeof(buf), 106 "pfctl -t%s -Tadd %s", args.arg1, ips); 107 break; 108 case FW_IS_IPFW: 109 r = snprintf(buf, sizeof(buf), 110 "ipfw add %s deny tcp from %s to me 22", 111 args.arg1, ips); 112 break; 113 } 114 115 if (r > 0 && (int)strlen(buf) == r) { 116 system(buf); 117 } 118 else { 119 syslog(LOG_ERR, "sshlockout: invalid command"); 120 } 121 } 122 123 /* 124 * Stupid simple string hash 125 */ 126 static __inline 127 int 128 iphash(const char *str) 129 { 130 int hv = 0xA1B3569D; 131 while (*str) { 132 hv = (hv << 5) ^ *str ^ (hv >> 23); 133 ++str; 134 } 135 return hv; 136 } 137 138 139 static bool 140 parse_args(int ac, char **av) 141 { 142 if (ac >= 2) { 143 if (strcmp(av[1], "-pf") == 0) { 144 // -pf <tablename> 145 char *tablename = av[2]; 146 if (ac == 3 && tablename != NULL) { 147 if (strlen(tablename) > 0 && 148 strlen(tablename) < MAX_TABLE_NAME) { 149 args.fw_type = FW_IS_PF; 150 args.arg1 = tablename; 151 args.arg2 = NULL; 152 return true; 153 } 154 } 155 } 156 if (strcmp(av[1], "-ipfw") == 0) { 157 // -ipfw <rule> 158 char *rule = av[2]; 159 if (ac == 3 && rule != NULL) { 160 if (strlen(rule) > 0 && strlen(rule) <= 5) { 161 for (char *s = rule; *s; ++s) { 162 if (!isdigit(*s)) 163 return false; 164 } 165 if (atoi(rule) < 1) 166 return false; 167 if (atoi(rule) > 65535) 168 return false; 169 args.fw_type = FW_IS_IPFW; 170 args.arg1 = rule; 171 args.arg2 = NULL; 172 return true; 173 } 174 } 175 } 176 } 177 178 return false; 179 } 180 181 int 182 main(int ac, char **av) 183 { 184 char buf[1024]; 185 186 args.fw_type = 0; 187 args.arg1 = NULL; 188 args.arg2 = NULL; 189 190 if (!parse_args(ac, av)) { 191 syslog(LOG_ERR, "sshlockout: invalid argument"); 192 return(1); 193 } 194 195 init_iphist(); 196 197 openlog("sshlockout", LOG_PID|LOG_CONS, LOG_AUTH); 198 syslog(LOG_ERR, "sshlockout starting up"); 199 freopen("/dev/null", "w", stdout); 200 freopen("/dev/null", "w", stderr); 201 202 while (fgets(buf, sizeof(buf), stdin) != NULL) { 203 if (strstr(buf, "sshd") == NULL) 204 continue; 205 checkline(buf); 206 } 207 syslog(LOG_ERR, "sshlockout exiting"); 208 return(0); 209 } 210 211 static 212 void 213 checkip(const char *str, const char *reason1, const char *reason2) { 214 char ips[128]; 215 int n1; 216 int n2; 217 int n3; 218 int n4; 219 time_t t = time(NULL); 220 221 ips[0] = '\0'; 222 223 if (sscanf(str, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) { 224 snprintf(ips, sizeof(ips), "%d.%d.%d.%d", n1, n2, n3, n4); 225 } 226 else { 227 /* 228 * Check for IPv6 address (primitive way) 229 */ 230 int cnt = 0; 231 while (str[cnt] == ':' || isxdigit(str[cnt])) { 232 ++cnt; 233 } 234 if (cnt > 0 && cnt < (int)sizeof(ips)) { 235 memcpy(ips, str, cnt); 236 ips[cnt] = '\0'; 237 } 238 } 239 240 /* 241 * We do not block localhost as is makes no sense. 242 */ 243 if (strcmp(ips, "127.0.0.1") == 0) 244 return; 245 if (strcmp(ips, "::1") == 0) 246 return; 247 248 if (strlen(ips) > 0) { 249 250 /* 251 * Check for DoS attack. When connections from too many 252 * IP addresses come in at the same time, our hash table 253 * would overflow, so we delete the oldest entries AND 254 * block it's IP when they are younger than 10 seconds. 255 * This prevents massive attacks from arbitrary IPs. 256 */ 257 if (hist_count > MAXHIST + 16) { 258 while (hist_count > MAXHIST) { 259 iphist_t *iph = hist_base; 260 int dt = (int)(t - iph->t); 261 if (dt < 10) { 262 syslog(LOG_ERR, 263 "Detected overflow attack, " 264 "locking out %s\n", 265 iph->ips); 266 block_ip(iph->ips); 267 } 268 delete_iph(iph); 269 } 270 } 271 272 if (insert_iph(ips, t)) { 273 syslog(LOG_ERR, 274 "Detected ssh %s attempt " 275 "for %s, locking out %s\n", 276 reason1, reason2, ips); 277 block_ip(ips); 278 } 279 } 280 } 281 282 static 283 void 284 checkline(char *buf) 285 { 286 char *str; 287 288 /* 289 * ssh login attempt with password (only hit if ssh allows 290 * password entry). Root or admin. 291 */ 292 if ((str = strstr(buf, "Failed password for root from")) != NULL || 293 (str = strstr(buf, "Failed password for admin from")) != NULL) { 294 while (*str && (*str < '0' || *str > '9')) 295 ++str; 296 checkip(str, "password login", "root or admin"); 297 return; 298 } 299 300 /* 301 * ssh login attempt with password (only hit if ssh allows password 302 * entry). Non-existant user. 303 */ 304 if ((str = strstr(buf, "Failed password for invalid user")) != NULL) { 305 str += 32; 306 while (*str == ' ') 307 ++str; 308 while (*str && *str != ' ') 309 ++str; 310 if (strncmp(str, " from", 5) == 0) { 311 checkip(str + 5, "password login", "an invalid user"); 312 } 313 return; 314 } 315 316 /* 317 * ssh login attempt for non-existant user. 318 */ 319 if ((str = strstr(buf, "Invalid user")) != NULL) { 320 str += 12; 321 while (*str == ' ') 322 ++str; 323 while (*str && *str != ' ') 324 ++str; 325 if (strncmp(str, " from", 5) == 0) { 326 checkip(str + 5, "login", "an invalid user"); 327 } 328 return; 329 } 330 331 /* 332 * Premature disconnect in pre-authorization phase, typically an 333 * attack but require 5 attempts in an hour before cleaning it out. 334 */ 335 if ((str = strstr(buf, "Received disconnect from ")) != NULL && 336 strstr(buf, "[preauth]") != NULL) { 337 checkip(str + 25, "preauth", "an invalid user"); 338 return; 339 } 340 } 341 342 /* 343 * Insert IP record 344 */ 345 static 346 int 347 insert_iph(const char *ips, time_t t) 348 { 349 iphist_t *ip = malloc(sizeof(*ip)); 350 iphist_t *scan; 351 int found; 352 353 ip->hv = iphash(ips); 354 ip->ips = strdup(ips); 355 ip->t = t; 356 357 ip->hnext = hist_hash[ip->hv & HMASK]; 358 hist_hash[ip->hv & HMASK] = ip; 359 ip->next = NULL; 360 *hist_tail = ip; 361 hist_tail = &ip->next; 362 ++hist_count; 363 364 /* 365 * hysteresis 366 */ 367 if (hist_count > MAXHIST + 16) { 368 while (hist_count > MAXHIST) 369 delete_iph(hist_base); 370 } 371 372 /* 373 * Check limit 374 */ 375 found = 0; 376 for (scan = hist_hash[ip->hv & HMASK]; scan; scan = scan->hnext) { 377 if (scan->hv == ip->hv && strcmp(scan->ips, ip->ips) == 0) { 378 int dt = (int)(t - ip->t); 379 if (dt < 60 * 60) { 380 ++found; 381 if (found > SSHLIMIT) 382 break; 383 } 384 } 385 } 386 return (found > SSHLIMIT); 387 } 388 389 /* 390 * Delete an ip record. Note that we always delete from the head of the 391 * list, but we will still wind up scanning hash chains. 392 */ 393 static 394 void 395 delete_iph(iphist_t *ip) 396 { 397 iphist_t **scanp; 398 iphist_t *scan; 399 400 scanp = &hist_base; 401 while ((scan = *scanp) != ip) { 402 scanp = &scan->next; 403 } 404 *scanp = ip->next; 405 if (hist_tail == &ip->next) 406 hist_tail = scanp; 407 408 scanp = &hist_hash[ip->hv & HMASK]; 409 while ((scan = *scanp) != ip) { 410 scanp = &scan->hnext; 411 } 412 *scanp = ip->hnext; 413 414 --hist_count; 415 free(ip); 416 } 417 418 static 419 void 420 init_iphist(void) { 421 hist_base = NULL; 422 hist_tail = &hist_base; 423 for (int i = 0; i < HSIZE; i++) { 424 hist_hash[i] = NULL; 425 } 426 hist_count = 0; 427 } 428