diff --git a/MANIFEST b/MANIFEST index a6f7f4605..94bb276c3 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1183,6 +1183,7 @@ src/env_hooks.c src/exec.c src/exec_common.c src/exec_intercept.c +src/exec_ptrace.c src/exec_monitor.c src/exec_nopty.c src/exec_preload.c diff --git a/src/Makefile.in b/src/Makefile.in index 56bcaecb3..bf48bc0c2 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -1,7 +1,7 @@ # # SPDX-License-Identifier: ISC # -# Copyright (c) 2010-2021 Todd C. Miller +# Copyright (c) 2010-2022 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 @@ -141,10 +141,10 @@ SHELL = @SHELL@ PROGS = @PROGS@ OBJS = conversation.o copy_file.o edit_open.o env_hooks.o exec.o exec_common.o \ - exec_intercept.o exec_monitor.o exec_nopty.o exec_preload.o exec_pty.o \ - get_pty.o hooks.o limits.o load_plugins.o net_ifs.o parse_args.o \ - preserve_fds.o signal.o sudo.o sudo_edit.o tcsetpgrp_nobg.o tgetpass.o \ - ttyname.o utmp.o @SUDO_OBJS@ + exec_intercept.o exec_monitor.o exec_nopty.o exec_preload.o \ + exec_ptrace.o exec_pty.o get_pty.o hooks.o limits.o load_plugins.o \ + net_ifs.o parse_args.o preserve_fds.o signal.o sudo.o sudo_edit.o \ + tcsetpgrp_nobg.o tgetpass.o ttyname.o utmp.o @SUDO_OBJS@ IOBJS = $(OBJS:.o=.i) sesh.i @@ -607,6 +607,24 @@ exec_preload.o: $(srcdir)/exec_preload.c $(incdir)/compat/stdbool.h \ $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \ $(top_builddir)/config.h $(top_builddir)/pathnames.h $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/exec_preload.c +exec_ptrace.o: $(srcdir)/exec_ptrace.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/exec_ptrace.c +exec_ptrace.i: $(srcdir)/exec_ptrace.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +exec_ptrace.plog: exec_ptrace.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/exec_ptrace.c --i-file $< --output-file $@ exec_pty.o: $(srcdir)/exec_pty.c $(incdir)/compat/stdbool.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ diff --git a/src/exec.c b/src/exec.c index b6e3736e8..fd8454d18 100644 --- a/src/exec.c +++ b/src/exec.c @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: ISC * - * Copyright (c) 2009-2021 Todd C. Miller + * Copyright (c) 2009-2022 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 @@ -86,6 +86,13 @@ exec_setup(struct command_details *details, int intercept_fd, int errfd) bool ret = false; debug_decl(exec_setup, SUDO_DEBUG_EXEC); +#ifdef HAVE_PTRACE_INTERCEPT + if (ISSET(details->flags, CD_USE_PTRACE)) { + if (!set_exec_filter()) + goto done; + } +#endif /* HAVE_PTRACE_INTERCEPT */ + if (details->pw != NULL) { #ifdef HAVE_PROJECT_H set_project(details->pw); diff --git a/src/exec_common.c b/src/exec_common.c index 35288f9e1..735a6b14d 100644 --- a/src/exec_common.c +++ b/src/exec_common.c @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: ISC * - * Copyright (c) 2009-2021 Todd C. Miller + * Copyright (c) 2009-2022 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 @@ -89,6 +89,31 @@ enable_intercept(char *envp[], const char *dso, int intercept_fd) debug_return_ptr(envp); } +/* + * Called right before execve(2). + * The tracee will be suspended until the tracer resumes it. + */ +static void +enable_ptrace(void) +{ +#ifdef HAVE_PTRACE_INTERCEPT + const pid_t pid = getpid(); + debug_decl(enable_ptrace, SUDO_DEBUG_UTIL); + + /* + * Parent will trace child and intercept execve(2). + * We stop the child here so the parent can seize control. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: suspending child %d", + __func__, (int)pid); + kill(pid, SIGSTOP); + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: resuming child %d", + __func__, (int)pid); + + debug_return; +#endif /* HAVE_PTRACE_INTERCEPT */ +} + /* * Like execve(2) but falls back to running through /bin/sh * ala execvp(3) if we get ENOEXEC. @@ -104,6 +129,8 @@ sudo_execve(int fd, const char *path, char *const argv[], char *envp[], /* Modify the environment as needed to trap execve(). */ if (ISSET(flags, CD_NOEXEC)) envp = disable_execute(envp, sudo_conf_noexec_path()); + else if (ISSET(flags, CD_USE_PTRACE)) + enable_ptrace(); else if (ISSET(flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) envp = enable_intercept(envp, sudo_conf_intercept_path(), intercept_fd); diff --git a/src/exec_intercept.c b/src/exec_intercept.c index e4edb43e9..3fd0a055a 100644 --- a/src/exec_intercept.c +++ b/src/exec_intercept.c @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: ISC * - * Copyright (c) 2021 Todd C. Miller + * Copyright (c) 2021-2022 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 @@ -89,6 +89,7 @@ intercept_setup(int fd, struct sudo_event_base *evbase, struct command_details *details) { struct intercept_closure *closure; + int rc; debug_decl(intercept_setup, SUDO_DEBUG_EXEC); sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, @@ -99,19 +100,24 @@ intercept_setup(int fd, struct sudo_event_base *evbase, sudo_warnx("%s", U_("unable to allocate memory")); goto bad; } - - /* If we've already seen an InterceptHello, expect a policy check first. */ - closure->state = sudo_token_isset(intercept_token) ? - RECV_SECRET : RECV_HELLO_INITIAL; closure->details = details; closure->listen_sock = -1; - if (sudo_ev_set(&closure->ev, fd, SUDO_EV_READ|SUDO_EV_PERSIST, intercept_cb, closure) == -1) { - /* This cannot (currently) fail. */ - sudo_warn("%s", U_("unable to add event to queue")); - goto bad; + if (ISSET(details->flags, CD_USE_PTRACE)) { + /* We can perform a policy check immediately using ptrace(2). */ + closure->state = RECV_POLICY_CHECK; + } else { + /* + * Not using ptrace(2), use LD_PRELOAD (or its equivalent). If + * we've already seen an InterceptHello, expect a policy check first. + */ + closure->state = sudo_token_isset(intercept_token) ? + RECV_SECRET : RECV_HELLO_INITIAL; } - if (sudo_ev_add(evbase, &closure->ev, NULL, false) == -1) { + + rc = sudo_ev_set(&closure->ev, fd, SUDO_EV_READ|SUDO_EV_PERSIST, + intercept_cb, closure); + if (rc == -1 || sudo_ev_add(evbase, &closure->ev, NULL, false) == -1) { sudo_warn("%s", U_("unable to add event to queue")); goto bad; } @@ -124,21 +130,18 @@ bad: } /* - * Close intercept socket and free closure when we are done with - * the connection. + * Reset intercept_closure so it can be re-used. */ static void -intercept_connection_close(struct intercept_closure *closure) +intercept_closure_reset(struct intercept_closure *closure) { - const int fd = sudo_ev_get_fd(&closure->ev); size_t n; - debug_decl(intercept_connection_close, SUDO_DEBUG_EXEC); + debug_decl(intercept_closure_reset, SUDO_DEBUG_EXEC); - sudo_ev_del(NULL, &closure->ev); - close(fd); - if (closure->listen_sock != -1) + if (closure->listen_sock != -1) { close(closure->listen_sock); - + closure->listen_sock = -1; + } free(closure->buf); free(closure->command); if (closure->run_argv != NULL) { @@ -151,6 +154,31 @@ intercept_connection_close(struct intercept_closure *closure) free(closure->run_envp[n]); free(closure->run_envp); } + closure->errstr = NULL; + closure->command = NULL; + closure->run_argv = NULL; + closure->run_envp = NULL; + closure->buf = NULL; + closure->len = 0; + closure->off = 0; + /* Does not currently reset token. */ + + debug_return; +} + +/* + * Close intercept socket and free closure when we are done with + * the connection. + */ +static void +intercept_connection_close(struct intercept_closure *closure) +{ + const int fd = sudo_ev_get_fd(&closure->ev); + debug_decl(intercept_connection_close, SUDO_DEBUG_EXEC); + + sudo_ev_del(NULL, &closure->ev); + close(fd); + intercept_closure_reset(closure); free(closure); debug_return; @@ -305,7 +333,7 @@ intercept_check_policy(PolicyCheckRequest *req, char **user_env_out = NULL; char **argv = NULL, **run_argv = NULL; bool ret = false; - int result; + int rc; size_t n; debug_decl(intercept_check_policy, SUDO_DEBUG_EXEC); @@ -338,13 +366,13 @@ intercept_check_policy(PolicyCheckRequest *req, if (ISSET(closure->details->flags, CD_INTERCEPT)) { /* We don't currently have a good way to validate the environment. */ sudo_debug_set_active_instance(policy_plugin.debug_instance); - result = policy_plugin.u.policy->check_policy(n, argv, NULL, + rc = policy_plugin.u.policy->check_policy(n, argv, NULL, &command_info, &run_argv, &user_env_out, &closure->errstr); sudo_debug_set_active_instance(sudo_debug_instance); sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "check_policy returns %d", result); + "check_policy returns %d", rc); - switch (result) { + switch (rc) { case 1: /* Rebuild command_info[] with runcwd and extract command. */ command_info_copy = update_command_info(command_info, NULL, @@ -475,7 +503,7 @@ static int intercept_verify_token(int fd, struct intercept_closure *closure) { ssize_t nread; - debug_decl(intercept_read_token, SUDO_DEBUG_EXEC); + debug_decl(intercept_verify_token, SUDO_DEBUG_EXEC); nread = recv(fd, closure->token.u8 + closure->off, sizeof(closure->token) - closure->off, 0); @@ -516,8 +544,9 @@ intercept_read(int fd, struct intercept_closure *closure) { struct sudo_event_base *evbase = sudo_ev_get_base(&closure->ev); InterceptRequest *req = NULL; - ssize_t nread; bool ret = false; + ssize_t nread; + int rc; debug_decl(intercept_read, SUDO_DEBUG_EXEC); if (closure->state == RECV_SECRET) { @@ -641,12 +670,9 @@ unpack: } /* Switch event to write mode for the reply. */ - if (sudo_ev_set(&closure->ev, fd, SUDO_EV_WRITE|SUDO_EV_PERSIST, intercept_cb, closure) == -1) { - /* This cannot (currently) fail. */ - sudo_warn("%s", U_("unable to add event to queue")); - goto done; - } - if (sudo_ev_add(evbase, &closure->ev, NULL, false) == -1) { + rc = sudo_ev_set(&closure->ev, fd, SUDO_EV_WRITE|SUDO_EV_PERSIST, + intercept_cb, closure); + if (rc == -1 || sudo_ev_add(evbase, &closure->ev, NULL, false) == -1) { sudo_warn("%s", U_("unable to add event to queue")); goto done; } @@ -770,8 +796,9 @@ static bool intercept_write(int fd, struct intercept_closure *closure) { struct sudo_event_base *evbase = sudo_ev_get_base(&closure->ev); - ssize_t nwritten; bool ret = false; + ssize_t nwritten; + int rc; debug_decl(intercept_write, SUDO_DEBUG_EXEC); sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "state %d", @@ -825,39 +852,38 @@ intercept_write(int fd, struct intercept_closure *closure) closure->len = 0; closure->off = 0; - switch (closure->state) { - case RECV_HELLO_INITIAL: - /* Re-use event for the listener. */ - close(fd); - if (sudo_ev_set(&closure->ev, closure->listen_sock, SUDO_EV_READ|SUDO_EV_PERSIST, intercept_accept_cb, closure) == -1) { - /* This cannot (currently) fail. */ - sudo_warn("%s", U_("unable to add event to queue")); - goto done; + if (ISSET(closure->details->flags, CD_USE_PTRACE)) { + /* Ready for the next policy check from the tracer. */ + closure->state = RECV_POLICY_CHECK; + } else { + switch (closure->state) { + case RECV_HELLO_INITIAL: + /* Re-use event for the listener. */ + close(fd); + rc = sudo_ev_set(&closure->ev, closure->listen_sock, + SUDO_EV_READ|SUDO_EV_PERSIST, intercept_accept_cb, closure); + if (rc == -1 || sudo_ev_add(evbase, &closure->ev, NULL, false) == -1) { + sudo_warn("%s", U_("unable to add event to queue")); + goto done; + } + closure->listen_sock = -1; + closure->state = RECV_CONNECTION; + accept_closure = closure; + break; + case POLICY_ACCEPT: + /* Re-use event to read InterceptHello from sudo_intercept.so ctor. */ + rc = sudo_ev_set(&closure->ev, fd, SUDO_EV_READ|SUDO_EV_PERSIST, + intercept_cb, closure); + if (rc == -1 || sudo_ev_add(evbase, &closure->ev, NULL, false) == -1) { + sudo_warn("%s", U_("unable to add event to queue")); + goto done; + } + closure->state = RECV_HELLO; + break; + default: + /* Done with this connection. */ + intercept_connection_close(closure); } - if (sudo_ev_add(evbase, &closure->ev, NULL, false) == -1) { - sudo_warn("%s", U_("unable to add event to queue")); - goto done; - } - closure->listen_sock = -1; - closure->state = RECV_CONNECTION; - accept_closure = closure; - break; - case POLICY_ACCEPT: - /* Re-use event to read InterceptHello from sudo_intercept.so ctor. */ - if (sudo_ev_set(&closure->ev, fd, SUDO_EV_READ|SUDO_EV_PERSIST, intercept_cb, closure) == -1) { - /* This cannot (currently) fail. */ - sudo_warn("%s", U_("unable to add event to queue")); - goto done; - } - if (sudo_ev_add(evbase, &closure->ev, NULL, false) == -1) { - sudo_warn("%s", U_("unable to add event to queue")); - goto done; - } - closure->state = RECV_HELLO; - break; - default: - /* Done with this connection. */ - intercept_connection_close(closure); } ret = true; diff --git a/src/exec_nopty.c b/src/exec_nopty.c index 542ee9697..77920df6d 100644 --- a/src/exec_nopty.c +++ b/src/exec_nopty.c @@ -371,10 +371,13 @@ exec_nopty(struct command_details *details, struct command_status *cstat) /* * Allocate a socketpair for communicating with sudo_intercept.so. * This must be inherited across exec, hence no FD_CLOEXEC. + * Check if the kernel supports the seccomp(2) filter "trap" action. */ if (ISSET(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) { if (socketpair(PF_UNIX, SOCK_STREAM, 0, intercept_sv) == -1) sudo_fatal("%s", U_("unable to create sockets")); + if (have_seccomp_action("trap")) + SET(details->flags, CD_USE_PTRACE); } /* @@ -448,6 +451,11 @@ exec_nopty(struct command_details *details, struct command_status *cstat) exit(EXIT_FAILURE); } + if (ISSET(details->flags, CD_USE_PTRACE)) { + /* Seize control of the command using ptrace(2). */ + exec_ptrace_seize(ec.cmnd_pid); + } + /* Restore signal mask now that signal handlers are setup. */ sigprocmask(SIG_SETMASK, &oset, NULL); @@ -487,10 +495,16 @@ static void handle_sigchld_nopty(struct exec_closure_nopty *ec) { pid_t pid; - int status, wflags = WUNTRACED|WNOHANG; + int status, wflags; char signame[SIG2STR_MAX]; debug_decl(handle_sigchld_nopty, SUDO_DEBUG_EXEC); +#ifdef __WALL + wflags = __WALL|WNOHANG; +#else + wflags = WUNTRACED|WNOHANG; +#endif + /* There may be multiple children in intercept mode. */ for (;;) { /* Read command status. */ @@ -519,6 +533,12 @@ handle_sigchld_nopty(struct exec_closure_nopty *ec) sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) stopped, SIG%s", __func__, (int)pid, signame); + if (ISSET(ec->details->flags, CD_USE_PTRACE)) { + /* Did exec_ptrace_handled() suppress the signal? */ + if (exec_ptrace_handled(pid, status)) + continue; + } + /* Only report status for the main command. */ if (ec->cmnd_pid != pid) continue; diff --git a/src/exec_ptrace.c b/src/exec_ptrace.c new file mode 100644 index 000000000..58cdd5be1 --- /dev/null +++ b/src/exec_ptrace.c @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2022 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 "sudo.h" +#include "sudo_exec.h" + +#ifdef HAVE_PTRACE_INTERCEPT +# include +# include +# include +# include +# include +# include +# include + +/* + * Check whether seccomp(2) filtering supports ptrace(2) traps. + * Only supported by Linux 4.14 and higher. + */ +bool +have_seccomp_action(const char *action) +{ + char line[LINE_MAX]; + bool ret = false; + FILE *fp; + debug_decl(have_seccomp_action, SUDO_DEBUG_EXEC); + + fp = fopen("/proc/sys/kernel/seccomp/actions_avail", "r"); + if (fp != NULL) { + if (fgets(line, sizeof(line), fp) != NULL) { + char *cp, *last; + + for ((cp = strtok_r(line, " \t\n", &last)); cp != NULL; + (cp = strtok_r(NULL, " \t\n", &last))) { + if (strcmp(cp, action) == 0) { + ret = true; + break; + } + } + } + fclose(fp); + } + debug_return_bool(ret); +} + +/* + * Intercept execve(2) using seccomp(2) and ptrace(2). + * If no tracer is present, execve(2) will fail with ENOSYS. + * Must be called with CAP_SYS_ADMIN, before privs are dropped. + */ +bool +set_exec_filter(void) +{ + struct sock_filter exec_filter[] = { + /* Load syscall number into the accumulator */ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + /* Jump to trace for execve(2), else allow. */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 0, 1), + /* Trace execve(2) syscall */ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE), + /* Allow non-matching syscalls */ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW) + }; + const struct sock_fprog exec_fprog = { + nitems(exec_filter), + exec_filter + }; + debug_decl(set_exec_filter, SUDO_DEBUG_UTIL); + + /* We must set SECCOMP_MODE_FILTER before dropping privileges. */ + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &exec_fprog) == -1) { + sudo_warn("%s", U_("unable to set seccomp filter")); + debug_return_bool(false); + } + debug_return_bool(true); +} + +/* + * Seize control of the specified child process which must be in + * ptrace wait. Returns true on success and false on failure. + */ +bool +exec_ptrace_seize(pid_t child) +{ + const long ptrace_opts = PTRACE_O_TRACESECCOMP|PTRACE_O_TRACECLONE| + PTRACE_O_TRACEFORK|PTRACE_O_TRACEVFORK; + int status; + pid_t pid; + debug_decl(exec_ptrace_seize, SUDO_DEBUG_UTIL); + + /* Seize control of the child process. */ + if (ptrace(PTRACE_SEIZE, child, NULL, ptrace_opts) == -1) { + sudo_warn("ptrace(PTRACE_SEIZE, %d, NULL, 0x%lx)", (int)child, + ptrace_opts); + debug_return_bool(false); + } + + /* The child will stop itself immediately before execve(2). */ + do { + pid = waitpid(child, &status, WUNTRACED); + } while (pid == -1 && errno == EINTR); + if (pid == -1) { + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + debug_return_bool(false); + } + if (!WIFSTOPPED(status)) { + sudo_warnx(U_("process %d exited unexpectedly"), (int)child); + debug_return_bool(false); + } + if (ptrace(PTRACE_CONT, child, NULL, NULL) == -1) { + sudo_warn("ptrace(PTRACE_CONT, %d, NULL, NULL)", (int)child); + debug_return_bool(false); + } + + debug_return_bool(true); +} + +/* + * Handle a process stopped due to ptrace. + * Returns true if the signal was suppressed and false if it was delivered. + */ +bool +exec_ptrace_handled(pid_t pid, int status) +{ + const int stopsig = WSTOPSIG(status); + const int sigtrap = status >> 8; + long signo = 0; + bool group_stop = false; + debug_decl(exec_ptrace_handled, SUDO_DEBUG_EXEC); + + if (sigtrap == (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) { + /* Trapped child exec. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %d called exec", + __func__, (int)pid); + /* + * XXX + * Get the exec arguments and perform a policy check either over + * the socketpair (pty case) or via a direct function call (no pty). + */ + } else if (sigtrap == (SIGTRAP | (PTRACE_EVENT_CLONE << 8)) || + sigtrap == (SIGTRAP | (PTRACE_EVENT_VFORK << 8)) || + sigtrap == (SIGTRAP | (PTRACE_EVENT_FORK << 8))) { + unsigned long new_pid; + + /* New child process, it will inherit the parent's trace flags. */ + if (sudo_debug_needed(SUDO_DEBUG_INFO)) { + if (ptrace(PTRACE_GETEVENTMSG, pid, NULL, &new_pid) != -1) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %d forked new child %lu", __func__, (int)pid, new_pid); + } else { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "ptrace(PTRACE_GETEVENTMSG, %d, NULL, %p)", (int)pid, + &new_pid); + } + } + } else { + switch (stopsig) { + case SIGSTOP: + case SIGTSTP: + case SIGTTIN: + case SIGTTOU: + /* Is this a group-stop? */ + if (status >> 16 == PTRACE_EVENT_STOP) { + /* Group-stop, do not deliver signal. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %d: group-stop signal %d", + __func__, (int)pid, stopsig); + group_stop = true; + } else { + /* Signal-delivery-stop, deliver signal. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %d: signal-delivery-stop signal %d", + __func__, (int)pid, stopsig); + signo = stopsig; + } + break; + default: + /* Not a stop signal so not a group-stop. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %d: signal %d", __func__, (int)pid, stopsig); + break; + } + } + + /* Continue child. */ + /* XXX - handle ptrace returning ESRCH if process dies */ + if (group_stop) { + /* + * Restart child but prevent it from executing + * until SIGCONT is received (simulate SIGSTOP, etc). + */ + if (ptrace(PTRACE_LISTEN, pid, NULL, 0L) == -1) + sudo_warn("ptrace(PTRACE_LISTEN,, %d, NULL, %d", pid, stopsig); + } else { + /* Restart child. */ + if (ptrace(PTRACE_CONT, pid, NULL, signo) == -1) + sudo_warn("ptrace(PTRACE_CONT, %d, NULL, %d", pid, stopsig); + } + + debug_return_bool(signo == 0); +} +#else +/* STUB */ +void +exec_ptrace_enable(void) +{ + return; +} + +/* STUB */ +bool +have_seccomp_action(const char *action) +{ + return false; +} + +/* STUB */ +bool +exec_ptrace_handled(pid_t pid, int status) +{ + return false; +} + +/* STUB */ +bool +exec_ptrace_seize(pid_t child) +{ + return true; +} +#endif /* HAVE_PTRACE_INTERCEPT */ diff --git a/src/exec_pty.c b/src/exec_pty.c index 1d7a9ed8d..b0d9f2caa 100644 --- a/src/exec_pty.c +++ b/src/exec_pty.c @@ -992,6 +992,10 @@ backchannel_cb(int fd, int what, void *v) ec->cmnd_pid = cstat.val; sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d", ec->details->command, (int)ec->cmnd_pid); + if (ISSET(ec->details->flags, CD_USE_PTRACE)) { + /* Seize control of the command using ptrace(2). */ + exec_ptrace_seize(ec->cmnd_pid); + } break; case CMD_WSTATUS: if (WIFSTOPPED(cstat.val)) { @@ -1041,10 +1045,16 @@ backchannel_cb(int fd, int what, void *v) static void handle_sigchld_pty(struct exec_closure_pty *ec) { - int n, status, wflags = WUNTRACED|WNOHANG; + int n, status, wflags; pid_t pid; debug_decl(handle_sigchld_pty, SUDO_DEBUG_EXEC); +#ifdef __WALL + wflags = __WALL|WNOHANG; +#else + wflags = WUNTRACED|WNOHANG; +#endif + /* There may be multiple children in intercept mode. */ for (;;) { do { @@ -1077,8 +1087,11 @@ handle_sigchld_pty(struct exec_closure_pty *ec) if (pid == ec->monitor_pid) ec->monitor_pid = -1; } else if (WIFSTOPPED(status)) { - if (pid != ec->monitor_pid) + if (pid != ec->monitor_pid) { + if (ISSET(ec->details->flags, CD_USE_PTRACE)) + exec_ptrace_handled(pid, status); continue; + } /* * If the monitor dies we get notified via backchannel_cb(). @@ -1405,10 +1418,13 @@ exec_pty(struct command_details *details, struct command_status *cstat) /* * Allocate a socketpair for communicating with sudo_intercept.so. * This must be inherited across exec, hence no FD_CLOEXEC. + * Check if the kernel supports the seccomp(2) filter "trap" action. */ if (ISSET(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) { if (socketpair(PF_UNIX, SOCK_STREAM, 0, intercept_sv) == -1) sudo_fatal("%s", U_("unable to create sockets")); + if (have_seccomp_action("trap")) + SET(details->flags, CD_USE_PTRACE); } /* diff --git a/src/sudo.h b/src/sudo.h index b291acab9..69242fa41 100644 --- a/src/sudo.h +++ b/src/sudo.h @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: ISC * - * Copyright (c) 1993-1996, 1998-2005, 2007-2021 + * Copyright (c) 1993-1996, 1998-2005, 2007-2022 * Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any @@ -140,6 +140,7 @@ struct user_details { #define CD_LOGIN_SHELL 0x080000 #define CD_OVERRIDE_UMASK 0x100000 #define CD_LOG_SUBCMDS 0x200000 +#define CD_USE_PTRACE 0x400000 struct preserved_fd { TAILQ_ENTRY(preserved_fd) entries; diff --git a/src/sudo_exec.h b/src/sudo_exec.h index ac4de791f..b4344d72f 100644 --- a/src/sudo_exec.h +++ b/src/sudo_exec.h @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: ISC * - * Copyright (c) 2010-2017, 2020-2021 Todd C. Miller + * Copyright (c) 2010-2017, 2020-2022 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 @@ -90,6 +90,18 @@ union sudo_token_un { #define sudo_token_isset(_t) ((_t).u64[0] || (_t).u64[1]) +/* + * Use ptrace-based intercept (using seccomp) on Linux if possible. + * TODO: test other architectures + */ +#if defined(_PATH_SUDO_INTERCEPT) && defined(__linux__) +# if defined(HAVE_DECL_SECCOMP_SET_MODE_FILTER) && HAVE_DECL_SECCOMP_SET_MODE_FILTER +# if defined(__amd64__) || defined(__i386__) || defined(__aarch64__) +# define HAVE_PTRACE_INTERCEPT 1 +# endif /* __amd64__ || __i386__ || __aarch64__ */ +# endif /* HAVE_DECL_SECCOMP_SET_MODE_FILTER */ +#endif /* _PATH_SUDO_INTERCEPT && __linux__ */ + /* * Symbols shared between exec.c, exec_nopty.c, exec_pty.c and exec_monitor.c */ @@ -132,4 +144,10 @@ bool utmp_logout(const char *line, int status); /* exec_preload.c */ char **sudo_preload_dso(char *envp[], const char *dso_file, int intercept_fd); +/* exec_ptrace.c */ +bool exec_ptrace_handled(pid_t pid, int status); +bool exec_ptrace_seize(pid_t child); +bool have_seccomp_action(const char *action); +bool set_exec_filter(void); + #endif /* SUDO_EXEC_H */