摘要。 CVE-2023-20768 是 MediaTek ION 分配器中的类型混淆漏洞(CWE-843)。我检查了它在运行 2022 年 7 月固件的三星 SM-M325F(Galaxy M32,Helio G80)上是否实际可利用。存在易受攻击的代码,并且我证明了当由非特权进程触发时,它确实会在此设备上运行。我未能将其武器化。ioctl 路径受到输入验证的限制,而包含真正越界读取的路径只会处理真正的 ION 缓冲区,因为对 dma_buf_ops 指针的第二次检查会拒绝我可能伪造的任何内容。本文涵盖了我是如何得出这个结论的,包括一个后来被证明是错误的中间结论。
该 CVE 已公开并被修复。所有测试均在我自己的设备上进行,已用 Magisk 获取 root 权限。
该漏洞位于 MediaTek 的 ION 代码中,而非上游 Linux 或三星编写的任何代码。MediaTek 在其 BSP 中提供 Android ION 分配器的自有分支,该 BSP 会提供给每一位基于其芯片构建的厂商。M32 使用 Helio G80,因此也会包含这段代码。同一款手机若是 Exynos SoC 则完全不受影响。
我提取了 2022 年(易受攻击)和 2023 年(已修补)的 vmlinux 镜像,并在 IDA 中对它们进行了差异对比。两个 ION 函数发生了变化:
| 函数 | 2022 | 2023 |
|---|---|---|
ion_drv_file_to_buffer | strstr(name, "dmabuf") | is_dma_buf_file() |
_ion_ioctl | strcmp(name, "ion") | is_dma_buf_file() |
is_dma_buf_file 在 2022 年的镜像中不存在,在 2023 年的镜像中才出现。因此,这两个函数此前都是通过名称来判断某个 struct file 是否为 dma_buf,而修复方式将其替换为真正的类型检查。在这里混淆对象,意味着内核会将非 dma_buf 当作 dma_buf 来读取。
在这两个函数中,_ion_ioctl 是非特权进程可以触达的那个:
open("/dev/ion")
-> ion_ioctl (.unlocked_ioctl)
-> ION_IOC_CUSTOM (0xC0104906)
-> ion_custom_ioctl
-> _ion_ioctl
-> case 0: ION_SYS_CACHE_SYNC
-> find_vma(user_VA) (call site at _ion_ioctl+0x9f0)
-> strcmp(vma->vm_file...name, "ion")
该请求是一个 ion_custom_data { u32 cmd = 0; u64 arg; },它指向一个 120 字节的 ion_sys_data:
| 偏移量 | 字段 |
|---|
spoof.c 构建了这个结构。
我无法直接追踪它。该设备通过三星的 SELinux 策略和内核加固屏蔽了 kprobe_events、set_ftrace_filter 和 function_graph,且 ION 的 printk 受调试开关控制,因此 dmesg 保持静默。
于是我改用返回码作为判定依据。发出四个请求,返回结果的模式会告诉你执行走到了哪里:
C 返回成功意味着 switch 确实在根据 sys_cmd 进行分发。A 和 B 结果不同意味着 VA 正在被处理,这使执行流程进入了 find_vma 内部。这就是易受攻击的路径,无需 root 即可触达。
触达检查并不等于绕过它。我测试了 strcmp 实际接受哪些内容:
ION_IOC_SHARE 映射再执行 mmap 的真实 ION 缓冲区能通过,返回 0。memfd:ion 的 memfd 失败,返回 -EFAULT。ion,同样失败,返回 -EFAULT。因此,在 vm_file+0x60 处被比较的字段并不是文件名。它是 dma_buf 内部字段,几乎可以肯定是 dma_buf->exp_name,ION 将其设置为 "ion"。该检查在设计上就是不健全的,但我从用户态所能创建的任何内容都无法设置该字段。
随后我对该路径进行了模糊测试:sync_type 取值范围 0 到 7,size 取值 {0, 1, 0x1000, 0x100000, 0xffffffff},VA 取值 {真实 ion 缓冲区, memfd, 0},共 120 个用例,外加一个针对释放后使用的已释放句柄探测。没有崩溃,设备保持运行。超大的 size 会在 find_vma 之前退出并返回 0。sync_type 3 到 5 会进入 m4u 路径并返回 -EPERM。大于 5 的值返回 -EINVAL。已释放的句柄返回 -EINVAL,因此 ION 会对其校验,那里不存在 UAF。
真正的越界读取位于 ion_drv_file_to_buffer 中。它执行 ldr [private_data+0x28],将非 dma_buf 的 private_data 当作 dma_buf 来读取。存在一个 ops == &ion_dma_buf_ops 比较(该表位于 0xFFFFFF800A097F18),但该比较发生在这次读取之后,因此并不能阻止它。在下游,__do_dump_share_fd 会从返回的缓冲区中读取 +0x28、+0x48、+0x50、+0xb8、+0xe4 处的字段并打印它们,而 ldr x8, [buf+0x28]; ldr [x8+0x30] 对于混淆对象来说是一次野指针解引用。
触发点是 ion_dump_all_share_fds,它使用 iterate_fd 遍历每个 ION 客户端进程的文件描述符。我最初的结论是,只有当你读取 ION 的 debugfs 节点时它才会运行,而这个内核未设置 CONFIG_DEBUG_FS。我通过三种方式验证了这一点:/proc/config.gz、debugfs 未出现在 /proc/filesystems 中,以及 mount -t debugfs 返回 ENODEV。于是我认为这条路径在结构上不可达,将其排除。
这个结论是错的。OOM killer 的内存转储函数 dump_header 在 0xffffff8008204b9c 处直接调用了 bl ion_mm_heap_memory_detail,而 dump_header 又是从 out_of_memory 和 oom_kill_process 调用的。无需 debugfs。
memcg_oom.c 证实了这一点。它在 /dev/memcg 下创建一个 cgroup,将 memory.limit_in_bytes 和 memory.memsw.limit_in_bytes 都限制为 8MB(只限制前者会让子进程逃逸到 zram 交换空间),并 fork 一个子进程不断分配内存直至其被杀死。随后 dmesg 显示:
dump_header <- oom_kill_process <- out_of_memory <- mem_cgroup_oom_synchronize
紧接着是完整的 ion_mm_heap_memory_detail 以及 __do_dump_share_fd 输出,它们解析出了真实的 gralloc dma_buf。因此,这个易受攻击的函数会在任何非特权进程都能引发的过程中执行。还有第三个触发点:MediaTek 看门狗中 hang_detect_dump_thread 的 ShowStatus。
我持有 32 个名为 memfd:dmabuf 的 memfd,并触发了相同的 memcg OOM。如果其中有任何一个被送入 ion_drv_file_to_buffer,strstr 就会通过,private_data 将为 NULL,内核会在 KERN_ERR 级别打印 [ION]ion_drv_file_to_buffer warnning, dmabuf is NULL。但这一行从未出现。只有 graphics 和 gralloc 客户端被转储。要么是普通的 /dev/ion 客户端的 fd 表不是这里的 iterate_fd 所遍历的对象,要么是 memfd 在打印之前就静默失败了。
无论哪种情况,OOM 转储都只会处理系统合法的 dma_buf,它们会干净地通过。memfd 也是唯一一种我能充分控制名称以包含 "dmabuf" 的 fd 类型,而它无法造成错误。
漏洞确实存在。易受攻击的代码在此构建中是可达的,并且确实会执行,无需 root 即可触发。但从用户态无法在此设备上将其武器化。ioctl 路径受到句柄校验、大小检查以及 access_ok 的限制。转储路径只会看到真正的 ION 缓冲区,伪造被 ops == &ion_dma_buf_ops 阻止。要想更进一步,需要一个 exp_name 为 "ion" 且可控的非 ION 对象,或者其他原语,例如 ION 缓冲区 UAF,或者 TOCTOU 竞态。
spoof.c — 用于 cache-sync ioctl 的 PoC 以及差分判定工具memcg_oom.c — 用于转储路径的 memcg OOM 触发程序trigger.c、oom_trigger.c — 早期的触发尝试boot_images/ — 提取的内核镜像(2022 和 2023)以及 2022 构建的 IDA 数据库+0x00 | sys_cmd = 0 |
+0x08 | ion 句柄(先分配一个,heap_id_mask = 0x1 即可) |
+0x10 | 用户虚拟地址 |
+0x18 | 低半部分 = size,高半部分 = sync_type,取值范围为 {0,1,2} |
| 用例 | 请求 | 结果 |
|---|
| A | sys_cmd=0, 伪造的 VA | -EFAULT |
| B | sys_cmd=0, VA = 0 | 0 |
| C | sys_cmd=4 | 0 |
| D | sys_cmd=99 | -EFAULT |