diff --git a/src/exec_ptrace.c b/src/exec_ptrace.c index 58cdd5be1..99e8cdb22 100644 --- a/src/exec_ptrace.c +++ b/src/exec_ptrace.c @@ -21,17 +21,23 @@ #include +#include +#include #include + #include #include +#include #include #include +#include #include #include "sudo.h" #include "sudo_exec.h" #ifdef HAVE_PTRACE_INTERCEPT +# include # include # include # include @@ -40,6 +46,316 @@ # include # include +/* + * See syscall(2) for a list of registers used in system calls. + * For example code, see tools/testing/selftests/seccomp/seccomp_bpf.c + * + * The structs and registers vary among the different platforms. + * We define user_regs_struct as the struct to use for the + * PTRACE_GETREGSET/PTRACE_SETREGSET command and define accessor + * macros to get/set the struct members. + */ +#if defined(__amd64__) +# define user_pt_regs user_regs_struct +# define reg_syscall(x) (x).orig_rax +# define reg_retval(x) (x).rax +# define reg_arg1(x) (x).rdi +# define reg_arg2(x) (x).rsi +# define reg_arg3(x) (x).rdx +# define reg_arg4(x) (x).r10 +#elif defined(__aarch64__) +# define reg_syscall(x) (x).regs[8] /* w8 */ +# define reg_retval(x) (x).regs[0] /* x0 */ +# define reg_arg1(x) (x).regs[0] /* x0 */ +# define reg_arg2(x) (x).regs[1] /* x1 */ +# define reg_arg3(x) (x).regs[2] /* x2 */ +# define reg_arg4(x) (x).regs[3] /* x3 */ +#elif defined(__arm__) +/* Note: assumes arm EABI, not OABI */ +/* Untested */ +# define user_pt_regs pt_regs +# define reg_syscall(x) (x).ARM_r7 +# define reg_retval(x) (x).ARM_r0 +# define reg_arg1(x) (x).ARM_r0 +# define reg_arg2(x) (x).ARM_r1 +# define reg_arg3(x) (x).ARM_r2 +# define reg_arg4(x) (x).ARM_r3 +#elif defined (__hppa__) +/* Untested */ +# define user_pt_regs user_regs_struct +# define reg_syscall(x) (x).gr[20] /* r20 */ +# define reg_retval(x) (x).gr[28] /* r28 */ +# define reg_arg1(x) (x).gr[26] /* r26 */ +# define reg_arg2(x) (x).gr[25] /* r25 */ +# define reg_arg3(x) (x).gr[24] /* r24 */ +# define reg_arg4(x) (x).gr[23] /* r23 */ +#elif defined(__i386__) +# define user_pt_regs user_regs_struct +# define reg_syscall(x) (x).orig_eax +# define reg_retval(x) (x).eax +# define reg_arg1(x) (x).ebx +# define reg_arg2(x) (x).ecx +# define reg_arg3(x) (x).edx +# define reg_arg4(x) (x).esi +#elif defined(__powerpc64__) +/* Untested */ +# define user_pt_regs pt_regs +# define reg_syscall(x) (x).gpr[0] /* r0 */ +# define reg_retval(x) (x).gpr[3] /* r3 */ +# define reg_arg1(x) (x).gpr[3] /* r3 */ +# define reg_arg2(x) (x).gpr[4] /* r4 */ +# define reg_arg3(x) (x).gpr[5] /* r5 */ +# define reg_arg4(x) (x).gpr[6] /* r6 */ +#elif defined(__powerpc__) +/* Untested */ +# define user_pt_regs pt_regs +# define reg_syscall(x) (x).gpr[0] /* r0 */ +# define reg_retval(x) (x).gpr[3] /* r3 */ +# define reg_arg1(x) (x).gpr[3] /* r3 */ +# define reg_arg2(x) (x).gpr[4] /* r4 */ +# define reg_arg3(x) (x).gpr[5] /* r5 */ +# define reg_arg4(x) (x).gpr[6] /* r6 */ +#elif defined(__riscv) && __riscv_xlen == 64 +/* Untested */ +# define user_pt_regs user_regs_struct +# define reg_syscall(x) (x).a7 +# define reg_retval(x) (x).a0 +# define reg_arg1(x) (x).a0 +# define reg_arg2(x) (x).a1 +# define reg_arg3(x) (x).a2 +# define reg_arg4(x) (x).a3 +#elif defined(__s390__) +/* Untested */ +# define user_pt_regs s390_regs +# define reg_syscall(x) (x).gprs[1] /* r1 */ +# define reg_retval(x) (x).gprs[2] /* r2 */ +# define reg_arg1(x) (x).gprs[2] /* r2 */ +# define reg_arg2(x) (x).gprs[3] /* r3 */ +# define reg_arg3(x) (x).gprs[4] /* r4 */ +# define reg_arg4(x) (x).gprs[5] /* r6 */ +#else +# error "Do not know how to find your architecture's registers" +#endif + +/* + * Read the string at addr and store in buf. + * Returns the number of bytes stored, including the NUL. + */ +static size_t +ptrace_read_string(pid_t pid, long addr, char *buf, size_t bufsize) +{ + const char *buf0 = buf; + const char *cp; + long word; + unsigned int i; + debug_decl(ptrace_read_string, SUDO_DEBUG_EXEC); + + /* Read the string via ptrace(2) one word at a time. */ + for (;;) { + word = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); + if (word == -1) { + sudo_warn("ptrace(PTRACE_PEEKTEXT, %d, %ld, NULL)", pid, addr); + debug_return_ssize_t(-1); + } + + /* XXX - this could be optimized. */ + cp = (char *)&word; + for (i = 0; i < sizeof(long); i++) { + if (bufsize == 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: %d: out of space reading string", __func__, (int)pid); + debug_return_ssize_t(-1); + } + *buf = cp[i]; + if (*buf++ == '\0') + debug_return_ssize_t(buf - buf0); + bufsize--; + } + addr += sizeof(long); + } +} + +/* + * Read the string vector at addr and store in vec, which must have + * sufficient space. Strings are stored in buf. + * Returns the number of bytes in buf consumed (including NULs). + */ +static size_t +ptrace_read_vec(pid_t pid, long addr, char **vec, char *buf, size_t bufsize) +{ + char *buf0 = buf; + int len = 0; + size_t slen; + debug_decl(ptrace_read_vec, SUDO_DEBUG_EXEC); + + /* Fill in vector. */ + for (;;) { + long word = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); + switch (word) { + case -1: + sudo_warn("ptrace(PTRACE_PEEKTEXT, %d, %ld, NULL)", pid, addr); + goto bad; + case 0: + vec[len] = NULL; + debug_return_size_t(buf - buf0); + default: + slen = ptrace_read_string(pid, word, buf, bufsize); + if (slen == (size_t)-1) + goto bad; + vec[len++] = buf; + buf += slen + 1; + bufsize -= slen + 1; + addr += sizeof(word); + continue; + } + } +bad: + while (len > 0) { + free(vec[len]); + len--; + } + debug_return_size_t(-1); +} + +/* + * Return the length of the string vector at addr or -1 on error. + */ +static int +ptrace_get_vec_len(pid_t pid, long addr) +{ + int len = 0; + debug_decl(ptrace_get_vec_len, SUDO_DEBUG_EXEC); + + for (;;) { + long word = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); + switch (word) { + case -1: + sudo_warn("ptrace(PTRACE_PEEKTEXT, %d, %ld, NULL)", pid, addr); + debug_return_int(-1); + case 0: + debug_return_int(len); + default: + len++; + addr += sizeof(word); + continue; + } + } +} + +/* + * Read the filename, argv and envp of the execve(2) system call. + * Returns a dynamically allocated buffer the parent is responsible for. + */ +static char * +get_execve_args(pid_t pid, char **pathname_out, char ***argv_out, char ***envp_out) +{ + char *argbuf, *strtab, *pathname, **argv, **envp; + long path_addr, argv_addr, envp_addr, syscallno; + struct user_pt_regs regs; + struct iovec iov; + int argc, envc; + size_t bufsize, len; + debug_decl(get_execve_args, SUDO_DEBUG_EXEC); + + bufsize = sysconf(_SC_ARG_MAX) + PATH_MAX; + argbuf = malloc(bufsize); + if (argbuf == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + /* XXX - for amd64 and i386 use PTRACE_GETREGS/PTRACE_SETREGS instead. */ + iov.iov_base = ®s; + iov.iov_len = sizeof(regs); + if (ptrace(PTRACE_GETREGSET, pid, (long)NT_PRSTATUS, &iov) == -1) { + sudo_warn(U_("unable to get registers for process %d"), (int)pid); + goto bad; + } + + /* System call number is stored in the lower 32-bits on 64-bit platforms. */ + syscallno = reg_syscall(regs) & 0xffffffff; + if (syscallno != __NR_execve) { + sudo_warnx("%s: unexpected system call %ld", __func__, syscallno); + goto bad; + } + + /* execve(2) takes three arguments: pathname, argv, envp. */ + path_addr = reg_arg1(regs); + argv_addr = reg_arg2(regs); + envp_addr = reg_arg3(regs); + +#ifdef notyet + /* Cause the syscall to fail by changing its number to -1. */ + reg_syscall(regs) |= 0xffffffff; + if (ptrace(PTRACE_SETREGSET, pid, (long)NT_PRSTATUS, &iov) == -1) { + sudo_warn("unable to set registers"); + goto bad; + } + + /* Allow the syscall to complete and change return value to EACCES. */ + ptrace(PTRACE_SYSCALL, pid, NULL, NULL); + waitpid(pid, NULL, 0); + reg_retval(regs) = -EACCES; + if (ptrace(PTRACE_SETREGSET, pid, (long)NT_PRSTATUS, &iov) == -1) { + sudo_warn("unable to set registers"); + goto bad; + } +#endif + + /* Count argv and envp */ + argc = ptrace_get_vec_len(pid, argv_addr); + envc = ptrace_get_vec_len(pid, envp_addr); + if (argc == -1 || envc == -1) + goto bad; + + /* Reserve argv and envp at the start of argbuf so they are alined. */ + if ((argc + 1 + envc + 1) * sizeof(long) >= bufsize) { + sudo_warnx("%s", U_("insufficent space for argv and envp")); + goto bad; + } + argv = (char **)argbuf; + envp = argv + argc + 1; + strtab = (char *)(envp + envc + 1); + bufsize -= strtab - argbuf; + + /* Read argv */ + len = ptrace_read_vec(pid, argv_addr, argv, strtab, bufsize); + if (len == (size_t)-1) { + sudo_warn(U_("unable to read execve argv for process %d"), (int)pid); + goto bad; + } + strtab += len; + bufsize -= len; + + /* Read envp */ + len = ptrace_read_vec(pid, envp_addr, envp, strtab, bufsize); + if (len == (size_t)-1) { + sudo_warn(U_("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 pathname for process %d"), (int)pid); + goto bad; + } + pathname = strtab; + strtab += len; + bufsize -= len; + + sudo_debug_execve(SUDO_DEBUG_INFO, pathname, argv, envp); + + *pathname_out = pathname; + *argv_out = argv; + *envp_out = envp; + + debug_return_ptr(argbuf); +bad: + free(argbuf); + debug_return_ptr(NULL); +} + /* * Check whether seccomp(2) filtering supports ptrace(2) traps. * Only supported by Linux 4.14 and higher. @@ -155,15 +471,23 @@ exec_ptrace_handled(pid_t pid, int status) bool group_stop = false; debug_decl(exec_ptrace_handled, SUDO_DEBUG_EXEC); - if (sigtrap == (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) { + if (sigtrap == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8))) { + char *pathname, **argv, **envp, *buf; + /* 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). + * XXX */ + buf = get_execve_args(pid, &pathname, &argv, &envp); + if (buf == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: %d: unable to get exec args", __func__, (int)pid); + } } else if (sigtrap == (SIGTRAP | (PTRACE_EVENT_CLONE << 8)) || sigtrap == (SIGTRAP | (PTRACE_EVENT_VFORK << 8)) || sigtrap == (SIGTRAP | (PTRACE_EVENT_FORK << 8))) {