changeset 1861:2b3a8026a6ce

Add re-exec for server This allows ASLR to re-randomize the address space for every connection, preventing some vulnerabilities from being exploitable by repeated probing. Overhead (memory and time) is yet to be confirmed. At present this is only enabled on Linux. Other BSD platforms with fexecve() would probably also work though have not been tested.
author Matt Johnston <matt@ucc.asn.au>
date Sun, 30 Jan 2022 10:14:56 +0800
parents 5001e9c5641f
children 6f265a35159a
files config.h.in configure configure.ac default_options.h includes.h runopts.h svr-chansession.c svr-main.c svr-runopts.c sysoptions.h test/parent_dropbear_map.py test/requirements.txt test/test_aslr.py
diffstat 13 files changed, 192 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- a/config.h.in	Thu Jan 27 15:09:29 2022 +0800
+++ b/config.h.in	Sun Jan 30 10:14:56 2022 +0800
@@ -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 <sys/prctl.h> header file. */
+#undef HAVE_SYS_PRCTL_H
+
 /* Define to 1 if you have the <sys/random.h> header file. */
 #undef HAVE_SYS_RANDOM_H
 
--- a/configure	Thu Jan 27 15:09:29 2022 +0800
+++ b/configure	Sun Jan 30 10:14:56 2022 +0800
@@ -5608,7 +5608,7 @@
 	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 @@
 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"
--- a/configure.ac	Thu Jan 27 15:09:29 2022 +0800
+++ b/configure.ac	Sun Jan 30 10:14:56 2022 +0800
@@ -386,7 +386,7 @@
 	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_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))
 
--- a/default_options.h	Thu Jan 27 15:09:29 2022 +0800
+++ b/default_options.h	Sun Jan 30 10:14:56 2022 +0800
@@ -37,7 +37,14 @@
 #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
 
--- a/includes.h	Thu Jan 27 15:09:29 2022 +0800
+++ b/includes.h	Sun Jan 30 10:14:56 2022 +0800
@@ -127,6 +127,10 @@
 #include <sys/random.h>
 #endif
 
+#ifdef HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
+
 #ifdef BUNDLED_LIBTOM
 #include "libtomcrypt/src/headers/tomcrypt.h"
 #include "libtommath/tommath.h"
@@ -171,6 +175,8 @@
 #include <dlfcn.h>
 #endif
 
+extern char** environ;
+
 #include "fake-rfc2553.h"
 
 #include "fuzz.h"
--- a/runopts.h	Thu Jan 27 15:09:29 2022 +0800
+++ b/runopts.h	Sun Jan 30 10:14:56 2022 +0800
@@ -72,13 +72,15 @@
 
 	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
--- a/svr-chansession.c	Thu Jan 27 15:09:29 2022 +0800
+++ b/svr-chansession.c	Sun Jan 30 10:14:56 2022 +0800
@@ -72,9 +72,6 @@
 	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) {
--- a/svr-main.c	Thu Jan 27 15:09:29 2022 +0800
+++ b/svr-main.c	Sun Jan 30 10:14:56 2022 +0800
@@ -35,12 +35,8 @@
 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 @@
 
 	disallow_core();
 
+	if (argc < 1) {
+		dropbear_exit("Bad argc");
+	}
+
 	/* get commandline options */
 	svr_getopts(argc, argv);
 
@@ -66,8 +66,21 @@
 	}
 #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 @@
 }
 #endif
 
-#if INETD_MODE
+#if INETD_MODE || DROPBEAR_DO_REEXEC
 static void main_inetd() {
 	char *host, *port = NULL;
 
@@ -85,23 +98,18 @@
 
 	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");
-	}
-#endif
+	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);
 
-	/* 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();
+		/* 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 @@
 #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 @@
 	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 @@
 	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 @@
 		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 @@
 		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 @@
 	for(;;) {
 
 		DROPBEAR_FD_ZERO(&fds);
-		
+
 		/* listening sockets */
 		for (i = 0; i < listensockcount; i++) {
 			FD_SET(listensocks[i], &fds);
@@ -201,7 +221,7 @@
 			unlink(svr_opts.pidfile);
 			dropbear_exit("Terminated by signal");
 		}
-		
+
 		if (val == 0) {
 			/* timeout reached - shouldn't happen. eh */
 			continue;
@@ -286,7 +306,7 @@
 			}
 
 			addrandom((void*)&fork_ret, sizeof(fork_ret));
-			
+
 			if (fork_ret > 0) {
 
 				/* parent */
@@ -316,6 +336,27 @@
 
 				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 */
--- a/svr-runopts.c	Thu Jan 27 15:09:29 2022 +0800
+++ b/svr-runopts.c	Sun Jan 30 10:14:56 2022 +0800
@@ -247,6 +247,12 @@
 					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;
 				  break;
@@ -419,6 +425,19 @@
 	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, ',');
--- a/sysoptions.h	Thu Jan 27 15:09:29 2022 +0800
+++ b/sysoptions.h	Sun Jan 30 10:14:56 2022 +0800
@@ -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". */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/parent_dropbear_map.py	Sun Jan 30 10:14:56 2022 +0800
@@ -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()
--- a/test/requirements.txt	Thu Jan 27 15:09:29 2022 +0800
+++ b/test/requirements.txt	Sun Jan 30 10:14:56 2022 +0800
@@ -6,3 +6,4 @@
 pyparsing==2.4.7
 pytest==6.2.5
 toml==0.10.2
+psutil==5.9.0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/test_aslr.py	Sun Jan 30 10:14:56 2022 +0800
@@ -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
+