Add scaffolding for ptrace-based intercept mode.

This commit is contained in:
Todd C. Miller
2022-04-29 12:35:31 -06:00
parent 22866f2423
commit 01733a5214
10 changed files with 468 additions and 76 deletions

View File

@@ -1183,6 +1183,7 @@ src/env_hooks.c
src/exec.c src/exec.c
src/exec_common.c src/exec_common.c
src/exec_intercept.c src/exec_intercept.c
src/exec_ptrace.c
src/exec_monitor.c src/exec_monitor.c
src/exec_nopty.c src/exec_nopty.c
src/exec_preload.c src/exec_preload.c

View File

@@ -1,7 +1,7 @@
# #
# SPDX-License-Identifier: ISC # SPDX-License-Identifier: ISC
# #
# Copyright (c) 2010-2021 Todd C. Miller <Todd.Miller@sudo.ws> # Copyright (c) 2010-2022 Todd C. Miller <Todd.Miller@sudo.ws>
# #
# Permission to use, copy, modify, and distribute this software for any # Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above # purpose with or without fee is hereby granted, provided that the above
@@ -141,10 +141,10 @@ SHELL = @SHELL@
PROGS = @PROGS@ PROGS = @PROGS@
OBJS = conversation.o copy_file.o edit_open.o env_hooks.o exec.o exec_common.o \ 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 \ exec_intercept.o exec_monitor.o exec_nopty.o exec_preload.o \
get_pty.o hooks.o limits.o load_plugins.o net_ifs.o parse_args.o \ exec_ptrace.o exec_pty.o get_pty.o hooks.o limits.o load_plugins.o \
preserve_fds.o signal.o sudo.o sudo_edit.o tcsetpgrp_nobg.o tgetpass.o \ net_ifs.o parse_args.o preserve_fds.o signal.o sudo.o sudo_edit.o \
ttyname.o utmp.o @SUDO_OBJS@ tcsetpgrp_nobg.o tgetpass.o ttyname.o utmp.o @SUDO_OBJS@
IOBJS = $(OBJS:.o=.i) sesh.i 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 \ $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \
$(top_builddir)/config.h $(top_builddir)/pathnames.h $(top_builddir)/config.h $(top_builddir)/pathnames.h
$(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/exec_preload.c $(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 \ exec_pty.o: $(srcdir)/exec_pty.c $(incdir)/compat/stdbool.h \
$(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
$(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: ISC * SPDX-License-Identifier: ISC
* *
* Copyright (c) 2009-2021 Todd C. Miller <Todd.Miller@sudo.ws> * Copyright (c) 2009-2022 Todd C. Miller <Todd.Miller@sudo.ws>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * 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; bool ret = false;
debug_decl(exec_setup, SUDO_DEBUG_EXEC); 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) { if (details->pw != NULL) {
#ifdef HAVE_PROJECT_H #ifdef HAVE_PROJECT_H
set_project(details->pw); set_project(details->pw);

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: ISC * SPDX-License-Identifier: ISC
* *
* Copyright (c) 2009-2021 Todd C. Miller <Todd.Miller@sudo.ws> * Copyright (c) 2009-2022 Todd C. Miller <Todd.Miller@sudo.ws>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * 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); 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 * Like execve(2) but falls back to running through /bin/sh
* ala execvp(3) if we get ENOEXEC. * 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(). */ /* Modify the environment as needed to trap execve(). */
if (ISSET(flags, CD_NOEXEC)) if (ISSET(flags, CD_NOEXEC))
envp = disable_execute(envp, sudo_conf_noexec_path()); 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)) else if (ISSET(flags, CD_INTERCEPT|CD_LOG_SUBCMDS))
envp = enable_intercept(envp, sudo_conf_intercept_path(), intercept_fd); envp = enable_intercept(envp, sudo_conf_intercept_path(), intercept_fd);

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: ISC * SPDX-License-Identifier: ISC
* *
* Copyright (c) 2021 Todd C. Miller <Todd.Miller@sudo.ws> * Copyright (c) 2021-2022 Todd C. Miller <Todd.Miller@sudo.ws>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * 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 command_details *details)
{ {
struct intercept_closure *closure; struct intercept_closure *closure;
int rc;
debug_decl(intercept_setup, SUDO_DEBUG_EXEC); debug_decl(intercept_setup, SUDO_DEBUG_EXEC);
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, 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")); sudo_warnx("%s", U_("unable to allocate memory"));
goto bad; 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->details = details;
closure->listen_sock = -1; closure->listen_sock = -1;
if (sudo_ev_set(&closure->ev, fd, SUDO_EV_READ|SUDO_EV_PERSIST, intercept_cb, closure) == -1) { if (ISSET(details->flags, CD_USE_PTRACE)) {
/* This cannot (currently) fail. */ /* We can perform a policy check immediately using ptrace(2). */
sudo_warn("%s", U_("unable to add event to queue")); closure->state = RECV_POLICY_CHECK;
goto bad; } 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")); sudo_warn("%s", U_("unable to add event to queue"));
goto bad; goto bad;
} }
@@ -124,21 +130,18 @@ bad:
} }
/* /*
* Close intercept socket and free closure when we are done with * Reset intercept_closure so it can be re-used.
* the connection.
*/ */
static void 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; 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); if (closure->listen_sock != -1) {
close(fd);
if (closure->listen_sock != -1)
close(closure->listen_sock); close(closure->listen_sock);
closure->listen_sock = -1;
}
free(closure->buf); free(closure->buf);
free(closure->command); free(closure->command);
if (closure->run_argv != NULL) { 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[n]);
free(closure->run_envp); 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); free(closure);
debug_return; debug_return;
@@ -305,7 +333,7 @@ intercept_check_policy(PolicyCheckRequest *req,
char **user_env_out = NULL; char **user_env_out = NULL;
char **argv = NULL, **run_argv = NULL; char **argv = NULL, **run_argv = NULL;
bool ret = false; bool ret = false;
int result; int rc;
size_t n; size_t n;
debug_decl(intercept_check_policy, SUDO_DEBUG_EXEC); debug_decl(intercept_check_policy, SUDO_DEBUG_EXEC);
@@ -338,13 +366,13 @@ intercept_check_policy(PolicyCheckRequest *req,
if (ISSET(closure->details->flags, CD_INTERCEPT)) { if (ISSET(closure->details->flags, CD_INTERCEPT)) {
/* We don't currently have a good way to validate the environment. */ /* We don't currently have a good way to validate the environment. */
sudo_debug_set_active_instance(policy_plugin.debug_instance); 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); &command_info, &run_argv, &user_env_out, &closure->errstr);
sudo_debug_set_active_instance(sudo_debug_instance); sudo_debug_set_active_instance(sudo_debug_instance);
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, 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: case 1:
/* Rebuild command_info[] with runcwd and extract command. */ /* Rebuild command_info[] with runcwd and extract command. */
command_info_copy = update_command_info(command_info, NULL, command_info_copy = update_command_info(command_info, NULL,
@@ -475,7 +503,7 @@ static int
intercept_verify_token(int fd, struct intercept_closure *closure) intercept_verify_token(int fd, struct intercept_closure *closure)
{ {
ssize_t nread; 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, nread = recv(fd, closure->token.u8 + closure->off,
sizeof(closure->token) - closure->off, 0); 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); struct sudo_event_base *evbase = sudo_ev_get_base(&closure->ev);
InterceptRequest *req = NULL; InterceptRequest *req = NULL;
ssize_t nread;
bool ret = false; bool ret = false;
ssize_t nread;
int rc;
debug_decl(intercept_read, SUDO_DEBUG_EXEC); debug_decl(intercept_read, SUDO_DEBUG_EXEC);
if (closure->state == RECV_SECRET) { if (closure->state == RECV_SECRET) {
@@ -641,12 +670,9 @@ unpack:
} }
/* Switch event to write mode for the reply. */ /* 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) { rc = sudo_ev_set(&closure->ev, fd, SUDO_EV_WRITE|SUDO_EV_PERSIST,
/* This cannot (currently) fail. */ intercept_cb, closure);
sudo_warn("%s", U_("unable to add event to queue")); if (rc == -1 || sudo_ev_add(evbase, &closure->ev, NULL, false) == -1) {
goto done;
}
if (sudo_ev_add(evbase, &closure->ev, NULL, false) == -1) {
sudo_warn("%s", U_("unable to add event to queue")); sudo_warn("%s", U_("unable to add event to queue"));
goto done; goto done;
} }
@@ -770,8 +796,9 @@ static bool
intercept_write(int fd, struct intercept_closure *closure) intercept_write(int fd, struct intercept_closure *closure)
{ {
struct sudo_event_base *evbase = sudo_ev_get_base(&closure->ev); struct sudo_event_base *evbase = sudo_ev_get_base(&closure->ev);
ssize_t nwritten;
bool ret = false; bool ret = false;
ssize_t nwritten;
int rc;
debug_decl(intercept_write, SUDO_DEBUG_EXEC); debug_decl(intercept_write, SUDO_DEBUG_EXEC);
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "state %d", 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->len = 0;
closure->off = 0; closure->off = 0;
switch (closure->state) { if (ISSET(closure->details->flags, CD_USE_PTRACE)) {
case RECV_HELLO_INITIAL: /* Ready for the next policy check from the tracer. */
/* Re-use event for the listener. */ closure->state = RECV_POLICY_CHECK;
close(fd); } else {
if (sudo_ev_set(&closure->ev, closure->listen_sock, SUDO_EV_READ|SUDO_EV_PERSIST, intercept_accept_cb, closure) == -1) { switch (closure->state) {
/* This cannot (currently) fail. */ case RECV_HELLO_INITIAL:
sudo_warn("%s", U_("unable to add event to queue")); /* Re-use event for the listener. */
goto done; 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; ret = true;

View File

@@ -371,10 +371,13 @@ exec_nopty(struct command_details *details, struct command_status *cstat)
/* /*
* Allocate a socketpair for communicating with sudo_intercept.so. * Allocate a socketpair for communicating with sudo_intercept.so.
* This must be inherited across exec, hence no FD_CLOEXEC. * 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 (ISSET(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) {
if (socketpair(PF_UNIX, SOCK_STREAM, 0, intercept_sv) == -1) if (socketpair(PF_UNIX, SOCK_STREAM, 0, intercept_sv) == -1)
sudo_fatal("%s", U_("unable to create sockets")); 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); 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. */ /* Restore signal mask now that signal handlers are setup. */
sigprocmask(SIG_SETMASK, &oset, NULL); sigprocmask(SIG_SETMASK, &oset, NULL);
@@ -487,10 +495,16 @@ static void
handle_sigchld_nopty(struct exec_closure_nopty *ec) handle_sigchld_nopty(struct exec_closure_nopty *ec)
{ {
pid_t pid; pid_t pid;
int status, wflags = WUNTRACED|WNOHANG; int status, wflags;
char signame[SIG2STR_MAX]; char signame[SIG2STR_MAX];
debug_decl(handle_sigchld_nopty, SUDO_DEBUG_EXEC); 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. */ /* There may be multiple children in intercept mode. */
for (;;) { for (;;) {
/* Read command status. */ /* Read command status. */
@@ -519,6 +533,12 @@ handle_sigchld_nopty(struct exec_closure_nopty *ec)
sudo_debug_printf(SUDO_DEBUG_INFO, sudo_debug_printf(SUDO_DEBUG_INFO,
"%s: command (%d) stopped, SIG%s", __func__, (int)pid, signame); "%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. */ /* Only report status for the main command. */
if (ec->cmnd_pid != pid) if (ec->cmnd_pid != pid)
continue; continue;

258
src/exec_ptrace.c Normal file
View File

@@ -0,0 +1,258 @@
/*
* Copyright (c) 2022 Todd C. Miller <Todd.Miller@sudo.ws>
*
* 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 <config.h>
#include <sys/wait.h>
#include <errno.h>
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include "sudo.h"
#include "sudo_exec.h"
#ifdef HAVE_PTRACE_INTERCEPT
# include <sys/prctl.h>
# include <sys/ptrace.h>
# include <sys/user.h>
# include <asm/unistd.h>
# include <linux/ptrace.h>
# include <linux/seccomp.h>
# include <linux/filter.h>
/*
* 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 */

View File

@@ -992,6 +992,10 @@ backchannel_cb(int fd, int what, void *v)
ec->cmnd_pid = cstat.val; ec->cmnd_pid = cstat.val;
sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d", sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d",
ec->details->command, (int)ec->cmnd_pid); 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; break;
case CMD_WSTATUS: case CMD_WSTATUS:
if (WIFSTOPPED(cstat.val)) { if (WIFSTOPPED(cstat.val)) {
@@ -1041,10 +1045,16 @@ backchannel_cb(int fd, int what, void *v)
static void static void
handle_sigchld_pty(struct exec_closure_pty *ec) handle_sigchld_pty(struct exec_closure_pty *ec)
{ {
int n, status, wflags = WUNTRACED|WNOHANG; int n, status, wflags;
pid_t pid; pid_t pid;
debug_decl(handle_sigchld_pty, SUDO_DEBUG_EXEC); 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. */ /* There may be multiple children in intercept mode. */
for (;;) { for (;;) {
do { do {
@@ -1077,8 +1087,11 @@ handle_sigchld_pty(struct exec_closure_pty *ec)
if (pid == ec->monitor_pid) if (pid == ec->monitor_pid)
ec->monitor_pid = -1; ec->monitor_pid = -1;
} else if (WIFSTOPPED(status)) { } 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; continue;
}
/* /*
* If the monitor dies we get notified via backchannel_cb(). * 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. * Allocate a socketpair for communicating with sudo_intercept.so.
* This must be inherited across exec, hence no FD_CLOEXEC. * 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 (ISSET(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) {
if (socketpair(PF_UNIX, SOCK_STREAM, 0, intercept_sv) == -1) if (socketpair(PF_UNIX, SOCK_STREAM, 0, intercept_sv) == -1)
sudo_fatal("%s", U_("unable to create sockets")); sudo_fatal("%s", U_("unable to create sockets"));
if (have_seccomp_action("trap"))
SET(details->flags, CD_USE_PTRACE);
} }
/* /*

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: ISC * SPDX-License-Identifier: ISC
* *
* Copyright (c) 1993-1996, 1998-2005, 2007-2021 * Copyright (c) 1993-1996, 1998-2005, 2007-2022
* Todd C. Miller <Todd.Miller@sudo.ws> * Todd C. Miller <Todd.Miller@sudo.ws>
* *
* Permission to use, copy, modify, and distribute this software for any * 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_LOGIN_SHELL 0x080000
#define CD_OVERRIDE_UMASK 0x100000 #define CD_OVERRIDE_UMASK 0x100000
#define CD_LOG_SUBCMDS 0x200000 #define CD_LOG_SUBCMDS 0x200000
#define CD_USE_PTRACE 0x400000
struct preserved_fd { struct preserved_fd {
TAILQ_ENTRY(preserved_fd) entries; TAILQ_ENTRY(preserved_fd) entries;

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: ISC * SPDX-License-Identifier: ISC
* *
* Copyright (c) 2010-2017, 2020-2021 Todd C. Miller <Todd.Miller@sudo.ws> * Copyright (c) 2010-2017, 2020-2022 Todd C. Miller <Todd.Miller@sudo.ws>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * 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]) #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 * 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 */ /* exec_preload.c */
char **sudo_preload_dso(char *envp[], const char *dso_file, int intercept_fd); 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 */ #endif /* SUDO_EXEC_H */