diff --git a/MANIFEST b/MANIFEST index 57f8f82ae..9d0705a89 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1129,6 +1129,8 @@ src/sudo.h src/sudo_edit.c src/sudo_edit.h src/sudo_exec.h +src/sudo_intercept.c +src/sudo_intercept_common.c src/sudo_noexec.c src/sudo_plugin_int.h src/sudo_usage.h.in diff --git a/src/Makefile.in b/src/Makefile.in index 8594aaf0e..02cf51f87 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -102,6 +102,8 @@ libexecdir = @libexecdir@ datarootdir = @datarootdir@ localedir = @localedir@ localstatedir = @localstatedir@ +interceptfile = @INTERCEPTFILE@ +interceptdir = @INTERCEPTDIR@ noexecfile = @NOEXECFILE@ noexecdir = @NOEXECDIR@ tmpfiles_d = @TMPFILES_D@ @@ -199,6 +201,9 @@ $(devdir)/intercept.pb-c.c: $(srcdir)/intercept.proto sudo: $(OBJS) $(LT_LIBS) @STATIC_SUDOERS@ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(OBJS) $(SUDO_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(LIBS) @STATIC_SUDOERS@ +sudo_intercept.la: sudo_intercept.lo sudo_intercept_common.lo intercept.pb-c.lo + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) $(LDFLAGS) $(LT_LDFLAGS) $(SSP_LDFLAGS) $(LT_LIBS) @LIBDL@ -o $@ sudo_intercept.lo sudo_intercept_common.lo intercept.pb-c.lo $(PRELOAD_MODULE) -avoid-version -rpath $(interceptdir) -shrext .so + sudo_noexec.la: sudo_noexec.lo $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) $(LDFLAGS) $(LT_LDFLAGS) $(SSP_LDFLAGS) @LIBDL@ -o $@ sudo_noexec.lo $(PRELOAD_MODULE) -avoid-version -rpath $(noexecdir) -shrext .so @@ -216,12 +221,13 @@ check_ttyname: $(CHECK_TTYNAME_OBJS) $(top_builddir)/lib/util/libsudo_util.la pre-install: -install: install-binaries install-rc @INSTALL_NOEXEC@ +install: install-binaries install-rc @INSTALL_INTERCEPT@ @INSTALL_NOEXEC@ install-dirs: # We only create the rc.d dir when installing to the actual system dir $(SHELL) $(scriptdir)/mkinstalldirs $(DESTDIR)$(bindir) \ - $(DESTDIR)$(libexecdir)/sudo $(DESTDIR)$(noexecdir) + $(DESTDIR)$(libexecdir)/sudo $(DESTDIR)$(noexecdir) \ + $(DESTDIR)$(interceptdir) if test -n "$(INIT_SCRIPT)"; then \ $(SHELL) $(scriptdir)/mkinstalldirs $(DESTDIR)$(INIT_DIR); \ if test -z "$(DESTDIR)"; then \ @@ -256,6 +262,9 @@ install-doc: install-includes: +install-intercept: install-dirs sudo_intercept.la + INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m $(shlib_mode) sudo_intercept.la $(DESTDIR)$(interceptdir) + install-noexec: install-dirs sudo_noexec.la INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m $(shlib_mode) sudo_noexec.la $(DESTDIR)$(noexecdir) @@ -265,7 +274,8 @@ install-fuzzer: uninstall: -$(LIBTOOL) $(LTFLAGS) --mode=uninstall \ - rm -f $(DESTDIR)$(noexecdir)/sudo_noexec.la + rm -f $(DESTDIR)$(interceptdir)/sudo_intercept.la \ + $(DESTDIR)$(noexecdir)/sudo_noexec.la -rm -f $(DESTDIR)$(bindir)/sudo \ $(DESTDIR)$(bindir)/sudoedit \ $(DESTDIR)$(libexecdir)/sudo/sesh \ @@ -273,6 +283,7 @@ uninstall: -test -z "$(INSTALL_BACKUP)" || \ rm -f $(DESTDIR)$(bindir)/sudo$(INSTALL_BACKUP) \ $(DESTDIR)$(libexecdir)/sudo/sesh$(INSTALL_BACKUP) \ + $(DESTDIR)$(interceptdir)/sudo_intercept.so$(INSTALL_BACKUP) \ $(DESTDIR)$(noexecdir)/sudo_noexec.so$(INSTALL_BACKUP) -test -z "$(INIT_SCRIPT)" || \ rm -f $(DESTDIR)$(RC_LINK) $(DESTDIR)$(INIT_DIR)/sudo @@ -333,6 +344,20 @@ cleandir: realclean .PHONY: clean mostlyclean distclean cleandir clobber realclean # *Not* auto-generated to avoid building with ASAN +sudo_intercept.lo: $(srcdir)/sudo_intercept.c $(incdir)/sudo_compat.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/sudo_intercept.c + +sudo_intercept_common.lo: $(srcdir)/sudo_intercept_common.c \ + $(incdir)/sudo_compat.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_gettext.h $(incdir)/intercept.pb-c.h \ + $(srcdir)/sudo_exec.h $(top_builddir)/config.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/sudo_intercept_common.c + +intercept.pb-c.lo: $(srcdir)/intercept.pb-c.c $(incdir)/intercept.pb-c.h \ + $(incdir)/protobuf-c/protobuf-c.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/intercept.pb-c.c + sudo_noexec.lo: $(srcdir)/sudo_noexec.c $(incdir)/sudo_compat.h \ $(top_builddir)/config.h $(top_builddir)/pathnames.h $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/sudo_noexec.c diff --git a/src/exec.c b/src/exec.c index 74ea79d0a..9f5730702 100644 --- a/src/exec.c +++ b/src/exec.c @@ -480,7 +480,7 @@ struct intercept_closure { char *command; /* dynamically allocated */ char **run_argv; /* owned by plugin */ char **run_envp; /* dynamically allocated */ - char *buf; /* dynamically allocated */ + uint8_t *buf; /* dynamically allocated */ size_t len; int policy_result; }; @@ -491,16 +491,24 @@ struct intercept_closure { static void intercept_closure_reset(struct intercept_closure *closure) { + size_t n; debug_decl(intercept_closure_reset, SUDO_DEBUG_EXEC); - /* Other parts of closure are freed at policy close time. */ - /* TODO: can we cause them to be freed earlier? */ free(closure->buf); free(closure->command); - free(closure->run_envp); + if (closure->run_argv != NULL) { + for (n = 0; closure->run_argv[n] != NULL; n++) + free(closure->run_argv[n]); + free(closure->run_argv); + } + if (closure->run_envp != NULL) { + for (n = 0; closure->run_envp[n] != NULL; n++) + free(closure->run_envp[n]); + free(closure->run_envp); + } sudo_ev_del(NULL, &closure->ev); - /* Reset all but the event. */ + /* Reset all but the event (which we may reuse). */ closure->errstr = NULL; closure->command = NULL; closure->run_argv = NULL; @@ -534,7 +542,7 @@ intercept_check_policy(PolicyCheckRequest *req, { char **command_info = NULL; char **user_env_out = NULL; - char **vec; + char **argv, **run_argv = NULL; size_t n; int ok; debug_decl(intercept_check_policy, SUDO_DEBUG_EXEC); @@ -554,54 +562,83 @@ intercept_check_policy(PolicyCheckRequest *req, } /* Rebuild argv from PolicyCheckReq so it is NULL-terminated. */ - vec = reallocarray(NULL, req->n_argv + 1, sizeof(char *)); - if (vec == NULL) { + argv = reallocarray(NULL, req->n_argv + 1, sizeof(char *)); + if (argv == NULL) { *errstr = N_("unable to allocate memory"); goto error; } for (n = 0; n < req->n_argv; n++) { - vec[n] = req->argv[n]; + argv[n] = req->argv[n]; } - vec[n] = NULL; + argv[n] = NULL; /* We don't currently have a good way to validate the environment. */ /* TODO: make sure LD_PRELOAD is preserved in environment */ sudo_debug_set_active_instance(policy_plugin.debug_instance); - ok = policy_plugin.u.policy->check_policy(n, vec, NULL, - &command_info, &closure->run_argv, &user_env_out, errstr); + ok = policy_plugin.u.policy->check_policy(n, argv, NULL, + &command_info, &run_argv, &user_env_out, errstr); sudo_debug_set_active_instance(sudo_debug_instance); - free(vec); - - /* Extract command path from command_info[] */ - if (command_info != NULL) { - for (n = 0; command_info[n] != NULL; n++) { - const char *cp = command_info[n]; - if (strncmp(cp, "command=", sizeof("command=") - 1) == 0) { - closure->command = strdup(cp + sizeof("command=") - 1); - if (closure->command == NULL) { - *errstr = N_("unable to allocate memory"); - goto error; - } - break; - } - } - } + free(argv); switch (ok) { case 1: /* TODO: call approval plugin too */ - /* Rebuild envp from PolicyCheckReq so it is NULL-terminated. */ - vec = reallocarray(NULL, req->n_envp + 1, sizeof(char *)); - if (vec == NULL) { + /* Extract command path from command_info[] */ + if (command_info != NULL) { + for (n = 0; command_info[n] != NULL; n++) { + const char *cp = command_info[n]; + if (strncmp(cp, "command=", sizeof("command=") - 1) == 0) { + closure->command = strdup(cp + sizeof("command=") - 1); + if (closure->command == NULL) { + *errstr = N_("unable to allocate memory"); + goto error; + } + break; + } + } + } + + if (sudo_debug_needed(SUDO_DEBUG_INFO)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "run_command: %s", closure->command); + for (n = 0; run_argv[n] != NULL; n++) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "run_argv[%zu]: %s", n, run_argv[n]); + } + } + + /* run_argv strings may be part of PolicyCheckReq, make a copy. */ + for (n = 0; run_argv[n] != NULL; n++) + continue; + closure->run_argv = reallocarray(NULL, n + 1, sizeof(char *)); + if (closure->run_argv == NULL) { + *errstr = N_("unable to allocate memory"); + goto error; + } + for (n = 0; run_argv[n] != NULL; n++) { + closure->run_argv[n] = strdup(run_argv[n]); + if (closure->run_argv[n] == NULL) { + *errstr = N_("unable to allocate memory"); + goto error; + } + } + closure->run_argv[n] = NULL; + + /* envp strings are part of PolicyCheckReq, make a copy. */ + closure->run_envp = reallocarray(NULL, req->n_envp + 1, sizeof(char *)); + if (closure->run_envp == NULL) { *errstr = N_("unable to allocate memory"); goto error; } for (n = 0; n < req->n_envp; n++) { - vec[n] = req->envp[n]; + closure->run_envp[n] = strdup(req->envp[n]); + if (closure->run_envp[n] == NULL) { + *errstr = N_("unable to allocate memory"); + goto error; + } } - vec[n] = NULL; - closure->run_envp = vec; + closure->run_envp[n] = NULL; /* Audit the event twice: once for the plugin, once for sudo. */ audit_accept(policy_plugin.name, SUDO_POLICY_PLUGIN, command_info, @@ -625,18 +662,18 @@ intercept_check_policy(PolicyCheckRequest *req, } } -#define MESSAGE_SIZE_MAX (2 * 1024 * 1024) - /* * Read a single message from sudo_intercept.so. */ -static void +static bool intercept_read(int fd, struct intercept_closure *closure) { - InterceptMessage *msg; + struct sudo_event_base *base = sudo_ev_get_base(&closure->ev); + InterceptMessage *msg = NULL; + uint8_t *cp, *buf = NULL; uint32_t msg_len; ssize_t nread; - char *buf = NULL; + bool ret = false; debug_decl(intercept_read, SUDO_DEBUG_EXEC); /* Read message size (uint32_t in host byte order). */ @@ -644,46 +681,49 @@ intercept_read(int fd, struct intercept_closure *closure) if (nread != sizeof(msg_len)) { if (nread != 0) sudo_warn("read"); - goto bad; + goto done; } if (msg_len > MESSAGE_SIZE_MAX) { sudo_warnx(U_("client message too large: %zu"), (size_t)msg_len); - goto bad; + goto done; } if (msg_len > 0) { + size_t rem = msg_len; + if ((buf = malloc(msg_len)) == NULL) { sudo_warnx("%s", U_("unable to allocate memory")); - goto bad; + goto done; } + cp = buf; do { - size_t len = MIN(msg_len, sizeof(buf)); - nread = read(fd, buf, len); + nread = read(fd, cp, rem); switch (nread) { case 0: /* EOF, other side must have exited. */ - goto bad; + goto done; case -1: sudo_warn("read"); - goto bad; + goto done; default: - msg_len -= nread; + rem -= nread; + cp += nread; break; } - } while (msg_len > 0); + } while (rem > 0); } msg = intercept_message__unpack(NULL, msg_len, buf); if (msg == NULL) { sudo_warnx("unable to unpack %s size %zu", "InterceptMessage", (size_t)msg_len); - goto bad; + goto done; } if (msg->type_case != INTERCEPT_MESSAGE__TYPE_POLICY_CHECK_REQ) { sudo_warnx(U_("unexpected type_case value %d in %s from %s"), msg->type_case, "InterceptMessage", "sudo_intercept.so"); - goto bad; + goto done; } closure->policy_result = intercept_check_policy(msg->u.policy_check_req, @@ -693,17 +733,20 @@ intercept_read(int fd, struct intercept_closure *closure) if (sudo_ev_set(&closure->ev, fd, SUDO_EV_WRITE, intercept_cb, closure) == -1) { /* This cannot (currently) fail. */ sudo_warn("%s", U_("unable to add event to queue")); - goto bad; + goto done; + } + if (sudo_ev_add(base, &closure->ev, NULL, false) == -1) { + sudo_warn("%s", U_("unable to add event to queue")); + goto done; } - goto done; - -bad: - intercept_close(fd, closure); + ret = true; done: + // XXX + //intercept_message__free_unpacked(msg, NULL); free(buf); - debug_return; + debug_return_bool(ret); } static bool @@ -796,72 +839,71 @@ fmt_error_message(struct intercept_closure *closure) /* * Write a response to sudo_intercept.so. */ -static void +static bool intercept_write(int fd, struct intercept_closure *closure) { - size_t len = closure->len; - char *buf = closure->buf; + size_t rem; + uint8_t *cp; ssize_t nwritten; + bool ret = false; debug_decl(intercept_write, SUDO_DEBUG_EXEC); switch (closure->policy_result) { case 1: if (!fmt_accept_message(closure)) - goto bad; + goto done; break; case 0: if (!fmt_reject_message(closure)) - goto bad; + goto done; break; default: if (!fmt_error_message(closure)) - goto bad; + goto done; break; } + cp = closure->buf; + rem = closure->len; do { - nwritten = write(fd, buf, len); + nwritten = write(fd, cp, rem); if (nwritten == -1) { sudo_warn("write"); - goto bad; + goto done; } - buf += nwritten; - len -= nwritten; - } while (len > 0); + cp += nwritten; + rem -= nwritten; + } while (rem > 0); - intercept_closure_reset(closure); + ret = true; - /* Switch event to read mode for the next request. */ - if (sudo_ev_set(&closure->ev, fd, SUDO_EV_READ, intercept_cb, closure) == -1) { - /* This cannot (currently) fail. */ - sudo_warn("%s", U_("unable to add event to queue")); - } - - debug_return; - -bad: - intercept_close(fd, closure); - debug_return; +done: + debug_return_bool(ret); } static void intercept_cb(int fd, int what, void *v) { struct intercept_closure *closure = v; + bool success = false; debug_decl(intercept_cb, SUDO_DEBUG_EXEC); switch (what) { case SUDO_EV_READ: - intercept_read(fd, closure); + success = intercept_read(fd, closure); break; case SUDO_EV_WRITE: - intercept_write(fd, closure); + success = intercept_write(fd, closure); break; default: sudo_warnx("%s: unexpected event type %d", __func__, what); break; } + if (!success || what == SUDO_EV_WRITE) { + intercept_close(fd, closure); + } + debug_return; } diff --git a/src/exec_common.c b/src/exec_common.c index 0998804b1..7c6a8a7e2 100644 --- a/src/exec_common.c +++ b/src/exec_common.c @@ -23,6 +23,7 @@ #include +#include #include #include #include @@ -40,10 +41,10 @@ * Add a DSO file to LD_PRELOAD or the system equivalent. */ static char ** -preload_dso(char *envp[], const char *dso_file) +preload_dso(char *envp[], const char *dso_file, char * const extra_envp[]) { char *preload = NULL; - int env_len; + int env_len, extra_len = 0; int preload_idx = -1; bool present = false; # ifdef RTLD_PRELOAD_ENABLE_VAR @@ -92,19 +93,28 @@ preload_dso(char *envp[], const char *dso_file) } # endif } + if (extra_envp != NULL) { + for (extra_len = 0; extra_envp[extra_len] != NULL; extra_len++) { + continue; + } + } /* * Make a new copy of envp as needed. * It would be nice to realloc the old envp[] but we don't know * whether it was dynamically allocated. [TODO: plugin API] */ - if (preload_idx == -1 || !enabled) { - const int env_size = env_len + 1 + (preload_idx == -1) + enabled; // -V547 + if (preload_idx == -1 || !enabled || extra_len != 0) { + const int env_size = env_len + 1 + (preload_idx == -1) + enabled + extra_len; // -V547 char **nenvp = reallocarray(NULL, env_size, sizeof(*envp)); if (nenvp == NULL) sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); memcpy(nenvp, envp, env_len * sizeof(*envp)); + if (extra_envp != NULL) { + for (extra_len = 0; extra_envp[extra_len] != NULL; extra_len++) + nenvp[env_len++] = extra_envp[extra_len]; + } nenvp[env_len] = NULL; envp = nenvp; } @@ -167,7 +177,7 @@ disable_execute(char *envp[], const char *dso) #ifdef RTLD_PRELOAD_VAR if (dso != NULL) - envp = preload_dso(envp, dso); + envp = preload_dso(envp, dso, NULL); #endif /* RTLD_PRELOAD_VAR */ debug_return_ptr(envp); @@ -178,15 +188,29 @@ disable_execute(char *envp[], const char *dso) * Uses LD_PRELOAD and the like to perform a policy check on child commands. */ static char ** -enable_intercept(char *envp[], const char *dso, int backchannel) +enable_intercept(char *envp[], const char *dso, int intercept_fd) { +#ifdef RTLD_PRELOAD_VAR + char *env_add[] = { NULL, NULL }; debug_decl(enable_intercept, SUDO_DEBUG_UTIL); -#ifdef RTLD_PRELOAD_VAR - if (dso != NULL) { - /* XXX - add backchannel fd number to environment too (minimum 64) */ - envp = preload_dso(envp, dso); + if (dso == NULL) + sudo_fatalx("%s: missing DSO", __func__); + if (intercept_fd == -1) + sudo_fatalx("%s: no intercept fd", __func__); + + if (intercept_fd < INTERCEPT_FD_MIN) { + intercept_fd = fcntl(intercept_fd, F_DUPFD, INTERCEPT_FD_MIN); + if (intercept_fd == -1) + sudo_fatal("%s", U_("unable to dup intercept fd")); } + if (asprintf(&env_add[0], "SUDO_INTERCEPT_FD=%d", intercept_fd) == -1) + debug_return_ptr(NULL); + envp = preload_dso(envp, dso, env_add); +#else + /* Intercept not supported, envp unchanged. */ + if (intercept_fd != -1) + close(intercept_fd); #endif /* RTLD_PRELOAD_VAR */ debug_return_ptr(envp); @@ -198,7 +222,7 @@ enable_intercept(char *envp[], const char *dso, int backchannel) */ int sudo_execve(int fd, const char *path, char *const argv[], char *envp[], - int backchannel, int flags) + int intercept_fd, int flags) { debug_decl(sudo_execve, SUDO_DEBUG_UTIL); @@ -208,7 +232,7 @@ sudo_execve(int fd, const char *path, char *const argv[], char *envp[], if (ISSET(flags, CD_NOEXEC)) envp = disable_execute(envp, sudo_conf_noexec_path()); else if (ISSET(flags, CD_INTERCEPT|CD_LOG_CHILDREN)) - envp = enable_intercept(envp, sudo_conf_intercept_path(), backchannel); + envp = enable_intercept(envp, sudo_conf_intercept_path(), intercept_fd); #ifdef HAVE_FEXECVE if (fd != -1) diff --git a/src/sudo_exec.h b/src/sudo_exec.h index 88abe7655..0ca1b20dd 100644 --- a/src/sudo_exec.h +++ b/src/sudo_exec.h @@ -79,6 +79,9 @@ #define SESH_ERR_NO_FILES 32 /* copy error, no files copied */ #define SESH_ERR_SOME_FILES 33 /* copy error, some files copied */ +#define INTERCEPT_FD_MIN 64 /* minimum fd so shell won't close it */ +#define MESSAGE_SIZE_MAX 2097152 /* 2Mib max intercept message size */ + /* * Symbols shared between exec.c, exec_nopty.c, exec_pty.c and exec_monitor.c */ diff --git a/src/sudo_intercept.c b/src/sudo_intercept.c new file mode 100644 index 000000000..95d5cc340 --- /dev/null +++ b/src/sudo_intercept.c @@ -0,0 +1,126 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_STDBOOL_H +# include +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_SHL_LOAD) +# include +#elif defined(HAVE_DLOPEN) +# include +#endif + +#include "sudo_compat.h" +#include "pathnames.h" + +extern bool command_allowed(const char *cmnd, char * const argv[], char * const envp[], char **ncmnd, char ***nargv, char ***nenvp); + +#ifdef HAVE___INTERPOSE +/* + * Mac OS X 10.4 and above has support for library symbol interposition. + * There is a good explanation of this in the Mac OS X Internals book. + */ +typedef struct interpose_s { + void *new_func; + void *orig_func; +} interpose_t; + +static int +my_execve(const char *cmnd, char * const argv[], char * const envp[]) +{ + char *ncmnd = NULL, **nargv = NULL, **nenvp = NULL; + + /* XXX - add SUDO_INTERCEPT_FD to environment as needed. */ + if (command_allowed(cmnd, argv, envp, &ncmnd, &nargv, &nenvp)) { + /* Execute the command using the "real" execve() function. */ + execve(ncmnd, nargv, nenvp); + } else { + errno = EACCES; + } + /* XXX - free ncmnd, nargv, nenvp */ + return -1; +} + +/* Magic to tell dyld to do symbol interposition. */ +__attribute__((__used__)) static const interpose_t interposers[] +__attribute__((__section__("__DATA,__interpose"))) = { + { (void *)my_execve, (void *)execve } +}; + +#else /* HAVE___INTERPOSE */ + +typedef int (*sudo_fn_execve_t)(const char *, char *const *, char *const *); + +sudo_dso_public int +execve(const char *cmnd, char * const argv[], char * const envp[]) +{ + char *ncmnd = NULL, **nargv = NULL, **nenvp = NULL; +# if defined(HAVE_DLOPEN) + void *fn = dlsym(RTLD_NEXT, "execve"); +# elif defined(HAVE_SHL_LOAD) + const char *name, *myname = _PATH_SUDO_INTERCEPT; + struct shl_descriptor *desc; + void *fn = NULL; + int idx = 0; + + /* Search for execve() but skip this shared object. */ + myname = sudo_basename(myname); + while (shl_get(idx++, &desc) == 0) { + name = sudo_basename(desc->filename); + if (strcmp(name, myname) == 0) + continue; + if (shl_findsym(&desc->handle, "execve", TYPE_PROCEDURE, &fn) == 0) + break; + } +# else + void *fn = NULL; +# endif + if (fn == NULL) { + errno = EACCES; + return -1; + } + + /* XXX - add SUDO_INTERCEPT_FD to environment as needed. */ + if (command_allowed(cmnd, argv, envp, &ncmnd, &nargv, &nenvp)) { + /* Execute the command using the "real" execve() function. */ + return ((sudo_fn_execve_t)fn)(ncmnd, nargv, nenvp); + } else { + errno = EACCES; + } + /* XXX - free ncmnd, nargv, nenvp */ + return -1; +} +#endif /* HAVE___INTERPOSE) */ diff --git a/src/sudo_intercept_common.c b/src/sudo_intercept_common.c new file mode 100644 index 000000000..39c93839f --- /dev/null +++ b/src/sudo_intercept_common.c @@ -0,0 +1,313 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_STDBOOL_H +# include +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ + +#define SUDO_ERROR_WRAP 0 + +#include "sudo_compat.h" +#include "sudo_fatal.h" +#include "sudo_exec.h" +#include "sudo_gettext.h" +#include "intercept.pb-c.h" + +extern char **environ; + +static pid_t mainpid = -1; +static int intercept_sock = -1; + +/* + * Look up SUDO_INTERCEPT_FD in the environment. + * This function is run when the shared library is loaded. + */ +__attribute__((constructor)) static void +sudo_interposer_init(void) +{ + static bool initialized; + char **p; + + if (!initialized) { + initialized = true; + mainpid = getpid(); + + /* + * Missing SUDO_INTERCEPT_FD will result in execve() failure. + * Note that we cannot use getenv(3) here on Linux at least. + */ + for (p = environ; *p != NULL; p++) { + if (strncmp(*p, "SUDO_INTERCEPT_FD=", sizeof("SUDO_INTERCEPT_FD=") -1) == 0) { + const char *fdstr = *p + sizeof("SUDO_INTERCEPT_FD=") - 1; + char *ep; + long ulval; + + /* XXX - debugging */ + ulval = strtoul(fdstr, &ep, 10); + if (*fdstr == '\0' || *ep != '\0' || ulval > INT_MAX) { + sudo_warnx(U_("invalid SUDO_INTERCEPT_FD: %s"), fdstr); + break; + } + intercept_sock = ulval; + break; + } + } + } +} + +static uint8_t * +fmt_policy_check_req(const char *cmnd, char * const argv[], char * const envp[], + size_t *buflen) +{ + InterceptMessage msg = INTERCEPT_MESSAGE__INIT; + PolicyCheckRequest req = POLICY_CHECK_REQUEST__INIT; + uint8_t *buf = NULL; + uint32_t msg_len; + size_t len; + + /* Setup policy check request. */ + req.command = (char *)cmnd; + req.argv = (char **)argv; + for (len = 0; argv[len] != NULL; len++) + continue; + req.n_argv = len; + req.envp = (char **)envp; + for (len = 0; envp[len] != NULL; len++) + continue; + req.n_envp = len; + msg.type_case = INTERCEPT_MESSAGE__TYPE_POLICY_CHECK_REQ; + msg.u.policy_check_req = &req; + + len = intercept_message__get_packed_size(&msg); + if (len > MESSAGE_SIZE_MAX) { + sudo_warnx(U_("client message too large: %zu"), len); + goto done; + } + /* Wire message size is used for length encoding, precedes message. */ + msg_len = len; + len += sizeof(msg_len); + + if ((buf = malloc(len)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + memcpy(buf, &msg_len, sizeof(msg_len)); + intercept_message__pack(&msg, buf + sizeof(msg_len)); + *buflen = len; + +done: + return buf; +} + +/* Send fd over a unix domain socket. */ +static bool +intercept_send_fd(int sock, int fd) +{ + struct msghdr msg; + union { + struct cmsghdr hdr; + char buf[CMSG_SPACE(sizeof(int))]; + } cmsgbuf; + struct cmsghdr *cmsg; + struct iovec iov[1]; + char ch = '\0'; + ssize_t nsent; + + /* + * We send a single byte of data along with the fd; some systems + * don't support sending file descriptors without data. + * Note that the intercept fd is *blocking*. + */ + iov[0].iov_base = &ch; + iov[0].iov_len = 1; + memset(&msg, 0, sizeof(msg)); + memset(&cmsgbuf, 0, sizeof(cmsgbuf)); + msg.msg_iov = iov; + msg.msg_iovlen = 1; + msg.msg_control = &cmsgbuf.buf; + msg.msg_controllen = sizeof(cmsgbuf.buf); + + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + *(int *)CMSG_DATA(cmsg) = fd; + + for (;;) { + nsent = sendmsg(sock, &msg, 0); + if (nsent != -1) + return true; + if (errno != EAGAIN && errno != EINTR) + break; + } + sudo_warn("sendmsg"); + return false; +} + +bool +command_allowed(const char *cmnd, char * const argv[], char * const envp[], + char **ncmnd, char ***nargv, char ***nenvp) +{ + PolicyCheckResult *res = NULL; + int sv[2] = { -1, -1 }; + ssize_t nread, nwritten; + uint8_t *cp, *buf = NULL; + bool ret = false; + uint32_t res_len; + size_t len; + + if (intercept_sock < INTERCEPT_FD_MIN) { + sudo_warnx("invalid intercept fd: %d", intercept_sock); // XXX debugging + errno = EINVAL; + goto done; + } + if (fcntl(intercept_sock, F_GETFD, 0) == -1) { + sudo_warnx("intercept fd %d not open", intercept_sock); // XXX debugging + errno = EINVAL; + goto done; + } + + /* Don't allow the original process to be replaced. */ + if (getpid() == mainpid) { + sudo_warnx("shell overwrite denied"); // XXX + // XXX debugging + errno = EACCES; + goto done; + } + + /* + * We communicate with the main sudo process over a socket pair + * which is passed over the intercept_sock. The reason for not + * using intercept_sock directly is that multiple processes + * could be trying to use it at once. Sending an fd like this + * is atomic but regular communication is not. + */ + if (socketpair(PF_UNIX, SOCK_STREAM, 0, sv) == -1) { + sudo_warn("socketpair"); + goto done; + } + if (!intercept_send_fd(intercept_sock, sv[1])) + goto done; + close(sv[1]); + sv[1] = -1; + + buf = fmt_policy_check_req(cmnd, argv, envp, &len); + if (buf == NULL) + goto done; + + /* Send request to sudo (blocking). */ + cp = buf; + do { + nwritten = write(sv[0], cp, len); + if (nwritten == -1) { + goto done; + } + len -= nwritten; + cp += nwritten; + } while (len > 0); + free(buf); + buf = NULL; + + /* Read message size (uint32_t in host byte order). */ + nread = read(sv[0], &res_len, sizeof(res_len)); + if ((size_t)nread != sizeof(res_len)) { + if (nread == 0) + sudo_warnx("unexpected EOF reading message size"); // XXX + else + sudo_warn("read"); + goto done; + } + if (res_len > MESSAGE_SIZE_MAX) { + sudo_warnx(U_("server message too large: %zu"), (size_t)res_len); + goto done; + } + + /* Read response from sudo (blocking). */ + if ((buf = malloc(res_len)) == NULL) { + goto done; + } + nread = read(sv[0], buf, res_len); + if ((size_t)nread != res_len) { + if (nread == 0) + sudo_warnx("unexpected EOF reading response"); // XXX + else + sudo_warn("read"); + goto done; + } + res = policy_check_result__unpack(NULL, res_len, buf); + if (res == NULL) { + sudo_warnx("unable to unpack %s size %zu", "PolicyCheckResult", + (size_t)res_len); + goto done; + } + switch (res->type_case) { + case POLICY_CHECK_RESULT__TYPE_ACCEPT_MSG: + // XXX - return value + *ncmnd = strdup(res->u.accept_msg->run_command); + *nargv = reallocarray(NULL, res->u.accept_msg->n_run_argv + 1, sizeof(char *)); + for (len = 0; len < res->u.accept_msg->n_run_argv; len++) { + (*nargv)[len] = strdup(res->u.accept_msg->run_argv[len]); + } + (*nargv)[len] = NULL; + /* XXX - add SUDO_INTERCEPT_FD to environment as needed. */ + *nenvp = (char **)envp; + ret = true; + break; + case POLICY_CHECK_RESULT__TYPE_REJECT_MSG: + /* XXX - display reject message */ + break; + case POLICY_CHECK_RESULT__TYPE_ERROR_MSG: + /* XXX - display error message */ + break; + default: + sudo_warnx(U_("unexpected type_case value %d in %s from %s"), + res->type_case, "PolicyCheckResult", "sudo"); + break; + } + +done: + policy_check_result__free_unpacked(res, NULL); + if (sv[0] != -1) + close(sv[0]); + if (sv[1] != -1) + close(sv[1]); + free(buf); + + return ret; +}