0x36/Pixel_GPU_Exploit
GitHub: 0x36/Pixel_GPU_Exploit
针对 Pixel 设备 Android 14 内核的本地提权漏洞利用工具,实现绕过 SELinux 与 KASLR 的稳定读写与 root 获取。
Stars: 547 | Forks: 85
# Mali GPU Kernel LPE
本文深入分析了 Mali GPU 中的两个内核漏洞,这些漏洞可通过默认应用程序沙箱访问,是我独立发现并报告给 Google 的。它包含一个内核漏洞利用程序,可实现任意内核读/写权限。因此,它禁用了 SELinux 并在运行以下 Android 14 版本的 Google Pixel 7 和 8 Pro 模型上提升了 root 权限:
- Pixel 8 Pro:`google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys`
- Pixel 7 Pro:`google/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keys`
- Pixel 7 Pro:`google/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keys`
- Pixel 7:`google/panther/panther:14/UP1A.231105.003/11010452:user/release-keys`(由 [m4b4 (Marcel)](https://github.com/m4b4) 提供)
## 漏洞
该漏洞利用利用了以下两个漏洞:`gpu_pixel_handle_buffer_liveness_update_ioctl` ioctl 命令处的整数溢出(由于不完整的补丁),以及时间线流消息缓冲区中的信息泄露。
### 由于错误的整数溢出修复导致 gpu_pixel_handle_buffer_liveness_update_ioctl() 中的缓冲区下溢
Google 在 [此提交](https://android.googlesource.com/kernel/google-modules/gpu/+/68073dce197709c025a520359b66ed12c5430914%5E%21/#F0) 中修复了 `gpu_pixel_handle_buffer_liveness_update_ioctl` ioctl 命令的整数溢出问题。起初,当我报告此问题时,我以为该漏洞是由前述补丁中的问题引起的。查看报告后,我意识到我对该漏洞的分析不准确。尽管我最初假设补丁不完整,但它实际上有效解决了计算中的下溢问题。这让我怀疑该更改未应用于生产构建。然而,虽然我可以在计算中造成下溢,但无法造成溢出。这表明该 ioctl 命令已被部分修复,尽管并非使用上述补丁所示的方式修复。通过 IDA 查看发现,生产版本中包含了另一个不完整的补丁,并且该补丁在 Mali GPU 内核模块的任何 git 分支中都不存在。
此漏洞最早在最新 Android 版本中发现,并于 2023 年 11 月 19 日报告。Google 后来告知我,他们已在内部识别该问题,并在 12 月 Android 安全公告中为其分配了 [CVE-2023-48409](https://source.android.com/docs/security/bulletin/pixel/2023-12-01),并将其标记为重复问题。
尽管我能够验证该漏洞在我报告前几个月(基于提交日期约为 8 月 30 日)已被内部识别,但仍存在混淆。具体来说,最新型号的 Android 14 设备的安全补丁级别(SPL)在 10 月和 11 月仍然受此漏洞影响——我尚未调查更早版本。因此,我无法确定这是否真的是一个重复问题,以及适当的补丁是否已在提交前安排在 12 月,或者是否存在处理此漏洞的疏忽。
无论如何,使此漏洞具有威胁性的原因如下:
- 缓冲区 `info.live_ranges` 完全由用户控制。
- 溢出值是用户控制的输入,因此我们可以溢出计算,使 `info.live_ranges` 指针在 `buff` 内核地址开始之前处于任意偏移位置。
- 分配大小也是用户控制的输入,这使我们能够从任意通用目的 slab 分配器中请求内存分配。
此漏洞与我发现并利用的 [DeCxt::RasterizeScaleBiasData() 缓冲区下溢漏洞](https://github.com/0x36/weightBufs) 相似,该漏洞曾在 2022 年 iOS 15 内核中被发现。
### 时间线流消息缓冲区中的内核指针泄露
Mali GPU 实现了一个自定义的 `timeline stream`,用于收集信息、序列化,然后按照特定格式写入环形缓冲区。用户可以调用 ioctl 命令 `kbase_api_tlstream_acquire` 来获取文件描述符,从而读取该环形缓冲区。消息的格式如下:
- 一个 [数据包头](https://android.googlesource.com/kernel/google-modules/gpu/+/refs/heads/android-gs-pantah-5.10-android14-qpr2-beta/mali_kbase/mali_kbase_mipe_proto.h#68)
- 一个 [消息 ID](https://android.googlesource.com/kernel/google-modules/gpu/+/refs/heads/android-gs-pantah-5.10-android14-qpr2-beta/mali_kbase/tl/mali_kbase_tracepoints.c#34)
- 一个序列化的消息缓冲区,其具体内容取决于消息 ID。
例如,`__kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait` 函数会将 `kbase_kcpu_command_queue` 和 `dma_fence` 内核指针序列化到消息缓冲区中,从而将内核指针泄露给用户空间进程。
```
void __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait(
struct kbase_tlstream *stream,
const void *kcpu_queue,
const void *fence
)
{
const u32 msg_id = KBASE_TL_KBASE_KCPUQUEUE_ENQUEUE_FENCE_WAIT;
const size_t msg_size = sizeof(msg_id) + sizeof(u64)
+ sizeof(kcpu_queue)
+ sizeof(fence)
;
char *buffer;
unsigned long acq_flags;
size_t pos = 0;
buffer = kbase_tlstream_msgbuf_acquire(stream, msg_size, &acq_flags);
pos = kbasep_serialize_bytes(buffer, pos, &msg_id, sizeof(msg_id));
pos = kbasep_serialize_timestamp(buffer, pos);
pos = kbasep_serialize_bytes(buffer,
pos, &kcpu_queue, sizeof(kcpu_queue));
pos = kbasep_serialize_bytes(buffer,
pos, &fence, sizeof(fence));
kbase_tlstream_msgbuf_release(stream, acq_flags);
}
```
概念验证(PoC)利用通过监控消息 ID `KBASE_TL_KBASE_NEW_KCPUQUEUE` 来泄露 `kbase_kcpu_command_queue` 对象地址,该 ID 由 `kbasep_kcpu_queue_new` 函数在每次分配新的 kcpu 队列对象时发送。
Google 告知我,该漏洞于 2023 年 3 月报告,并在其安全公告中分配了 [CVE-2023-26083](https://source.android.com/docs/security/bulletin/2023-07-01)。尽管如此,我仍能在搭载 10 月和 11 月安全补丁级别(SPL)的最新 Pixel 设备上复现该问题,表明修复未正确应用或根本没有应用。随后,Google 在 12 月安全更新中快速解决了该问题,但未给予致谢,后来告知我该问题被视为重复。然而,将其标记为重复的理由仍值得质疑。
## 漏洞利用
因此,我发现了两个有趣的漏洞。第一个提供了修改任意 16 字节对齐内核地址内容的能力(该地址位于分配的 ~buff~ 地址之前)。第二个漏洞提供了内核内存中对象位置的线索。
### 关于 buffer_count 和 live_ranges_count 值的说明
通过完全控制 `buffer_count` 和 `live_ranges_count` 字段,我可以选择目标 slab 和要写入的精确偏移量。然而,选择这些值需要谨慎考虑,因为存在多个约束和因素:
- 这两个值相关,只有绕过所有新引入的检查时才会发生溢出。
- 负偏移量需要 16 字节对齐,这限制了写入任意位置的能力。但这通常不是重大障碍。
- 选择较大的偏移量会导致大量数据写入可能不是预期目标的内存区域。例如,如果分配大小溢出到 `0x3004`,则 `live_ranges` 指针将指向距离 `buff` 对象分配位置 `-0x4000` 字节处。`copy_from_user` 函数将写入 `0x7004` 字节,基于 `update->live_ranges_count` 乘以 4 的计算。因此,用户控制的数据将覆盖从 `live_ranges` 指针到 `buff` 分配之间的内存区域。因此,必须仔细确保不会意外覆盖该范围内的关键系统对象。由于使用了 `copy_from_user` 调用,可以考虑在用户源缓冲区之后取消映射不希望的内存区域以触发 `EFAULT` 来防止数据写入敏感位置。然而,这种方法无效,因为如果 `raw_copy_from_user` 失败,它会将目标内核缓冲区的剩余字节清零。此行为旨在确保在因错误导致部分复制时,缓冲区的其余部分不包含未初始化数据。
```
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
unsigned long res = n;
might_fault();
if (!should_fail_usercopy() && likely(access_ok(from, n))) {
instrument_copy_from_user(to, from, n);
res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
memset(to + (n - res), 0, res);
return res;
}
```
考虑到这一点,我们需要仔细选择要覆盖的对象以及要写入的数据。
### 选择要覆盖的正确对象
因为我受限于这个不幸的检查,我的策略是找到一个对象,如果将其置空,不会产生不良后果。但在深入之前,还有另一个问题需要处理。请记住,我说过可以选择任意分配大小,从而使用任意通用目的 slab 缓存分配器来服务我的分配缓冲区?这并不正确,原因同样是 `copy_from_user`!这是由于 [CONFIG_HARDENED_USERCOPY](https://lwn.net/Articles/693745/) 缓解措施。它禁止指定不满足对应 slab 缓存大小的尺寸,其中内核目标缓冲区(在此情况下)为堆对象。它会判断缓冲区的页是否为 slab 页,如果是,则检索匹配的 `kmem_cache->size` 并检查用户提供的尺寸是否不超过该值;否则,内核会因尺寸不匹配而崩溃。因此,我不能针对属于通用分配器的对象,但仍然可以针对尺寸较大的对象(即由页分配器直接服务的对象)。
首先想到的是使用 `pipe_buffer` 技术,这是一种非常优雅的获取任意读/写原语的方法。我不会详细解释该技术,但读者可以参考 [Interrupt Labs](https://www.interruptlabs.co.uk/articles/pipe-buffer) 的这篇出色博客。创建 `pipe_buffer` 对象时,初始数组包含 16 个元素;不过可以通过 `fcntl(F_SETPIPE_SZ)` 调整数组大小。因此,`pipe_buffer` 数组分配可以被调整为由页分配器服务,使其成为攻击的理想目标对象。
选择 `pipe_buffer` 数组作为目标候选后,下一步是实现内核读/写,需要通过下溢漏洞覆盖其内容,从而允许我读取/写入任意内存位置(前提是该位置所在的页被覆盖 `pipe_buffer->page` 字段)。
由于该漏洞允许我写入任意数据,我可以 `pipe_buffer` 的全部内容,包括其 `page` 字段。为此,我需要在脆弱 `kbuff` 对象之前分配 `pipe_buffer` 数组,并且它们必须相邻。
### 将 pipe_buffer 和 buff 对象相邻放置
我在内核内存中喷洒了大量 `kbase_kcpu_command_queue` 对象,随后紧跟大量 `pipe_buffer` 数组。
我不能仅使用 `pipe_buffer` 数组进行喷洒,因为受 `pipe_max_size` 的限制。因此,我决定从 `kbase_kcpu_command_queue` 对象开始喷洒。选择该对象有两个原因:其分配大小为 `0x38C8`,由页分配器处理;并且我可以使用信息泄露漏洞可靠地获取其内核地址,使其成为良好的喷洒和攻击目标。
如前所述,我使用 `fcntl(F_SETPIPE_SZ)` 来增加 `pipe_buffer` 数组分配的大小,使其由页分配器服务。具体来说,我选择分配大小为 0x4000 字节(4 * PAGE_SIZE),以与 `kbase_kcpu_command_queue` 分配保持一致。
### 获取 struct page 地址
为了正确使用 `pipe_buffer`,需要页地址。能够识别我创建并销毁的 `kbase_kcpu_command_queue` 对象的核地址使其成为良好候选,而找到其对应的 `struct page` 可以通过 `virt_to_page` 实现。
### 写入 pipe_buffer 的内容
`pipe_buffer` 对象的结构如下:
```
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
```
如前所述,`page` 字段必须包含有效的页地址。`offset` 和 `len` 字段不得超过 `PAGE_SIZE`,否则管道将增加头/尾计数器,导致使用新的 `pipe_buffer` 对象并失去对伪造 pipe buffer 的控制。
此外,`flags` 必须为 `PIPE_BUF_FLAG_CAN_MERGE`,这样后续的 `pipe_write` 调用不会盲目增加头计数器,而是首先检查当前 `pipe_buffer` 是否有空间容纳写入请求,如果有,则从 `len` 字段存储的值开始将数据追加到同一 pipe buffer。
为了避免在 `pipe_buf_confirm`(由 `pipe_write` 和 `pipe_read` 调用)处崩溃设备,`ops` 指针也必须是一个有效的内核地址,并且其 `ops->confirm` 字段设置为 _NULL_。我可以使用 `kbase_kcpu_command_queue` 对象中某个始终为 NULL 且不会改变的偏移量来满足这一条件。
### 为下溢选择最优偏移量
虽然 `buff`、`kbase_kcpu_command_queue` 和 `pipe_buffer` 的分配大小均约为 0x4000 字节,但我选择使用 **0x8000** 字节进行下溢。为什么?
让我们简要回顾一下 `pipe_buffers` 在读写操作期间的更新方式。假设我们可以塑造 `pipe_buffer` 如下:
```
struct pipe_buffer {
.page = virt_to_page(addr),
.offset = 0,
.len = 0x40,
.ops = kcpu_addr + 0x50,
.flags = PIPE_BUF_FLAG_CAN_MERGE,
unsigned long private = 0
};
```
虽然漏洞允许我们任意控制该对象的内容,但仅能控制 **一次**,因为下溢的对象会立即被释放。这实际上带来了一个问题:我需要手动更新 `pipe_buffer` 对象以使其可用于后续操作,因为每次 pipe 读/写操作都会:
- `.page` 字段不会更新,它保持不变。当缓冲区为空时,该页会被释放,而我并不希望这种情况发生,因为 `.ops` 字段未正确设置。
- 由于 `pipe_buffer` 在读操作时会更新 `.offset` 字段,因此我无法再次读取同一内存区域。
- 写入 `pipe_buffer` 的数据将从 `.len` 值处(假设设置了 `PIPE_BUF_FLAG_CAN_MERGE` 标志)追加,并且 `.len` 会相应更新。也就是说,我们无法两次向同一地址写入数据。
因此,除非我正确更新 `pipe_buffer`,否则无法对同一 pipe 进行读写操作。这就是为什么下溢 0x8000 字节更加实用,因为它会覆盖 **两个不同的 pipe_buffer 实例**,分别用于两个不同的管道对象:一个用于读操作,另一个用于写操作。
```
#define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */
pipe_read = (struct pipe_buffer *)( ptr);
pipe_read->page = virt_to_page(ta->kcpu_kaddr);
pipe_read->offset = 0;
pipe_read->len = 0xfff;
pipe_read->ops = (const void *)(ta->kcpu_kaddr + 0x50);
pipe_read->flags = PIPE_BUF_FLAG_CAN_MERGE;
pipe_read->private = 0;
pipe_write = (struct pipe_buffer *)( ptr + 0x4000);
pipe_write->page = virt_to_page(ta->kcpu_kaddr);
pipe_write->offset = 0;
pipe_write->len = 0; /* This is the starting position of the pipe_write */
pipe_write->ops = (const void *)(ta->kcpu_kaddr + 0x50);
pipe_write->flags = PIPE_BUF_FLAG_CAN_MERGE;
pipe_write->private = 0;
```
`pipe_read` 是一个伪造的 pipe buffer,用于从目标页的 `.offset = 0` 处读取最多 `0xfff` 字节的数据,而 `pipe_write` 是一个伪造的 `pipe_buffer`,用于从 `.len = 0` 处写入最多 `0xfff` 字节的数据。
同样重要的是,再次强调,写入超过 `PAGE_SIZE` 字节会使管道递增头计数器,从而使用一个新的 `pipe_buffer` 并导致我们失去对伪造 `pipe_write` 的控制。另一方面,从 `fake_read` 缓冲区读取 0xfff 字节数据会使内核通过调用 `ops→release` 释放实际页面,从而导致内核崩溃,因为我仍然没有内核文本地址。
尽管我设法将 pipe 读和写操作分离,使得对一个 pipe 端的写入不会干扰另一个 pipe 缓冲区,但我仍未解决核心问题:如何可靠地更新 pipe buffer?显而易见的答案是反复重复喷洒过程,但这会对漏洞利用的可靠性产生显著影响。在下一节中,我将从两个子目标入手:首先专注于 `.page` 字段,其次再处理 `.len/.offset` 字段。
### 修改 pipe_buffer→page 字段
令我惊讶的是,我完全不需要或不需要更新 `.page` 字段,因为我可以简单地将 `pipe_buffer→page` 覆盖为泄露的 `kbase_kcpu_command_queue` 页地址。因此,**我只需要释放 `kbase_kcpu_command_queue` 对象并将其与新的 `pipe_buffer` 对象重叠。现在我拥有一个指向合法 pipe_buffer 对象的 `pipe_buffer→page`!**
用 `pipe_buffer` 替换 `kbase_kcpu_command_queue` 使我们能够操作合法的 pipe buffer,而无需定期更新 `.page` 字段。不过,我仍然需要处理 `.len` 和 `.offset` 字段。
### 修改 pipe_buffer→len/.offset 字段
正如我之前提到的,对 pipe 进行读/写操作会更新 `.len` 和 `.offset` 字段,使得即使在两个不同的管道上,对同一页的后续读写操作也无法使用。这里有一个技巧:**有一种技术可以在不触碰 `.len/.offset` 字段的情况下读写数据!** 通过使 `copy_page_from_iter` 和 `copy_page_to_iter` 调用产生错误即可实现。
继续前面的示例,如果希望向某个地址写入 8 字节数据,用户空间缓冲区大小必须为 8,接着是一个不可读或不可映射的内存区域,然后以 `9` 作为大小参数传递给 `write` 系统调用,表示希望写入的数据量。此操作将写入 8 字节,并在第九个字节处失败,因为它遇到了不可读/不可映射的内存位置。结果,数据已有效写入目标内核缓冲区,且 `.len` 字段未被修改。`pipe_write` 内核函数将直接返回而不更新 `buf->len` 字段。
```
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
ret = pipe_buf_confirm(pipe, buf);
if (ret)
goto out;
ret = copy_page_from_iter(buf->page, offset, chars, from);
if (unlikely(ret < chars)) {
ret = -EFAULT;
goto out;
}
buf->len += ret;
if (!iov_iter_count(from))
goto out;
}
```
读操作也是如此;如果希望读取 8 字节,使缓冲区的第九个字节不可读,然后声称要读取 9 字节,数据将被复制到用户缓冲区且 `.offset` 字段不会改变。
因此,我们能够在不反复进行喷洒过程的情况下,对任意内核内存地址执行无限的读写操作。
### 获取 root 权限
现在我已经拥有了强大的任意读写原语,我遍历了 `VMEMMAP_START` 数组中的所有 `struct page`,以使用 [Interrupt Labs](https://www.interruptlabs.co.uk/articles/pipe-buffer) 博客中描述的技术确定内核文本起始地址。然后我意识到 `init_task` 在 _Android November Security Updates_ 中已被置空,因此我改用 `kthreadd_task`。获得 `kthreadd_task` 的内核地址后,我可以遍历 `task->tasks` 列表以获取当前任务(`current`)的内核地址,然后将 `cred` 结构清零以获得 root 权限。
后来,我意识到遍历所有页地址是不必要的,因为我已从 `pipe_buffer` 对象获得了 `anon_pipe_buf_ops` 内核文本地址。利用此信息,我可以推导出内核文本基址,从而有效绕过 KASLR。
### 禁用 SELinux
该漏洞利用程序也禁用了 SELinux。获得内核文本基址后,我只需找到 `selinux_state` 全局结构的位置,然后将 `.enforcing` 值清零即可。
## 概念验证
附带报告的 PoC 已在运行 Android 14 并搭载十月和十一月 ASB 的 Pixel 7 和 8 Pro 设备上测试,成功率接近 100%。
还需注意的是,该漏洞利用程序无法在其他设备上开箱即用地运行,因为使用了一些硬编码偏移量。要为新区块设备添加支持,必须提供以下信息:
- `kthreadd_task` 相对于内核基地址的偏移量。
- `selinux_state` 相对于内核基地址的偏移量。
- `task_struct->cred`、`task_struct->pid` 和 `task_struct->tasks` 结构的偏移量。
- `anon_pipe_buf_ops` 相对于内核基地址的偏移量。
### 编译
要编译该漏洞利用程序为独立二进制文件,请使用以下命令,然后通过 `adb shell` 运行:
```
$ aarch64-linux-androidXX-clang++ -static-libstdc++ -w -Wno-c++11-narrowing -DUSE_STANDALONE -o poc poc.cpp -llog
$ adb push poc /data/local/tmp/
$ adb shell /data/local/tmp/poc
```
你也可以通过 Android Studio App运行该漏洞利用程序,将此目录嵌入其中,并确保通过添加 `-w -Wno-c++11-narrowing` 到 cmake 文件来禁用无用的 C++ 警告。
### 演示
```
$ adb logcat |grep -i EXPLOIT
11-28 16:04:12.500 7989 7989 E EXPLOIT : [+] Target device: 'google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys' 0xa9027bfdd10203ff 0xa90467faa9036ffc
11-28 16:04:15.563 7989 7989 E EXPLOIT : [+] Got the kcpu_id (0) kernel address = 0xffffff8901390000 from context (0x0)
11-28 16:04:18.441 7989 7989 E EXPLOIT : [+] Got the kcpu_id (255) kernel address = 0xffffff89b0bf8000 from context (0xff)
11-28 16:04:18.442 7989 7989 E EXPLOIT : [+] Found corrupted pipe with size 0xfff
11-28 16:04:18.442 7989 7989 E EXPLOIT : [+] SUCCESS! we have a fake pipe_buffer (0)!
11-28 16:04:18.444 7989 7989 E EXPLOIT : 10 00 39 01 89 FF FF FF 10 00 39 01 89 FF FF FF | ..9.......9.....
11-28 16:04:18.444 7989 7989 E EXPLOIT : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
11-28 16:04:18.444 7989 7989 E EXPLOIT : 00 B0 CD 12 C0 FF FF FF 00 00 00 00 00 00 00 00 | ................
11-28 16:04:18.444 7989 7989 E EXPLOIT : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
11-28 16:04:18.445 7989 7989 E EXPLOIT : [+] Freeing kcpu_id = 0 (0xffffff8901390000)
11-28 16:04:18.446 7989 7989 E EXPLOIT : [+] Allocating 61 pipes with 256 slots
11-28 16:04:18.462 7989 7989 E EXPLOIT : [+] Successfully overlapped the kcpuqueue object with a pipe buffer
11-28 16:04:18.463 7989 7989 E EXPLOIT : 40 AB BA 26 FE FF FF FF 00 00 00 00 30 00 00 00 | @..&........0...
11-28 16:04:18.463 7989 7989 E EXPLOIT : 70 37 8D F1 DA FF FF FF 10 00 00 00 00 00 00 00 | p7..............
11-28 16:04:18.463 7989 7989 E EXPLOIT : 00 00 00 00 00 00 00 00 | ........
11-28 16:04:18.463 7989 7989 E EXPLOIT : [+] pipe_buffer {.page = 0xfffffffe26baab40, .offset = 0x0, .len = 0x30, ops = 0xffffffdaf18d3770}
11-28 16:04:18.463 7989 7989 E EXPLOIT : [+] kernel base = 0xffffffdaf0010000, kthreadd_task = 0xffffff8002da3780 selinux_state = 0xffffffdaf28a3168
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Found our own task struct 0xffffff88416c5c80
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Successfully got root: getuid() = 0 getgid() = 0
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Successfully disabled SELinux
11-28 16:04:20.102 7989 7989 E EXPLOIT : [+] Cleanup ... OK
```
标签:Android 14, Google 安全响应, GPU 漏洞, Ioctl 漏洞, Kernel Exploit, LPE, Mali GPU, Pixel 7, Pixel 8 Pro, Privilege Escalation, SELinux 禁用, UML, 信息泄露, 内核利用, 内核漏洞, 内核读/写, 协议分析, 整数溢出, 权限提升, 漏洞披露, 目录枚举, 移动安全