diff --git a/MANIFEST b/MANIFEST index 91cb4d7c2..527092c4f 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1204,6 +1204,7 @@ src/openbsd.c src/parse_args.c src/preload.c src/preserve_fds.c +src/regress/intercept/test_ptrace.c src/regress/net_ifs/check_net_ifs.c src/regress/noexec/check_noexec.c src/regress/ttyname/check_ttyname.c diff --git a/src/Makefile.in b/src/Makefile.in index 22ad985c8..3efb98725 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -161,6 +161,8 @@ CHECK_NOEXEC_OBJS = check_noexec.o exec_common.o exec_preload.o CHECK_TTYNAME_OBJS = check_ttyname.o ttyname.o +TEST_PTRACE_OBJS = test_ptrace.o + LIBOBJDIR = $(top_builddir)/@ac_config_libobj_dir@/ VERSION = @PACKAGE_VERSION@ @@ -235,6 +237,9 @@ check_noexec: $(CHECK_NOEXEC_OBJS) $(top_builddir)/lib/util/libsudo_util.la sudo check_ttyname: $(CHECK_TTYNAME_OBJS) $(top_builddir)/lib/util/libsudo_util.la $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_TTYNAME_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(TEST_LIBS) +test_ptrace: $(TEST_PTRACE_OBJS) $(top_builddir)/lib/util/libsudo_util.la + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(TEST_PTRACE_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(TEST_LIBS) + pre-install: install: install-binaries install-rc @INSTALL_INTERCEPT@ @INSTALL_NOEXEC@ @@ -966,6 +971,28 @@ tcsetpgrp_nobg.i: $(srcdir)/tcsetpgrp_nobg.c $(incdir)/compat/stdbool.h \ $(CC) -E -o $@ $(CPPFLAGS) $< tcsetpgrp_nobg.plog: tcsetpgrp_nobg.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/tcsetpgrp_nobg.c --i-file $< --output-file $@ +test_ptrace.o: $(srcdir)/regress/intercept/test_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)/exec_intercept.h $(srcdir)/exec_ptrace.c \ + $(srcdir)/exec_ptrace.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)/regress/intercept/test_ptrace.c +test_ptrace.i: $(srcdir)/regress/intercept/test_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)/exec_intercept.h $(srcdir)/exec_ptrace.c \ + $(srcdir)/exec_ptrace.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +test_ptrace.plog: test_ptrace.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/intercept/test_ptrace.c --i-file $< --output-file $@ tgetpass.o: $(srcdir)/tgetpass.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_intercept.h b/src/exec_intercept.h index d2d848e97..e9d3a6ac3 100644 --- a/src/exec_intercept.h +++ b/src/exec_intercept.h @@ -27,6 +27,7 @@ enum intercept_state { RECV_CONNECTION, POLICY_ACCEPT, POLICY_REJECT, + POLICY_TEST, POLICY_ERROR }; diff --git a/src/exec_ptrace.c b/src/exec_ptrace.c index 38ffc6bcf..7884a7aef 100644 --- a/src/exec_ptrace.c +++ b/src/exec_ptrace.c @@ -790,10 +790,13 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure) char *pathname, **argv, **envp, *buf; int argc, envc, syscallno; struct sudo_ptrace_regs regs; + bool path_mismatch = false; + bool argv_mismatch = false; char cwd[PATH_MAX]; unsigned long msg; bool ret = false; struct stat sb; + int i; debug_decl(ptrace_intercept_execve, SUDO_DEBUG_UTIL); /* Do not check the policy if we are executing the initial command. */ @@ -888,21 +891,29 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure) sudo_warnx("%s", U_(closure->errstr)); } - if (closure->state == POLICY_ACCEPT) { + switch (closure->state) { + case POLICY_TEST: + path_mismatch = true; + argv_mismatch = true; + if (closure->command == NULL) + closure->command = pathname; + if (closure->run_argv == NULL) + closure->run_argv = argv; + FALLTHROUGH; + case POLICY_ACCEPT: /* * Update pathname and argv if the policy modified it. * We don't currently ever modify envp. */ - bool path_mismatch = strcmp(pathname, closure->command) != 0; - bool argv_mismatch = false; - int i; - + if (strcmp(pathname, closure->command) != 0) + path_mismatch = true; for (i = 0; closure->run_argv[i] != NULL && argv[i] != NULL; i++) { if (strcmp(closure->run_argv[i], argv[i]) != 0) { argv_mismatch = true; break; } } + if (path_mismatch || argv_mismatch) { /* * Need to rewrite pathname and/or argv. @@ -993,9 +1004,11 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure) goto done; } } - } else { - /* If denied, fake the syscall and set return to EACCES */ + break; + default: + /* If rejected, fake the syscall and set return to EACCES */ ptrace_fail_syscall(pid, ®s, EACCES); + break; } ret = true; diff --git a/src/regress/intercept/test_ptrace.c b/src/regress/intercept/test_ptrace.c new file mode 100644 index 000000000..88da491df --- /dev/null +++ b/src/regress/intercept/test_ptrace.c @@ -0,0 +1,215 @@ +/* + * 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 + */ + +/* + * Test program to exercise seccomp(2) and ptrace(2) intercept code. + * + * Usage: test_ptrace [-v] [command] + */ + +/* Ignore architecture restrictions and define this unilaterally. */ +#define HAVE_PTRACE_INTERCEPT +#include "exec_ptrace.c" + +static sig_atomic_t got_sigchld; +static int verbose; +int sudo_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER; + +sudo_dso_public int main(int argc, char *argv[]); + +static void +handler(int signo) +{ + if (signo == SIGCHLD) + got_sigchld = 1; +} + +void +intercept_closure_reset(struct intercept_closure *closure) +{ + memset(closure, 0, sizeof(*closure)); +} + +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; + struct stat sb1, sb2; + bool is_denied; + debug_decl(intercept_check_policy, SUDO_DEBUG_EXEC); + + /* Fake policy decisions. */ + is_denied = stat(command, &sb1) == 0 && stat("/usr/bin/who", &sb2) == 0 && + sb1.st_ino == sb2.st_ino && sb1.st_dev == sb2.st_dev; + if (is_denied) { + sudo_debug_printf(SUDO_DEBUG_DIAG, "denied %s", command); + closure->state = POLICY_REJECT; + } else { + sudo_debug_printf(SUDO_DEBUG_DIAG, "allowed %s", command); + closure->state = POLICY_TEST; + } + + debug_return_bool(true); +} + +static void +init_debug_files(struct sudo_conf_debug_file_list *file_list, + struct sudo_debug_file *file) +{ + debug_decl(init_debug_files, SUDO_DEBUG_EXEC); + + TAILQ_INIT(file_list); + switch (verbose) { + case 0: + file->debug_flags = NULL; + break; + case 1: + file->debug_flags = "exec@diag"; + break; + case 2: + file->debug_flags = "exec@info"; + break; + default: + file->debug_flags = "exec@debug"; + break; + } + file->debug_file = "/dev/stderr"; + TAILQ_INSERT_HEAD(file_list, file, entries); + + debug_return; +} + +int +main(int argc, char *argv[]) +{ + struct sudo_conf_debug_file_list debug_files; + struct sudo_debug_file debug_file; + const char *base, *shell = _PATH_SUDO_BSHELL; + struct intercept_closure closure = { 0 }; + sigset_t blocked, empty; + struct sigaction sa; + pid_t child, pid; + int ch, status; + debug_decl_vars(main, SUDO_DEBUG_MAIN); + + initprogname(argc > 0 ? argv[0] : "test_ptrace"); + + while ((ch = getopt(argc, argv, "v")) != -1) { + switch (ch) { + case 'v': + verbose++; + break; + default: + fprintf(stderr, "usage: %s [-v] [command]\n", getprogname()); + return EXIT_FAILURE; + } + } + argc -= optind; + argv += optind; + + if (argc > 0) + shell = argv[0]; + base = strrchr(shell, '/'); + base = base ? base + 1 : shell; + + /* Set debug level based on the verbose flag. */ + init_debug_files(&debug_files, &debug_file); + sudo_debug_instance = sudo_debug_register(getprogname(), + NULL, NULL, &debug_files, -1); + if (sudo_debug_instance == SUDO_DEBUG_INSTANCE_ERROR) + return EXIT_FAILURE; + + /* Block SIGCHLD and SIGUSR during critical section. */ + sigemptyset(&empty); + sigemptyset(&blocked); + sigaddset(&blocked, SIGCHLD); + sigaddset(&blocked, SIGUSR1); + sigprocmask(SIG_BLOCK, &blocked, NULL); + + /* Signal handler sets a flag for SIGCHLD, nothing for SIGUSR1. */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = handler; + sigaction(SIGCHLD, &sa, NULL); + sigaction(SIGUSR1, &sa, NULL); + + /* Fork a shell. */ + child = fork(); + switch (child) { + case -1: + sudo_fatal("fork"); + case 0: + /* child */ + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) + sudo_fatal("%s", "unable to set no_new_privs bit"); + if (!set_exec_filter()) + _exit(EXIT_FAILURE); + + /* Suspend child until tracer seizes control and sends SIGUSR1. */ + sigsuspend(&empty); + execl(shell, base, NULL); + sudo_fatal("execl"); + default: + /* Parent attaches to child and allows it to continue. */ + if (exec_ptrace_seize(child) == -1) + return EXIT_FAILURE; + break; + } + + /* Wait for SIGCHLD. */ + for (;;) { + sigsuspend(&empty); + if (!got_sigchld) + continue; + got_sigchld = 0; + + for (;;) { + do { + pid = waitpid(-1, &status, __WALL|WNOHANG); + } while (pid == -1 && errno == EINTR); + if (pid <= 0) { + if (pid == -1 && errno != ECHILD) + sudo_fatal("waitpid"); + /* No child to wait for. */ + break; + } + + if (WIFEXITED(status)) { + sudo_debug_printf(SUDO_DEBUG_DIAG, "%d: exited %d", + pid, WEXITSTATUS(status)); + if (pid == child) + return WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + sudo_debug_printf(SUDO_DEBUG_DIAG, "%d: killed by signal %d", + pid, WTERMSIG(status)); + if (pid == child) + return WTERMSIG(status) | 128; + } else if (WIFSTOPPED(status)) { + /* XXX - verbose info to stderr, not just debug log */ + exec_ptrace_handled(pid, status, &closure); + } else { + sudo_fatalx("%d: unknown status 0x%x", pid, status); + } + } + } +} diff --git a/src/sudo_exec.h b/src/sudo_exec.h index 9f1dbb737..8b6b422d0 100644 --- a/src/sudo_exec.h +++ b/src/sudo_exec.h @@ -97,7 +97,9 @@ union sudo_token_un { #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 +# ifndef HAVE_PTRACE_INTERCEPT +# define HAVE_PTRACE_INTERCEPT 1 +# endif /* HAVE_PTRACE_INTERCEPT */ # endif /* __amd64__ || __i386__ || __aarch64__ */ # endif /* HAVE_DECL_SECCOMP_SET_MODE_FILTER */ #endif /* _PATH_SUDO_INTERCEPT && __linux__ */