diff options
Diffstat (limited to 'src/exec_ptrace.c')
-rw-r--r-- | src/exec_ptrace.c | 1032 |
1 files changed, 881 insertions, 151 deletions
diff --git a/src/exec_ptrace.c b/src/exec_ptrace.c index dc00dd8af..81cd10bc2 100644 --- a/src/exec_ptrace.c +++ b/src/exec_ptrace.c @@ -26,14 +26,21 @@ #include <sys/uio.h> #include <sys/wait.h> +#include <ctype.h> #include <errno.h> +#include <fcntl.h> #include <limits.h> #include <signal.h> +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif #include <stddef.h> -#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <unistd.h> #if defined(HAVE_ENDIAN_H) # include <endian.h> #elif defined(HAVE_SYS_ENDIAN_H) @@ -59,6 +66,10 @@ # endif static int seccomp_trap_supported = -1; +#ifdef HAVE_PROCESS_VM_READV +static size_t page_size; +#endif +static size_t arg_max; /* Register getters and setters. */ # ifdef SECCOMP_AUDIT_ARCH_COMPAT @@ -269,7 +280,7 @@ set_sc_arg4(struct sudo_ptrace_regs *regs, unsigned long addr) * Returns true on success, else false. */ static bool -ptrace_getregs(int pid, struct sudo_ptrace_regs *regs, bool compat) +ptrace_getregs(int pid, struct sudo_ptrace_regs *regs, int compat) { debug_decl(ptrace_getregs, SUDO_DEBUG_EXEC); @@ -284,6 +295,19 @@ ptrace_getregs(int pid, struct sudo_ptrace_regs *regs, bool compat) if (ptrace(PTRACE_GETREGSET, pid, (void *)NT_PRSTATUS, &iov) == -1) debug_return_bool(false); # endif /* __mips__ */ + if (compat == -1) { +# ifdef SECCOMP_AUDIT_ARCH_COMPAT + if (sizeof(regs->u.native) != sizeof(regs->u.compat)) { + /* Guess compat based on size of register struct returned. */ + compat = iov.iov_len != sizeof(regs->u.native); + } else { + /* Assume a 64-bit executable will have a 64-bit stack pointer. */ + compat = reg_sp(regs->u.native) < 0xffffffff; + } +# else + compat = false; +# endif /* SECCOMP_AUDIT_ARCH_COMPAT */ + } /* Machine-dependent parameters to support compat binaries. */ if (compat) { @@ -291,7 +315,7 @@ ptrace_getregs(int pid, struct sudo_ptrace_regs *regs, bool compat) regs->wordsize = sizeof(int); } else { regs->compat = false; - regs->wordsize = sizeof(unsigned long); + regs->wordsize = sizeof(long); } debug_return_bool(true); @@ -321,19 +345,86 @@ ptrace_setregs(int pid, struct sudo_ptrace_regs *regs) debug_return_bool(true); } +#ifdef HAVE_PROCESS_VM_READV +/* + * Read the string at addr and store in buf using process_vm_readv(2). + * Returns the number of bytes stored, including the NUL. + */ +static size_t +ptrace_readv_string(pid_t pid, unsigned long addr, char *buf, size_t bufsize) +{ + const char *cp, *buf0 = buf; + struct iovec local, remote; + ssize_t nread; + debug_decl(ptrace_read_string, SUDO_DEBUG_EXEC); + + /* + * Read the string via process_vm_readv(2) one page at a time. + * We could do larger reads but since we don't know the length + * of the string, going one page at a time is simplest. + */ + for (;;) { + if (bufsize == 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: %d: out of space reading string", __func__, (int)pid); + errno = ENOSPC; + debug_return_ssize_t(-1); + } + + local.iov_base = buf; + local.iov_len = bufsize; + remote.iov_base = (void *)addr; + remote.iov_len = MIN(bufsize, page_size); + + nread = process_vm_readv(pid, &local, 1, &remote, 1, 0); + switch (nread) { + case -1: + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "process_vm_readv(%d, [0x%lx, %zu], 1, [0x%lx, %zu], 1, 0)", + (int)pid, (unsigned long)local.iov_base, local.iov_len, + (unsigned long)remote.iov_base, remote.iov_len); + debug_return_ssize_t(-1); + case 0: + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "process_vm_readv(%d, [0x%lx, %zu], 1, [0x%lx, %zu], 1, 0): %s", + (int)pid, (unsigned long)local.iov_base, local.iov_len, + (unsigned long)remote.iov_base, remote.iov_len, "premature EOF"); + debug_return_ssize_t(-1); + default: + /* Check for NUL terminator in page. */ + cp = memchr(buf, '\0', nread); + if (cp != NULL) + debug_return_size_t((cp - buf0) + 1); /* includes NUL */ + buf += nread; + bufsize -= nread; + addr += sizeof(unsigned long); + break; + } + } + debug_return_ssize_t(-1); +} +#endif /* HAVE_PROCESS_VM_READV */ + /* - * Read the string at addr and store in buf. + * Read the string at addr and store in buf using ptrace(2). * Returns the number of bytes stored, including the NUL. */ static size_t ptrace_read_string(pid_t pid, unsigned long addr, char *buf, size_t bufsize) { - const char *buf0 = buf; - const char *cp; + const char *cp, *buf0 = buf; unsigned long word; - unsigned int i; + size_t i; debug_decl(ptrace_read_string, SUDO_DEBUG_EXEC); +#ifdef HAVE_PROCESS_VM_READV + i = ptrace_readv_string(pid, addr, buf, bufsize); + if (i != (size_t)-1 || errno != ENOSYS) + debug_return_size_t(i); +#endif /* HAVE_PROCESS_VM_READV */ + /* * Read the string via ptrace(2) one (native) word at a time. * We use the native word size even in compat mode because that @@ -342,8 +433,9 @@ ptrace_read_string(pid_t pid, unsigned long addr, char *buf, size_t bufsize) for (;;) { word = ptrace(PTRACE_PEEKDATA, pid, addr, NULL); if (word == (unsigned long)-1) { - sudo_warn("%s: ptrace(PTRACE_PEEKDATA, %d, 0x%lx, NULL)", - __func__, (int)pid, addr); + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "ptrace(PTRACE_PEEKDATA, %d, 0x%lx, NULL)", (int)pid, addr); debug_return_ssize_t(-1); } @@ -352,7 +444,8 @@ ptrace_read_string(pid_t pid, unsigned long addr, char *buf, size_t bufsize) if (bufsize == 0) { sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: %d: out of space reading string", __func__, (int)pid); - debug_return_size_t(-1); + errno = ENOSPC; + debug_return_ssize_t(-1); } *buf = cp[i]; if (*buf++ == '\0') @@ -364,25 +457,124 @@ ptrace_read_string(pid_t pid, unsigned long addr, char *buf, size_t bufsize) } /* - * Read the string vector at addr and store in vec, which must have - * sufficient space. Strings are stored in buf. + * Expand buf by doubling its size. + * Updates bufp and bufsizep and recalculates curp and remp if non-NULL. + * Returns true on success, else false. + */ +static bool +growbuf(char **bufp, size_t *bufsizep, char **curp, size_t *remp) +{ + const size_t oldsize = *bufsizep; + char *newbuf; + debug_decl(growbuf, SUDO_DEBUG_EXEC); + + /* Double the size of the buffer. */ + newbuf = reallocarray(*bufp, 2, oldsize); + if (newbuf == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + if (curp != NULL) + *curp = newbuf + (*curp - *bufp); + if (remp != NULL) + *remp += oldsize; + *bufp = newbuf; + *bufsizep = 2 * oldsize; + debug_return_bool(true); +} + +/* + * Build a NULL-terminated string vector from a string table. + * On success, returns number of bytes used for the vector and sets + * vecp to the start of the vector and countp to the number of elements + * (not including the NULL). The buffer is resized as needed. + * Both vecp and its elements are stored as offsets into buf, not pointers. + * However, NULL is still stored as NULL. + * Returns (size_t)-1 on failure. + */ +static size_t +strtab_to_vec(char *strtab, size_t strtab_len, int *countp, char ***vecp, + char **bufp, size_t *bufsizep, size_t remainder) +{ + char *strend = strtab + strtab_len; + char **vec, **vp; + int count = 0; + debug_decl(strtab_to_vec, SUDO_DEBUG_EXEC); + + /* Store vector in buf after string table and make it aligned. */ + while (remainder < 2 * sizeof(char *)) { + if (!growbuf(bufp, bufsizep, &strtab, &remainder)) + debug_return_ssize_t(-1); + strend = strtab + strtab_len; + } + vec = (char **)LONGALIGN(strend); + remainder -= (char *)vec - strend; + + /* Fill in vector with the strings we read. */ + for (vp = vec; strtab < strend; ) { + while (remainder < 2 * sizeof(char *)) { + if (!growbuf(bufp, bufsizep, &strtab, &remainder)) + debug_return_ssize_t(-1); + strend = strtab + strtab_len; + vec = (char **)LONGALIGN(strend); + vp = vec + count; + } + /* Store offset into buf (not a pointer) in case of realloc(). */ + *vp++ = (char *)(strtab - *bufp); + remainder -= sizeof(char *); + strtab = memchr(strtab, '\0', strend - strtab); + if (strtab == NULL) + break; + strtab++; + count++; + } + *vp++ = NULL; /* we always leave room for NULL */ + + *countp = count; + *vecp = (char **)((char *)vec - *bufp); + + debug_return_size_t((char *)vp - strend); +} + +/* + * Read the string vector at addr and store it in bufp, which + * is reallocated as needed. The actual vector is returned in vecp. + * The count stored in countp does not include the terminating NULL pointer. + * The vecp and its contents are _offsets_, not pointers, in case the buffer + * gets reallocated later. The caller is responsible for converting the + * offsets into pointers based on the buffer before using. * Returns the number of bytes in buf consumed (including NULs). */ static size_t ptrace_read_vec(pid_t pid, struct sudo_ptrace_regs *regs, unsigned long addr, - char **vec, char *buf, size_t bufsize) + int *countp, char ***vecp, char **bufp, size_t *bufsizep, size_t off) { # ifdef SECCOMP_AUDIT_ARCH_COMPAT unsigned long next_word = -1; # endif + size_t remainder = *bufsizep - off; + char *strtab = *bufp + off; unsigned long word; - char *buf0 = buf; - int len = 0; - size_t slen; + size_t len, strtab_len; debug_decl(ptrace_read_vec, SUDO_DEBUG_EXEC); - /* Fill in vector. */ - for (;;) { + /* Treat a NULL vector as empty, thanks Linux. */ + if (addr == 0) { + char **vp; + + while (remainder < 2 * sizeof(char *)) { + if (!growbuf(bufp, bufsizep, &strtab, &remainder)) + debug_return_ssize_t(-1); + } + vp = (char **)LONGALIGN(strtab); + *vecp = (char **)((char *)vp - *bufp); + *countp = 0; + *vp++ = NULL; + debug_return_size_t((char *)vp - strtab); + } + + /* Fill in string table. */ + do { # ifdef SECCOMP_AUDIT_ARCH_COMPAT if (next_word == (unsigned long)-1) { word = ptrace(PTRACE_PEEKDATA, pid, addr, NULL); @@ -408,88 +600,115 @@ ptrace_read_vec(pid_t pid, struct sudo_ptrace_regs *regs, unsigned long addr, case -1: sudo_warn("%s: ptrace(PTRACE_PEEKDATA, %d, 0x%lx, NULL)", __func__, (int)pid, addr); - debug_return_size_t(-1); + debug_return_ssize_t(-1); case 0: - vec[len] = NULL; - debug_return_size_t(buf - buf0); + /* NULL terminator */ + break; default: - slen = ptrace_read_string(pid, word, buf, bufsize); - if (slen == (size_t)-1) - debug_return_size_t(-1); - vec[len++] = buf; - buf += slen + 1; - bufsize -= slen + 1; + for (;;) { + len = ptrace_read_string(pid, word, strtab, remainder); + if (len != (size_t)-1) + break; + if (errno != ENOSPC) + debug_return_ssize_t(-1); + if (!growbuf(bufp, bufsizep, &strtab, &remainder)) + debug_return_ssize_t(-1); + } + strtab += len; + remainder -= len; addr += regs->wordsize; continue; } - } + } while (word != 0); + + /* Store strings in a vector after the string table. */ + strtab_len = strtab - (*bufp + off); + strtab = *bufp + off; + len = strtab_to_vec(strtab, strtab_len, countp, vecp, bufp, bufsizep, + remainder); + if (len == (size_t)-1) + debug_return_ssize_t(-1); + + debug_return_size_t(strtab_len + len); } +#ifdef HAVE_PROCESS_VM_READV /* - * Return the length of the string vector at addr or -1 on error. + * Write the NUL-terminated string str to addr in the tracee using + * process_vm_writev(2). + * Returns the number of bytes written, including trailing NUL. */ -static int -ptrace_get_vec_len(pid_t pid, struct sudo_ptrace_regs *regs, unsigned long addr) +static size_t +ptrace_writev_string(pid_t pid, unsigned long addr, const char *str0) { -# ifdef SECCOMP_AUDIT_ARCH_COMPAT - unsigned long next_word = -1; -# endif - unsigned long word; - int len = 0; - debug_decl(ptrace_get_vec_len, SUDO_DEBUG_EXEC); + const char *str = str0; + size_t len = strlen(str) + 1; + debug_decl(ptrace_writev_string, SUDO_DEBUG_EXEC); + /* + * Write the string via process_vm_writev(2), handling partial writes. + */ for (;;) { -# ifdef SECCOMP_AUDIT_ARCH_COMPAT - if (next_word == (unsigned long)-1) { - word = ptrace(PTRACE_PEEKDATA, pid, addr, NULL); - if (regs->compat) { - /* Stash the next compat word in next_word. */ -# if BYTE_ORDER == BIG_ENDIAN - next_word = word & 0xffffffffU; - word >>= 32; -# else - next_word = word >> 32; - word &= 0xffffffffU; -# endif - } - } else { - /* Use the stashed value of the next word. */ - word = next_word; - next_word = (unsigned long)-1; - } -# else /* SECCOMP_AUDIT_ARCH_COMPAT */ - word = ptrace(PTRACE_PEEKDATA, pid, addr, NULL); -# endif /* SECCOMP_AUDIT_ARCH_COMPAT */ - switch (word) { + struct iovec local, remote; + ssize_t nwritten; + + local.iov_base = (void *)str; + local.iov_len = len; + remote.iov_base = (void *)addr; + remote.iov_len = len; + + nwritten = process_vm_writev(pid, &local, 1, &remote, 1, 0); + switch (nwritten) { case -1: - sudo_warn("%s: ptrace(PTRACE_PEEKDATA, %d, 0x%lx, NULL)", - __func__, (int)pid, addr); - debug_return_int(-1); + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "process_vm_writev(%d, [0x%lx, %zu], 1, [0x%lx, %zu], 1, 0)", + (int)pid, (unsigned long)local.iov_base, local.iov_len, + (unsigned long)remote.iov_base, remote.iov_len); + debug_return_ssize_t(-1); case 0: - debug_return_int(len); + /* Should not be possible. */ + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "process_vm_writev(%d, [0x%lx, %zu], 1, [0x%lx, %zu], 1, 0): %s", + (int)pid, (unsigned long)local.iov_base, local.iov_len, + (unsigned long)remote.iov_base, remote.iov_len, + "zero bytes written"); + debug_return_ssize_t(-1); default: - len++; - addr += regs->wordsize; - continue; + str += nwritten; + len -= nwritten; + addr += nwritten; + if (len == 0) + debug_return_size_t(str - str0); /* includes NUL */ + break; } } + debug_return_ssize_t(-1); } +#endif /* HAVE_PROCESS_VM_READV */ /* - * Write the NUL-terminated string str to addr in the tracee. + * Write the NUL-terminated string str to addr in the tracee using ptrace(2). * Returns the number of bytes written, including trailing NUL. */ static size_t ptrace_write_string(pid_t pid, unsigned long addr, const char *str) { const char *str0 = str; - unsigned int i; + size_t i; union { unsigned long word; char buf[sizeof(unsigned long)]; } u; debug_decl(ptrace_write_string, SUDO_DEBUG_EXEC); +#ifdef HAVE_PROCESS_VM_READV + i = ptrace_writev_string(pid, addr, str); + if (i != (size_t)-1 || errno != ENOSYS) + debug_return_size_t(i); +#endif /* HAVE_PROCESS_VM_READV */ + /* * Write the string via ptrace(2) one (native) word at a time. * We use the native word size even in compat mode because that @@ -507,7 +726,7 @@ ptrace_write_string(pid_t pid, unsigned long addr, const char *str) if (ptrace(PTRACE_POKEDATA, pid, addr, u.word) == -1) { sudo_warn("%s: ptrace(PTRACE_POKEDATA, %d, 0x%lx, %.*s)", __func__, (int)pid, addr, (int)sizeof(u.buf), u.buf); - debug_return_size_t(-1); + debug_return_ssize_t(-1); } if ((u.word & 0xff) == 0) { /* If the last byte we wrote is a NUL we are done. */ @@ -517,6 +736,137 @@ ptrace_write_string(pid_t pid, unsigned long addr, const char *str) } } +#ifdef HAVE_PROCESS_VM_READV +/* + * Write the string vector vec to addr in the tracee which must have + * sufficient space. Strings are written to strtab. + * Returns the number of bytes used in strtab (including NULs). + * process_vm_writev() version. + */ +static size_t +ptrace_writev_vec(pid_t pid, struct sudo_ptrace_regs *regs, char **vec, + unsigned long addr, unsigned long strtab) +{ + const unsigned long addr0 = addr; + const unsigned long strtab0 = strtab; + unsigned long *addrbuf = NULL; + struct iovec *local, *remote; + struct iovec local_addrs, remote_addrs; + size_t i, j, len, off = 0; + ssize_t expected = -1, nwritten, total_written = 0; + debug_decl(ptrace_writev_vec, SUDO_DEBUG_EXEC); + + /* Build up local and remote iovecs for process_vm_writev(2). */ + for (len = 0; vec[len] != NULL; len++) + continue; + local = reallocarray(NULL, len, sizeof(struct iovec)); + remote = reallocarray(NULL, len, sizeof(struct iovec)); + j = regs->compat && (len & 1) != 0; /* pad for final NULL in compat */ + addrbuf = reallocarray(NULL, len + 1 + j, regs->wordsize); + if (local == NULL || remote == NULL || addrbuf == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + for (i = 0, j = 0; i < len; i++) { + unsigned long word = strtab; + + /* Store remote string. */ + const size_t size = strlen(vec[i]) + 1; + local[i].iov_base = vec[i]; + local[i].iov_len = size; + remote[i].iov_base = (void *)strtab; + remote[i].iov_len = size; + strtab += size; + + /* Store address of remote string. */ +# ifdef SECCOMP_AUDIT_ARCH_COMPAT + if (regs->compat) { + /* + * For compat binaries we need to pack two 32-bit string addresses + * into a single 64-bit word. If this is the last string, NULL + * will be written as the second 32-bit address. + */ + if ((i & 1) == 1) { + /* Wrote this string address last iteration. */ + continue; + } +# if BYTE_ORDER == BIG_ENDIAN + word <<= 32; + if (vec[i + 1] != NULL) + word |= strtab; +# else + if (vec[i + 1] != NULL) + word |= strtab << 32; +# endif + } +# endif + addrbuf[j++] = word; + addr += sizeof(unsigned long); + } + if (!regs->compat || (len & 1) == 0) { + addrbuf[j] = 0; + } + + /* Write strings addresses to addr0 on remote. */ + local_addrs.iov_base = addrbuf; + local_addrs.iov_len = (len + 1) * regs->wordsize; + remote_addrs.iov_base = (void *)addr0; + remote_addrs.iov_len = local_addrs.iov_len; + if (process_vm_writev(pid, &local_addrs, 1, &remote_addrs, 1, 0) == -1) + goto done; + + /* Copy the strings to the (remote) string table. */ + expected = strtab - strtab0; + for (;;) { + nwritten = process_vm_writev(pid, local + off, len - off, + remote + off, len - off, 0); + switch (nwritten) { + case -1: + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "process_vm_writev(%d, 0x%lx, %zu, 0x%lx, %zu, 0)", + (int)pid, (unsigned long)local + off, len - off, + (unsigned long)remote + off, len - off); + goto done; + case 0: + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "process_vm_writev(%d, 0x%lx, %zu, 0x%lx, %zu, 0): %s", + (int)pid, (unsigned long)local + off, len - off, + (unsigned long)remote + off, len - off, + "zero bytes written"); + goto done; + default: + total_written += nwritten; + if (total_written >= expected) + goto done; + + /* Adjust offset for partial write (doesn't cross iov boundary). */ + while (off < len) { + nwritten -= local[off].iov_len; + off++; + if (nwritten <= 0) + break; + } + if (off == len) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "overflow while resuming process_vm_writev()"); + goto done; + } + break; + } + } +done: + free(local); + free(remote); + free(addrbuf); + if (total_written == expected) + debug_return_size_t(total_written); + debug_return_ssize_t(-1); +} +#endif /* HAVE_PROCESS_VM_READV */ + /* * Write the string vector vec to addr in the tracee which must have * sufficient space. Strings are written to strtab. @@ -530,6 +880,12 @@ ptrace_write_vec(pid_t pid, struct sudo_ptrace_regs *regs, char **vec, size_t i, len; debug_decl(ptrace_write_vec, SUDO_DEBUG_EXEC); +#ifdef HAVE_PROCESS_VM_READV + i = ptrace_writev_vec(pid, regs, vec, addr, strtab); + if (i != (size_t)-1 || errno != ENOSYS) + debug_return_size_t(i); +#endif /* HAVE_PROCESS_VM_READV */ + /* Copy string vector into tracee one word at a time. */ for (i = 0; vec[i] != NULL; i++) { unsigned long word = strtab; @@ -583,23 +939,23 @@ ptrace_write_vec(pid_t pid, struct sudo_ptrace_regs *regs, char **vec, } /* - * Use /proc/PID/cwd to determine the current working directory. + * Read a link from /proc/PID and store the result in buf. + * Used to read the cwd and exe links in /proc/PID. * Returns true on success, else false. */ static bool -getcwd_by_pid(pid_t pid, char *buf, size_t bufsize) +proc_read_link(pid_t pid, const char *name, char *buf, size_t bufsize) { size_t len; char path[PATH_MAX]; - debug_decl(getcwd_by_pid, SUDO_DEBUG_EXEC); + debug_decl(proc_read_link, SUDO_DEBUG_EXEC); - len = snprintf(path, sizeof(path), "/proc/%d/cwd", (int)pid); + len = snprintf(path, sizeof(path), "/proc/%d/%s", (int)pid, name); if (len < sizeof(path)) { - len = readlink(path, buf, bufsize); + len = readlink(path, buf, bufsize - 1); if (len != (size_t)-1) { - /* Check for truncation. */ - if (len >= bufsize) - buf[bufsize - 1] = '\0'; + /* readlink(2) does not add the NUL for us. */ + buf[len] = '\0'; debug_return_bool(true); } } @@ -614,66 +970,82 @@ static char * get_execve_info(pid_t pid, struct sudo_ptrace_regs *regs, char **pathname_out, int *argc_out, char ***argv_out, int *envc_out, char ***envp_out) { - char *argbuf, *strtab, *pathname, **argv, **envp; + char *pathname, **argv, **envp, *argbuf = NULL; unsigned long path_addr, argv_addr, envp_addr; - int argc, envc; - size_t bufsize, len; + size_t bufsize, len, off = 0; + int i, argc, envc = 0; debug_decl(get_execve_info, SUDO_DEBUG_EXEC); - bufsize = sysconf(_SC_ARG_MAX) + PATH_MAX; + bufsize = PATH_MAX + arg_max; argbuf = malloc(bufsize); - if (argbuf == NULL) - sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (argbuf == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } /* execve(2) takes three arguments: pathname, argv, envp. */ path_addr = get_sc_arg1(regs); argv_addr = get_sc_arg2(regs); envp_addr = get_sc_arg3(regs); + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %d: path 0x%lx, argv 0x%lx, envp 0x%lx", __func__, + (int)pid, path_addr, argv_addr, envp_addr); - /* Count argv and envp */ - argc = ptrace_get_vec_len(pid, regs, argv_addr); - envc = ptrace_get_vec_len(pid, regs, envp_addr); - if (argc == -1 || envc == -1) - goto bad; - - /* Reserve argv and envp at the start of argbuf so they are aligned. */ - if ((argc + 1 + envc + 1) * sizeof(unsigned long) >= bufsize) { - sudo_warnx("%s", U_("insufficient space for execve arguments")); - goto bad; + /* Read the pathname. */ + if (path_addr == 0) { + /* execve(2) will fail with EINVAL */ + pathname = NULL; + } else { + len = ptrace_read_string(pid, path_addr, argbuf, bufsize); + if (len == (size_t)-1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to read execve pathname for process %d", (int)pid); + goto bad; + } + pathname = argbuf; + off = len; } - argv = (char **)argbuf; - envp = argv + argc + 1; - strtab = (char *)(envp + envc + 1); - bufsize -= strtab - argbuf; /* Read argv */ - len = ptrace_read_vec(pid, regs, argv_addr, argv, strtab, bufsize); + len = ptrace_read_vec(pid, regs, argv_addr, &argc, &argv, &argbuf, + &bufsize, off); if (len == (size_t)-1) { - sudo_warn(U_("unable to read execve %s for process %d"), - "argv", (int)pid); + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to read execve argv for process %d", (int)pid); goto bad; } - strtab += len; - bufsize -= len; + off += len; + + if (argc == 0) { + /* Reserve an extra slot so we can store argv[0]. */ + while (bufsize - off < sizeof(char *)) { + if (!growbuf(&argbuf, &bufsize, NULL, NULL)) + goto bad; + } + off += sizeof(char *); + } /* Read envp */ - len = ptrace_read_vec(pid, regs, envp_addr, envp, strtab, bufsize); + len = ptrace_read_vec(pid, regs, envp_addr, &envc, &envp, &argbuf, + &bufsize, off); if (len == (size_t)-1) { - sudo_warn(U_("unable to read execve %s for process %d"), - "envp", (int)pid); + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to read execve envp for process %d", (int)pid); goto bad; } - strtab += len; - bufsize -= len; - /* Read the pathname. */ - len = ptrace_read_string(pid, path_addr, strtab, bufsize); - if (len == (size_t)-1) { - sudo_warn(U_("unable to read execve %s for process %d"), - "pathname", (int)pid); - goto bad; + /* Convert offsets in argv and envp to pointers. */ + argv = (char **)(argbuf + (unsigned long)argv); + for (i = 0; i < argc; i++) { + argv[i] = argbuf + (unsigned long)argv[i]; + } + envp = (char **)(argbuf + (unsigned long)envp); + for (i = 0; i < envc; i++) { + envp[i] = argbuf + (unsigned long)envp[i]; } - pathname = strtab; sudo_debug_execve(SUDO_DEBUG_DIAG, pathname, argv, envp); @@ -847,11 +1219,21 @@ int exec_ptrace_seize(pid_t child) { const long ptrace_opts = PTRACE_O_TRACESECCOMP|PTRACE_O_TRACECLONE| - PTRACE_O_TRACEFORK|PTRACE_O_TRACEVFORK; + PTRACE_O_TRACEFORK|PTRACE_O_TRACEVFORK| + PTRACE_O_TRACEEXEC; int ret = -1; int status; debug_decl(exec_ptrace_seize, SUDO_DEBUG_EXEC); +#ifdef HAVE_PROCESS_VM_READV + page_size = sysconf(_SC_PAGESIZE); + if (page_size == (size_t)-1) + page_size = 4096; +#endif + arg_max = sysconf(_SC_ARG_MAX); + if (arg_max == (size_t)-1) + arg_max = 128 * 1024; + /* Seize control of the child process. */ if (ptrace(PTRACE_SEIZE, child, NULL, ptrace_opts) == -1) { /* @@ -904,45 +1286,249 @@ done: } /* - * Verify that the execve(2) argument we wrote match the contents of closure. - * Returns true if they match, else false. + * Compare two pathnames. If do_stat is true, fall back to stat(2)ing + * the paths for a dev/inode match if the strings don't match. + * Returns true on match, else false. */ static bool -verify_execve_info(pid_t pid, struct sudo_ptrace_regs *regs, - struct intercept_closure *closure) +pathname_matches(const char *path1, const char *path2, bool do_stat) { - char *pathname, **argv, **envp, *buf; - int argc, envc, i; - bool ret = true; - debug_decl(verify_execve_info, SUDO_DEBUG_EXEC); + struct stat sb1, sb2; + debug_decl(pathname_matches, SUDO_DEBUG_EXEC); - buf = get_execve_info(pid, regs, &pathname, &argc, &argv, - &envc, &envp); - if (buf == NULL) + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: compare %s to %s", __func__, + path1 ? path1 : "(NULL)", path2 ? path2 : "(NULL)"); + + if (path1 == NULL || path2 == NULL) debug_return_bool(false); - if (pathname == NULL || strcmp(pathname, closure->command) != 0) { - sudo_warn( + if (strcmp(path1, path2) == 0) + debug_return_bool(true); + + if (do_stat && stat(path1, &sb1) == 0 && stat(path2, &sb2) == 0) { + if (sb1.st_dev == sb2.st_dev && sb1.st_ino == sb2.st_ino) + debug_return_bool(true); + } + + debug_return_bool(false); +} + +/* + * Open script and check for '#!' magic number followed by an interpreter. + * If present, check the interpreter against execpath, and argument string + * (if any) against argv[1]. + * Returns number of argv entries to skip on success, else 0. + */ +static int +script_matches(const char *script, const char *execpath, int argc, + char * const *argv) +{ + char * const *orig_argv = argv; + size_t linesize = 0; + char *interp, *interp_args, *line = NULL; + char magic[2]; + int count; + FILE *fp = NULL; + ssize_t len; + debug_decl(get_interpreter, SUDO_DEBUG_EXEC); + + /* Linux allows up to 4 nested interpreters. */ + for (count = 0; count < 4; count++) { + if (fp != NULL) + fclose(fp); + fp = fopen(script, "r"); + if (fp == NULL) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_ERRNO, + "%s: unable to open %s for reading", __func__, script); + goto done; + } + + if (fread(magic, 1, 2, fp) != 2 || memcmp(magic, "#!", 2) != 0) { + sudo_debug_printf(SUDO_DEBUG_DEBUG, "%s: %s: not a script", + __func__, script); + goto done; + } + + /* Check interpreter, skipping the shebang and trim trailing space. */ + len = getdelim(&line, &linesize, '\n', fp); + if (len == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: %s: can't get interpreter", + __func__, script); + goto done; + } + while (len > 0 && isspace((unsigned char)line[len - 1])) { + len--; + line[len] = '\0'; + } + sudo_debug_printf(SUDO_DEBUG_DEBUG, "%s: %s: shebang line \"%s\"", + __func__, script, line); + + /* + * Split line into interpreter and args. + * Whitespace is not supported in the interpreter path. + */ + for (interp = line; isspace((unsigned char)*interp); interp++) + continue; + interp_args = strpbrk(interp, " \t"); + if (interp_args != NULL) { + *interp_args++ = '\0'; + while (isspace((unsigned char)*interp_args)) + interp_args++; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: interpreter %s, args \"%s\"", + __func__, interp, interp_args ? interp_args : ""); + + /* Match interpreter. */ + if (!pathname_matches(execpath, interp, true)) { + /* It is possible for the interpreter to be a script too. */ + if (argv > 0 && strcmp(interp, argv[1]) == 0) { + /* Interpreter args must match for *this* interpreter. */ + if (interp_args == NULL || + (argc > 1 && strcmp(interp_args, argv[2]) == 0)) { + script = interp; + argv++; + argc--; + if (interp_args != NULL) { + argv++; + argc--; + } + /* Check whether interp is itself a script. */ + continue; + } + } + } + if (argc > 0 && interp_args != NULL) { + if (strcmp(interp_args, argv[1]) != 0) { + sudo_warnx( + U_("interpreter argument , expected \"%s\", got \"%s\""), + interp_args, argc > 1 ? argv[1] : "(NULL)"); + goto done; + } + argv++; + } + argv++; + break; + } + +done: + free(line); + if (fp != NULL) + fclose(fp); + debug_return_int((int)(argv - orig_argv)); +} + +static size_t +proc_read_vec(pid_t pid, const char *name, int *countp, char ***vecp, + char **bufp, size_t *bufsizep, size_t off) +{ + size_t remainder = *bufsizep - off; + size_t len, strtab_len; + char path[PATH_MAX], *strtab = *bufp + off; + int fd; + ssize_t nread; + debug_decl(proc_read_vec, SUDO_DEBUG_EXEC); + + len = snprintf(path, sizeof(path), "/proc/%d/%s", (int)pid, name); + if (len >= sizeof(path)) + debug_return_ssize_t(-1); + + fd = open(path, O_RDONLY); + if (fd == -1) + debug_return_ssize_t(-1); + + /* Read in strings until EOF. */ + do { + nread = read(fd, strtab, remainder); + if (nread == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to read %s", __func__, path); + close(fd); + debug_return_ssize_t(-1); + } + strtab += nread; + remainder -= nread; + if (remainder < sizeof(char *)) { + while (!growbuf(bufp, bufsizep, &strtab, &remainder)) { + close(fd); + debug_return_ssize_t(-1); + } + } + } while (nread != 0); + close(fd); + + /* Trim off the extra NUL byte at the end of the string table. */ + if (strtab - *bufp >= 2 && strtab[-1] == '\0' && strtab[-2] == '\0') { + strtab--; + remainder++; + } + + /* Store strings in a vector after the string table. */ + strtab_len = strtab - (*bufp + off); + strtab = *bufp + off; + len = strtab_to_vec(strtab, strtab_len, countp, vecp, bufp, bufsizep, + remainder); + if (len == (size_t)-1) + debug_return_ssize_t(-1); + + debug_return_size_t(strtab_len + len); +} + +/* + * Check if the execve(2) arguments match the contents of closure. + * Returns true if they match, else false. + */ +static bool +execve_args_match(const char *pathname, int argc, char * const *argv, + int envc, char * const *envp, bool do_stat, + struct intercept_closure *closure) +{ + bool ret = true; + int i; + debug_decl(execve_args_match, SUDO_DEBUG_EXEC); + + if (!pathname_matches(pathname, closure->command, do_stat)) { + /* For scripts, pathname will refer to the interpreter instead. */ + if (do_stat) { + int skip = script_matches(closure->command, pathname, + argc, argv); + if (skip != 0) { + /* Skip interpreter (and args) in argv. */ + argv += skip; + argc -= skip; + goto check_argv; + } + } + sudo_warnx( U_("pathname mismatch, expected \"%s\", got \"%s\""), closure->command, pathname ? pathname : "(NULL)"); ret = false; } +check_argv: for (i = 0; i < argc; i++) { if (closure->run_argv[i] == NULL) { ret = false; - sudo_warn( + sudo_warnx( U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), "argv", i, "(NULL)", argv[i] ? argv[i] : "(NULL)"); break; - } else if (argv[i] == NULL) { + } + if (argv[i] == NULL) { ret = false; - sudo_warn( + sudo_warnx( U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), "argv", i, closure->run_argv[i], "(NULL)"); break; - } else if (strcmp(argv[i], closure->run_argv[i]) != 0) { + } + if (strcmp(argv[i], closure->run_argv[i]) != 0) { + if (i == 0) { + /* Special case for argv[0] which may contain the basename. */ + const char *base = sudo_basename(closure->run_argv[0]); + if (strcmp(argv[i], base) == 0) + continue; + } ret = false; - sudo_warn( + sudo_warnx( U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), "argv", i, closure->run_argv[i], argv[i]); } @@ -950,24 +1536,144 @@ verify_execve_info(pid_t pid, struct sudo_ptrace_regs *regs, for (i = 0; i < envc; i++) { if (closure->run_envp[i] == NULL) { ret = false; - sudo_warn( + sudo_warnx( U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), "envp", i, "(NULL)", envp[i] ? envp[i] : "(NULL)"); break; } else if (envp[i] == NULL) { ret = false; - sudo_warn( + sudo_warnx( U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), "envp", i, closure->run_envp[i], "(NULL)"); break; } else if (strcmp(envp[i], closure->run_envp[i]) != 0) { ret = false; - sudo_warn( + sudo_warnx( U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), "envp", i, closure->run_envp[i], envp[i]); } } - free(buf); + + debug_return_bool(ret); +} + +/* + * Verify that the execve(2) argument we wrote match the contents of closure. + * Returns true if they match, else false. + */ +static bool +verify_execve_args(pid_t pid, struct sudo_ptrace_regs *regs, + struct intercept_closure *closure) +{ + char *pathname, **argv, **envp, *buf; + int argc, envc; + bool ret = false; + debug_decl(verify_execve_args, SUDO_DEBUG_EXEC); + + buf = get_execve_info(pid, regs, &pathname, &argc, &argv, + &envc, &envp); + if (buf != NULL) { + ret = execve_args_match(pathname, argc, argv, envc, envp, false, closure); + free(buf); + } + + debug_return_bool(ret); +} + +/* + * Verify that the command executed matches the arguments we checked. + * Returns true on success and false on error. + */ +static bool +ptrace_verify_post_exec(pid_t pid, struct sudo_ptrace_regs *regs, + struct intercept_closure *closure) +{ + char **argv, **envp, *argbuf = NULL; + char pathname[PATH_MAX]; + sigset_t chldmask; + bool ret = false; + int argc, envc, i, status; + size_t bufsize, len; + debug_decl(ptrace_verify_post_exec, SUDO_DEBUG_EXEC); + + /* Block SIGCHLD for the critical section (waitpid). */ + sigemptyset(&chldmask); + sigaddset(&chldmask, SIGCHLD); + sigprocmask(SIG_BLOCK, &chldmask, NULL); + + /* Allow execve(2) to continue and wait for PTRACE_EVENT_EXEC. */ + ptrace(PTRACE_SYSCALL, pid, NULL, NULL); + for (;;) { + if (waitpid(pid, &status, __WALL) != -1) + break; + if (errno == EINTR) + continue; + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + goto done; + } + if (!WIFSTOPPED(status)) { + sudo_warnx(U_("process %d exited unexpectedly"), (int)pid); + goto done; + } + if (status >> 8 != (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) { + sudo_warnx(U_("process %d unexpected status 0x%x"), (int)pid, status); + goto done; + } + + /* Get the executable path. */ + if (!proc_read_link(pid, "exe", pathname, sizeof(pathname))) { + /* Missing /proc file system is not a fatal error. */ + sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: unable to read /proc/%d/exe", + __func__, (int)pid); + ret = true; + goto done; + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %d: verify %s", __func__, + (int)pid, pathname); + + /* Allocate a single buffer for argv, envp and their strings. */ + bufsize = arg_max; + argbuf = malloc(bufsize); + if (argbuf == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + + len = proc_read_vec(pid, "cmdline", &argc, &argv, &argbuf, &bufsize, 0); + if (len == (size_t)-1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to read execve argv for process %d", (int)pid); + goto done; + } + + len = proc_read_vec(pid, "environ", &envc, &envp, &argbuf, &bufsize, len); + if (len == (size_t)-1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to read execve envp for process %d", (int)pid); + goto done; + } + + /* Convert offsets in argv and envp to pointers. */ + argv = (char **)(argbuf + (unsigned long)argv); + for (i = 0; i < argc; i++) { + argv[i] = argbuf + (unsigned long)argv[i]; + } + envp = (char **)(argbuf + (unsigned long)envp); + for (i = 0; i < envc; i++) { + envp[i] = argbuf + (unsigned long)envp[i]; + } + + ret = execve_args_match(pathname, argc, argv, envc, envp, true, closure); + if (!ret) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: %d new execve args don't match closure", __func__, (int)pid); + } + +done: + free(argbuf); + sigprocmask(SIG_UNBLOCK, &chldmask, NULL); debug_return_bool(ret); } @@ -1007,10 +1713,13 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure) } /* Get the registers. */ + memset(®s, 0, sizeof(regs)); if (!ptrace_getregs(pid, ®s, msg)) { sudo_warn(U_("unable to get registers for process %d"), (int)pid); debug_return_bool(false); } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %d: compat: %s, wordsize: %u", + __func__, (int)pid, regs.compat ? "true" : "false", regs.wordsize); # ifdef SECCOMP_AUDIT_ARCH_COMPAT if (regs.compat) { @@ -1053,18 +1762,24 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure) } /* Get the current working directory and execve info. */ - if (!getcwd_by_pid(pid, cwd, sizeof(cwd))) + if (!proc_read_link(pid, "cwd", cwd, sizeof(cwd))) (void)strlcpy(cwd, "unknown", sizeof(cwd)); buf = get_execve_info(pid, ®s, &pathname, &argc, &argv, &envc, &envp); if (buf == NULL) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, "%s: %d: unable to get execve info", __func__, (int)pid); + /* EIO from ptrace is like EFAULT from the kernel. */ + if (errno == EIO) + errno = EFAULT; + ptrace_fail_syscall(pid, ®s, errno); + goto done; + } - /* Unrecoverable error, kill the process if it still exists. */ - if (errno != ESRCH) - kill(pid, SIGKILL); - debug_return_bool(false); + /* Must have a pathname. */ + if (pathname == NULL) { + ptrace_fail_syscall(pid, ®s, EINVAL); + goto done; } /* @@ -1077,10 +1792,18 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure) goto done; } + /* We can only pass the pathname to exececute via argv[0] (plugin API). */ + argv[0] = pathname; + if (argc == 0) { + /* Rewrite an empty argv[] with the path to execute. */ + argv[1] = NULL; + argc = 1; + argv_mismatch = true; + } + /* Perform a policy check. */ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %d: checking policy for %s", __func__, (int)pid, pathname); - argv[0] = pathname; if (!intercept_check_policy(pathname, argc, argv, envc, envp, cwd, closure)) { sudo_warnx("%s", U_(closure->errstr)); @@ -1110,6 +1833,8 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure) break; } } + if (closure->run_argv[i] != NULL || argv[i] != NULL) + argv_mismatch = true; if (path_mismatch || argv_mismatch) { /* @@ -1188,12 +1913,21 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure) if (closure->state == POLICY_TEST) { /* Verify the contents of what we just wrote. */ - if (!verify_execve_info(pid, ®s, closure)) { + if (!verify_execve_args(pid, ®s, closure)) { sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: new execve args don't match closure", __func__); } } } + if (closure->state == POLICY_ACCEPT) { + if (ISSET(closure->details->flags, CD_INTERCEPT_VERIFY)) { + /* Verify execve(2) args post-exec. */ + if (!ptrace_verify_post_exec(pid, ®s, closure)) { + if (errno != ESRCH) + kill(pid, SIGKILL); + } + } + } break; default: /* If rejected, fake the syscall and set return to EACCES */ @@ -1231,6 +1965,9 @@ exec_ptrace_stopped(pid_t pid, int status, void *intercept) sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: %d failed to intercept execve", __func__, (int)pid); } + } else if (sigtrap == (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: %d PTRACE_EVENT_EXEC", __func__, (int)pid); } else if (sigtrap == (SIGTRAP | (PTRACE_EVENT_CLONE << 8)) || sigtrap == (SIGTRAP | (PTRACE_EVENT_VFORK << 8)) || sigtrap == (SIGTRAP | (PTRACE_EVENT_FORK << 8))) { @@ -1317,13 +2054,6 @@ exec_ptrace_subcmds_supported(void) #else /* STUB */ bool -have_seccomp_action(const char *action) -{ - return false; -} - -/* STUB */ -bool exec_ptrace_stopped(pid_t pid, int status, void *intercept) { return true; |