diff options
author | djm@openbsd.org <djm@openbsd.org> | 2024-06-06 19:15:25 +0200 |
---|---|---|
committer | Damien Miller <djm@mindrot.org> | 2024-06-06 19:35:40 +0200 |
commit | 81c1099d22b81ebfd20a334ce986c4f753b0db29 (patch) | |
tree | 5cabf3d270bc3b2a48cef2b631d695d63248fad4 /sshd.c | |
parent | whitespace (diff) | |
download | openssh-81c1099d22b81ebfd20a334ce986c4f753b0db29.tar.xz openssh-81c1099d22b81ebfd20a334ce986c4f753b0db29.zip |
upstream: Add a facility to sshd(8) to penalise particular
problematic client behaviours, controlled by two new sshd_config(5) options:
PerSourcePenalties and PerSourcePenaltyExemptList.
When PerSourcePenalties are enabled, sshd(8) will monitor the exit
status of its child pre-auth session processes. Through the exit
status, it can observe situations where the session did not
authenticate as expected. These conditions include when the client
repeatedly attempted authentication unsucessfully (possibly indicating
an attack against one or more accounts, e.g. password guessing), or
when client behaviour caused sshd to crash (possibly indicating
attempts to exploit sshd).
When such a condition is observed, sshd will record a penalty of some
duration (e.g. 30 seconds) against the client's address. If this time
is above a minimum threshold specified by the PerSourcePenalties, then
connections from the client address will be refused (along with any
others in the same PerSourceNetBlockSize CIDR range).
Repeated offenses by the same client address will accrue greater
penalties, up to a configurable maximum. A PerSourcePenaltyExemptList
option allows certain address ranges to be exempt from all penalties.
We hope these options will make it significantly more difficult for
attackers to find accounts with weak/guessable passwords or exploit
bugs in sshd(8) itself.
PerSourcePenalties is off by default, but we expect to enable it
automatically in the near future.
much feedback markus@ and others, ok markus@
OpenBSD-Commit-ID: 89ded70eccb2b4926ef0366a4d58a693de366cca
Diffstat (limited to 'sshd.c')
-rw-r--r-- | sshd.c | 428 |
1 files changed, 368 insertions, 60 deletions
@@ -1,4 +1,4 @@ -/* $OpenBSD: sshd.c,v 1.605 2024/06/01 07:03:37 djm Exp $ */ +/* $OpenBSD: sshd.c,v 1.606 2024/06/06 17:15:25 djm Exp $ */ /* * Copyright (c) 2000, 2001, 2002 Markus Friedl. All rights reserved. * Copyright (c) 2002 Niels Provos. All rights reserved. @@ -89,6 +89,7 @@ #include "version.h" #include "ssherr.h" #include "sk-api.h" +#include "addr.h" #include "srclimit.h" /* Re-exec fds */ @@ -138,6 +139,8 @@ struct { } sensitive_data; /* This is set to true when a signal is received. */ +static volatile sig_atomic_t received_siginfo = 0; +static volatile sig_atomic_t received_sigchld = 0; static volatile sig_atomic_t received_sighup = 0; static volatile sig_atomic_t received_sigterm = 0; @@ -145,8 +148,9 @@ static volatile sig_atomic_t received_sigterm = 0; u_int utmp_len = HOST_NAME_MAX+1; /* - * startup_pipes/flags are used for tracking children of the listening sshd - * process early in their lifespans. This tracking is needed for three things: + * The early_child/children array below is used for tracking children of the + * listening sshd process early in their lifespans, before they have + * completed authentication. This tracking is needed for four things: * * 1) Implementing the MaxStartups limit of concurrent unauthenticated * connections. @@ -155,14 +159,31 @@ u_int utmp_len = HOST_NAME_MAX+1; * after it restarts. * 3) Ensuring that rexec'd sshd processes have received their initial state * from the parent listen process before handling SIGHUP. + * 4) Tracking and logging unsuccessful exits from the preauth sshd monitor, + * including and especially those for LoginGraceTime timeouts. * * Child processes signal that they have completed closure of the listen_socks * and (if applicable) received their rexec state by sending a char over their - * sock. Child processes signal that authentication has completed by closing - * the sock (or by exiting). + * sock. + * + * Child processes signal that authentication has completed by sending a + * second char over the socket before closing it, otherwise the listener will + * continue tracking the child (and using up a MaxStartups slot) until the + * preauth subprocess exits, whereupon the listener will log its exit status. + * preauth processes will exit with a status of EXIT_LOGIN_GRACE to indicate + * they did not authenticate before the LoginGraceTime alarm fired. */ -static int *startup_pipes = NULL; -static int *startup_flags = NULL; /* Indicates child closed listener */ +struct early_child { + int pipefd; + int early; /* Indicates child closed listener */ + char *id; /* human readable connection identifier */ + pid_t pid; + struct xaddr addr; + int have_addr; + int status, have_status; +}; +static struct early_child *children; +static int children_active; static int startup_pipe = -1; /* in child */ /* sshd_config buffer */ @@ -192,15 +213,257 @@ close_listen_socks(void) num_listen_socks = 0; } +/* Allocate and initialise the children array */ +static void +child_alloc(void) +{ + int i; + + children = xcalloc(options.max_startups, sizeof(*children)); + for (i = 0; i < options.max_startups; i++) { + children[i].pipefd = -1; + children[i].pid = -1; + } +} + +/* Register a new connection in the children array; child pid comes later */ +static struct early_child * +child_register(int pipefd, int sockfd) +{ + int i, lport, rport; + char *laddr = NULL, *raddr = NULL; + struct early_child *child = NULL; + struct sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + struct sockaddr *sa = (struct sockaddr *)&addr; + + for (i = 0; i < options.max_startups; i++) { + if (children[i].pipefd != -1 || children[i].pid > 0) + continue; + child = &(children[i]); + break; + } + if (child == NULL) { + fatal_f("error: accepted connection when all %d child " + " slots full", options.max_startups); + } + child->pipefd = pipefd; + child->early = 1; + /* record peer address, if available */ + if (getpeername(sockfd, sa, &addrlen) == 0 && + addr_sa_to_xaddr(sa, addrlen, &child->addr) == 0) + child->have_addr = 1; + /* format peer address string for logs */ + if ((lport = get_local_port(sockfd)) == 0 || + (rport = get_peer_port(sockfd)) == 0) { + /* Not a TCP socket */ + raddr = get_peer_ipaddr(sockfd); + xasprintf(&child->id, "connection from %s", raddr); + } else { + laddr = get_local_ipaddr(sockfd); + raddr = get_peer_ipaddr(sockfd); + xasprintf(&child->id, "connection from %s to %s", laddr, raddr); + } + free(laddr); + free(raddr); + if (++children_active > options.max_startups) + fatal_f("internal error: more children than max_startups"); + + return child; +} + +/* + * Finally free a child entry. Don't call this directly. + */ +static void +child_finish(struct early_child *child) +{ + if (children_active == 0) + fatal_f("internal error: children_active underflow"); + if (child->pipefd != -1) + close(child->pipefd); + free(child->id); + memset(child, '\0', sizeof(*child)); + child->pipefd = -1; + child->pid = -1; + children_active--; +} + +/* + * Close a child's pipe. This will not stop tracking the child immediately + * (it will still be tracked for waitpid()) unless force_final is set, or + * child has already exited. + */ +static void +child_close(struct early_child *child, int force_final, int quiet) +{ + if (!quiet) + debug_f("enter%s", force_final ? " (forcing)" : ""); + if (child->pipefd != -1) { + close(child->pipefd); + child->pipefd = -1; + } + if (child->pid == -1 || force_final) + child_finish(child); +} + +/* Record a child exit. Safe to call from signal handlers */ +static void +child_exit(pid_t pid, int status) +{ + int i; + + if (children == NULL || pid <= 0) + return; + for (i = 0; i < options.max_startups; i++) { + if (children[i].pid == pid) { + children[i].have_status = 1; + children[i].status = status; + break; + } + } +} + +/* + * Reap a child entry that has exited, as previously flagged + * using child_exit(). + * Handles logging of exit condition and will finalise the child if its pipe + * had already been closed. + */ +static void +child_reap(struct early_child *child) +{ + LogLevel level = SYSLOG_LEVEL_DEBUG1; + int was_crash, penalty_type = SRCLIMIT_PENALTY_NONE; + + /* Log exit information */ + if (WIFSIGNALED(child->status)) { + /* + * Increase logging for signals potentially associated + * with serious conditions. + */ + if ((was_crash = signal_is_crash(WTERMSIG(child->status)))) + level = SYSLOG_LEVEL_ERROR; + do_log2(level, "session process %ld for %s killed by " + "signal %d%s", (long)child->pid, child->id, + WTERMSIG(child->status), child->early ? " (early)" : ""); + if (was_crash) + penalty_type = SRCLIMIT_PENALTY_CRASH; + } else if (!WIFEXITED(child->status)) { + penalty_type = SRCLIMIT_PENALTY_CRASH; + error("session process %ld for %s terminated abnormally, " + "status=0x%x%s", (long)child->pid, child->id, child->status, + child->early ? " (early)" : ""); + } else { + /* Normal exit. We care about the status */ + switch (WEXITSTATUS(child->status)) { + case 0: + debug3_f("preauth child %ld for %s completed " + "normally %s", (long)child->pid, child->id, + child->early ? " (early)" : ""); + break; + case EXIT_LOGIN_GRACE: + penalty_type = SRCLIMIT_PENALTY_GRACE_EXCEEDED; + logit("Timeout before authentication for %s, " + "pid = %ld%s", child->id, (long)child->pid, + child->early ? " (early)" : ""); + break; + case EXIT_CHILD_CRASH: + penalty_type = SRCLIMIT_PENALTY_CRASH; + logit("Session process %ld unpriv child crash for %s%s", + (long)child->pid, child->id, + child->early ? " (early)" : ""); + break; + case EXIT_AUTH_ATTEMPTED: + penalty_type = SRCLIMIT_PENALTY_AUTHFAIL; + debug_f("preauth child %ld for %s exited " + "after unsuccessful auth attempt %s", + (long)child->pid, child->id, + child->early ? " (early)" : ""); + break; + default: + penalty_type = SRCLIMIT_PENALTY_NOAUTH; + debug_f("preauth child %ld for %s exited " + "with status %d%s", (long)child->pid, child->id, + WEXITSTATUS(child->status), + child->early ? " (early)" : ""); + break; + } + } + /* + * XXX would be nice to have more subtlety here. + * - Different penalties + * a) authentication failures without success (e.g. brute force) + * b) login grace exceeded (penalise DoS) + * c) monitor crash (penalise exploit attempt) + * d) unpriv preauth crash (penalise exploit attempt) + * - Unpriv auth exit status/WIFSIGNALLED is not available because + * the "mm_request_receive: monitor fd closed" fatal kills the + * monitor before waitpid() can occur. It would be good to use the + * unpriv exit status to detect crashes. + * + * For now, just penalise (a), (b) and (c), since that is what we have + * readily available. The authentication failures detection cannot + * discern between failed authentication and other connection problems + * until we have the unpriv exist status plumbed through (and the unpriv + * child modified to use a different exit status when auth has been + * attempted), but it's a start. + */ + if (child->have_addr) + srclimit_penalise(&child->addr, penalty_type); + + child->pid = -1; + child->have_status = 0; + if (child->pipefd == -1) + child_finish(child); +} + +/* Reap all children that have exited; called after SIGCHLD */ +static void +child_reap_all_exited(void) +{ + int i; + + if (children == NULL) + return; + for (i = 0; i < options.max_startups; i++) { + if (!children[i].have_status) + continue; + child_reap(&(children[i])); + } +} + static void close_startup_pipes(void) { int i; - if (startup_pipes) - for (i = 0; i < options.max_startups; i++) - if (startup_pipes[i] != -1) - close(startup_pipes[i]); + if (children == NULL) + return; + for (i = 0; i < options.max_startups; i++) { + if (children[i].pipefd != -1) + child_close(&(children[i]), 1, 1); + } +} + +/* Called after SIGINFO */ +static void +show_info(void) +{ + int i; + + /* XXX print listening sockets here too */ + if (children == NULL) + return; + logit("%d active startups", children_active); + for (i = 0; i < options.max_startups; i++) { + if (children[i].pipefd == -1 && children[i].pid <= 0) + continue; + logit("child %d: fd=%d pid=%ld %s%s", i, children[i].pipefd, + (long)children[i].pid, children[i].id, + children[i].early ? " (early)" : ""); + } + srclimit_penalty_info(); } /* @@ -244,6 +507,14 @@ sigterm_handler(int sig) received_sigterm = sig; } +#ifdef SIGINFO +static void +siginfo_handler(int sig) +{ + received_siginfo = 1; +} +#endif + /* * SIGCHLD handler. This is called whenever a child dies. This will then * reap any zombies left by exited children. @@ -255,9 +526,17 @@ main_sigchld_handler(int sig) pid_t pid; int status; - while ((pid = waitpid(-1, &status, WNOHANG)) > 0 || - (pid == -1 && errno == EINTR)) - ; + for (;;) { + if ((pid = waitpid(-1, &status, WNOHANG)) == 0) + break; + else if (pid == -1) { + if (errno == EINTR) + continue; + break; + } + child_exit(pid, status); + received_sigchld = 1; + } errno = save_errno; } @@ -290,7 +569,7 @@ should_drop_connection(int startups) } /* - * Check whether connection should be accepted by MaxStartups. + * Check whether connection should be accepted by MaxStartups or for penalty. * Returns 0 if the connection is accepted. If the connection is refused, * returns 1 and attempts to send notification to client. * Logs when the MaxStartups condition is entered or exited, and periodically @@ -300,12 +579,17 @@ static int drop_connection(int sock, int startups, int notify_pipe) { char *laddr, *raddr; - const char msg[] = "Exceeded MaxStartups\r\n"; + const char *reason = NULL, msg[] = "Not allowed at this time\r\n"; static time_t last_drop, first_drop; static u_int ndropped; LogLevel drop_level = SYSLOG_LEVEL_VERBOSE; time_t now; + if (!srclimit_penalty_check_allow(sock, &reason)) { + drop_level = SYSLOG_LEVEL_INFO; + goto handle; + } + now = monotime(); if (!should_drop_connection(startups) && srclimit_check_allow(sock, notify_pipe) == 1) { @@ -335,12 +619,16 @@ drop_connection(int sock, int startups, int notify_pipe) } last_drop = now; ndropped++; + reason = "past Maxstartups"; + handle: laddr = get_local_ipaddr(sock); raddr = get_peer_ipaddr(sock); - do_log2(drop_level, "drop connection #%d from [%s]:%d on [%s]:%d " - "past MaxStartups", startups, raddr, get_peer_port(sock), - laddr, get_local_port(sock)); + do_log2(drop_level, "drop connection #%d from [%s]:%d on [%s]:%d %s", + startups, + raddr, get_peer_port(sock), + laddr, get_local_port(sock), + reason); free(laddr); free(raddr); /* best-effort notification to client */ @@ -547,8 +835,12 @@ server_listen(void) u_int i; /* Initialise per-source limit tracking. */ - srclimit_init(options.max_startups, options.per_source_max_startups, - options.per_source_masklen_ipv4, options.per_source_masklen_ipv6); + srclimit_init(options.max_startups, + options.per_source_max_startups, + options.per_source_masklen_ipv4, + options.per_source_masklen_ipv6, + &options.per_source_penalty, + options.per_source_penalty_exempt); for (i = 0; i < options.num_listen_addrs; i++) { listen_on_addrs(&options.listen_addrs[i]); @@ -574,32 +866,32 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, int log_stderr) { struct pollfd *pfd = NULL; - int i, j, ret, npfd; - int ostartups = -1, startups = 0, listening = 0, lameduck = 0; + int i, ret, npfd; + int oactive = -1, listening = 0, lameduck = 0; int startup_p[2] = { -1 , -1 }, *startup_pollfd; char c = 0; struct sockaddr_storage from; + struct early_child *child; socklen_t fromlen; - pid_t pid; u_char rnd[256]; sigset_t nsigset, osigset; /* pipes connected to unauthenticated child sshd processes */ - startup_pipes = xcalloc(options.max_startups, sizeof(int)); - startup_flags = xcalloc(options.max_startups, sizeof(int)); + child_alloc(); startup_pollfd = xcalloc(options.max_startups, sizeof(int)); - for (i = 0; i < options.max_startups; i++) - startup_pipes[i] = -1; /* * Prepare signal mask that we use to block signals that might set - * received_sigterm or received_sighup, so that we are guaranteed + * received_sigterm/hup/chld/info, so that we are guaranteed * to immediately wake up the ppoll if a signal is received after * the flag is checked. */ sigemptyset(&nsigset); sigaddset(&nsigset, SIGHUP); sigaddset(&nsigset, SIGCHLD); +#ifdef SIGINFO + sigaddset(&nsigset, SIGINFO); +#endif sigaddset(&nsigset, SIGTERM); sigaddset(&nsigset, SIGQUIT); @@ -621,11 +913,19 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, unlink(options.pid_file); exit(received_sigterm == SIGTERM ? 0 : 255); } - if (ostartups != startups) { + if (received_sigchld) { + child_reap_all_exited(); + received_sigchld = 0; + } + if (received_siginfo) { + show_info(); + received_siginfo = 0; + } + if (oactive != children_active) { setproctitle("%s [listener] %d of %d-%d startups", - listener_proctitle, startups, + listener_proctitle, children_active, options.max_startups_begin, options.max_startups); - ostartups = startups; + oactive = children_active; } if (received_sighup) { if (!lameduck) { @@ -646,8 +946,8 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, npfd = num_listen_socks; for (i = 0; i < options.max_startups; i++) { startup_pollfd[i] = -1; - if (startup_pipes[i] != -1) { - pfd[npfd].fd = startup_pipes[i]; + if (children[i].pipefd != -1) { + pfd[npfd].fd = children[i].pipefd; pfd[npfd].events = POLLIN; startup_pollfd[i] = npfd++; } @@ -665,34 +965,46 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, continue; for (i = 0; i < options.max_startups; i++) { - if (startup_pipes[i] == -1 || + if (children[i].pipefd == -1 || startup_pollfd[i] == -1 || !(pfd[startup_pollfd[i]].revents & (POLLIN|POLLHUP))) continue; - switch (read(startup_pipes[i], &c, sizeof(c))) { + switch (read(children[i].pipefd, &c, sizeof(c))) { case -1: if (errno == EINTR || errno == EAGAIN) continue; if (errno != EPIPE) { error_f("startup pipe %d (fd=%d): " - "read %s", i, startup_pipes[i], + "read %s", i, children[i].pipefd, strerror(errno)); } /* FALLTHROUGH */ case 0: - /* child exited or completed auth */ - close(startup_pipes[i]); - srclimit_done(startup_pipes[i]); - startup_pipes[i] = -1; - startups--; - if (startup_flags[i]) + /* child exited preauth */ + if (children[i].early) listening--; + srclimit_done(children[i].pipefd); + child_close(&(children[i]), 0, 0); break; case 1: - /* child has finished preliminaries */ - if (startup_flags[i]) { + if (children[i].early && c == '\0') { + /* child has finished preliminaries */ listening--; - startup_flags[i] = 0; + children[i].early = 0; + debug2_f("child %lu for %s received " + "config", (long)children[i].pid, + children[i].id); + } else if (!children[i].early && c == '\001') { + /* child has completed auth */ + debug2_f("child %lu for %s auth done", + (long)children[i].pid, + children[i].id); + child_close(&(children[i]), 1, 0); + } else { + error_f("unexpected message 0x%02x " + "child %ld for %s in state %d", + (int)c, (long)children[i].pid, + children[i].id, children[i].early); } break; } @@ -721,7 +1033,8 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, close(*newsock); continue; } - if (drop_connection(*newsock, startups, startup_p[0])) { + if (drop_connection(*newsock, + children_active, startup_p[0])) { close(*newsock); close(startup_p[0]); close(startup_p[1]); @@ -738,14 +1051,6 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, continue; } - for (j = 0; j < options.max_startups; j++) - if (startup_pipes[j] == -1) { - startup_pipes[j] = startup_p[0]; - startups++; - startup_flags[j] = 1; - break; - } - /* * Got connection. Fork a child to handle it, unless * we are in debugging mode. @@ -763,7 +1068,6 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, close(startup_p[0]); close(startup_p[1]); startup_pipe = -1; - pid = getpid(); send_rexec_state(config_s[0], cfg); close(config_s[0]); free(pfd); @@ -777,7 +1081,8 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, */ platform_pre_fork(); listening++; - if ((pid = fork()) == 0) { + child = child_register(startup_p[0], *newsock); + if ((child->pid = fork()) == 0) { /* * Child. Close the listening and * max_startup sockets. Start using @@ -802,11 +1107,11 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, } /* Parent. Stay in the loop. */ - platform_post_fork_parent(pid); - if (pid == -1) + platform_post_fork_parent(child->pid); + if (child->pid == -1) error("fork: %.100s", strerror(errno)); else - debug("Forked child %ld.", (long)pid); + debug("Forked child %ld.", (long)child->pid); close(startup_p[1]); @@ -1428,6 +1733,9 @@ main(int ac, char **av) ssh_signal(SIGCHLD, main_sigchld_handler); ssh_signal(SIGTERM, sigterm_handler); ssh_signal(SIGQUIT, sigterm_handler); +#ifdef SIGINFO + ssh_signal(SIGINFO, siginfo_handler); +#endif platform_post_listen(); |