From 6c02deb271622b4f14ad53a19aa26b1c194f43da Mon Sep 17 00:00:00 2001 From: Trumeet Date: Fri, 23 Dec 2022 15:02:44 -0800 Subject: First Commit --- .gitignore | 77 ++++++++++++++++++++ .idea/.gitignore | 8 +++ .idea/misc.xml | 4 ++ .idea/modules.xml | 8 +++ .idea/secdesk.iml | 2 + .idea/vcs.xml | 6 ++ CMakeLists.txt | 6 ++ README.md | 127 +++++++++++++++++++++++++++++++++ common.h | 45 ++++++++++++ consent.c | 73 +++++++++++++++++++ log.c | 45 ++++++++++++ log.h | 42 +++++++++++ main.c | 54 ++++++++++++++ sd.c | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 702 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/secdesk.iml create mode 100644 .idea/vcs.xml create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 common.h create mode 100644 consent.c create mode 100644 log.c create mode 100644 log.h create mode 100644 main.c create mode 100644 sd.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3649d6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..79b3c94 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2ca5aac --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/secdesk.iml b/.idea/secdesk.iml new file mode 100644 index 0000000..f08604b --- /dev/null +++ b/.idea/secdesk.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3a9a6f9 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.24) +project(secdesk C) + +set(CMAKE_C_STANDARD 11) + +add_executable(secdesk main.c log.c log.h common.h consent.c sd.c) diff --git a/README.md b/README.md new file mode 100644 index 0000000..dec9818 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# SecDesk + +An attempt to port Windows *secure desktop* to Linux. + +## Background + +On Windows, every window belongs to a [desktop](https://learn.microsoft.com/en-us/windows/win32/winstation/desktops), +and processes can (?) read the input of other windows, unless they are privileged (?). + +Winlogon and UAC consent protects user inputs (e.g. passwords) from being stolen by 1) running as `NT Authority\SYSTEM` +, and running on the secure desktop, which is a separate desktop from the users'. This way, user processes cannot read +inputs. + +macOS should (?) has a similar security measurement, but I am not sure. + +On Unix, X11 does not have such a protection, and user processes are free to read any other process's input. This +includes your lock screen (e.g. `i3lock(1)`) or terminal (running `sudo(1)`). + +Wayland fixes this issue by implementing a security control, while X11 users are left unprotected. + +This proof-of-concept project aims at porting the secure desktop concept on Windows to Unix. + +## Threat model + +This project is aimed at preventing malware running as the unprivileged user to capture or hijack the input of +sensitive dialogues. It assumes that the operating system (kernel, libraries, binaries, daemons, and everything +privileged) is trusted. + +## Basic idea + +Without modifying the kernel, the easiest approach is to use virtual terminals. While most current Unix operating +systems has virtual terminal support, this project is mainly focused on Linux due to the author's familiarity. Porting +it to other Unix could be possible. + +Linux has multiple virtual terminals (defined in `MAX_NR_CONSOLES`), and each one can run a text terminal or GUI. The +kernel provides text terminals: programs read and write to `/dev/ttyN`, and the kernel handles input and outputs from +the keyboard and to the screen. Display servers can use framebuffer or DRM, as well as `/dev/input/`, to draw their own +graphics. + +Popular implementations of display servers, like Xorg or wayland compositors using `seatd` are all using DRM and +`/dev/input`. + +Each virtual terminal is independent of each other, and users may switch from one to another using `Ctrl + Alt + Fx` or +`VT_ACTIVATE` ioctl. + +Each virtual terminal is isolated from each other. For the text terminals, `/dev/ttyN` has the default permission of +`0620 root:tty`, and privileged display servers holding `/dev/input/*` does not pass keystrokes to user processes after +switching to other virtual terminals (Xorg will keep these files open, while `seated` will close them). +This is ideal for implementing a secure desktop on Linux. + +The approach is to start a separate display server (e.g. Xorg) on a free (unused) virtual terminal, switch to it, read +sensitive data, switch back, and close the virtual terminal. This guarantees that unprivileged processes have no way to +hijack the password dialogue, with the limitation that the password dialogue must be trusted and ran by root. +For example, a dedicated X server coule be started using: + +```shell +Xorg -background none :$NEW_DISPLAY vt$NEW_VT -nolisten tcp +# Setup XAuth, so only root can connect. +``` + +Then, start a password dialogue: + +```shell +DISPLAY=:$NEW_DISPLAY /usr/lib/ssh/x11-ssh-askpass +``` + +This would be the simplest form of a secure desktop. + +## Implementation + +To make the startup process faster, more portable, and simpler, I wrote this PoC that uses the text terminal instead of +a display server. It is not a *desktop* in terms of the Windows secure desktop, but it satisfies the requirement of +securely reading sensitive data. + +This PoC makes uses of various TTY and Console related ioctl (see `ioctl_console(2)` and `ioctl_tty(2)`), and it uses +codes from [kbd](http://kbd-project.org/). How this PoC works is obvious: + +1. Open the TTY of the current process. +2. Find an open virtual terminal (`VT_OPENQRY`) and open it. +3. Make the virtual terminal the controlling terminal (`setsid(2)` and `TIOCSCTTY`). +4. Read / write as usual using the file descriptor from step 2. +5. Switch to it or switch back using `VT_ACTIVATE` and `VT_WAITACTIVATE`. + +## SAK + +Although the above process can safely read passwords, one more security measure must be taken into consideration: the +user must know the authenticity of the password dialogue. That is, a user process may create a full-screen window to +mimic the password dialogue in order to obtain the password. + +Windows mitigates this issue by having a SAK (Secure attention key), which is `Ctrl + Alt + Delete`. The NT kernel +directly handles this key combo, and it notifies winlogon to show a privileged screen (either login screen or the +security options page). Users can trust the page is authentic because no other programs shall capture the key combo. +Moreover, domain administrators can enable the Require Ctrl Alt Del on Logon group policy to train users that they must +press the SAK before login to ensure the authenticity of the login screen. + +However, this feature is missing on most current Unix operating systems. Linux is the only known Unix operating system +to have a SAK, and it is less known and has little use. On Linux, the SAK is `SysRq + K`. The kernel will kill all +processes (including the display server) in the current virtual terminal, so the service manager will restart the login +prompt (either the display manger or getty), and it is authentic. This behaviour makes SAK on Linux very limited, as +most users do not want their desktop programs to be killed just for logging in. + +This project took the advantage of the Linux SAK by forking a child to display the message `Press SysRq + K to continue` +on the new virtual terminal and wait for it to be killed by `SIGKILL`. Although race condition could happen after +`waitpid(2)` and `TIOCSCTTY`, it is still safe because the `/dev/ttyN` file does not allow non-root processes to write. + +## In the future + +This project is far from perfect: its authentication UI / UX is naive, and it requires running as root to open the TTY. +In the future, I will make it an AskPass / PolKit agent, where unprivileged user can run use it to securely authenticate +themselves. I will also fix the behaviour when running from environments like SSH (pts) or serial, where virtual +terminals do not exist. In these environments, it should simply use the current terminal and inform the user that the +terminal is insecure, which is what Windows RDP does regarding remote UAC consent. + +## Build and run + +```shell +mkdir build +cd build +cmake .. +sudo ./secdesktop password test # mode prompt +``` + +The code is ugly: it is written in 4 hours. I will try to make it pretty. + +## License + +WTFPL \ No newline at end of file diff --git a/common.h b/common.h new file mode 100644 index 0000000..0bf7c6d --- /dev/null +++ b/common.h @@ -0,0 +1,45 @@ +/* + * Created by yuuta on 12/23/22. + */ + +#ifndef SECDESK_COMMON_H +#define SECDESK_COMMON_H + +#include + +enum modes { + /* Yes / No on action . */ + mode_consent, + /* Enter your password for . + * The program will have a copy of your password. */ + mode_password, + /* Enter username and password for . + * The program will neither know your password nor have access to your account. + * It just knows who you are. */ + mode_auth +}; + +struct auth_env { + enum modes mode; + uid_t usr; + pid_t pid; + const char *prompt; +}; + +struct proc_env { + int in; + int out; + int err; +}; + +extern struct proc_env p_env; + +extern struct auth_env a_env; + +int sd_setup(void); + +void sd_cleanup(void); + +int main_consent(int secure); + +#endif /* SECDESK_COMMON_H */ diff --git a/consent.c b/consent.c new file mode 100644 index 0000000..c2ab677 --- /dev/null +++ b/consent.c @@ -0,0 +1,73 @@ +/* + * Created by yuuta on 12/23/22. + */ + +#include "common.h" +#include "log.h" + +#include +#include + +int main_consent(int secure) { + if (!secure) { + printf("WARNING: The terminal you are about to enter passwords is insecure.\n"); + } else { + printf("\033[r\033[H\033[J"); + } + printf("Program %s (process %d: '%s'), is requesting your authorization.\n", + "/TODO", + a_env.pid, + "todo"); + switch (a_env.mode) { + case mode_consent: { + printf("Do you consent '%s'? (Y / N) ", a_env.prompt); + char buf[5]; + if (read(STDIN_FILENO, buf, sizeof(buf) - 1) < 0) { + int r = errno; + LOGFV("read: %m", r); + printf("Cannot read your response: %m. Press any key to return.\n", + errno); + fgetc(stdin); + if (secure) { + printf("\033[r\033[H\033[J"); + } + return r; + } + if (buf[0] == 'Y' || buf[0] == 'y') { + dprintf(p_env.out, "1"); + } else { + dprintf(p_env.out, "0"); + } + break; + } + case mode_password: { + printf("Enter your password for '%s'. " + "The program will know your password.\n" + "Password: ", a_env.prompt); + char buf[10]; + if (read(STDIN_FILENO, buf, sizeof(buf) - 1) < 0) { + int r = errno; + LOGFV("read: %m", r); + printf("Cannot read your response: %m. Press any key to return.\n", + errno); + fgetc(stdin); + if (secure) { + printf("\033[r\033[H\033[J"); + } + return r; + } + buf[sizeof(buf) - 1] = '\0'; + dprintf(p_env.out, "%s", buf); + break; + } + case mode_auth: { + // TODO + fgetc(stdin); + break; + } + } + if (secure) { + printf("\033[r\033[H\033[J"); + } + return 0; +} diff --git a/log.c b/log.c new file mode 100644 index 0000000..bdf7198 --- /dev/null +++ b/log.c @@ -0,0 +1,45 @@ +/* + * Created by yuuta on 1/1/22. + */ + +#include "common.h" +#include "log.h" + +#include +#include +#include + +void g_log(enum log_level level, + const char *file, + int line, + const char *format, + ...) { + int stream = level <= log_warn || level == log_debug ? p_env.err : p_env.out; + switch (level) { + case log_fetal: + dprintf(stream, "F"); + break; + case log_error: + dprintf(stream, "E"); + break; + case log_warn: + dprintf(stream, "W"); + break; + case log_info: + dprintf(stream, "I"); + break; + case log_debug: + dprintf(stream, "D"); + break; + default: + dprintf(p_env.err, "Unknown log level: %d.\n", level); + assert(0); + } + dprintf(stream, "[%s:%d]: ", + file, line); + va_list list; + va_start(list, format); + vdprintf(stream, format, list); + va_end(list); + dprintf(stream, "\n"); +} diff --git a/log.h b/log.h new file mode 100644 index 0000000..7d41fbc --- /dev/null +++ b/log.h @@ -0,0 +1,42 @@ +/* + * Created by yuuta on 1/1/22. + */ + +#ifndef LOG_H +#define LOG_H + +enum log_level { + log_fetal = 1, + log_error = 2, + log_warn = 3, + log_info = 4, + log_debug = 5 +}; + +void g_log(enum log_level level, + const char *file, + int line, + const char *format, + ...); + +#define LOGF(X) g_log(log_fetal, __FUNCTION__, __LINE__, X) + +#define LOGFV(X, ...) g_log(log_fetal, __FUNCTION__, __LINE__, X, __VA_ARGS__) + +#define LOGE(X) g_log(log_error, __FUNCTION__, __LINE__, X) + +#define LOGEV(X, ...) g_log(log_error, __FUNCTION__, __LINE__, X, __VA_ARGS__) + +#define LOGW(X) g_log(log_warn, __FUNCTION__, __LINE__, X) + +#define LOGWV(X, ...) g_log(log_warn, __FUNCTION__, __LINE__, X, __VA_ARGS__) + +#define LOGI(X) g_log(log_info, __FUNCTION__, __LINE__, X) + +#define LOGIV(X, ...) g_log(log_info, __FUNCTION__, __LINE__, X, __VA_ARGS__) + +#define LOGD(X) g_log(log_debug, __FUNCTION__, __LINE__, X) + +#define LOGDV(X, ...) g_log(log_debug, __FUNCTION__, __LINE__, X, __VA_ARGS__) + +#endif /* LOG_H */ diff --git a/main.c b/main.c new file mode 100644 index 0000000..4652583 --- /dev/null +++ b/main.c @@ -0,0 +1,54 @@ +#include "log.h" +#include "common.h" + +#include +#include +#include + +struct auth_env a_env; +struct proc_env p_env = { + .in = STDIN_FILENO, + .out = STDOUT_FILENO, + .err = STDERR_FILENO +}; + +int main(int argc, char **argv) { + if (argc != 2 && argc != 3) { + fprintf(stderr, "Usage: %s consent|password|auth [prompt]\n", + argv[0]); + return 64; + } + if (!strcmp(argv[1], "consent")) { + a_env.mode = mode_consent; + } else if (!strcmp(argv[1], "password")) { + a_env.mode = mode_password; + } else if (!strcmp(argv[1], "auth")) { + a_env.mode = mode_auth; + } else { + fprintf(stderr, "Unknown mode: %s\n", argv[1]); + return 64; + } + + if ((a_env.prompt = argv[2])) { + char *p = argv[2]; + do { + if ((*p) < 32 || (*p) > 126) { + fprintf(stderr, "The given prompt is illegal.\n"); + return 64; + } + } while (*(++ p)); + } + + /* Because we need to frequently dup(2). */ + setbuf(stdout, NULL); + a_env.pid = getppid(); + signal(SIGHUP, SIG_IGN); + if (sd_setup()) { + sd_cleanup(); + return main_consent(0); + } else { + int r = main_consent(1); + sd_cleanup(); + return r; + } +} diff --git a/sd.c b/sd.c new file mode 100644 index 0000000..c987df3 --- /dev/null +++ b/sd.c @@ -0,0 +1,205 @@ +/* + * Created by yuuta on 12/23/22. + */ + +#include "common.h" +#include "log.h" + +#define IOCTL(a, b, ...) \ +if (ioctl(a, b, __VA_ARGS__) < 0) { LOGFV("ioctl(%d, 0x%x): %m", a, b, errno); return errno; } + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +static const char *conspath[] = { + "/proc/self/fd/0", + "/proc/self/fd/1", + "/proc/self/fd/2", + "/dev/tty", + "/dev/tty0", + "/dev/vc/0", + "/dev/systty", + "/dev/console" +}; + +static int fd_con = -1; +static struct vt_stat vts; +static int vt_num = -1; +static char vt_dev[PATH_MAX + 1]; +static int vt_fd = -1; +static pid_t chld = -1; + +static int vt_switch(int to) { + if (to) { + IOCTL(fd_con, VT_ACTIVATE, vt_num) + IOCTL(fd_con, VT_WAITACTIVE, vt_num) + } else { + IOCTL(fd_con, VT_ACTIVATE, vts.v_active) + IOCTL(fd_con, VT_WAITACTIVE, vts.v_active) + } + return 0; +} + +static int find_current_tty(void) { + for (unsigned int i = 0; i < sizeof(conspath) / sizeof(char *); i++) { + if ((fd_con = open(conspath[i], O_RDONLY)) == -1) { + LOGDV("Cannot open '%s': %m.", + conspath[i], + errno); + continue; + } + char arg; + errno = 0; + if (!isatty(fd_con) || ioctl(fd_con, KDGKBTYPE, &arg) || + ((arg != KB_101) && (arg != KB_84))) { + if (errno) { + LOGDV("Cannot open '%s': %m.", + conspath[i]); + } else { + LOGDV("Cannot open '%s': Not a TTY.", conspath[i]); + } + close(fd_con); + fd_con = -1; + continue; + } + LOGDV("Opened '%s': %d.", conspath[i], fd_con); + break; + } + if (fd_con == -1) { + LOGE("Cannot get the current console. Try running as root to get tty0."); + return 1; + } + return 0; +} + +static int main_sak(void) { + /* We have to take the ownership here - otherwise SAK will kill the parent. */ + if (setsid() == -1) { + LOGFV("Cannot set session ID: %m.", errno); + return errno; + } + IOCTL(vt_fd, TIOCSCTTY, 0) + vt_switch(1); + + dprintf(vt_fd, "\033[r\033[H\033[J" + "Press SysRq + K to continue.\n"); + /* Wait for signal. */ + while (1) { + pause(); + } +} + +int sd_setup(void) { + if (find_current_tty()) { + return 1; + } + IOCTL(fd_con, VT_GETSTATE, &vts) + IOCTL(fd_con, VT_OPENQRY, &vt_num) + snprintf(vt_dev, PATH_MAX, "/dev/tty%d", vt_num); + if ((vt_fd = open(vt_dev, O_RDWR)) == -1) { + LOGFV("Cannot open '%s': %m.", vt_dev, errno); + return errno; + } + LOGDV("Opened '%s': %d.", vt_dev, vt_fd); + + if (!(chld = fork())) { + exit(main_sak()); + } + if (chld == -1) { + LOGFV("Cannot fork: %m.", errno); + return errno; + } + int chld_status = -1; + if (waitpid(chld, &chld_status, 0) != chld) { + LOGFV("Cannot wait for child: %m.", errno); + return errno; + } + /* It seems like we need to open(2) again, or TIOCSCTTY will fail with EIO. */ + close(vt_fd); + vt_fd = -1; + + if (!WIFSIGNALED(chld_status)) { + LOGF("Not killed by signal. Considered insecure."); + return 1; + } + switch (WTERMSIG(chld_status)) { + case SIGKILL: { + break; + } + case SIGINT: + case SIGTERM: { + exit(13); + } + default: { + /* Consider insecure. + * Switch back and use the insecure view. */ + LOGF("Killed by an incorrect signal. Considered insecure."); + return 1; + } + } + + /* Take ownership */ + if ((vt_fd = open(vt_dev, O_RDWR)) == -1) { + LOGFV("Cannot open '%s': %m.", vt_dev, errno); + return errno; + } + LOGDV("Opened '%s': %d.", vt_dev, vt_fd); + if (setsid() == -1) { + LOGFV("Cannot set session ID: %m.", errno); + return errno; + } + IOCTL(vt_fd, TIOCSCTTY, 1) + + int in_bak, out_bak, err_bak; + if ((in_bak = dup(p_env.in)) == -1 || + (out_bak = dup(p_env.out)) == -1 || + (err_bak = dup(p_env.err)) == -1) { + LOGFV("dup: %m", errno); + return errno; + } + + p_env.in = in_bak; + p_env.out = out_bak; + p_env.err = err_bak; + + if (dup2(vt_fd, STDIN_FILENO) == -1 || + dup2(vt_fd, STDOUT_FILENO) == -1 || + dup2(vt_fd, STDERR_FILENO) == -1) { + LOGFV("dup2: %m", errno); + } + + return 0; +} + +void sd_cleanup(void) { + if (vt_fd >= 0) { + if (ioctl(vt_fd, TIOCNOTTY) < 0) { + LOGEV("Cannot deallocate console: %m", errno); + } + if (p_env.in != STDIN_FILENO) p_env.in = dup2(p_env.in, STDIN_FILENO); + if (p_env.out != STDOUT_FILENO) p_env.out = dup2(p_env.out, STDOUT_FILENO); + if (p_env.err != STDERR_FILENO) p_env.err = dup2(p_env.err, STDERR_FILENO); + /* It should already be closed? */ + close(vt_fd); + vt_fd = -1; + } + if (fd_con >= 0) { + if (vt_switch(0)) { + LOGEV("Cannot return to the previous console: %m", errno); + } + if (ioctl(fd_con, VT_DISALLOCATE, vt_num) < 0) { + LOGEV("Cannot deallocate console: %m", errno); + } + close(fd_con); + fd_con = -1; + } +} -- cgit v1.2.3