summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config.h.in6
-rwxr-xr-xconfigure4
-rw-r--r--configure.ac4
-rw-r--r--default_options.h9
-rw-r--r--includes.h6
-rw-r--r--runopts.h4
-rw-r--r--svr-chansession.c3
-rw-r--r--svr-main.c97
-rw-r--r--svr-runopts.c19
-rw-r--r--sysoptions.h3
-rwxr-xr-xtest/parent_dropbear_map.py39
-rw-r--r--test/requirements.txt1
-rw-r--r--test/test_aslr.py34
13 files changed, 192 insertions, 37 deletions
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 <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
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 <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 @@ typedef u_int32_t uint32_t;
#include <dlfcn.h>
#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
@@ -247,6 +247,12 @@ void svr_getopts(int argc, char ** argv) {
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 @@ 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
+