恶意意图:利用GPU漏洞实现Android提权
想象一下从第三方应用商店下载游戏。您授予它看似无害的权限,但隐藏在应用中的恶意漏洞利用程序允许攻击者窃取您的照片、窃听对话,甚至完全控制设备。这就是CVE-2022-22706和CVE-2021-39793等漏洞带来的威胁,本文将深入剖析这些影响众多Android设备中Mali GPU的漏洞,它们允许非特权应用获得root访问权限。
漏洞概述
受影响产品
产品 | Mali GPU内核驱动 |
---|---|
厂商 | ARM |
严重性 | 高危 - 非特权用户可对只读内存页进行写操作 |
受影响版本 | - Midgard GPU内核驱动:r26p0 - r31p0所有版本 - Bifrost GPU内核驱动:r0p0 - r35p0所有版本 - Valhall GPU内核驱动:r19p0 - r35p0所有版本 |
测试版本 | Pixel 6, MP1.0, 2022 - 降级至Android 12.0.0 (SD1A.210817.015.A4, Oct 2021) |
CWE | CWE-119: 内存缓冲区操作限制不当 |
CVSS3.1评分
基础评分:7.8(高危) 向量字符串:CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Termux
Termux输出显示了漏洞利用的上下文,从非特权不受信任的应用上下文运行:
|
|
根本原因分析
我们在Mali GPU内核驱动的kbase_jd_user_buf_pin_pages()
函数中发现了一个关键漏洞。该函数至关重要:它管理GPU如何访问内存,准备用户提供的内存缓冲区,并(应该)确保应用具有正确的权限(读或写)。
查看补丁变更列表,问题变得清晰。漏洞在于kbase_jd_user_buf_pin_pages()
检查这些权限的方式。这完全关于KBASE_REG_GPU_WR(GPU写)和KBASE_REG_CPU_WR(CPU写)标志——它们告诉驱动程序需要哪种访问权限。
应用应该需要同时设置这两个标志才能获得GPU写访问权限,但代码只检查KBASE_REG_GPU_WR标志,留下了一个巨大的安全漏洞。
|
|
补丁显示了问题的核心。如果设置了KBASE_REG_GPU_WR但不设置KBASE_REG_CPU_WR,FOLL_WRITE标志也会被设置。这是不正确的。它应该只在两个标志都设置时才被设置。
由于这个疏忽,恶意应用可以请求CPU写访问(通过设置KBASE_REG_CPU_WR),而不需要所需的GPU写访问(KBASE_REG_GPU_WR)。这允许应用绕过预期的安全检查,获得对不应允许修改的内存的写访问权限。
这种对只读内存的写入能力是整个漏洞利用所依赖的基本原语。它允许攻击者将恶意代码注入特权进程,并最终获得root访问权限。
触发漏洞
通过利用此漏洞,我们可以强制Mali驱动程序授予对只读内存区域的写权限。以下是漏洞利用步骤:
- 分配读写内存页:首先分配具有读写权限的内存页
- 使用KBASE_REG_CPU_WR导入映射(不设置KBASE_REG_GPU_WR):由于驱动程序中缺少检查,这会无意中授予GPU写访问权限
- 将导入的缓冲区映射到GPU VA空间:为缓冲区分配GPU地址空间中的虚拟地址
- 取消映射原始读写映射:然后移除原始映射
- 使用只读页重新映射相同地址:这导致CPU将页面视为只读,而GPU保留写访问权限
- 提交具有GPU VA映射的GPU作业作为BASE_JD_REQ_EXTERNAL_RESOURCES:这会触发易受攻击的函数
kbase_jd_user_buf_pin_pages()
,实现对只读内存的写入
此时,我们可以通过利用来自用户空间的GPU VA映射来修改从CPU角度应该是只读的内存页。
利用原语
能力:能够写入文件的只读内存页
影响:
- 文件的修改内存页被缓存供其他进程使用
- 修改不会保存到磁盘
先决条件:
- 能够打开和读取目标文件
- 对gpu_device使用ioctl、读取和写入的权限
通过将钩子和有效负载注入到只读共享库中,我们可以操纵特权进程(如init)中的执行流。由于所有域都可以读取system_lib_file类型的文件,这种技术具有广泛适用性:
|
|
然而,并非所有域都可以在gpu_device上调用ioctl、读取和写入。幸运的是,低特权域如shell和untrusted_app被允许这样做:
|
|
这使得能够通过system_lib_file修改从shell和untrusted_app劫持特权进程。
攻击策略:Root反向Shell
目标:将权限提升以从untrusted_app_27获取root反向shell
挑战:绕过SELinux强制执行
解决方案:加载任意内核模块
首先,我们使用位于/sys/fs/selinux/policy
的设备SELinux策略识别具有module_load权限的域:
|
|
其中,只有init-insmod-sh具有自动类型转换,它是目标:
|
|
由于init-insmod-sh可以通过运行init-insmod-sh_exec类型的文件来执行,我们在设备上找到相关的可执行文件(/vendor/bin/init.insmod.sh)。
通过init劫持提升到Root
为了实现完全系统妥协,我们针对init进程。init在do_epoll_wait中运行两个线程,使其成为可行的攻击向量:
|
|
其中一个线程是主线程,另一个是PropertyServiceThread,它从SecondStageMain中的StartPropertyService生成。通过针对这些线程中的任何一个,我们可以潜在地利用init进程获得进一步控制。
|
|
PropertyServiceThread在property_set_fd监听套接字上注册一个epoll处理程序,然后进入一个循环,在其中重复调用具有无限超时的epoll_wait。这创建了一个长期存在的阻塞操作,如果识别出适当的触发器或攻击向量,可以利用它来劫持线程的执行流。
|
|
要唤醒此线程,我们可以使用任何有效的名称和值参数调用/system/bin/setprop
。这将触发向套接字发送PROP_MSG_SETPROP2命令,导致epoll_wait返回并运行handle_property_set_fd。然后可以通过使用Mali写入来钩住handle_property_set_fd中调用的任何导入库函数来劫持init。
|
|
如果setprop缺乏足够的SELinux权限,CheckPermissions中的CheckMacPerms(由HandlePropertySet调用)将失败,触发LOG(ERROR),进而调用/system/lib64/libbase.so
中的android::base::LogMessage::LogMessage
。利用Mali写入原语,我们可以钩住这些导入函数之一来劫持执行并将权限提升到root。
LogMessage内部调用/system/lib64/libc++.so
中的std::ios_base::init
,我们可以钩住它以劫持init。
然而,对于untrusted_app,直接调用/system/bin/setprop
来触发劫持是行不通的。虽然setprop的执行是允许的,但由于setprop实际上指向/system/bin/toolbox
(类型toolbox_exec),不会有域转换,setprop将在untrusted_app的域中执行。在该域中,由于SELinux策略,它无法触发与套接字的通信。
|
|