Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
487 lines (429 sloc) 10.7 KB
/*
* Copyright (c) 2015 The DragonFly Project. All rights reserved.
*
* This code is derived from software contributed to The DragonFly Project
* by Matthew Dillon <dillon@dragonflybsd.org>
* by Venkatesh Srinivas <vsrinivas@dragonflybsd.org>
*
* 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 <lockout> 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 <rulenum>).
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <net/if.h>
#include <net/ipfw/ip_fw.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdarg.h>
#include <syslog.h>
#include <ctype.h>
#include <stdbool.h>
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 <tablename> */
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 <rule> */
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 <tableid> */
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;
}
You can’t perform that action at this time.