From 8e375445fb8165a7526f043b30073ffbb8b4f1b5 Mon Sep 17 00:00:00 2001 From: "Todd C. Miller" Date: Fri, 29 Apr 2022 13:08:59 -0600 Subject: [PATCH] Check the policy for ptrace-based intercept mode. --- MANIFEST | 3 +- src/Makefile.in | 16 +- src/exec_intercept.c | 357 +++++++++++++++++++++---------------------- src/exec_intercept.h | 52 +++++++ src/exec_nopty.c | 26 ++-- src/exec_ptrace.c | 316 ++++++++++++++++++++++++++------------ src/exec_pty.c | 26 ++-- src/sudo_exec.h | 4 +- 8 files changed, 490 insertions(+), 310 deletions(-) create mode 100644 src/exec_intercept.h diff --git a/MANIFEST b/MANIFEST index 94bb276c3..bcccad64e 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1183,10 +1183,11 @@ src/env_hooks.c src/exec.c src/exec_common.c src/exec_intercept.c -src/exec_ptrace.c +src/exec_intercept.h src/exec_monitor.c src/exec_nopty.c src/exec_preload.c +src/exec_ptrace.c src/exec_pty.c src/get_pty.c src/hooks.c diff --git a/src/Makefile.in b/src/Makefile.in index bf48bc0c2..f5690449b 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -525,7 +525,8 @@ exec_intercept.o: $(srcdir)/exec_intercept.c $(incdir)/compat/stdbool.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_rand.h $(incdir)/sudo_util.h $(srcdir)/sudo.h \ + $(incdir)/sudo_rand.h $(incdir)/sudo_util.h \ + $(srcdir)/exec_intercept.h $(srcdir)/sudo.h \ $(srcdir)/sudo_exec.h $(srcdir)/sudo_plugin_int.h \ $(top_builddir)/config.h $(top_builddir)/pathnames.h $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/exec_intercept.c @@ -535,7 +536,8 @@ exec_intercept.i: $(srcdir)/exec_intercept.c $(incdir)/compat/stdbool.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_rand.h $(incdir)/sudo_util.h $(srcdir)/sudo.h \ + $(incdir)/sudo_rand.h $(incdir)/sudo_util.h \ + $(srcdir)/exec_intercept.h $(srcdir)/sudo.h \ $(srcdir)/sudo_exec.h $(srcdir)/sudo_plugin_int.h \ $(top_builddir)/config.h $(top_builddir)/pathnames.h $(CC) -E -o $@ $(CPPFLAGS) $< @@ -612,16 +614,18 @@ exec_ptrace.o: $(srcdir)/exec_ptrace.c $(incdir)/compat/stdbool.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 + $(incdir)/sudo_util.h $(srcdir)/exec_intercept.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 + $(incdir)/sudo_util.h $(srcdir)/exec_intercept.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 $@ diff --git a/src/exec_intercept.c b/src/exec_intercept.c index 3fd0a055a..64b53a84c 100644 --- a/src/exec_intercept.c +++ b/src/exec_intercept.c @@ -43,48 +43,16 @@ #include "sudo_plugin_int.h" #include "sudo_rand.h" #include "intercept.pb-c.h" +#include "exec_intercept.h" #ifdef _PATH_SUDO_INTERCEPT - -/* TCSASOFT is a BSD extension that ignores control flags and speed. */ -# ifndef TCSASOFT -# define TCSASOFT 0 -# endif - -enum intercept_state { - RECV_HELLO_INITIAL, - RECV_HELLO, - RECV_SECRET, - RECV_POLICY_CHECK, - RECV_CONNECTION, - POLICY_ACCEPT, - POLICY_REJECT, - POLICY_ERROR -}; - -/* Closure for intercept_cb() */ -struct intercept_closure { - union sudo_token_un token; - struct command_details *details; - struct sudo_event ev; - const char *errstr; - char *command; /* dynamically allocated */ - char **run_argv; /* owned by plugin */ - char **run_envp; /* dynamically allocated */ - uint8_t *buf; /* dynamically allocated */ - uint32_t len; - uint32_t off; - int listen_sock; - enum intercept_state state; -}; - static union sudo_token_un intercept_token; static in_port_t intercept_listen_port; static struct intercept_closure *accept_closure; static void intercept_accept_cb(int fd, int what, void *v); static void intercept_cb(int fd, int what, void *v); -bool +void * intercept_setup(int fd, struct sudo_event_base *evbase, struct command_details *details) { @@ -113,26 +81,26 @@ intercept_setup(int fd, struct sudo_event_base *evbase, */ closure->state = sudo_token_isset(intercept_token) ? RECV_SECRET : RECV_HELLO_INITIAL; + + 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; + } } - 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; - } - - debug_return_bool(true); + debug_return_ptr(closure); bad: free(closure); - debug_return_bool(false); + debug_return_ptr(NULL); } /* * Reset intercept_closure so it can be re-used. */ -static void +void intercept_closure_reset(struct intercept_closure *closure) { size_t n; @@ -324,19 +292,165 @@ bad: debug_return_ptr(NULL); } -static bool -intercept_check_policy(PolicyCheckRequest *req, - struct intercept_closure *closure) +/* + * Perform a policy check for the given command. + * While argv must be NULL-terminated, envp need not be. + * The status of the policy check is stored in closure->state. + * Return false on error, else true. + */ +bool +intercept_check_policy(const char *command, int argc, char **argv, int envc, + char **envp, const char *runcwd, void *v) { + struct intercept_closure *closure = v; char **command_info = NULL; char **command_info_copy = NULL; char **user_env_out = NULL; - char **argv = NULL, **run_argv = NULL; + char **run_argv = NULL; bool ret = false; - int rc; - size_t n; + int i, rc; debug_decl(intercept_check_policy, SUDO_DEBUG_EXEC); + 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); + rc = policy_plugin.u.policy->check_policy(argc, 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", rc); + + switch (rc) { + case 1: + /* Rebuild command_info[] with runcwd and extract command. */ + command_info_copy = update_command_info(command_info, NULL, + runcwd ? runcwd : "unknown", &closure->command); + if (command_info_copy == NULL) { + closure->errstr = N_("unable to allocate memory"); + goto done; + } + command_info = command_info_copy; + closure->state = POLICY_ACCEPT; + break; + case 0: + if (closure->errstr == NULL) + closure->errstr = N_("command rejected by policy"); + audit_reject(policy_plugin.name, SUDO_POLICY_PLUGIN, + closure->errstr, command_info); + closure->state = POLICY_REJECT; + ret = true; + goto done; + default: + goto done; + } + } else { + /* No actual policy check, just logging child processes. */ + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "not checking policy, audit only"); + closure->command = strdup(command); + if (closure->command == NULL) { + closure->errstr = N_("unable to allocate memory"); + goto done; + } + + /* Rebuild command_info[] with new command and runcwd. */ + command_info = update_command_info(closure->details->info, + command, runcwd ? runcwd : "unknown", NULL); + if (command_info == NULL) { + closure->errstr = N_("unable to allocate memory"); + goto done; + } + closure->state = POLICY_ACCEPT; + run_argv = argv; + } + + if (sudo_debug_needed(SUDO_DEBUG_INFO)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "run_command: %s", closure->command); + for (i = 0; command_info[i] != NULL; i++) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "command_info[%d]: %s", i, command_info[i]); + } + for (i = 0; run_argv[i] != NULL; i++) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "run_argv[%d]: %s", i, run_argv[i]); + } + } + + /* Make a copy of run_argv, it may share contents of argv. */ + for (i = 0; run_argv[i] != NULL; i++) + continue; + closure->run_argv = reallocarray(NULL, i + 1, sizeof(char *)); + if (closure->run_argv == NULL) { + closure->errstr = N_("unable to allocate memory"); + goto done; + } + for (i = 0; run_argv[i] != NULL; i++) { + closure->run_argv[i] = strdup(run_argv[i]); + if (closure->run_argv[i] == NULL) { + closure->errstr = N_("unable to allocate memory"); + goto done; + } + } + closure->run_argv[i] = NULL; + + /* Make a copy of envp, which may not be NULL-terminated. */ + closure->run_envp = reallocarray(NULL, envc + 1, sizeof(char *)); + if (closure->run_envp == NULL) { + closure->errstr = N_("unable to allocate memory"); + goto done; + } + for (i = 0; i < envc; i++) { + closure->run_envp[i] = strdup(envp[i]); + if (closure->run_envp[i] == NULL) { + closure->errstr = N_("unable to allocate memory"); + goto done; + } + } + closure->run_envp[i] = NULL; + + if (ISSET(closure->details->flags, CD_INTERCEPT)) { + audit_accept(policy_plugin.name, SUDO_POLICY_PLUGIN, command_info, + closure->run_argv, closure->run_envp); + + /* Call approval plugins and audit the result. */ + if (!approval_check(command_info, closure->run_argv, closure->run_envp)) + debug_return_int(0); + } + + /* Audit the event again for the sudo front-end. */ + audit_accept("sudo", SUDO_FRONT_END, command_info, closure->run_argv, + closure->run_envp); + + ret = true; + +done: + if (!ret) { + if (closure->errstr == NULL) + closure->errstr = N_("policy plugin error"); + audit_error(policy_plugin.name, SUDO_POLICY_PLUGIN, closure->errstr, + command_info ? command_info : closure->details->info); + closure->state = POLICY_ERROR; + } + if (command_info_copy != NULL) { + for (i = 0; command_info_copy[i] != NULL; i++) { + free(command_info_copy[i]); + } + free(command_info_copy); + } + + debug_return_bool(ret); +} + +static bool +intercept_check_policy_req(PolicyCheckRequest *req, + struct intercept_closure *closure) +{ + char **argv = NULL; + bool ret = false; + size_t n; + debug_decl(intercept_check_policy_req, SUDO_DEBUG_EXEC); + if (req->command == NULL || req->n_argv == 0 || req->n_envp == 0) { closure->errstr = N_("invalid PolicyCheckRequest"); goto done; @@ -363,133 +477,10 @@ intercept_check_policy(PolicyCheckRequest *req, } argv[n] = NULL; - 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); - 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", rc); - - switch (rc) { - case 1: - /* Rebuild command_info[] with runcwd and extract command. */ - command_info_copy = update_command_info(command_info, NULL, - req->cwd ? req->cwd : "unknown", &closure->command); - if (command_info_copy == NULL) { - closure->errstr = N_("unable to allocate memory"); - goto done; - } - command_info = command_info_copy; - closure->state = POLICY_ACCEPT; - break; - case 0: - if (closure->errstr == NULL) - closure->errstr = N_("command rejected by policy"); - audit_reject(policy_plugin.name, SUDO_POLICY_PLUGIN, - closure->errstr, command_info); - closure->state = POLICY_REJECT; - ret = true; - goto done; - default: - goto done; - } - } else { - /* No actual policy check, just logging child processes. */ - sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "not checking policy, audit only"); - closure->command = strdup(req->command); - if (closure->command == NULL) { - closure->errstr = N_("unable to allocate memory"); - goto done; - } - - /* Rebuild command_info[] with new command and runcwd. */ - command_info = update_command_info(closure->details->info, - req->command, req->cwd ? req->cwd : "unknown", NULL); - if (command_info == NULL) { - closure->errstr = N_("unable to allocate memory"); - goto done; - } - closure->state = POLICY_ACCEPT; - run_argv = argv; - } - - if (sudo_debug_needed(SUDO_DEBUG_INFO)) { - sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "run_command: %s", closure->command); - for (n = 0; command_info[n] != NULL; n++) { - sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "command_info[%zu]: %s", n, command_info[n]); - } - 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) { - closure->errstr = N_("unable to allocate memory"); - goto done; - } - for (n = 0; run_argv[n] != NULL; n++) { - closure->run_argv[n] = strdup(run_argv[n]); - if (closure->run_argv[n] == NULL) { - closure->errstr = N_("unable to allocate memory"); - goto done; - } - } - 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) { - closure->errstr = N_("unable to allocate memory"); - goto done; - } - for (n = 0; n < req->n_envp; n++) { - closure->run_envp[n] = strdup(req->envp[n]); - if (closure->run_envp[n] == NULL) { - closure->errstr = N_("unable to allocate memory"); - goto done; - } - } - closure->run_envp[n] = NULL; - - if (ISSET(closure->details->flags, CD_INTERCEPT)) { - audit_accept(policy_plugin.name, SUDO_POLICY_PLUGIN, command_info, - closure->run_argv, closure->run_envp); - - /* Call approval plugins and audit the result. */ - if (!approval_check(command_info, closure->run_argv, closure->run_envp)) - debug_return_int(0); - } - - /* Audit the event again for the sudo front-end. */ - audit_accept("sudo", SUDO_FRONT_END, command_info, closure->run_argv, - closure->run_envp); - - ret = true; + ret = intercept_check_policy(req->command, req->n_argv, argv, req->n_envp, + req->envp, req->cwd, closure); done: - if (!ret) { - if (closure->errstr == NULL) - closure->errstr = N_("policy plugin error"); - audit_error(policy_plugin.name, SUDO_POLICY_PLUGIN, closure->errstr, - command_info ? command_info : closure->details->info); - closure->state = POLICY_ERROR; - } - if (command_info_copy != NULL) { - for (n = 0; command_info_copy[n] != NULL; n++) { - free(command_info_copy[n]); - } - free(command_info_copy); - } free(argv); debug_return_bool(ret); @@ -644,7 +635,7 @@ unpack: goto done; } - ret = intercept_check_policy(req->u.policy_check_req, closure); + ret = intercept_check_policy_req(req->u.policy_check_req, closure); if (!ret) goto done; break; @@ -918,8 +909,9 @@ intercept_cb(int fd, int what, void *v) } /* - * Accept a new connection from the client and fill in a client closure. - * Registers a new event for the connection. + * Accept a new connection from the client register a new event for it. + * Returns an opaque pointer to the intercept closure, which is + * also passed to the event callback. */ static void intercept_accept_cb(int fd, int what, void *v) @@ -949,7 +941,10 @@ intercept_accept_cb(int fd, int what, void *v) if (flags != -1) (void)fcntl(client_sock, F_SETFL, flags | O_NONBLOCK); - if (!intercept_setup(client_sock, evbase, closure->details)) { + /* + * Create a new intercept closure and register an event for client_sock. + */ + if (intercept_setup(client_sock, evbase, closure->details) == NULL) { goto bad; } @@ -961,7 +956,7 @@ bad: debug_return; } #else /* _PATH_SUDO_INTERCEPT */ -bool +void * intercept_setup(int fd, struct sudo_event_base *evbase, struct command_details *details) { @@ -969,7 +964,7 @@ intercept_setup(int fd, struct sudo_event_base *evbase, /* Intercept support not compiled in. */ - debug_return_bool(false); + debug_return_ptr(NULL); } void diff --git a/src/exec_intercept.h b/src/exec_intercept.h new file mode 100644 index 000000000..1ca417d43 --- /dev/null +++ b/src/exec_intercept.h @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: ISC + * + * 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 + * 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. + */ + +#ifndef SUDO_INTERCEPT_H +#define SUDO_INTERCEPT_H + +enum intercept_state { + RECV_HELLO_INITIAL, + RECV_HELLO, + RECV_SECRET, + RECV_POLICY_CHECK, + RECV_CONNECTION, + POLICY_ACCEPT, + POLICY_REJECT, + POLICY_ERROR +}; + +/* Closure for intercept_cb() */ +struct intercept_closure { + union sudo_token_un token; + struct command_details *details; + struct sudo_event ev; + const char *errstr; + char *command; /* dynamically allocated */ + char **run_argv; /* owned by plugin */ + char **run_envp; /* dynamically allocated */ + uint8_t *buf; /* dynamically allocated */ + uint32_t len; + uint32_t off; + int listen_sock; + enum intercept_state state; +}; + +void intercept_closure_reset(struct intercept_closure *closure); +bool intercept_check_policy(const char *command, int argc, char **argv, int envc, char **envp, const char *runcwd, void *closure); + +#endif /* SUDO_INTERCEPT_H */ diff --git a/src/exec_nopty.c b/src/exec_nopty.c index 77920df6d..f52cea920 100644 --- a/src/exec_nopty.c +++ b/src/exec_nopty.c @@ -61,6 +61,7 @@ struct exec_closure_nopty { struct sudo_event *sigcont_event; struct sudo_event *siginfo_event; struct command_status *cstat; + void *intercept; pid_t cmnd_pid; pid_t ppgrp; }; @@ -368,16 +369,18 @@ exec_nopty(struct command_details *details, struct command_status *cstat) if (pipe2(errpipe, O_CLOEXEC) != 0) sudo_fatal("%s", U_("unable to create pipe")); - /* - * 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")) + if (have_seccomp_action("trap")) { + /* Kernel supports the seccomp(2) filter "trap" action. */ SET(details->flags, CD_USE_PTRACE); + } else { + /* + * Allocate a socketpair for communicating with sudo_intercept.so. + * This must be inherited across exec, hence no FD_CLOEXEC. + */ + if (socketpair(PF_UNIX, SOCK_STREAM, 0, intercept_sv) == -1) + sudo_fatal("%s", U_("unable to create sockets")); + } } /* @@ -446,8 +449,9 @@ exec_nopty(struct command_details *details, struct command_status *cstat) fill_exec_closure_nopty(&ec, cstat, details, errpipe[0]); /* Create event and closure for intercept mode. */ - if (intercept_sv[0] != -1) { - if (!intercept_setup(intercept_sv[0], ec.evbase, details)) + if (ISSET(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) { + ec.intercept = intercept_setup(intercept_sv[0], ec.evbase, details); + if (ec.intercept == NULL) exit(EXIT_FAILURE); } @@ -535,7 +539,7 @@ handle_sigchld_nopty(struct exec_closure_nopty *ec) if (ISSET(ec->details->flags, CD_USE_PTRACE)) { /* Did exec_ptrace_handled() suppress the signal? */ - if (exec_ptrace_handled(pid, status)) + if (exec_ptrace_handled(pid, status, ec->intercept)) continue; } diff --git a/src/exec_ptrace.c b/src/exec_ptrace.c index 99e8cdb22..498641881 100644 --- a/src/exec_ptrace.c +++ b/src/exec_ptrace.c @@ -29,12 +29,14 @@ #include #include #include +#include #include #include #include #include "sudo.h" #include "sudo_exec.h" +#include "exec_intercept.h" #ifdef HAVE_PTRACE_INTERCEPT # include @@ -57,82 +59,82 @@ */ #if defined(__amd64__) # define user_pt_regs user_regs_struct -# define reg_syscall(x) (x).orig_rax -# define reg_retval(x) (x).rax -# define reg_arg1(x) (x).rdi -# define reg_arg2(x) (x).rsi -# define reg_arg3(x) (x).rdx -# define reg_arg4(x) (x).r10 +# define reg_syscall(x) (x)->orig_rax +# define reg_retval(x) (x)->rax +# define reg_arg1(x) (x)->rdi +# define reg_arg2(x) (x)->rsi +# define reg_arg3(x) (x)->rdx +# define reg_arg4(x) (x)->r10 #elif defined(__aarch64__) -# define reg_syscall(x) (x).regs[8] /* w8 */ -# define reg_retval(x) (x).regs[0] /* x0 */ -# define reg_arg1(x) (x).regs[0] /* x0 */ -# define reg_arg2(x) (x).regs[1] /* x1 */ -# define reg_arg3(x) (x).regs[2] /* x2 */ -# define reg_arg4(x) (x).regs[3] /* x3 */ +# define reg_syscall(x) (x)->regs[8] /* w8 */ +# define reg_retval(x) (x)->regs[0] /* x0 */ +# define reg_arg1(x) (x)->regs[0] /* x0 */ +# define reg_arg2(x) (x)->regs[1] /* x1 */ +# define reg_arg3(x) (x)->regs[2] /* x2 */ +# define reg_arg4(x) (x)->regs[3] /* x3 */ #elif defined(__arm__) /* Note: assumes arm EABI, not OABI */ /* Untested */ # define user_pt_regs pt_regs -# define reg_syscall(x) (x).ARM_r7 -# define reg_retval(x) (x).ARM_r0 -# define reg_arg1(x) (x).ARM_r0 -# define reg_arg2(x) (x).ARM_r1 -# define reg_arg3(x) (x).ARM_r2 -# define reg_arg4(x) (x).ARM_r3 +# define reg_syscall(x) (x)->ARM_r7 +# define reg_retval(x) (x)->ARM_r0 +# define reg_arg1(x) (x)->ARM_r0 +# define reg_arg2(x) (x)->ARM_r1 +# define reg_arg3(x) (x)->ARM_r2 +# define reg_arg4(x) (x)->ARM_r3 #elif defined (__hppa__) /* Untested */ # define user_pt_regs user_regs_struct -# define reg_syscall(x) (x).gr[20] /* r20 */ -# define reg_retval(x) (x).gr[28] /* r28 */ -# define reg_arg1(x) (x).gr[26] /* r26 */ -# define reg_arg2(x) (x).gr[25] /* r25 */ -# define reg_arg3(x) (x).gr[24] /* r24 */ -# define reg_arg4(x) (x).gr[23] /* r23 */ +# define reg_syscall(x) (x)->gr[20] /* r20 */ +# define reg_retval(x) (x)->gr[28] /* r28 */ +# define reg_arg1(x) (x)->gr[26] /* r26 */ +# define reg_arg2(x) (x)->gr[25] /* r25 */ +# define reg_arg3(x) (x)->gr[24] /* r24 */ +# define reg_arg4(x) (x)->gr[23] /* r23 */ #elif defined(__i386__) # define user_pt_regs user_regs_struct -# define reg_syscall(x) (x).orig_eax -# define reg_retval(x) (x).eax -# define reg_arg1(x) (x).ebx -# define reg_arg2(x) (x).ecx -# define reg_arg3(x) (x).edx -# define reg_arg4(x) (x).esi +# define reg_syscall(x) (x)->orig_eax +# define reg_retval(x) (x)->eax +# define reg_arg1(x) (x)->ebx +# define reg_arg2(x) (x)->ecx +# define reg_arg3(x) (x)->edx +# define reg_arg4(x) (x)->esi #elif defined(__powerpc64__) /* Untested */ # define user_pt_regs pt_regs -# define reg_syscall(x) (x).gpr[0] /* r0 */ -# define reg_retval(x) (x).gpr[3] /* r3 */ -# define reg_arg1(x) (x).gpr[3] /* r3 */ -# define reg_arg2(x) (x).gpr[4] /* r4 */ -# define reg_arg3(x) (x).gpr[5] /* r5 */ -# define reg_arg4(x) (x).gpr[6] /* r6 */ +# define reg_syscall(x) (x)->gpr[0] /* r0 */ +# define reg_retval(x) (x)->gpr[3] /* r3 */ +# define reg_arg1(x) (x)->gpr[3] /* r3 */ +# define reg_arg2(x) (x)->gpr[4] /* r4 */ +# define reg_arg3(x) (x)->gpr[5] /* r5 */ +# define reg_arg4(x) (x)->gpr[6] /* r6 */ #elif defined(__powerpc__) /* Untested */ # define user_pt_regs pt_regs -# define reg_syscall(x) (x).gpr[0] /* r0 */ -# define reg_retval(x) (x).gpr[3] /* r3 */ -# define reg_arg1(x) (x).gpr[3] /* r3 */ -# define reg_arg2(x) (x).gpr[4] /* r4 */ -# define reg_arg3(x) (x).gpr[5] /* r5 */ -# define reg_arg4(x) (x).gpr[6] /* r6 */ +# define reg_syscall(x) (x)->gpr[0] /* r0 */ +# define reg_retval(x) (x)->gpr[3] /* r3 */ +# define reg_arg1(x) (x)->gpr[3] /* r3 */ +# define reg_arg2(x) (x)->gpr[4] /* r4 */ +# define reg_arg3(x) (x)->gpr[5] /* r5 */ +# define reg_arg4(x) (x)->gpr[6] /* r6 */ #elif defined(__riscv) && __riscv_xlen == 64 /* Untested */ # define user_pt_regs user_regs_struct -# define reg_syscall(x) (x).a7 -# define reg_retval(x) (x).a0 -# define reg_arg1(x) (x).a0 -# define reg_arg2(x) (x).a1 -# define reg_arg3(x) (x).a2 -# define reg_arg4(x) (x).a3 +# define reg_syscall(x) (x)->a7 +# define reg_retval(x) (x)->a0 +# define reg_arg1(x) (x)->a0 +# define reg_arg2(x) (x)->a1 +# define reg_arg3(x) (x)->a2 +# define reg_arg4(x) (x)->a3 #elif defined(__s390__) /* Untested */ # define user_pt_regs s390_regs -# define reg_syscall(x) (x).gprs[1] /* r1 */ -# define reg_retval(x) (x).gprs[2] /* r2 */ -# define reg_arg1(x) (x).gprs[2] /* r2 */ -# define reg_arg2(x) (x).gprs[3] /* r3 */ -# define reg_arg3(x) (x).gprs[4] /* r4 */ -# define reg_arg4(x) (x).gprs[5] /* r6 */ +# define reg_syscall(x) (x)->gprs[1] /* r1 */ +# define reg_retval(x) (x)->gprs[2] /* r2 */ +# define reg_arg1(x) (x)->gprs[2] /* r2 */ +# define reg_arg2(x) (x)->gprs[3] /* r3 */ +# define reg_arg3(x) (x)->gprs[4] /* r4 */ +# define reg_arg4(x) (x)->gprs[5] /* r6 */ #else # error "Do not know how to find your architecture's registers" #endif @@ -154,7 +156,7 @@ ptrace_read_string(pid_t pid, long addr, char *buf, size_t bufsize) for (;;) { word = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); if (word == -1) { - sudo_warn("ptrace(PTRACE_PEEKTEXT, %d, %ld, NULL)", pid, addr); + sudo_warn("ptrace(PTRACE_PEEKTEXT, %d, 0x%lx, NULL)", pid, addr); debug_return_ssize_t(-1); } @@ -193,7 +195,7 @@ ptrace_read_vec(pid_t pid, long addr, char **vec, char *buf, size_t bufsize) long word = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); switch (word) { case -1: - sudo_warn("ptrace(PTRACE_PEEKTEXT, %d, %ld, NULL)", pid, addr); + sudo_warn("ptrace(PTRACE_PEEKTEXT, %d, 0x%lx, NULL)", pid, addr); goto bad; case 0: vec[len] = NULL; @@ -230,7 +232,7 @@ ptrace_get_vec_len(pid_t pid, long addr) long word = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); switch (word) { case -1: - sudo_warn("ptrace(PTRACE_PEEKTEXT, %d, %ld, NULL)", pid, addr); + sudo_warn("ptrace(PTRACE_PEEKTEXT, %d, 0x%lx, NULL)", pid, addr); debug_return_int(-1); case 0: debug_return_int(len); @@ -242,20 +244,43 @@ ptrace_get_vec_len(pid_t pid, long addr) } } +/* + * Use /proc/PID/cwd to determine the current working directory. + */ +static bool +getcwd_by_pid(pid_t pid, char *buf, size_t bufsize) +{ + size_t len; + char path[PATH_MAX]; + debug_decl(getcwd_by_pid, SUDO_DEBUG_EXEC); + + len = snprintf(path, sizeof(path), "/proc/%d/cwd", (int)pid); + if (len < sizeof(path)) { + len = readlink(path, buf, bufsize); + if (len != (size_t)-1) { + /* Check for truncation. */ + if (len >= bufsize) + buf[bufsize - 1] = '\0'; + debug_return_bool(true); + } + } + debug_return_bool(false); +} + /* * Read the filename, argv and envp of the execve(2) system call. * Returns a dynamically allocated buffer the parent is responsible for. */ static char * -get_execve_args(pid_t pid, char **pathname_out, char ***argv_out, char ***envp_out) +get_execve_info(pid_t pid, struct user_pt_regs *regs, char **pathname_out, + int *argc_out, char ***argv_out, int *envc_out, char ***envp_out) { char *argbuf, *strtab, *pathname, **argv, **envp; long path_addr, argv_addr, envp_addr, syscallno; - struct user_pt_regs regs; struct iovec iov; int argc, envc; size_t bufsize, len; - debug_decl(get_execve_args, SUDO_DEBUG_EXEC); + debug_decl(get_execve_info, SUDO_DEBUG_EXEC); bufsize = sysconf(_SC_ARG_MAX) + PATH_MAX; argbuf = malloc(bufsize); @@ -263,8 +288,8 @@ get_execve_args(pid_t pid, char **pathname_out, char ***argv_out, char ***envp_o sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); /* XXX - for amd64 and i386 use PTRACE_GETREGS/PTRACE_SETREGS instead. */ - iov.iov_base = ®s; - iov.iov_len = sizeof(regs); + iov.iov_base = regs; + iov.iov_len = sizeof(*regs); if (ptrace(PTRACE_GETREGSET, pid, (long)NT_PRSTATUS, &iov) == -1) { sudo_warn(U_("unable to get registers for process %d"), (int)pid); goto bad; @@ -282,24 +307,6 @@ get_execve_args(pid_t pid, char **pathname_out, char ***argv_out, char ***envp_o argv_addr = reg_arg2(regs); envp_addr = reg_arg3(regs); -#ifdef notyet - /* Cause the syscall to fail by changing its number to -1. */ - reg_syscall(regs) |= 0xffffffff; - if (ptrace(PTRACE_SETREGSET, pid, (long)NT_PRSTATUS, &iov) == -1) { - sudo_warn("unable to set registers"); - goto bad; - } - - /* Allow the syscall to complete and change return value to EACCES. */ - ptrace(PTRACE_SYSCALL, pid, NULL, NULL); - waitpid(pid, NULL, 0); - reg_retval(regs) = -EACCES; - if (ptrace(PTRACE_SETREGSET, pid, (long)NT_PRSTATUS, &iov) == -1) { - sudo_warn("unable to set registers"); - goto bad; - } -#endif - /* Count argv and envp */ argc = ptrace_get_vec_len(pid, argv_addr); envc = ptrace_get_vec_len(pid, envp_addr); @@ -347,7 +354,9 @@ get_execve_args(pid_t pid, char **pathname_out, char ***argv_out, char ***envp_o sudo_debug_execve(SUDO_DEBUG_INFO, pathname, argv, envp); *pathname_out = pathname; + *argc_out = argc; *argv_out = argv; + *envc_out = envc; *envp_out = envp; debug_return_ptr(argbuf); @@ -356,6 +365,61 @@ bad: debug_return_ptr(NULL); } +/* + * Cause the current syscall to fail and set the error value to ecode. + */ +static bool +ptrace_fail_syscall(pid_t pid, struct user_pt_regs *regs, int ecode) +{ + struct iovec iov; + sigset_t chldmask; + bool ret = false; + int status; + debug_decl(ptrace_fail_syscall, SUDO_DEBUG_EXEC); + + iov.iov_base = regs; + iov.iov_len = sizeof(*regs); + + /* Cause the syscall to fail by changing its number to -1. */ + reg_syscall(regs) |= 0xffffffff; + if (ptrace(PTRACE_SETREGSET, pid, (long)NT_PRSTATUS, &iov) == -1) { + sudo_warn(U_("unable to set registers for process %d"), (int)pid); + debug_return_bool(false); + } + + /* Block SIGCHLD for the critical section (waitpid). */ + sigemptyset(&chldmask); + sigaddset(&chldmask, SIGCHLD); + sigprocmask(SIG_BLOCK, &chldmask, NULL); + + /* Allow the syscall to continue and change return value to ecode. */ + ptrace(PTRACE_SYSCALL, pid, NULL, NULL); + for (;;) { + if (waitpid(pid, &status, WUNTRACED) != -1) + break; + if (errno == EINTR) + continue; + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + goto done; + } + if (!WIFSTOPPED(status)) { + sudo_warnx(U_("process %d exited unexpectedly"), (int)pid); + goto done; + } + reg_retval(regs) = -ecode; + if (ptrace(PTRACE_SETREGSET, pid, (long)NT_PRSTATUS, &iov) == -1) { + sudo_warn(U_("unable to set registers for process %d"), (int)pid); + goto done; + } + + ret = true; + +done: + sigprocmask(SIG_UNBLOCK, &chldmask, NULL); + + debug_return_bool(ret); +} + /* * Check whether seccomp(2) filtering supports ptrace(2) traps. * Only supported by Linux 4.14 and higher. @@ -458,13 +522,83 @@ exec_ptrace_seize(pid_t child) debug_return_bool(true); } +/* + * Intercept execve(2) and perform a policy check. + * Reads current registers and execve(2) arguments. + * If the command is not allowed by policy, fail with EACCES. + * If the command is allowed, update argv if needed before continuing. + * Returns false on error, else true. + */ +static bool +ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure) +{ + char *pathname, **argv, **envp, *buf; + struct user_pt_regs regs; + char cwd[PATH_MAX]; + int argc, envc; + debug_decl(ptrace_intercept_execve, SUDO_DEBUG_UTIL); + + /* Get the current working directory and execve info. */ + if (!getcwd_by_pid(pid, cwd, sizeof(cwd))) + (void)strlcpy(cwd, "unknown", sizeof(cwd)); + buf = get_execve_info(pid, ®s, &pathname, &argc, &argv, &envc, &envp); + if (buf == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: %d: unable to get execve info", __func__, (int)pid); + + /* Unrecoverable error, kill the process if it still exists. */ + if (errno != ESRCH) + kill(pid, SIGKILL); + debug_return_bool(false); + } + + /* Perform a policy check. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %d: checking policy for %s", + __func__, (int)pid, pathname); + + argv[0] = pathname; + if (!intercept_check_policy(pathname, argc, argv, envc, envp, cwd, + closure)) { + sudo_warnx("%s", U_(closure->errstr)); + } + + if (closure->state == POLICY_ACCEPT) { + /* + * Update argv if the policy modified it. + * We don't currently ever modify envp. + */ + bool match = strcmp(pathname, closure->command) == 0; + if (match) { + int i; + for (i = 0; closure->run_argv[i] != NULL && argv[i] != NULL; i++) { + if (strcmp(closure->run_argv[i], argv[i]) != 0) { + match = false; + break; + } + } + } + if (!match) { + /* Need to replace argv with run_argv. */ + /* XXX */ + } + } else { + /* If denied, fake the syscall and set return to EACCES */ + ptrace_fail_syscall(pid, ®s, EACCES); + } + + intercept_closure_reset(closure); + + 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) +exec_ptrace_handled(pid_t pid, int status, void *intercept) { + struct intercept_closure *closure = intercept; const int stopsig = WSTOPSIG(status); const int sigtrap = status >> 8; long signo = 0; @@ -472,22 +606,8 @@ exec_ptrace_handled(pid_t pid, int status) debug_decl(exec_ptrace_handled, SUDO_DEBUG_EXEC); if (sigtrap == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8))) { - char *pathname, **argv, **envp, *buf; - - /* Trapped child exec. */ - sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %d called exec", - __func__, (int)pid); - - /* - * Get the exec arguments and perform a policy check either over - * the socketpair (pty case) or via a direct function call (no pty). - * XXX - */ - buf = get_execve_args(pid, &pathname, &argv, &envp); - if (buf == NULL) { - sudo_debug_printf(SUDO_DEBUG_ERROR, - "%s: %d: unable to get exec args", __func__, (int)pid); - } + if (!ptrace_intercept_execve(pid, closure)) + debug_return_bool(true); } else if (sigtrap == (SIGTRAP | (PTRACE_EVENT_CLONE << 8)) || sigtrap == (SIGTRAP | (PTRACE_EVENT_VFORK << 8)) || sigtrap == (SIGTRAP | (PTRACE_EVENT_FORK << 8))) { @@ -568,7 +688,7 @@ have_seccomp_action(const char *action) /* STUB */ bool -exec_ptrace_handled(pid_t pid, int status) +exec_ptrace_handled(pid_t pid, int status, void *intercept) { return false; } diff --git a/src/exec_pty.c b/src/exec_pty.c index b0d9f2caa..661a9ad09 100644 --- a/src/exec_pty.c +++ b/src/exec_pty.c @@ -77,6 +77,7 @@ struct exec_closure_pty { struct sudo_event *sigchld_event; struct sudo_event *sigwinch_event; struct command_status *cstat; + void *intercept; struct monitor_message_list monitor_messages; pid_t monitor_pid; pid_t cmnd_pid; @@ -1089,7 +1090,7 @@ handle_sigchld_pty(struct exec_closure_pty *ec) } else if (WIFSTOPPED(status)) { if (pid != ec->monitor_pid) { if (ISSET(ec->details->flags, CD_USE_PTRACE)) - exec_ptrace_handled(pid, status); + exec_ptrace_handled(pid, status, ec->intercept); continue; } @@ -1415,16 +1416,18 @@ exec_pty(struct command_details *details, struct command_status *cstat) fcntl(sv[1], F_SETFD, FD_CLOEXEC) == -1) sudo_fatal("%s", U_("unable to create sockets")); - /* - * 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")) + if (have_seccomp_action("trap")) { + /* Kernel supports the seccomp(2) filter "trap" action. */ SET(details->flags, CD_USE_PTRACE); + } else { + /* + * Allocate a socketpair for communicating with sudo_intercept.so. + * This must be inherited across exec, hence no FD_CLOEXEC. + */ + if (socketpair(PF_UNIX, SOCK_STREAM, 0, intercept_sv) == -1) + sudo_fatal("%s", U_("unable to create sockets")); + } } /* @@ -1669,8 +1672,9 @@ exec_pty(struct command_details *details, struct command_status *cstat) fill_exec_closure_pty(&ec, cstat, details, ppgrp, sv[0]); /* Create event and closure for intercept mode. */ - if (intercept_sv[0] != -1) { - if (!intercept_setup(intercept_sv[0], ec.evbase, details)) + if (ISSET(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) { + ec.intercept = intercept_setup(intercept_sv[0], ec.evbase, details); + if (ec.intercept == NULL) exit(EXIT_FAILURE); } diff --git a/src/sudo_exec.h b/src/sudo_exec.h index b4344d72f..fcc6e6f2a 100644 --- a/src/sudo_exec.h +++ b/src/sudo_exec.h @@ -121,7 +121,7 @@ char **disable_execute(char *envp[], const char *dso); char **enable_monitor(char *envp[], const char *dso); /* exec_intercept.c */ -bool intercept_setup(int fd, struct sudo_event_base *evbase, struct command_details *details); +void *intercept_setup(int fd, struct sudo_event_base *evbase, struct command_details *details); void intercept_cleanup(void); /* exec_nopty.c */ @@ -145,7 +145,7 @@ bool utmp_logout(const char *line, int status); 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_handled(pid_t pid, int status, void *intercept); bool exec_ptrace_seize(pid_t child); bool have_seccomp_action(const char *action); bool set_exec_filter(void);