背景
在大多数用户的印象里,GPU(图形处理器)是游戏流畅、视频绚丽的保障,是沉浸在虚拟世界背后的无名英雄。然而,在现代移动计算架构,尤其是在安卓生态中,GPU的角色早已超越了“图形画师”的范畴。它通过诸如OpenCL、Vulkan等通用计算框架,深度参与到机器学习、图像处理、甚至安全计算等关键任务中,成为了SoC(系统级芯片)中与CPU平起平坐的“第二颗大脑”。
正是这种权限与复杂性的与日俱增,使其成为了安全攻防的新前线。传统的安全模型高度聚焦于加固CPU和操作系统内核,却往往忽视了GPU这个拥有独立驱动、固件(Firmware)和强大DMA(直接内存访问)能力的“特权邻居”。一旦这个邻居的“家门”被撬开,攻击者获得的将是一个绕过所有主流内核防护机制的绝佳跳板。
近年来,GPU安全漏洞呈现显著上升趋势。正如Google Project Zero在2022年的博客中提到的那样:
There were 7 Android in-the-wild 0-days detected and disclosed this year. Prior to 2021 there had only been 1 and it was in 2019: CVE-2019-2215.
For the 7 Android 0-days they targeted the following components:
Qualcomm Adreno GPU driver (CVE-2020-11261, CVE-2021-1905, CVE-2021-1906)
ARM Mali GPU driver (CVE-2021-28663, CVE-2021-28664
Upstream Linux kernel (CVE-2021-1048, CVE-2021-0920)5 of the 7 0-days from 2021 targeted GPU drivers.
在2022年的7个在野0day中,有5个是关于GPU漏洞的,比例高达70%。且GPU漏洞的危害性往往极高,正如我们将要深入剖析的CVE-2025-21479一样,一旦被利用将引发从用户态直通物理内存操作的本地权限提升。
这个存在于高通Adreno GPU固件中的逻辑缺陷,正是一个典型的“核弹级”案例。它允许攻击者实现任意物理地址的读写操作。这意味着,攻击者不再需要与复杂的软件 mitigations(如KASLR、DEP、CFI)缠斗,而是直接“改写物理内存”。接下来,让我们一起揭开这场“上帝模式”漏洞的利用之旅。
Catch the bug from diff
高通的6月bulletin 为我们带来了3个漏洞:CVE-2025-21479,CVE-2025-21480,CVE-2025-27038。但可惜没有一个注明了commit修复链接,这给漏洞研究带来了挑战。但没多久,就有一名大佬 zhuowei 在 github 公开了他的漏洞poc和分析过程。
zhuowei 在文中提到,他通过Freedreno的 afuc 反汇编器逆向了三星S24的前后两个版本的GPU固件,并发现了一些端倪:
1 | Galaxy S24 firmware: gen70900_sqe.fw |
这两个固件仅有一处差异,即每次使用寄存器 $12
时,mask 从 0x3(0b11) 变成 0x7(0b111),多了1个bit,而这个$12
代表着IB level。
1 | 0163: b80300a4 CP_ME_INIT: |
自高通Adreno A7xx系列GPU起,其硬件引入了一个新的指令缓冲区(IB)层级,将原有的四级架构(RB、IB1、IB2、SDS)扩展为五级(RB、IB1、IB2、IB3、SDS)。关键问题在于,用于权限检查的微码逻辑未随之更新。微码通过 当前层级值 AND 3
的位运算来判定当前所处级别,若结果为0(即RB级),则允许执行特权指令。
在未打补丁的A7xx硬件上,当于第4级SDS执行指令时,计算 4 AND 3
得到结果0。这导致微码产生致命误判,错误地认为当前处于最高权限的RB级别,从而违规批准了特权指令的执行。
利用过程始于对 CP_SET_DRAW_STATE
指令的调用。此指令能让用户态应用在SDS层级上提交并执行命令序列。由于CVE-2025-21479漏洞的存在,当这个由用户控制的命令序列在SDS层级被执行时,GPU微码会错误地授予其RB层级的最高特权。攻击者可以滥用 CP_SMMU_TABLE_UPDATE
特权指令修改TTBR0寄存器对GPU的一级页表进行重配置。通过植入一个精心构造的恶意页表,攻击者可以完全掌控GPU对系统物理地址空间的映射关系。从而GPU沦为攻击者向任意物理内存地址发起读写攻击的武器,为后续获取内核权限奠定了决定性基础。
构建内核物理地址读写原语
成功利用 CP_SMMU_TABLE_UPDATE
获得配置页表的能力后,攻击的下一个阶段是构建一个稳定可靠的内核物理地址读写原语。其核心在于构造并定位一个受控的页表,并利用GPU指令实现任意物理内存的访问。
为TTBR0构建受控页表
攻击的首要步骤是准备一个恶意的一级页表,并将其物理地址配置到TTBR0寄存器中。该页表将负责定义GPU的虚拟地址到物理地址的映射关系。
内核物理地址读写的第一步,是构造出GPU的TTBR0页表,其中0x1234567841414141
处就是TTBR0一级页表的物理地址:
1 | // 构造用于执行 CP_SMMU_TABLE_UPDATE 的指令序列 |
然而,用户态无法直接获知自身内存的物理地址。为解决此问题,采用以下策略:
- 大量喷射匿名内存页:以此占据大量的物理页面,提高猜测命中概率。
- 物理地址猜测:选取多个高概率命中的候选物理地址 P 作为假定的页表地址。
- 精心设计虚拟地址映射:选择一个虚拟地址 V(例如
0x40403000
),使其在各级页表行走中的索引均相同。此设计允许通过修改单个页表页 (P) 即可完成从 V 到任意目标物理地址P_target
的映射,极大简化了页表操作。
定位受控页表
为确认哪个被喷射的页面其物理地址恰好为猜测的 P,需进行以下验证:
- 通过GPU特权指令向虚拟地址 V 写入一个特定魔数 x。若 P 此时确实是我们的匿名页,则GPU可以通过TTBR找到V对应的P 并向其中写入魔数。
1 | *drawstate_cmds++ = cp_type7_packet(CP_MEM_WRITE, 3); |
- 在用户态遍历所有喷射的匿名页,检查其中内容。若发现某个页面包含魔数 x,则可断定该页的物理地址即为 P。至此,我们获得了一个已知其物理地址 (P) 和虚拟地址的可读写内存页。
实现任意物理地址写
一旦确定了页表页 P,写入任意物理地址 P_target
的过程变得直接:
修改映射:在已定位的页表页 P 中,修改虚拟地址 V 对应的页表项,使其指向目标物理地址 P_target
。
执行写入:再次使用 CP_MEM_WRITE
指令向虚拟地址 V 写入数据。GPU会根据恶意页表,将数据实际写入物理地址 P_target
。
实现任意物理地址读
GPU指令集未提供直接的内存读取指令,因此读取操作需要通过内存拷贝指令 CP_MEM_TO_MEM
间接实现。有两种策略:
-
拷贝至TTBR0页 (P):将目标物理地址
P_target
的内容拷贝至已控制的物理页 P 的某个偏移处。随后,在用户态直接读取该匿名页的对应虚拟地址,即可获得P_target
的数据。- 风险:此操作会覆盖页表页的部分内容,需谨慎管理拷贝偏移量,或在执行后重新修复页表项。
-
使用专用接收页 (P3):更安全的方法是喷射并定位另一个物理页 P3 作为专用的数据接收缓冲区。将
P_target
的数据拷贝至 P3,然后在用户态读取 P3 对应的虚拟地址。这避免了污染关键的页表结构。
此外,CP_MEM_TO_MEM
指令只能拷贝4或8字节的内存,若需要拷贝16字节,则需要执行两遍该指令。由于漏洞只存在于A7xx设备,不妨直接使用A6xx引入的CP_MEMCPY指令,它实现了两个地址指定length的对拷,效率更高。
1 | <value name="CP_MEMCPY" value="0x75" variants="A6XX-"/> |
物理基址探测与内核信息泄露
在成功构建物理地址读写原语后,攻击者便具备了直接探查和篡改内核运行时数据的能力,这是实现最终提权的关键跳板。
定位内核物理内存基址
绝大多数安卓设备的内核物理内存加载基址在每次启动时是固定的。通过查询设备信息或利用已知的通用偏移(例如 0xA8000000
, 0x80080000
等),攻击者可以从此地址开始直接读取内存,从而将内核的代码和数据段完整地dump下来。这份原始内存镜像足以让攻击者进行离线分析,精确计算出该特定内核版本中关键数据结构(如 selinux_state
)、函数地址以及防护机制(如 KASLR
偏移)的偏移量,进而编写出最终的利用载荷。
对于少数实施了内核物理地址空间布局随机化(PhyASLR)的厂商(如三星),其随机化强度通常较弱。正如研究员Pan Zhenpeng和Jheng Bing Jhong在其研究中指出的:
“Samsung’s PhyASLR is aligned with 0x10000 and quite weak, by checking the instructions of _stext, can bypass it within 20 times.”
因此,通过遍历数个按0x10000对齐的候选地址并验证其内容是否包含有效的内核指令签名,即可快速爆破出随机化后的实际基址。
直接内存dump的效率瓶颈
然而,通过 CP_MEM_TO_MEM
或是CP_MEMCPY
进行大规模数据转储存在显著性能瓶颈:
- 规模限制:该指令一次操作最多拷贝一页(0x1000字节)的数据。若要传输更多数据,必须准备大量的专用物理页( P4、P5、…)作为接收缓冲区,并进行多次迭代。
- 缓存同步开销:每次内存操作后,都必须同步CPU与GPU缓存,以确保数据一致性,这引入了巨大的性能开销。
完整泄露一个50MB大小的内核镜像需要数万次这样的操作,效率捉襟见肘。
策略转变: targeting PTE 页表
鉴于直接dump效率低下,且此时尚未获取到内核虚拟地址布局的具体信息(如 swapper_pg_dir
),我转变了利用策略:转而在物理内存中扫描寻找存有用户空间页表项(PTE)的页表页。
通过修改这些PTE项,可以重新映射用户态虚拟地址,从而绕过传统的内存读取指令,以更高的效率实现大规模数据泄露。这为后续更复杂的利用步骤提供了一个高效的数据通道。
关于PTE页表的攻击,Nicolas Wu在Dirty Pagetable研究中做出了杰出贡献,这里不在过多展开。
文中提到,给PTE用的内存分配自MIGRATE_UNMOVABLE
free_area,而用户的匿名页分配自MIGRATE_MOVABLE
free_area,这两者会让相同的虚拟地址非常难以复用。不过现在的场景是希望系统复用相同的物理地址,实际测试发现这似乎并没有那么困难。
“The root cause for this is that the physical pages allocated by anonymous mmap() usually come from the MIGRATE_MOVABLE free_area of the memory zone, while user page tables are usually allocated from the MIGRATE_UNMOVABLE free_area of the memory zone. This makes heap spray difficult to succeed. The research presented by Yong Wang explained such difficulty very well.“
假设一台拥有8GB物理内存的设备,在攻击初期剩余约6GB可用内存。在定位TTBR0页的步骤中,通过喷射匿名页已消耗约2GB(记为 M0),系统仍剩余约4GB可用内存。可以通过检查/proc/meminfo
来查看系统内存的实时状态。
操作流程如下:
0. 预留PTE喷射内存:通过fork创建 N 个worker,每个worker通过mmap()预留大量(例如 0xfe00
个)2MB的虚拟地址空间区间,但暂不进行读写操作。此步骤仅保留虚拟地址范围,并未分配物理内存或创建PTE。
- 选择 0xfe00
是为了规避 /proc/sys/vm/max_map_count
的系统限制(通常为 0xFFFA
)。
- 每个2MB映射最终将对应一个PTE页,因此此预备步骤为后续最多喷射254MB的PTE页做好了地址空间准备。
- 耗尽Movable内存:分配大量匿名页(M1,约3GB),将系统中剩余的空闲内存几乎耗尽。可通过监控
/proc/meminfo
中的MemAvailable
字段确认。 - 释放非关键内存:释放之前用于定位TTBR0页的非关键匿名页(M0’,约2GB)。此操作释放d的内存使
MemAvailable
回升约2GB。 - 喷射PTE页:按顺序激活所有worker,通过读取每个2MB区间的首个字节来触发页错误。内核在处理错误时,会为该虚拟地址区间分配并初始化一个PTE页。持续此过程,直到系统可用内存再次耗尽(甚至开始使用交换分区),此时大量PTE页已被喷射到物理内存中。
- 扫描定位PTE页:利用已有的物理内存读写能力,在已知的TTBR0页附近扫描具有PTE页特征(如特定格式的页表项)的物理页。测试证明,此方法能高效发现PTE页:
1 | [+] main.c:1612 [STEP] searching ttbr0 page .... |
一旦定位到PTE页,即可构建一个高性能的数据泄露引擎:
- 建立映射:修改该PTE页中的第一项,使其指向该PTE页自身的物理地址。这意味着,某个特定的用户态虚拟地址(由worker的mmap地址决定)将被映射到这个PTE页的物理内存。
- 轮询检测:让所有worker检查其各自2MB区间的头8个字节。哪个worker读到了我们写入的特定魔数(即自指向的PTE值),就证明其虚拟地址空间映射到了目标PTE页。
1 | [+] main.c:1568 [STEP] searching modified pte ... |
- 大规模泄露:通过修改这个受控的PTE页,可以将其余PTE项指向任意目标物理地址。随后,直接读取对应的用户态虚拟地址即可一次性获取连续2MB的内核物理内存数据,效率相比单次0x1000字节的拷贝提升了数个数量级。
通过构造恶意的PTE页表,现在可以一次读取连续的2MB内存,大大提升内核镜像信息泄露的速度。
也可以通过mmap文件并修改page属性来修改文件的page cache,正如PAN ZHENPENG 和 JHENG BING JHONG 在他们的slide中所做的那样,也可以通过改文件page cache以注入代码到init来泄露内核信息:
Obtain kernel offsets without firmwares - Combine together:
- Hijack init to chmod 0777 for dir /data/local/tmp, thus make root user can create/write the file under this dir (or setuid later to skip this)
- Read target offsets info and copy contents into /data/local/tests dir
- Transition our context to
u:r:shell:s0
and cp boot.img from /data/local/tests to /data/local/tmp- Untrusted_app can get the offsets from /data/local/tmp
- (Optional) get offsets from boot.img
Example Demo:
带着内核符号攻克内核
在获取了内核信息并建立了稳定的物理内存读写原语后,实现本地权限提升便成为可能。具体的利用路径则取决于目标设备所启用的内核防护机制。
策略一:直接代码段修补(针对默认防护)
在缺乏额外强内核保护(如Samsung KNOX, Huawei HKIP)的标准安卓系统上,最直接的方法是篡改内核代码段(__text
)本身。
思路如下:
- 重映射代码段为可写:通过修改GPU的TTBR0页表中对应内核代码段物理地址的页表项(PTE),将其内存属性从“只读”更改为“可写”。这绕过了CPU侧的内存管理单元(MMU)对只读页的硬件保护。
- 修补关键安全函数:直接在内核镜像中定位并修补关键的安全检查函数。常见的修补目标包括:
- 禁用SELinux:将
selinux_state
中的enforcing
字段置0,或修改selinux_enforcing
指针。 - 提权系统调用:修补
setresuid
、setresgid
、capset
等函数的实现,使其跳过权限检查逻辑,直接成功返回。
- 禁用SELinux:将
- 触发提权:完成修补后,在用户态直接调用被篡改的系统调用,即可获得root权限。此方法直接、高效,但依赖于对代码段的写入能力。
策略二: data only attack
在搭载了KNOX、HKIP或类似强内核完整性保护机制的设备(如三星、华为旗舰机型)上,上述方法通常失效。这些保护机制会监控内核代码段和只读数据段的完整性,任何修改都会触发系统重启。因此,利用策略必须转向攻击其保护范围的盲区。
在默认保护的系统上,我们可以通过修改GPU的TTBR0页表项中PTE项所描述的的页属性将内核代码段对应的内存重映射可写,进而修补内核镜像中关键的函数保护点。如关闭selinux,或是patch setresuid、setresgid、capset等关键安全函数,之后用户态便可直接调用这些修补后的syscall提权。
而在有额外保护的机型上(如三星、华为等)由于KNOX、HKIP等保护的存在,将代码段重映射为RW并不可行,仅能在内核data段和修改文件page cache中寻找提权的机会。
正如DARKNAY在《「AVSS研报」iOS•Android•鸿蒙安全对抗能力初评报告-内核篇》中所指出的:
HarmonyOS 4.2版中HKIP机制与RKP机制保护能力相当,其保护了代码段、只读数据段、内核重要结构体等不被篡改。但是对于应用程序页表缺乏保护,攻击者可以通过修改应用程序页表任意读写未被HKIP保护的物理地址,该利用方法也被验证在Pixel、Samsung Galaxy上同样可行。
在HarmonyOS NEXT中,可以看到HKIP提供了多种保护机制,但是在实际研究过程中发现,除了代码段、只读数据段以及内核页表存在HKIP保护外,其余重要结构体并未被保护。
以下为部分HKIP机制与RKP机制中的防御能力覆盖范围比较:
这份评估揭示了此类防护体系的一个关键弱点:其保护重心在于内核自身的静态完整性,而对于动态的用户空间映射机制和策略数据结构的保护存在不足。这为高级利用提供了两条清晰的路径:
- 操纵应用程序页表:正如我们之前构建PTE攻击链所做的那样,HKIP/RKP通常不保护用户进程的页表。攻击者可以继续利用此原语,篡改用户态PTE来达成劫持用户态应用的目的
- 篡改SELinux策略:SELinux的策略强制执行依赖于内核内存中的策略数据库。这些策略数据通常不被HKIP/RKP机制视为需要保护的“只读数据”或“代码”。因此,攻击者可以篡改数据以禁用selinux。
攻克SELinux:精准绕过强制策略
在获取内核物理地址读写能力后,绕过SELinux成为提权路径上的关键一步。最直观的思路是修改 selinux_state
结构中的 enforcing
字段。然而,在目标设备上,内核编译时未启用 CONFIG_SECURITY_SELINUX_DEVELOP
选项,导致该字段不存在,此路径无效。
1 | $ adb shell zcat /proc/config.gz | grep CONFIG_SECURITY_SELINUX_DEVELOP |
1 | struct selinux_state { |
策略一:禁用SELinux初始化状态(已弃用)
深入分析SELinux的权限检查函数 security_compute_av
,发现其存在一个初始化状态检查:若 selinux_initialized(state)
返回 false
,则直接跳转至 allow
标签,授予所有权限。
1 | avc_has_perm |
因此,将 selinux_state->initialized
字段修改为 0
即可全局禁用SELinux。然而经测试,此举会导致系统出现一些副作用,故弃用此方案。
策略二:操纵策略库实现恒允访
我们转而采用一种更精准、副作用更小的方案。其核心在于利用 security_compute_av
函数中另一处逻辑,通过控制策略数据使所有权限检查恒返回允许。
关键逻辑位于 unmap_class()
调用之后:
1 | void security_compute_av(struct selinux_state *state, |
该代码段的逻辑是:如果一个orig_tclass
在经过 unmap_class()
后结果为0,且系统策略配置为允许未知对象类(policydb->allow_unknown == true
),则授予全部权限。
这为实现绕过提供了两条清晰的修改路径:
- 启用未知类允许:修改
selinux_state->policy->policydb->allow_unknown
为1
(true)。 - 破坏类映射表:修改
selinux_state->policy->map->mapping
指向的映射数组,确保对于任何有效的orig_tclass
,函数unmap_class
都返回0
。这可以通过篡改selinux_mapping
结构中的value
字段实现。
1 | /* Mapping for a single class */ |
此方法仅篡改策略数据,不干扰SELinux的状态机,有效避免了直接禁用初始化所带来的副作用。
向 init 注入 reverse shell shellcode
将init用到的共享库用只读方式映射到内存空间,控制PTE修改页表项为读写即可修改文件page cache,从而劫持init的执行流。由于共享库会被很多进程使用,而修改page cache后的变化会应用到所有进程,shellcode需要额外判断一下调用者是否为init,并在fork后执行反弹shell代码。
example:
最后是完整提权演示:
总结与思考
CVE-2025-21479从GPU固件的逻辑缺陷到最终的本地提权,攻击者需要精准地操控内存分配、利用硬件特性绕过软件保护、并深刻理解内核安全子系统的内部机理。这一过程充分展现了现代漏洞利用技术的高度复杂性和精巧性。
然而,正如我们所看到的,即便拥有如此强大的原始能力——任意物理地址读写——在实际攻击中仍面临重重障碍:需要对抗内存分配器的隔离策略,需要爆破或扫描以定位关键数据,需要精心设计才能稳定地篡改策略而非导致系统崩溃。这一切都清晰地指向一个结论:当今安卓系统的防御体系已经日趋成熟,攻守双方的力量对比正在发生根本性的转变。防御方通过多年的积累,构建起一个由硬件隔离(如TrustZone)、内核完整性保护(如RKP/HKIP)、运行时监控(如SELinux)、和沙箱隔离组成的多层次、纵深化防御体系。攻击者往往需要连续突破多个互不信任的安全层,其门槛和不确定性已被极大地提高。一个漏洞,即便强如CVE-2025-21479,也更多是作为一把开启后续攻击链的“钥匙”,而并非一锤定音的终极武器。
CVE-2025-21479既是对过去攻击技术的总结,也是对未来防御的启示。它证明,尽管攻击技术依然在向更高精尖的方向发展,但防御体系已经构筑了足够坚固的壁垒,使得攻击的成本和代价变得极其高昂。安全不再是单点技术的比拼,而是整个系统生态、供应链响应速度、以及攻防思维理解的全面竞争。移动安全进入了一个新的阶段,在这个阶段中,防御者正逐渐夺回主导权,但丝毫的松懈都可能让局势逆转。这场永恒的博弈,将继续驱动着技术创新,迈向更安全的数字未来。
参考
- https://docs.qualcomm.com/product/publicresources/securitybulletin/june-2025-bulletin.html
- https://github.com/zhuowei/cheese
- https://web.archive.org/web/20241114073403/https://powerofcommunity.net/poc2024/Pan%20Zhenpeng%20&%20Jheng%20Bing%20Jhong,%20GPUAF%20-%20Two%20ways%20of%20rooting%20All%20Qualcomm%20based%20Android%20phones.pdf
- https://i.blackhat.com/BH-US-24/Presentations/REVISED02-US24-Gong-The-Way-to-Android-Root-Wednesday.pdf
- https://i.blackhat.com/USA-20/Thursday/us-20-Gong-TiYunZong-An-Exploit-Chain-To-Remotely-Root-Modern-Android-Devices.pdf
- https://i.blackhat.com/USA-22/Thursday/US-22-WANG-Ret2page-The-Art-of-Exploiting-Use-After-Free-Vulnerabilities-in-the-Dedicated-Cache.pdf
- https://yanglingxi1993.github.io/dirty_pagetable/dirty_pagetable.html
- https://github.com/a13xp0p0v/kernel-hack-drill
- https://mp.weixin.qq.com/s?__biz=MzkyMjM5MTk3NQ==&mid=2247485773&idx=1&sn=da3dd8be7ee3a6fabce17efcad7b91ed&poc_token=HE2ppmij8iLV5w7sYoRwCQCnPTo_PPqJDLxD3uVd