From: Todd C. Miller Date: Fri, 24 Sep 2004 17:30:23 +0000 (+0000) Subject: systrace(4) support for sudo. On systems with the systrace(4) kernel X-Git-Tag: SUDO_1_7_0~957 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=d492138ff0fc302b24f8c3cdc51b0ac6c471b1ac;p=sudo systrace(4) support for sudo. On systems with the systrace(4) kernel facility (OpenBSD, NetBSD, Linux w/ patches) sudo can intercept exec calls and check the exec args against the sudoers file. In other words, sudo can now control subcommands and shell escapes. --- diff --git a/mon_systrace.c b/mon_systrace.c new file mode 100644 index 000000000..b99376dfe --- /dev/null +++ b/mon_systrace.c @@ -0,0 +1,594 @@ +/* + * Copyright (c) 2004 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. + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_DEV_SYSTRACE_H +# include +#else +# ifdef HAVE_SYS_SYSTRACE_H +# include +# else +# include +# endif +#endif +#include +#ifdef STDC_HEADERS +# include +# include +#else +# ifdef HAVE_STDLIB_H +# include +# endif +#endif /* STDC_HEADERS */ +#ifdef HAVE_STRING_H +# include +#else +# ifdef HAVE_STRINGS_H +# include +# endif +#endif /* HAVE_STRING_H */ +#ifdef HAVE_UNISTD_H +# include +#endif /* HAVE_UNISTD_H */ +#ifdef HAVE_ERR_H +# include +#else +# include "emul/err.h" +#endif /* HAVE_ERR_H */ +#include +#include +#include +#include +#include + +#include "sudo.h" + +#ifndef lint +static const char rcsid[] = "$Sudo$"; +#endif /* lint */ + +struct listhead { + void *first; +}; +struct childinfo { + pid_t pid; + uid_t uid; + struct childinfo *next; +}; +struct syscallhandler { + int num; + int (*handler) __P((int, int *, struct str_message *)); + struct syscallhandler *next; +}; + +int check_exec __P((int, int *, struct str_message *)); +int check_syscall __P((int, int *, int, struct str_message *, + struct listhead *)); +int decode_args __P((int, struct str_message *)); +int set_policy __P((int, pid_t, struct listhead *)); +int systrace_open __P((void)); +int systrace_read __P((int, pid_t, void *, void *, size_t)); +int systrace_run __P((char *, char **, int)); +ssize_t read_string __P((int, pid_t, void *, char *, size_t)); +void new_child __P((struct listhead *, pid_t, uid_t)); +void new_handler __P((struct listhead *, int, + int (*)(int, int *, struct str_message *))); +void rm_child __P((struct listhead *, pid_t)); +void update_child __P((struct listhead *, pid_t, uid_t)); + +/* + * Open the systrace device and return the fd or -1 on failure. + * XXX - warn here on error or in caller? + */ +int +systrace_open() +{ + int serrno, fd; + + fd = open(_PATH_DEV_SYSTRACE, O_RDONLY, 0644); + if (fd == -1) + return(-1); + serrno = errno; + +#ifdef SYSTR_CLONE + { + int tfd; + if (ioctl(fd, STRIOCCLONE, &tfd) == -1) + goto bad; + close(fd); + fd = tfd; + } +#endif + if (fcntl(fd, F_SETFD, 1) == -1) /* really needed? */ + goto bad; + + return(fd); +bad: + close(fd); + errno = serrno; + return(-1); +} + +void +sigusr1(signo) + int signo; +{ + return; +} + +/* + * Fork a process that traces the command to be run and its descendents. + * + * TODO: + * note euid changes and update runas info + * set SUDO_* env variables for sub-execs + */ +void +systrace_attach(pid) + pid_t pid; +{ + struct systrace_answer ans; + struct str_message msg; + struct listhead children, handlers; + sigaction_t sa, osa; + sigset_t set, oset; + ssize_t nread; + int fd, initialized = 0; + + fflush(stdout); + if ((fd = systrace_open()) == -1) + err(1, "unable to open systrace"); + + /* + * Do signal setup early so there is no race between when the tracer + * kill()s the tracee and when the tracee calls sigsuspend(). + */ + sigfillset(&set); + if (sigprocmask(SIG_BLOCK, &set, &oset) != 0) + err(1, "sigprocmask"); + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_handler = sigusr1; + if (sigaction(SIGUSR1, &sa, &osa) != 0) + err(1, "sigaction"); + + switch (fork()) { + case -1: + err(1, "can't fork"); + case 0: + /* tracer */ + break; + default: + /* tracee, sleep until the tracer process wakes us up. */ + sigdelset(&set, SIGUSR1); + (void) sigsuspend(&set); + if (sigprocmask(SIG_SETMASK, &oset, NULL) != 0) { + warn("sigprocmask"); + exit(1); + } + return; + } + + /* reset signal state for tracer */ + if (sigaction(SIGUSR1, &osa, NULL) != 0 || + sigprocmask(SIG_SETMASK, &oset, NULL) != 0) + goto fail; + + /* become a daemon */ + if (setsid() == -1) { + warn("setsid"); + kill(pid, SIGKILL); + _exit(1); + } + (void) chdir("/"); +#ifdef HAVE_SETPROCTITLE + setproctitle("systrace %s %s", user_base, user_args); +#endif + + children.first = NULL; + new_child(&children, pid, runas_pw->pw_uid); + + /* + * Open systrace device and set a policy to generate + * ask events when the traced process does an exec. + */ + if (ioctl(fd, STRIOCATTACH, &pid) == -1 || set_policy(fd, pid, &handlers) != 0) + goto fail; + + if (kill(pid, SIGUSR1) != 0) { + warn("unable to wake up sleeping child"); + _exit(1); + } + + /* handle systrace events until the child finishes */ + for (;;) { + nread = read(fd, &msg, sizeof(msg)); + if (nread != sizeof(msg)) { + if (nread == -1 && (errno == EINTR || errno == EAGAIN)) + continue; + kill(pid, SIGKILL); /* XXX - kill all pids in list */ + _exit(nread != 0); /* shouldn't happen */ + } + + switch (msg.msg_type) { + case SYSTR_MSG_CHILD: + /* either a fork or an exit */ + if (msg.msg_data.msg_child.new_pid != -1) { + /* XXX - runas_pw->pw_uid may be wrong */ + new_child(&children, msg.msg_data.msg_child.new_pid, + runas_pw->pw_uid); + } else { + rm_child(&children, msg.msg_pid); + if (children.first == NULL) + _exit(0); + } + break; + + case SYSTR_MSG_UGID: + /* uid/gid change */ + /* XXX - how is this triggered? */ + warn("new uid %d", msg.msg_data.msg_ugid.uid); + update_child(&children, msg.msg_pid, msg.msg_data.msg_ugid.uid); + break; + + case SYSTR_MSG_ASK: + memset(&ans, 0, sizeof(ans)); + ans.stra_pid = msg.msg_pid; + ans.stra_seqnr = msg.msg_seqnr; + ans.stra_policy = check_syscall(fd, &initialized, + msg.msg_data.msg_ask.code, &msg, &handlers); + if ((ioctl(fd, STRIOCANSWER, &ans)) == -1) + goto fail; + break; + + case SYSTR_MSG_EMUL: + /* + * XXX - need to redo policy if we change emulation. + * that means we need to know in advance what + * the various emulations are. + */ + warnx("change in emul"); + break; +#ifdef SUDO_DEVEL + default: + warnx("unexpected message type %d", msg.msg_type); + break; +#endif + } + } + +fail: + warn("unable to systrace %s", user_cmnd); + kill(pid, SIGKILL); /* XXX - kill all pids in list */ + _exit(1); +} + +/* + * Push a new handler to the head of the list. + */ +void +new_handler(head, num, handler) + struct listhead *head; + int num; + int (*handler) __P((int, int *, struct str_message *)); +{ + struct syscallhandler *entry; + + entry = (struct syscallhandler *) emalloc(sizeof(*entry)); + entry->num = num; + entry->handler = handler; + entry->next = head->first; + head->first = entry; +} + +/* + * Push a new child to the head of the list. + */ +void +new_child(head, pid, uid) + struct listhead *head; + pid_t pid; + uid_t uid; +{ + struct childinfo *entry; + + entry = (struct childinfo *) emalloc(sizeof(*entry)); + entry->pid = pid; + entry->uid = uid; + entry->next = head->first; + head->first = entry; +} + +/* + * Remove the named pid from the list. + */ +void +rm_child(head, pid) + struct listhead *head; + pid_t pid; +{ + struct childinfo *cur, *prev; + + for (prev = NULL, cur = head->first; cur != NULL; cur = cur->next) { + if (cur->pid == pid) { + if (prev != NULL) + prev->next = cur->next; + else + head->first = cur->next; + free(cur); + break; + } + prev = cur; + } +} + +/* + * Update the uid associated with a pid. + */ +void +update_child(head, pid, uid) + struct listhead *head; + pid_t pid; + uid_t uid; +{ + struct childinfo *cur; + + for (cur = head->first; cur != NULL; cur = cur->next) { + if (cur->pid == pid) { + cur->uid = uid; + break; + } + } +} + +/* + * Create a policy that intercepts execve and lets all others go free. + */ +int +set_policy(fd, pid, handlers) + int fd; + pid_t pid; + struct listhead *handlers; +{ + int i; + struct systrace_policy pol; + + pol.strp_op = SYSTR_POLICY_NEW; + pol.strp_num = -1; + pol.strp_maxents = SYS_MAXSYSCALL; + if (ioctl(fd, STRIOCPOLICY, &pol) == -1) + return(-1); + + for (i = 0; i < SYS_MAXSYSCALL; i++) { + pol.strp_op = SYSTR_POLICY_ASSIGN; + pol.strp_pid = pid; + if (ioctl(fd, STRIOCPOLICY, &pol) == -1) + return(-1); + + pol.strp_op = SYSTR_POLICY_MODIFY; + pol.strp_code = i; +#ifdef SYS_exec + if (i == SYS_exec) { + pol.strp_policy = SYSTR_POLICY_ASK; + new_handler(handlers, i, check_exec); + } else +#endif +#ifdef SYS_execv + if (i == SYS_execv) { + pol.strp_policy = SYSTR_POLICY_ASK; + new_handler(handlers, i, check_exec); + } else +#endif +#ifdef SYS_execve + if (i == SYS_execve) { + pol.strp_policy = SYSTR_POLICY_ASK; + new_handler(handlers, i, check_exec); + } else +#endif +#ifdef SYS_fexecve + if (i == SYS_fexecve) + pol.strp_policy = SYSTR_POLICY_NEVER; /* not checkable */ + else +#endif + pol.strp_policy = SYSTR_POLICY_PERMIT; + if (ioctl(fd, STRIOCPOLICY, &pol) == -1) + return(-1); + } + return(0); +} + +/* + * Read from an address and store in buf. + * XXX - should deal with EBUSY from STRIOCIO + */ +int +systrace_read(fd, pid, addr, buf, bufsiz) + int fd; + pid_t pid; + void *addr; + void *buf; + size_t bufsiz; +{ + struct systrace_io io; + + memset(&io, 0, sizeof(io)); + io.strio_pid = pid; + io.strio_addr = buf; + io.strio_len = bufsiz; + io.strio_offs = addr; + io.strio_op = SYSTR_READ; + return(ioctl(fd, STRIOCIO, &io)); +} + +/* + * Read up to bufsiz bytes from addr into buf, stopping when we hit + * a NUL byte. Reads are done in chunks since STRIOCIO cannot + * handle a strio_len > the actual kernel buffer. + * XXX - could pass a hint for chunksiz + * XXX - need to indicate oflow + */ +ssize_t +read_string(fd, pid, addr, buf, bufsiz) + int fd; + pid_t pid; + void *addr; + char *buf; + size_t bufsiz; +{ + size_t chunksiz = 32; + char *cp = buf, *ep; + + while (bufsiz >= chunksiz) { + if (systrace_read(fd, pid, addr, cp, chunksiz) == 0) { + if ((ep = memchr(cp, '\0', chunksiz)) != NULL) { + cp = ep; /* found NUL byte in chunk, done */ + break; + } + cp += chunksiz; + bufsiz -= chunksiz; + } else { + if (errno != EINVAL || chunksiz == 4) + return(-1); + chunksiz >>= 1; /* chunksiz too big, half it */ + } + } + *cp = '\0'; + return(cp - buf); +} + +int +check_syscall(fd, initialized, num, msgp, handlers) + int fd; + int *initialized; + int num; + struct str_message *msgp; + struct listhead *handlers; +{ + struct syscallhandler *h; + + for (h = handlers->first; h != NULL; h = h->next) { + if (h->num == num) + return(h->handler(fd, initialized, msgp)); + } + return(SYSTR_POLICY_PERMIT); /* accept unhandled syscalls */ +} + +/* + * Decode path and argv from systrace and fill in user_cmnd, + * user_base and user_args. + */ +int +decode_args(fd, msgp) + int fd; + struct str_message *msgp; +{ + size_t len; + char *off, *ap, *cp, *ep; + static char pbuf[PATH_MAX], abuf[ARG_MAX]; + + memset(pbuf, 0, sizeof(pbuf)); + if (read_string(fd, msgp->msg_pid, (void *)msgp->msg_data.msg_ask.args[0], + pbuf, sizeof(pbuf)) == -1) + return(-1); + if ((user_base = strrchr(user_cmnd = pbuf, '/')) != NULL) + user_base++; + else + user_base = user_cmnd; + user_args = NULL; + + /* + * Loop through argv, collapsing it into a single string and reading + * until we hit the terminating NULL. We skip argv[0]. + */ + off = (char *)msgp->msg_data.msg_ask.args[1]; + for (cp = abuf, ep = abuf + sizeof(abuf); cp < ep; off += sizeof(char *)) { + if (systrace_read(fd, msgp->msg_pid, off, &ap, sizeof(ap)) != 0) { + warn("STRIOCIO"); + return(-1); + } + if (ap == NULL) { + if (cp != abuf) { + cp[-1] = '\0'; /* replace final space with a NUL */ + user_args = abuf; + } + break; + } + if (off == (char *)msgp->msg_data.msg_ask.args[1]) + continue; /* skip argv[0] */ + if ((len = read_string(fd, msgp->msg_pid, ap, cp, ep - cp)) == -1) { + warn("STRIOCIO"); + return(-1); + } + cp += len; + *cp++ = ' '; /* replace NUL with a space */ + } + /* XXX - detect cp >= ep */ + return(0); +} + +/* + * Decode the args to exec and check the command in sudoers. + */ +int +check_exec(fd, initialized, msgp) + int fd; + int *initialized; + struct str_message *msgp; +{ + int validated; + + /* We're not really initialized until the first exec finishes. */ + if (*initialized == 0) { + *initialized = 1; + return(SYSTR_POLICY_PERMIT); + } + + /* Fill in user_cmnd, user_base, user_args and user_stat. */ + decode_args(fd, msgp); + if (user_cmnd[0] != '/' || !sudo_goodpath(user_cmnd, user_stat)) + return(SYSTR_POLICY_NEVER); + + /* Get processes's cwd. */ + if (ioctl(fd, STRIOCGETCWD, &msgp->msg_pid) == -1 || + !getcwd(user_cwd, sizeof(user_cwd))) { + warnx("cannot get working directory"); + (void) strlcpy(user_cwd, "unknown", sizeof(user_cwd)); + } else + (void) ioctl(fd, STRIOCRESCWD, 0); + + /* XXX - should update user_runas and _runas_pw too! */ + + /* Check sudoers and log the result. */ + init_defaults(); + def_authenticate = FALSE; + validated = sudoers_lookup(0); +#ifdef DEBUG + warnx("intercepted: %s %s in %s -> 0x%x", user_cmnd, user_args, user_cwd, validated); +#endif + log_auth(validated, 1); + if (ISSET(validated, VALIDATE_OK)) + return(SYSTR_POLICY_PERMIT); + else + return(SYSTR_POLICY_NEVER); +}