]> granicus.if.org Git - sudo/commitdiff
systrace(4) support for sudo. On systems with the systrace(4) kernel
authorTodd C. Miller <Todd.Miller@courtesan.com>
Fri, 24 Sep 2004 17:30:23 +0000 (17:30 +0000)
committerTodd C. Miller <Todd.Miller@courtesan.com>
Fri, 24 Sep 2004 17:30:23 +0000 (17:30 +0000)
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.

mon_systrace.c [new file with mode: 0644]

diff --git a/mon_systrace.c b/mon_systrace.c
new file mode 100644 (file)
index 0000000..b99376d
--- /dev/null
@@ -0,0 +1,594 @@
+/*
+ * Copyright (c) 2004 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * 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 <sys/types.h>
+#include <sys/param.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <sys/syscall.h>
+#include <sys/ioctl.h>
+#ifdef HAVE_DEV_SYSTRACE_H
+# include <dev/systrace.h>
+#else
+# ifdef HAVE_SYS_SYSTRACE_H
+#  include <sys/systrace.h>
+# else
+#  include <systrace.h>
+# endif
+#endif
+#include <stdio.h>
+#ifdef STDC_HEADERS
+# include <stdlib.h>
+# include <stddef.h>
+#else
+# ifdef HAVE_STDLIB_H
+#  include <stdlib.h>
+# endif
+#endif /* STDC_HEADERS */
+#ifdef HAVE_STRING_H
+# include <string.h>
+#else
+# ifdef HAVE_STRINGS_H
+#  include <strings.h>
+# endif
+#endif /* HAVE_STRING_H */
+#ifdef HAVE_UNISTD_H
+# include <unistd.h>
+#endif /* HAVE_UNISTD_H */
+#ifdef HAVE_ERR_H
+# include <err.h>
+#else
+# include "emul/err.h"
+#endif /* HAVE_ERR_H */
+#include <pwd.h>
+#include <signal.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <time.h>
+
+#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);
+}