From 468bfda91be14b76bb15ff8cd4d66bbc7d3db12e Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Tue, 29 Nov 2016 15:15:26 +0100 Subject: [PATCH 1/1] Initial commit --- .gitignore | 1 + Makefile | 11 ++ README | 64 ++++++++ ptyas.c | 454 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 530 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README create mode 100644 ptyas.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2dfeeec --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/ptyas diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ea1b2e6 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +CFLAGS := -g -O2 -fPIE -fstack-protector-strong -Wall -Wextra -Wconversion +CPPFLAGS := -D_FORTIFY_SOURCE=2 +LDFLAGS := -fPIE -pie -Wl,-z,relro -Wl,-z,now + + +ptyas: ptyas.c + +clean: + rm -f ptyas + +.PHONY: clean diff --git a/README b/README new file mode 100644 index 0000000..9dd3800 --- /dev/null +++ b/README @@ -0,0 +1,64 @@ +README +====== + +ptyas is a small su/sudo replacement which prevents TTY hijacking by starting +a new session with a separate terminal and proxying all input. It's licensed +under the GPL 3 or later. + +It must be run as root and changes the owner to the specified user name, +permanently dropping all root permissions. + +For details about the possible attacks see +https://ruderich.org/simon/notes/su-sudo-from-root-tty-hijacking + + +DEPENDENCIES +------------ + +- C99 compiler +- UNIX 98 pseudoterminals + + +USAGE +----- + + ptyas [] + +If no command is given, the user's shell is started. Otherwise the command is +executed (which is useful if the user's shell is disabled). + + +BUGS +---- + +If you find any bugs not mentioned in this document please report them to + with ptyas in the subject. + + +AUTHORS +------- + +Written by Simon Ruderich . + + +LICENSE +------- + +ptyas is licensed under GPL version 3 or later. + +Copyright (C) 2016 Simon Ruderich + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +// vim: ft=asciidoc diff --git a/ptyas.c b/ptyas.c new file mode 100644 index 0000000..f96962e --- /dev/null +++ b/ptyas.c @@ -0,0 +1,454 @@ +/* + * Run the login shell or command as the given user in a new pty to prevent + * terminal injection attacks. + * + * Copyright (C) 2016 Simon Ruderich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +static void die(const char *s) { + perror(s); + exit(EXIT_FAILURE); +} +static void die_msg(const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + exit(EXIT_FAILURE); +} + +static void open_pty_or_die(int *pty_master, int *pty_slave, uid_t uid) { + char *slave_path; + + *pty_master = posix_openpt(O_RDWR | O_NOCTTY); + if (*pty_master == -1) { + die("posix_openpt"); + } + slave_path = ptsname(*pty_master); + if (!slave_path) { + die("ptsname"); + } + if (grantpt(*pty_master) != 0) { + die("grantpt"); + } + if (unlockpt(*pty_master) != 0) { + die("unlockpt"); + } + + *pty_slave = open(slave_path, O_RDWR | O_NOCTTY); + if (*pty_slave == -1) { + die("open slave tty"); + } + /* The user must be able to write to the new TTY. Normally grantpt() would + * do this for us, but we don't trust the user and thus don't want to pass + * the pty_master to a process running under that uid. */ + // TODO: is this a problem? + if (chown(slave_path, uid, (gid_t)-1) != 0) { + die("chown slave tty"); + } +} + +static void close_or_die(int fd) { + if (close(fd) != 0) { + die("close"); + } +} +static void dup2_or_die(int oldfd, int newfd) { + if (dup2(oldfd, newfd) != newfd) { + die("dup2"); + } +} +static int snprintf_or_assert(char *str, size_t size, const char *format, ...) { + int ret; + va_list ap; + + va_start(ap, format); + ret = vsnprintf(str, size, format, ap); + assert(size <= (size_t)INT_MAX); + assert(ret < (int)size); /* assert output fit into buffer */ + va_end(ap); + + return ret; +} + +static void drop_privileges_or_die(uid_t uid, gid_t gid) { + /* Drop all supplementary group IDs. */ + if (setgroups(0, NULL) != 0) { + die("setgroups"); + } + if (getgroups(0, NULL) != 0) { + die_msg("failed to drop all groups"); + } + + /* Dropping groups may require privileges, do that first. */ + if (setresgid(gid, gid, gid) != 0) { + die("setresgid"); + } + if (setresuid(uid, uid, uid) != 0) { + die("setresuid"); + } + + /* Ensure we dropped all privileges. */ + { + uid_t ruid, euid, suid; + gid_t rgid, egid, sgid; + + if (getresuid(&ruid, &euid, &suid) != 0) { + die("getresuid"); + } + if (getresgid(&rgid, &egid, &sgid) != 0) { + die("getresgid"); + } + if ( uid != ruid || uid != euid || uid != suid + || gid != rgid || gid != egid || gid != sgid) { + die_msg("failed to drop privileges"); + } + } + /* Just to be safe. */ + if (setuid(0) != -1) { + die_msg("failed to drop privileges (setuid)"); + } +} + +static void quit_with_matching_code(int status) { + if (WIFEXITED(status)) { + exit(WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + kill(getpid(), WTERMSIG(status)); + /* Fall-through, should not happen. */ + } + abort(); /* Should never happen, die painfully. */ +} + +static bool read_from_write_to(int from, int to) { + char buf[4096]; + + ssize_t r = read(from, buf, sizeof(buf)); + if (r < 0) { + return false; + } + + size_t left = (size_t)r; + char *data = buf; + + while (left > 0) { + ssize_t w = write(to, data, left); + if (w < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + left -= (size_t)w; + data += (size_t)w; + } + + return true; +} + +static void proxy_input_between_ttys(int pty_master, int ctty, volatile pid_t *pid_to_wait_for) { + struct pollfd fds[] = { + { /* 0 */ + .fd = pty_master, + .events = POLLIN, + }, + { /* 1 */ + .fd = ctty, + .events = POLLIN, + }, + }; + + sigset_t sigset, sigset_old; + sigemptyset(&sigset); + sigaddset(&sigset, SIGCHLD); + if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) { + die("sigprocmask block sigchld proxy"); + } + + /* Proxy data until our child has terminated. */ + while (*pid_to_wait_for != 0) { + /* + * If a signal happens here _and_ the child hasn't closed pty_slave, + * we will hang in poll(); therefore ppoll() is requred. + */ + nfds_t nfds = sizeof(fds)/sizeof(*fds); + if (ppoll(fds, nfds, NULL /* no timeout */, &sigset_old) == -1) { + if (errno == EAGAIN || errno == EINTR) { + continue; + } else { + perror("poll"); + } + break; + } + + /* Handle errors first. */ + if (fds[0].revents & (POLLERR | POLLNVAL)) { + fprintf(stderr, "poll: error on master: %d\n", fds[0].revents); + break; + } + if (fds[1].revents & (POLLERR | POLLNVAL)) { + fprintf(stderr, "poll: error on ctty: %d\n", fds[1].revents); + break; + } + + /* Then read data if available. */ + if (fds[0].revents & POLLIN) { + if (!read_from_write_to(pty_master, ctty)) { + perror("read from master write to ctty"); + break; + } + } + if (fds[1].revents & POLLIN) { + if (!read_from_write_to(ctty, pty_master)) { + perror("read from ctty write to master"); + break; + } + } + + /* Finally we are done if either side of the pty has disconnected. */ + if ((fds[0].revents & POLLHUP) || (fds[1].revents & POLLHUP)) { + break; + } + } + + if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) { + die("sigprocmask setmask proxy"); + } +} + + +/* Not sig_atomic_t but I don't know how to do that any other way. */ +static volatile pid_t pid_to_wait_for; +static int pid_to_wait_for_status; + +static void sigchld_handler() { + int status; + pid_t pid; + + while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { + if (pid == pid_to_wait_for) { + /* Mark that our child has died and we should exit as well. */ + pid_to_wait_for = 0; + /* We must exit like our child, save status. */ + pid_to_wait_for_status = status; + } + } +} + + +int main(int argc, char **argv) { + char *exec_argv_shell[] = { NULL, NULL }; /* filled below */ + char **exec_argv = NULL; + + if (argc == 2) { + /* exec_argv set below */ + } else if (argc > 2) { + exec_argv = argv + 2; + } else { + die_msg("%s [...]\n", argv[0]); + } + + const char *user = argv[1]; + + struct passwd *passwd = getpwnam(user); + if (!passwd) { + die_msg("unknown user name '%s'\n", user); + } + + uid_t uid = passwd->pw_uid; + gid_t gid = passwd->pw_gid; + + if (!exec_argv) { + assert(argc == 2); + exec_argv_shell[0] = passwd->pw_shell; + exec_argv = exec_argv_shell; + } + + int pty_master, pty_slave; + + open_pty_or_die(&pty_master, &pty_slave, uid); + + int ctty = open("/dev/tty", O_RDWR | O_NOCTTY); /* controlling TTY */ + if (ctty == -1) { + die("open /dev/tty"); + } + + sigset_t sigset, sigset_old; + sigemptyset(&sigset); + sigaddset(&sigset, SIGCHLD); + if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) { + die("sigprocmask block sigchld"); + } + + pid_t pid = fork(); + if (pid == -1) { + die("fork parent"); + } else if (pid == 0) { + if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) { + die("sigprocmask setmask child"); + } + + struct winsize size; + if (ioctl(ctty, TIOCGWINSZ, &size) == -1) { + die("ioctl TIOCGWINSZ"); + } + + close_or_die(pty_master); + close_or_die(ctty); + + /* Start a new session and attach controlling TTY. */ + if (setsid() == -1) { + die("setsid"); + } + if (ioctl(pty_slave, TIOCSCTTY, 0) == -1) { + die("ioctl TIOCSCTTY"); + } + + if (ioctl(pty_slave, TIOCSWINSZ, &size) == -1) { + die("ioctl TIOCSWINSZ"); + } + + pid_t pid = fork(); + if (pid == -1) { + die("fork child"); + } else if (pid == 0) { + drop_privileges_or_die(uid, gid); + + dup2_or_die(pty_slave, STDIN_FILENO); + dup2_or_die(pty_slave, STDOUT_FILENO); + dup2_or_die(pty_slave, STDERR_FILENO); + close_or_die(pty_slave); + + const char *term_orig = getenv("TERM"); + const char *term = term_orig; + if (!term) { + term = ""; /* for strlen() below */ + } + const char *home = passwd->pw_dir; + + char envp_user[strlen("USER=") + strlen(user) + 1]; + char envp_home[strlen("HOME=") + strlen(home) + 1]; + char envp_term[strlen("TERM=") + strlen(term) + 1]; + snprintf_or_assert(envp_user, sizeof(envp_user), "USER=%s", user); + snprintf_or_assert(envp_home, sizeof(envp_home), "HOME=%s", home); + snprintf_or_assert(envp_term, sizeof(envp_term), "TERM=%s", term); + + char *exec_envp[] = { + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + envp_user, + envp_home, + term_orig ? envp_term : NULL, + NULL, + }; + + execve(exec_argv[0], exec_argv, exec_envp); + die("execve"); + } + close_or_die(pty_slave); + close_or_die(STDIN_FILENO); + close_or_die(STDOUT_FILENO); + close_or_die(STDERR_FILENO); + + // TODO: EINTR? + int status; + if (waitpid(pid, &status, 0) <= 0) { + die("waitpid child"); + } + quit_with_matching_code(status); + } + close_or_die(pty_slave); + + pid_to_wait_for = pid; + struct sigaction action = { + .sa_handler = sigchld_handler, + }; + if (sigaction(SIGCHLD, &action, NULL) != 0) { + die("sigaction"); + } + + if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) { + die("sigprocmask setmask parent"); + } + + struct termios old_term, term; + + /* Change terminal to raw mode. */ + if (tcgetattr(ctty, &old_term) != 0) { + die("tcgetattr"); + } + term = old_term; + /* From man 3 cfmakeraw which is non-standard. */ + term.c_iflag &= ~(tcflag_t)(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); + term.c_oflag &= ~(tcflag_t)(OPOST); + term.c_lflag &= ~(tcflag_t)(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + term.c_cflag &= ~(tcflag_t)(CSIZE | PARENB); + term.c_cflag |= CS8; + if (tcsetattr(ctty, TCSADRAIN, &term) != 0) { + die("tcsetattr"); + } + + proxy_input_between_ttys(pty_master, ctty, &pid_to_wait_for); + + /* Restore terminal mode. */ + if (tcsetattr(ctty, TCSADRAIN, &old_term) != 0) { + die("tcsetattr restore"); + } + + /* Wait until we got the status code from our child. poll() might also + * exit after POLLHUP while we haven't collected the child yet. */ + if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) { + die("sigprocmask block sigchld loop"); + } + while (pid_to_wait_for != 0) { + sigsuspend(&sigset_old); + if (errno != EINTR) { + die("sigsuspend"); + } + } + if (sigprocmask(SIG_SETMASK, &sigset, &sigset_old) != 0) { + die("sigprocmask setmask sigchld loop"); + } + + /* Try to exit the same way as the spawned process. */ + if (pid_to_wait_for == 0) { + quit_with_matching_code(pid_to_wait_for_status); + } + return EXIT_FAILURE; +} -- 2.45.2