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.
This commit is contained in:
Matt Johnston 2022-01-30 10:14:56 +08:00
parent ebb4018889
commit c7b7c9a99d
13 changed files with 192 additions and 37 deletions

View File

@ -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

4
configure vendored
View File

@ -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"

View File

@ -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))

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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) {

View File

@ -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 */

View File

@ -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, ',');

View File

@ -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". */

39
test/parent_dropbear_map.py Executable file
View File

@ -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()

View File

@ -6,3 +6,4 @@ py==1.10.0
pyparsing==2.4.7
pytest==6.2.5
toml==0.10.2
psutil==5.9.0

34
test/test_aslr.py Normal file
View File

@ -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