diff --git a/config.h.in b/config.h.in index 20a7f2e..94c914e 100644 --- a/config.h.in +++ b/config.h.in @@ -93,6 +93,9 @@ /* Define to 1 if you have the `explicit_bzero' function. */ #undef HAVE_EXPLICIT_BZERO +/* Define to 1 if you have the `fexecve' function. */ +#undef HAVE_FEXECVE + /* Define to 1 if you have the `fork' function. */ #undef HAVE_FORK @@ -318,6 +321,9 @@ /* Define to 1 if `ut_type' is a member of `struct utmp'. */ #undef HAVE_STRUCT_UTMP_UT_TYPE +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_PRCTL_H + /* Define to 1 if you have the header file. */ #undef HAVE_SYS_RANDOM_H diff --git a/configure b/configure index ef7b171..8374714 100755 --- a/configure +++ b/configure @@ -5608,7 +5608,7 @@ for ac_header in netinet/in.h netinet/tcp.h \ pty.h libutil.h libgen.h inttypes.h stropts.h utmp.h \ utmpx.h lastlog.h paths.h util.h netdb.h security/pam_appl.h \ pam/pam_appl.h netinet/in_systm.h sys/uio.h linux/pkt_sched.h \ - sys/random.h + sys/random.h sys/prctl.h do : as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` ac_fn_c_check_header_mongrel "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" @@ -7352,7 +7352,7 @@ _ACEOF fi done -for ac_func in freeaddrinfo getnameinfo fork writev getgrouplist +for ac_func in freeaddrinfo getnameinfo fork writev getgrouplist fexecve do : as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh` ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var" diff --git a/configure.ac b/configure.ac index 743ff9e..6a19479 100644 --- a/configure.ac +++ b/configure.ac @@ -386,7 +386,7 @@ AC_CHECK_HEADERS([netinet/in.h netinet/tcp.h \ pty.h libutil.h libgen.h inttypes.h stropts.h utmp.h \ utmpx.h lastlog.h paths.h util.h netdb.h security/pam_appl.h \ pam/pam_appl.h netinet/in_systm.h sys/uio.h linux/pkt_sched.h \ - sys/random.h]) + sys/random.h sys/prctl.h]) # Checks for typedefs, structures, and compiler characteristics. AC_C_CONST @@ -841,7 +841,7 @@ AC_FUNC_MEMCMP AC_FUNC_SELECT_ARGTYPES AC_CHECK_FUNCS([getpass getspnam getusershell putenv]) AC_CHECK_FUNCS([clearenv strlcpy strlcat daemon basename _getpty getaddrinfo ]) -AC_CHECK_FUNCS([freeaddrinfo getnameinfo fork writev getgrouplist]) +AC_CHECK_FUNCS([freeaddrinfo getnameinfo fork writev getgrouplist fexecve]) AC_SEARCH_LIBS(basename, gen, AC_DEFINE(HAVE_BASENAME)) diff --git a/default_options.h b/default_options.h index 62f2a39..aa9df78 100644 --- a/default_options.h +++ b/default_options.h @@ -37,7 +37,14 @@ IMPORTANT: Some options will require "make clean" after changes */ #define NON_INETD_MODE 1 #define INETD_MODE 1 -/* Include verbose debug output, enabled with -v at runtime. +/* By default Dropbear will re-execute itself for each incoming connection so + that memory layout may be re-randomised (ASLR) - exploiting + vulnerabilities becomes harder. Re-exec causes slightly more memory use + per connection. + This option is ignored on non-Linux platforms at present */ +#define DROPBEAR_REEXEC 1 + +/* Include verbose debug output, enabled with -v at runtime. * This will add a reasonable amount to your executable size. */ #define DEBUG_TRACE 0 diff --git a/includes.h b/includes.h index df1df2d..1e00002 100644 --- a/includes.h +++ b/includes.h @@ -127,6 +127,10 @@ #include #endif +#ifdef HAVE_SYS_PRCTL_H +#include +#endif + #ifdef BUNDLED_LIBTOM #include "libtomcrypt/src/headers/tomcrypt.h" #include "libtommath/tommath.h" @@ -171,6 +175,8 @@ typedef u_int32_t uint32_t; #include #endif +extern char** environ; + #include "fake-rfc2553.h" #include "fuzz.h" diff --git a/runopts.h b/runopts.h index 00fd930..5a59e6f 100644 --- a/runopts.h +++ b/runopts.h @@ -72,13 +72,15 @@ typedef struct svr_runopts { int forkbg; - /* ports and addresses are arrays of the portcount + /* ports and addresses are arrays of the portcount listening ports. strings are malloced. */ char *ports[DROPBEAR_MAX_PORTS]; unsigned int portcount; char *addresses[DROPBEAR_MAX_PORTS]; int inetdmode; + /* Hidden "-2" flag indicates it's re-executing itself */ + int reexec_child; /* Flags indicating whether to use ipv4 and ipv6 */ /* not used yet diff --git a/svr-chansession.c b/svr-chansession.c index e0e1d31..9ecda79 100644 --- a/svr-chansession.c +++ b/svr-chansession.c @@ -72,9 +72,6 @@ const struct ChanType svrchansess = { cleanupchansess /* cleanup */ }; -/* required to clear environment */ -extern char** environ; - /* Returns whether the channel is ready to close. The child process must not be running (has never started, or has exited) */ static int sesscheckclose(struct Channel *channel) { diff --git a/svr-main.c b/svr-main.c index 52ac9ff..f39f9fc 100644 --- a/svr-main.c +++ b/svr-main.c @@ -35,12 +35,8 @@ static size_t listensockets(int *sock, size_t sockcount, int *maxfd); static void sigchld_handler(int dummy); static void sigsegv_handler(int); static void sigintterm_handler(int fish); -#if INETD_MODE static void main_inetd(void); -#endif -#if NON_INETD_MODE -static void main_noinetd(void); -#endif +static void main_noinetd(int argc, char ** argv); static void commonsetup(void); #if defined(DBMULTI_dropbear) || !DROPBEAR_MULTI @@ -55,6 +51,10 @@ int main(int argc, char ** argv) disallow_core(); + if (argc < 1) { + dropbear_exit("Bad argc"); + } + /* get commandline options */ svr_getopts(argc, argv); @@ -66,8 +66,21 @@ int main(int argc, char ** argv) } #endif +#if DROPBEAR_DO_REEXEC + if (svr_opts.reexec_child) { +#ifdef PR_SET_NAME + /* Fix the "Name:" in /proc/pid/status, otherwise it's + a FD number from fexecve. + Failure doesn't really matter, it's mostly aesthetic */ + prctl(PR_SET_NAME, basename(argv[0]), 0, 0); +#endif + main_inetd(); + /* notreached */ + } +#endif + #if NON_INETD_MODE - main_noinetd(); + main_noinetd(argc, argv); /* notreached */ #endif @@ -76,7 +89,7 @@ int main(int argc, char ** argv) } #endif -#if INETD_MODE +#if INETD_MODE || DROPBEAR_DO_REEXEC static void main_inetd() { char *host, *port = NULL; @@ -85,23 +98,18 @@ static void main_inetd() { seedrandom(); -#if DEBUG_TRACE - if (debug_trace) { - /* -v output goes to stderr which would get sent over the inetd network socket */ - dropbear_exit("Dropbear inetd mode is incompatible with debug -v"); + if (!svr_opts.reexec_child) { + /* In case our inetd was lax in logging source addresses */ + get_socket_address(0, NULL, NULL, &host, &port, 0); + dropbear_log(LOG_INFO, "Child connection from %s:%s", host, port); + m_free(host); + m_free(port); + + /* Don't check the return value - it may just fail since inetd has + * already done setsid() after forking (xinetd on Darwin appears to do + * this */ + setsid(); } -#endif - - /* In case our inetd was lax in logging source addresses */ - get_socket_address(0, NULL, NULL, &host, &port, 0); - dropbear_log(LOG_INFO, "Child connection from %s:%s", host, port); - m_free(host); - m_free(port); - - /* Don't check the return value - it may just fail since inetd has - * already done setsid() after forking (xinetd on Darwin appears to do - * this */ - setsid(); /* Start service program * -1 is a dummy childpipe, just something we can close() without @@ -113,7 +121,7 @@ static void main_inetd() { #endif /* INETD_MODE */ #if NON_INETD_MODE -static void main_noinetd() { +static void main_noinetd(int argc, char ** argv) { fd_set fds; unsigned int i, j; int val; @@ -121,6 +129,7 @@ static void main_noinetd() { int listensocks[MAX_LISTEN_ADDR]; size_t listensockcount = 0; FILE *pidfile = NULL; + int execfd = -1; int childpipes[MAX_UNAUTH_CLIENTS]; char * preauth_addrs[MAX_UNAUTH_CLIENTS]; @@ -128,6 +137,9 @@ static void main_noinetd() { int childsock; int childpipe[2]; + (void)argc; + (void)argv; + /* Note: commonsetup() must happen before we daemon()ise. Otherwise daemon() will chdir("/"), and we won't be able to find local-dir hostkeys. */ @@ -138,7 +150,7 @@ static void main_noinetd() { childpipes[i] = -1; } memset(preauth_addrs, 0x0, sizeof(preauth_addrs)); - + /* Set up the listening sockets */ listensockcount = listensockets(listensocks, MAX_LISTEN_ADDR, &maxsock); if (listensockcount == 0) @@ -150,6 +162,14 @@ static void main_noinetd() { FD_SET(listensocks[i], &fds); } +#if DROPBEAR_DO_REEXEC + execfd = open(argv[0], O_CLOEXEC|O_RDONLY); + if (execfd < 0) { + /* Just fallback to straight fork */ + TRACE(("Couldn't open own binary %s, disabling re-exec: %s", argv[0], strerror(errno))) + } +#endif + /* fork */ if (svr_opts.forkbg) { int closefds = 0; @@ -181,7 +201,7 @@ static void main_noinetd() { for(;;) { DROPBEAR_FD_ZERO(&fds); - + /* listening sockets */ for (i = 0; i < listensockcount; i++) { FD_SET(listensocks[i], &fds); @@ -201,7 +221,7 @@ static void main_noinetd() { unlink(svr_opts.pidfile); dropbear_exit("Terminated by signal"); } - + if (val == 0) { /* timeout reached - shouldn't happen. eh */ continue; @@ -286,7 +306,7 @@ static void main_noinetd() { } addrandom((void*)&fork_ret, sizeof(fork_ret)); - + if (fork_ret > 0) { /* parent */ @@ -316,6 +336,27 @@ static void main_noinetd() { m_close(childpipe[0]); + if (execfd >= 0) { +#if DROPBEAR_DO_REEXEC + /* Add "-2" to the args and re-execute ourself */ + char **new_argv = m_malloc(sizeof(char*) * (argc+1)); + memcpy(new_argv, argv, sizeof(char*) * argc); + new_argv[argc] = "-2"; + + if ((dup2(childsock, STDIN_FILENO) < 0)) { + dropbear_exit("dup2 failed: %s", strerror(errno)); + } + m_close(childsock); + /* Re-execute ourself */ + fexecve(execfd, new_argv, environ); + /* Not reached on success */ + + /* Fall back on plain fork otherwise */ + TRACE(("fexecve failed, disabling re-exec: %s", strerror(errno))) + m_free(new_argv); +#endif /* DROPBEAR_DO_REEXEC */ + } + /* start the session */ svr_session(childsock, childpipe[1]); /* don't return */ diff --git a/svr-runopts.c b/svr-runopts.c index 02ec2d4..ada2e08 100644 --- a/svr-runopts.c +++ b/svr-runopts.c @@ -246,6 +246,12 @@ void svr_getopts(int argc, char ** argv) { case 'i': svr_opts.inetdmode = 1; break; +#endif +#if DROPBEAR_DO_REEXEC && NON_INETD_MODE + /* For internal use by re-exec */ + case '2': + svr_opts.reexec_child = 1; + break; #endif case 'p': nextisport = 1; @@ -419,6 +425,19 @@ void svr_getopts(int argc, char ** argv) { if (svr_opts.forced_command) { dropbear_log(LOG_INFO, "Forced command set to '%s'", svr_opts.forced_command); } + +#if INETD_MODE + if (svr_opts.inetdmode && ( + opts.usingsyslog == 0 +#if DEBUG_TRACE + || debug_trace +#endif + )) { + /* log output goes to stderr which would get sent over the inetd network socket */ + dropbear_exit("Dropbear inetd mode is incompatible with debug -v or non-syslog"); + } +#endif + #if DROPBEAR_PLUGIN if (pubkey_plugin) { char *args = strchr(pubkey_plugin, ','); diff --git a/sysoptions.h b/sysoptions.h index 10c485b..8bd3f3f 100644 --- a/sysoptions.h +++ b/sysoptions.h @@ -29,6 +29,9 @@ #error "NON_INETD_MODE or INETD_MODE (or both) must be enabled." #endif +/* Would probably work on freebsd but hasn't been tested */ +#define DROPBEAR_DO_REEXEC (defined(HAVE_FEXECVE) && DROPBEAR_REEXEC && defined(__linux__)) + /* A client should try and send an initial key exchange packet guessing * the algorithm that will match - saves a round trip connecting, has little * overhead if the guess was "wrong". */ diff --git a/test/parent_dropbear_map.py b/test/parent_dropbear_map.py new file mode 100755 index 0000000..34a8b9f --- /dev/null +++ b/test/parent_dropbear_map.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +import os +import sys +import time +import psutil + +from pathlib import Path + + +want_name = "dropbear" +# Walks up the parent process tree, prints the first line of /proc/pid/maps when +# it finds the wanted name + +def main(): + + try: + for p in psutil.Process().parents(): + print(p.pid, file=sys.stderr) + print(p.name(), file=sys.stderr) + print(p.cmdline(), file=sys.stderr) + + if want_name in p.name(): + with (Path('/proc') / str(p.pid) / "maps").open() as f: + map0 = f.readline().rstrip() + print(map0) + return + + raise RuntimeError(f"Couldn't find parent {want_name} process") + except Exception as e: + print(psutil.Process().parents()) + for p in psutil.Process().parents(): + print(p.name()) + print(e) + # time.sleep(100) + raise + +if __name__ == "__main__": + main() diff --git a/test/requirements.txt b/test/requirements.txt index 36f6f91..50e8214 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -6,3 +6,4 @@ py==1.10.0 pyparsing==2.4.7 pytest==6.2.5 toml==0.10.2 +psutil==5.9.0 diff --git a/test/test_aslr.py b/test/test_aslr.py new file mode 100644 index 0000000..6f997b8 --- /dev/null +++ b/test/test_aslr.py @@ -0,0 +1,34 @@ +from pathlib import Path +import sys + +from test_dropbear import * + +def test_reexec(request, dropbear): + """ + Tests that two consecutive connections have different address layouts. + This indicates that re-exec makes ASLR work + """ + cmd = (Path(request.node.fspath).parent / "parent_dropbear_map.py").resolve() + r = dbclient(request, cmd, capture_output=True, text=True) + map1 = r.stdout.rstrip() + print(r.stderr, file=sys.stderr) + r.check_returncode() + + r = dbclient(request, cmd, capture_output=True, text=True) + map2 = r.stdout.rstrip() + print(r.stderr, file=sys.stderr) + r.check_returncode() + + print(map1) + print(map2) + # expect something like + # "563174d59000-563174d5d000 r--p 00000000 00:29 4242372 /home/matt/src/dropbear/build/dropbear" + assert map1.endswith('/dropbear') + assert ' r--p ' in map1 + a1 = map1.split()[0] + a2 = map2.split()[0] + print(a1) + print(a2) + # relocation addresses should differ + assert a1 != a2 +