/* $OpenBSD: intercept.c,v 1.54 2007/05/15 19:42:46 sturm Exp $ */ /* * Copyright 2002 Niels Provos * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * This product includes software developed by Niels Provos. * 4. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "intercept.h" void simplify_path(char *); struct intercept_syscall { SPLAY_ENTRY(intercept_syscall) node; char name[64]; char emulation[16]; short (*cb)(int, pid_t, int, const char *, int, const char *, void *, int, struct intercept_replace *, struct intercept_tlq *, void *); void *cb_arg; struct intercept_tlq tls; }; static int sccompare(struct intercept_syscall *, struct intercept_syscall *); static int pidcompare(struct intercept_pid *, struct intercept_pid *); static struct intercept_syscall *intercept_sccb_find(const char *, const char *); static void sigusr1_handler(int); static SPLAY_HEAD(pidtree, intercept_pid) pids; static SPLAY_HEAD(sctree, intercept_syscall) scroot; static volatile int got_sigusr1 = 0; /* Generic callback functions */ void (*intercept_newimagecb)(int, pid_t, int, const char *, const char *, void *) = NULL; void *intercept_newimagecbarg = NULL; short (*intercept_gencb)(int, pid_t, int, const char *, int, const char *, void *, int, void *) = NULL; void *intercept_gencbarg = NULL; void (*intercept_pfreecb)(int, void*); void *intercept_pfreearg = NULL; int sccompare(struct intercept_syscall *a, struct intercept_syscall *b) { int diff; diff = strcmp(a->emulation, b->emulation); if (diff) return (diff); return (strcmp(a->name, b->name)); } int pidcompare(struct intercept_pid *a, struct intercept_pid *b) { int diff = a->pid - b->pid; if (diff == 0) return (0); if (diff > 0) return (1); return (-1); } SPLAY_PROTOTYPE(sctree, intercept_syscall, node, sccompare) SPLAY_GENERATE(sctree, intercept_syscall, node, sccompare) SPLAY_PROTOTYPE(pidtree, intercept_pid, next, pidcompare) SPLAY_GENERATE(pidtree, intercept_pid, next, pidcompare) extern struct intercept_system intercept; int ic_abort; int intercept_init(void) { SPLAY_INIT(&pids); SPLAY_INIT(&scroot); intercept_newimagecb = NULL; intercept_gencb = NULL; return (intercept.init()); } struct intercept_syscall * intercept_sccb_find(const char *emulation, const char *name) { struct intercept_syscall tmp; strlcpy(tmp.name, name, sizeof(tmp.name)); strlcpy(tmp.emulation, emulation, sizeof(tmp.emulation)); return (SPLAY_FIND(sctree, &scroot, &tmp)); } struct intercept_translate * intercept_register_translation(char *emulation, char *name, int offset, struct intercept_translate *tl) { struct intercept_syscall *tmp; struct intercept_translate *tlnew; if (offset >= INTERCEPT_MAXSYSCALLARGS) errx(1, "%s: %s-%s: offset too large", __func__, emulation, name); tmp = intercept_sccb_find(emulation, name); if (tmp == NULL) errx(1, "%s: %s-%s: can't find call back", __func__, emulation, name); tlnew = malloc(sizeof(struct intercept_translate)); if (tlnew == NULL) err(1, "%s: %s-%s: malloc", __func__, emulation, name); memcpy(tlnew, tl, sizeof(struct intercept_translate)); tlnew->off = offset; TAILQ_INSERT_TAIL(&tmp->tls, tlnew, next); return (tlnew); } void * intercept_sccb_cbarg(char *emulation, char *name) { struct intercept_syscall *tmp; if ((tmp = intercept_sccb_find(emulation, name)) == NULL) return (NULL); return (tmp->cb_arg); } int intercept_register_sccb(char *emulation, char *name, short (*cb)(int, pid_t, int, const char *, int, const char *, void *, int, struct intercept_replace *, struct intercept_tlq *, void *), void *cbarg) { struct intercept_syscall *tmp; if (intercept_sccb_find(emulation, name)) return (-1); if (intercept.getsyscallnumber(emulation, name) == -1) { warnx("%s: %d: unknown syscall: %s-%s", __func__, __LINE__, emulation, name); return (-1); } if ((tmp = calloc(1, sizeof(struct intercept_syscall))) == NULL) { warn("%s:%d: malloc", __func__, __LINE__); return (-1); } TAILQ_INIT(&tmp->tls); strlcpy(tmp->name, name, sizeof(tmp->name)); strlcpy(tmp->emulation, emulation, sizeof(tmp->emulation)); tmp->cb = cb; tmp->cb_arg = cbarg; SPLAY_INSERT(sctree, &scroot, tmp); return (0); } int intercept_register_gencb(short (*cb)(int, pid_t, int, const char *, int, const char *, void *, int, void *), void *arg) { intercept_gencb = cb; intercept_gencbarg = arg; return (0); } int intercept_register_execcb(void (*cb)(int, pid_t, int, const char *, const char *, void *), void *arg) { intercept_newimagecb = cb; intercept_newimagecbarg = arg; return (0); } int intercept_register_pfreecb(void (*cb)(int, void *), void *arg) { intercept_pfreecb = cb; intercept_pfreearg = arg; return (0); } /* ARGSUSED */ static void sigusr1_handler(int signum) { /* all we need to do is pretend to handle it */ got_sigusr1 = 1; } void intercept_setpid(struct intercept_pid *icpid, uid_t uid, gid_t gid) { struct passwd *pw; icpid->uid = uid; icpid->gid = gid; if ((pw = getpwuid(icpid->uid)) == NULL) { snprintf(icpid->username, sizeof(icpid->username), "unknown(%d)", icpid->uid); strlcpy(icpid->home, "/var/empty", sizeof(icpid->home)); } else { strlcpy(icpid->username, pw->pw_name, sizeof(icpid->username)); strlcpy(icpid->home, pw->pw_dir, sizeof(icpid->home)); } } pid_t intercept_run(int bg, int fd, uid_t uid, gid_t gid, char *path, char *const argv[]) { struct intercept_pid *icpid; sigset_t none, set, oset; sig_t ohandler; pid_t pid, cpid; int status; /* Block signals so that timing on signal delivery does not matter */ sigemptyset(&none); sigemptyset(&set); sigaddset(&set, SIGUSR1); if (sigprocmask(SIG_BLOCK, &set, &oset) == -1) err(1, "sigprocmask"); ohandler = signal(SIGUSR1, sigusr1_handler); if (ohandler == SIG_ERR) err(1, "signal"); pid = getpid(); cpid = fork(); if (cpid == -1) return (-1); /* * If the systrace process should be in the background and we're * the parent, or vice versa. */ if ((!bg && cpid == 0) || (bg && cpid != 0)) { /* Needs to be closed */ close(fd); if (bg) { /* Wait for child to "detach" */ cpid = wait(&status); if (cpid == -1) err(1, "wait"); if (status != 0) errx(1, "wait: child gave up"); } /* Sleep */ sigsuspend(&none); if (!got_sigusr1) errx(1, "wrong signal"); /* * Woken up, restore signal handling state. * * Note that there is either no child or we have no idea * what pid it might have at this point. If we fail. */ if (signal(SIGUSR1, ohandler) == SIG_ERR) err(1, "signal"); if (sigprocmask(SIG_SETMASK, &oset, NULL) == -1) err(1, "sigprocmask"); /* Change to different user */ if (uid || gid) { if (setresgid(gid, gid, gid) == -1) err(1, "setresgid"); if (setgroups(1, &gid) == -1) err(1, "setgroups"); if (setresuid(uid, uid, uid) == -1) err(1, "setresuid"); } execvp(path, argv); /* Error */ err(1, "execvp"); } /* Choose the pid of the systraced process */ pid = bg ? pid : cpid; icpid = intercept_getpid(pid); /* Set up user related information */ if (!uid && !gid) { uid = getuid(); gid = getgid(); } intercept_setpid(icpid, uid, gid); /* Setup done, restore signal handling state */ if (signal(SIGUSR1, ohandler) == SIG_ERR) { kill(pid, SIGKILL); err(1, "signal"); } if (sigprocmask(SIG_SETMASK, &oset, NULL) == -1) { kill(pid, SIGKILL); err(1, "sigprocmask"); } if (bg) { if (daemon(1, 1) == -1) { kill(pid, SIGKILL); err(1, "daemon"); } } return (pid); } int intercept_existpids(void) { return (SPLAY_ROOT(&pids) != NULL); } void intercept_freepid(pid_t pidnr) { struct intercept_pid *pid, tmp2; tmp2.pid = pidnr; pid = SPLAY_FIND(pidtree, &pids, &tmp2); if (pid == NULL) return; intercept.freepid(pid); SPLAY_REMOVE(pidtree, &pids, pid); if (pid->name) free(pid->name); if (pid->newname) free(pid->newname); free(pid); } struct intercept_pid * intercept_findpid(pid_t pid) { struct intercept_pid *tmp, tmp2; tmp2.pid = pid; tmp = SPLAY_FIND(pidtree, &pids, &tmp2); return (tmp); } struct intercept_pid * intercept_getpid(pid_t pid) { struct intercept_pid *tmp, tmp2; tmp2.pid = pid; tmp = SPLAY_FIND(pidtree, &pids, &tmp2); if (tmp) return (tmp); if ((tmp = malloc(sizeof(struct intercept_pid))) == NULL) err(1, "%s: malloc", __func__); memset(tmp, 0, sizeof(struct intercept_pid)); tmp->pid = pid; SPLAY_INSERT(pidtree, &pids, tmp); return (tmp); } int intercept_open(void) { int fd; if ((fd = intercept.open()) == -1) return (-1); if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) warn("fcntl(O_NONBLOCK)"); return (fd); } int intercept_attach(int fd, pid_t pid) { return (intercept.attach(fd, pid)); } int intercept_attachpid(int fd, pid_t pid, char *name) { struct intercept_pid *icpid; int res; res = intercept.attach(fd, pid); if (res == -1) return (-1); icpid = intercept_getpid(pid); if ((icpid->newname = strdup(name)) == NULL) err(1, "strdup"); if (intercept.report(fd, pid) == -1) return (-1); /* Indicates a running attach */ icpid->execve_code = -1; return (0); } int intercept_detach(int fd, pid_t pid) { int res; res = intercept.detach(fd, pid); if (res != -1) intercept_freepid(pid); return (res); } int intercept_read(int fd) { struct pollfd pollfd; int n; pollfd.fd = fd; pollfd.events = POLLIN; do { n = poll(&pollfd, 1, -1); if (n == -1) { if (errno != EINTR && errno != EAGAIN) return (-1); } } while (n <= 0); if (!(pollfd.revents & (POLLIN|POLLRDNORM))) return (-1); return (intercept.read(fd)); } int intercept_replace_init(struct intercept_replace *repl) { memset(repl, 0, sizeof(struct intercept_replace)); return (0); } int intercept_replace_add(struct intercept_replace *repl, int off, u_char *addr, size_t len, u_int flags) { int ind = repl->num; if (ind >= INTERCEPT_MAXSYSCALLARGS) return (-1); repl->ind[ind] = off; repl->address[ind] = addr; repl->len[ind] = len; repl->flags[ind] = flags; repl->num++; return (0); } int intercept_replace(int fd, pid_t pid, u_int16_t seqnr, struct intercept_replace *repl) { if (repl->num == 0) return (0); return (intercept.replace(fd, pid, seqnr, repl)); } char * intercept_get_string(int fd, pid_t pid, void *addr) { static char name[8192]; int off = 0, done = 0, stride; if (addr == NULL) return (NULL); stride = 32; do { if (intercept.io(fd, pid, INTERCEPT_READ, (char *)addr + off, &name[off], stride) == -1) { /* Did the current system call get interrupted? */ if (errno == EBUSY) return (NULL); if (errno != EINVAL || stride == 1) { warn("%s: ioctl", __func__); return (NULL); } /* Try smaller stride */ stride /= 2; continue; } off += stride; name[off] = '\0'; if (strlen(name) < off) done = 1; } while (!done && off + stride + 1 < sizeof(name)); if (!done) { warnx("%s: string too long", __func__); return (NULL); } return (name); } char * intercept_filename(int fd, pid_t pid, void *addr, int userp, char *before) { char *name; if ((name = intercept_get_string(fd, pid, addr)) == NULL) goto abort; if (before != NULL) strlcpy(before, name, MAXPATHLEN); if ((name = normalize_filename(fd, pid, name, userp)) == NULL) goto abort; return (name); abort: ic_abort = 1; return (NULL); } /* * Normalizes a pathname so that Systrace policies entries are * invariant to symlinks. */ char * normalize_filename(int fd, pid_t pid, char *name, int userp) { static char cwd[2*MAXPATHLEN]; int havecwd = 0; /* * The empty filename does not receive normalization. * System calls are supposed to fail on it. */ if (strcmp(name, "") == 0) return (name); if (fd != -1 && intercept.setcwd(fd, pid) == -1) { if (errno == EBUSY) return (NULL); getcwderr: if (strcmp(name, "/") == 0) return (name); err(1, "%s: getcwd", __func__); } if (userp == ICLINK_NONE) { if (getcwd(cwd, sizeof(cwd)) == NULL) goto getcwderr; havecwd = 1; } if (havecwd && name[0] != '/') { if (strlcat(cwd, "/", sizeof(cwd)) >= sizeof(cwd)) return (NULL); if (strlcat(cwd, name, sizeof(cwd)) >= sizeof(cwd)) return (NULL); } else { if (strlcpy(cwd, name, sizeof(cwd)) >= sizeof(cwd)) return (NULL); } if (userp != ICLINK_NONE) { static char rcwd[2*MAXPATHLEN]; char *base = basename(cwd); int failed = 0; /* The dot maybe used by rmdir("/tmp/something/.") */ if (strcmp(base, ".") == 0) goto nolast; if (userp == ICLINK_NOLAST) { /* Check if the last component has special meaning */ if (strcmp(base, "..") == 0 || strcmp(base, "/") == 0) userp = ICLINK_ALL; else goto nolast; } /* If realpath fails then the filename does not exist, * or we are supposed to not resolve the last component */ if (realpath(cwd, rcwd) == NULL) { char *dir, *file; struct stat st; if (errno != EACCES) { failed = 1; goto out; } nolast: /* Component of path could not be entered */ if (strlcpy(rcwd, cwd, sizeof(rcwd)) >= sizeof(rcwd)) goto error; if ((file = basename(rcwd)) == NULL) goto error; if ((dir = dirname(rcwd)) == NULL) goto error; /* So, try again */ if (realpath(dir, rcwd) == NULL) { failed = 1; goto out; } /* If path is not "/" append a "/" */ if (strlen(rcwd) > 1 && strlcat(rcwd, "/", sizeof(rcwd)) >= sizeof(rcwd)) goto error; if (strlcat(rcwd, file, sizeof(rcwd)) >= sizeof(rcwd)) goto error; /* * At this point, filename has to exist and has to * be a directory. */ if (userp != ICLINK_NOLAST) { if (lstat(rcwd, &st) == -1 || !S_ISDIR(st.st_mode)) failed = 1; } } out: if (failed) snprintf(rcwd, sizeof(rcwd), "/: %s", cwd); name = rcwd; } else { simplify_path(cwd); name = cwd; } /* Restore working directory and change root space after realpath */ if (fd != -1 && intercept.restcwd(fd) == -1) err(1, "%s: restcwd", __func__); return (name); error: errx(1, "%s: filename too long", __func__); /* NOTREACHED */ } void intercept_syscall(int fd, pid_t pid, u_int16_t seqnr, int policynr, const char *name, int code, const char *emulation, void *args, int argsize) { short action, flags = 0; struct intercept_syscall *sc; struct intercept_pid *icpid; struct elevate *elevate = NULL; int error = 0; action = ICPOLICY_PERMIT; flags = 0; icpid = intercept_getpid(pid); /* Special handling for the exec call */ if (!strcmp(name, "execve")) { void *addr; char *argname, before[MAXPATHLEN]; icpid->execve_code = code; icpid->policynr = policynr; if (icpid->newname) free(icpid->newname); intercept.getarg(0, args, argsize, &addr); argname = intercept_filename(fd, pid, addr, ICLINK_ALL, before); if (argname == NULL) err(1, "%s:%d: intercept_filename", __func__, __LINE__); if (intercept.scriptname(fd, pid, before) != 0) err(1, "%s:%d: ioctl", __func__, __LINE__); icpid->newname = strdup(argname); if (icpid->newname == NULL) err(1, "%s:%d: strdup", __func__, __LINE__); /* We need to know the result from this system call */ flags = ICFLAGS_RESULT; } icpid->elevate = NULL; sc = intercept_sccb_find(emulation, name); if (sc != NULL) { struct intercept_translate *tl; ic_abort = 0; TAILQ_FOREACH(tl, &sc->tls, next) { if (intercept_translate(tl, fd, pid, tl->off, args, argsize) == -1) break; } if (!ic_abort) { struct intercept_replace repl; intercept_replace_init(&repl); action = (*sc->cb)(fd, pid, policynr, name, code, emulation, args, argsize, &repl, &sc->tls, sc->cb_arg); if (action < ICPOLICY_NEVER) { /* if we can not rewrite the arguments, * system call fails. */ if (intercept_replace(fd, pid, seqnr, &repl) == -1) action = ICPOLICY_NEVER; } } else action = ICPOLICY_NEVER; } else if (intercept_gencb != NULL) action = (*intercept_gencb)(fd, pid, policynr, name, code, emulation, args, argsize, intercept_gencbarg); if (action > 0) { error = action; action = ICPOLICY_NEVER; } else { icpid = intercept_findpid(pid); if (icpid != NULL) elevate = icpid->elevate; else elevate = NULL; } /* Resume execution of the process */ intercept.answer(fd, pid, seqnr, action, error, flags, elevate); } void intercept_syscall_result(int fd, pid_t pid, u_int16_t seqnr, int policynr, const char *name, int code, const char *emulation, void *args, int argsize, int result, void *rval) { struct intercept_pid *icpid; if (result > 0) goto out; icpid = intercept_getpid(pid); if (!strcmp("execve", name)) { intercept_newimage(fd, pid, policynr, emulation, icpid->newname, icpid); /* we might have detached by now */ if (intercept_findpid(pid) == NULL) return; } out: /* Resume execution of the process */ intercept.answer(fd, pid, seqnr, 0, 0, 0, NULL); } void intercept_newimage(int fd, pid_t pid, int policynr, const char *emulation, char *newname, struct intercept_pid *icpid) { if (icpid == NULL) icpid = intercept_getpid(pid); if (icpid->name) free(icpid->name); if ((icpid->name = strdup(newname)) == NULL) err(1, "%s:%d: strdup", __func__, __LINE__); if (icpid->newname != NULL) { free(icpid->newname); icpid->newname = NULL; } if (intercept_newimagecb != NULL) (*intercept_newimagecb)(fd, pid, policynr, emulation, icpid->name, intercept_newimagecbarg); } int intercept_newpolicy(int fd) { int policynr; policynr = intercept.newpolicy(fd); return (policynr); } int intercept_assignpolicy(int fd, pid_t pid, int policynr) { return (intercept.assignpolicy(fd, pid, policynr)); } int intercept_modifypolicy_nr(int fd, int policynr, int code, short policy) { return (intercept.policy(fd, policynr, code, policy)); } int intercept_modifypolicy(int fd, int policynr, const char *emulation, const char *name, short policy) { int code; code = intercept.getsyscallnumber(emulation, name); if (code == -1) return (-1); return (intercept.policy(fd, policynr, code, policy)); } void intercept_child_info(pid_t opid, pid_t npid) { struct intercept_pid *ipid, *inpid, tmp; /* A child just died on us */ if (npid == -1) { intercept_freepid(opid); return; } tmp.pid = opid; ipid = SPLAY_FIND(pidtree, &pids, &tmp); if (ipid == NULL) return; inpid = intercept_getpid(npid); inpid->policynr = ipid->policynr; if (ipid->name != NULL) { inpid->name = strdup(ipid->name); if (inpid->name == NULL) err(1, "%s:%d: strdup", __func__, __LINE__); } /* Process tree */ inpid->ppid = opid; /* Copy some information */ inpid->uid = ipid->uid; inpid->gid = ipid->gid; strlcpy(inpid->username, ipid->username, sizeof(inpid->username)); strlcpy(inpid->home, ipid->home, sizeof(inpid->home)); /* XXX - keeps track of emulation */ intercept.clonepid(ipid, inpid); } void intercept_ugid(struct intercept_pid *icpid, uid_t uid, gid_t gid) { /* Update current home dir */ if (icpid->uid != uid) { struct passwd *pw; if ((pw = getpwuid(uid)) == NULL) { snprintf(icpid->username, sizeof(icpid->username), "uid %d", uid); strlcpy(icpid->home, "/", sizeof(icpid->home)); } else { strlcpy(icpid->username, pw->pw_name, sizeof(icpid->username)); strlcpy(icpid->home, pw->pw_dir, sizeof(icpid->home)); } } icpid->uid = uid; icpid->gid = gid; } /* * Returns the number of a system call */ int intercept_getsyscallnumber(const char *emulation, const char *name) { int nr = intercept.getsyscallnumber(emulation, name); if (nr >= INTERCEPT_MAXSYSCALLNR) err(1, "%s: system call number too high: %d", __func__, nr); return (nr); } /* * Checks if the given emulation has a certain system call. * This is a very slow function. */ int intercept_isvalidsystemcall(const char *emulation, const char *name) { int res; res = intercept.getsyscallnumber(emulation, name); return (res != -1); } /* * Call back when a user has exhausted the number of allowed policies * in the kernel. The kernel returns the policy number of a policy * that has been purged. */ void intercept_policy_free(int policynr) { (*intercept_pfreecb)(policynr, intercept_pfreearg); }