fastrpc_mmap_find
中的搜索算法不正确,导致内核地址空间信息泄露。
这个洞也被 CVE-2024-33060 的patch给修复了,因此共享同一个cve
1 漏洞
fastrpc_mmap_create
函数使用攻击者可控的参数调用 fastrpc_mmap_find
,以查找是否存在已满足映射创建请求的现有映射。fastrpc_mmap_find
函数如果找到满足的map,则返回0,否则返回-ENXIO
。
static int fastrpc_mmap_create(struct fastrpc_file *fl, int fd,
unsigned int attr, uintptr_t va, size_t len, int mflags,
struct fastrpc_mmap **ppmap)
{
......
chan = &apps->channel[cid];
if (!fastrpc_mmap_find(fl, fd, va, len, mflags, 1, ppmap))
return 0;
......
}
如果找到满足请求的映射,代码就会对该映射的引用计数进行加一,然后直接返回。具体的查找过程里,会根据mflags来选择在全局还是本地的映射里去寻找:
- 当在本地映射里找时(31行),map->va 被设置成用户态提供的地址,这段代码是合乎逻辑的。
- 但对于全局映射(11行),其 map->va 实际上是一个内核的 struct page 指针,用来充当所分配内存的opaque cookie(不透明句柄,即不该让用户知道)。
- map->va的来源是什么?
static int fastrpc_mmap_find(struct fastrpc_file *fl, int fd,
uintptr_t va, size_t len, int mflags, int refs,
struct fastrpc_mmap **ppmap)
{
struct fastrpc_apps *me = &gfa;
struct fastrpc_mmap *match = NULL, *map = NULL;
struct hlist_node *n;
if ((va + len) < va)
return -EFAULT;
// 全局映射里找
if (mflags == ADSP_MMAP_HEAP_ADDR ||
mflags == ADSP_MMAP_REMOTE_HEAP_ADDR) {
spin_lock(&me->hlock);
hlist_for_each_entry_safe(map, n, &me->maps, hn) {
if (va >= map->va &&
va + len <= map->va + map->len &&
map->fd == fd) { // 用户态提供的 va 和 len 是否在映射范围内,以及fd是否相同
if (refs) {
if (map->refs + 1 == INT_MAX) {
spin_unlock(&me->hlock);
return -ETOOMANYREFS;
}
map->refs++;
}
match = map;
break;
}
}
spin_unlock(&me->hlock);
// 本地映射里找
} else {
hlist_for_each_entry_safe(map, n, &fl->maps, hn) {
if (va >= map->va &&
va + len <= map->va + map->len &&
map->fd == fd) {
if (refs) {
if (map->refs + 1 == INT_MAX)
return -ETOOMANYREFS;
map->refs++;
}
match = map;
break;
}
}
}
......
}
当是全局映射时,即va是堆地址(ADSP_MMAP_REMOTE_HEAP_ADDR
),map->va
的值来源于 fastrpc_alloc_cma_memory
中调用的 dma_alloc_attrs
函数。
该函数有两种工作模式:
- 返回内核虚拟地址:指向分配的内存。
- 返回
struct page
指针:作为内存的“不透明句柄”(opaque cookie)。
当是全局映射时,dma_alloc_attrs
函数是第二种工作模式。有两个依据,第一个是代码,如下所示。
创建map时,fastrpc_mmap_create
先设置map的attr等(重点是DMA_ATTR_NO_KERNEL_MAPPING
,这涉及哪种工作模式),然后调用函数fastrpc_alloc_cma_memory
去分配cma内存。
// fastrpc_mmap_device_ioctl ->
// case FASTRPC_IOCTL_MEM_MAP ->
// fastrpc_internal_mem_map ->
fastrpc_mmap_create{ //
......
if (mflags == ADSP_MMAP_HEAP_ADDR ||
mflags == ADSP_MMAP_REMOTE_HEAP_ADDR) {
map->apps = me;
map->fl = NULL;
map->attr |= DMA_ATTR_SKIP_ZEROING | DMA_ATTR_NO_KERNEL_MAPPING;
err = fastrpc_alloc_cma_memory(®ion_phys, ®ion_vaddr,
len, (unsigned long) map->attr);
}
......
}
函数fastrpc_alloc_cma_memory
调用dma_alloc_attrs
,并传入前面设置的attr。
static int fastrpc_alloc_cma_memory(dma_addr_t *region_phys, void **vaddr, size_t size, unsigned long dma_attr)
......
*vaddr = dma_alloc_attrs(me->dev, size, region_phys, GFP_KERNEL, dma_attr);
......
}
继续往下调用dma_direct_alloc
void *dma_alloc_attrs(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag, unsigned long attrs)
{
......
if (dma_is_direct(ops))
cpu_addr = dma_direct_alloc(dev, size, dma_handle, flag, attrs);
......
}
而函数dma_direct_alloc
中,如果attr指定了 DMA_ATTR_NO_KERNEL_MAPPING
,则不会返回内核虚拟地址,而是返回一个 struct page
指针。
void *dma_direct_alloc_pages(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp, unsigned long attrs)
{
......
page = __dma_direct_alloc_pages(dev, size, dma_handle, gfp, attrs);
......
if ((attrs & DMA_ATTR_NO_KERNEL_MAPPING) && // 如果attr指定了 DMA_ATTR_NO_KERNEL_MAPPING
!force_dma_unencrypted(dev)) {
/* remove any dirty cache lines on the kernel alias */
if (!PageHighMem(page))
arch_dma_prep_coherent(page, size);
*dma_handle = phys_to_dma(dev, page_to_phys(page)); // 返回一个page指针作为 opaque cookie
/* return the page pointer as the opaque cookie */
return page;
}
}
此外,我们看一下创建本地映射时
map->va
的赋值是怎样的:
fastrpc_mmap_create{ //
......
if (mflags == ADSP_MMAP_HEAP_ADDR ||
mflags == ADSP_MMAP_REMOTE_HEAP_ADDR) {
......
} else if (mflags == FASTRPC_DMAHANDLE_NOMAP) {
......
map->va = 0; // 设置为0
......
}else {
......
map->va = va; // 设置为用户态传入的va
......
}
第二个依据是通过 debugfs
查看 adsprpc
目录下的全局映射文件(GMAPS
)。
https://project-zero.issues.chromium.org/issues/42451713
=================================== GMAPS ====================================
fd |phys |size |va
--------------------------------------------------------------------------------
-1 |0xE883A000 |0x1000 |0xFFFFFFFE01A20E80
-1 |0xE8839000 |0x1000 |0xFFFFFFFE01A20E40
-1 |0xE8838000 |0x1000 |0xFFFFFFFE01A20E00
-1 |0xE8837000 |0x1000 |0xFFFFFFFE01A20DC0
-1 |0xE8836000 |0x1000 |0xFFFFFFFE01A20D80
-1 |0xE8835000 |0x1000 |0xFFFFFFFE01A20D40
0 |0xE8834000 |0x1000 |0xFFFFFFFE01A20D00
0 |0xE8833000 |0x1000 |0xFFFFFFFE01A20CC0
0 |0xE8832000 |0x1000 |0xFFFFFFFE01A20C80
-1 |0xE8900000 |0x200000 |0xFFFFFFFE01A24000
这里的 va 字段实际上是struct page
指针,而非真实内存地址。(物理地址的 offset 为一页,而对应 va 的 offset 则为0x40 -- page结构体的size)。
因此,函数fastrpc_mmap_find
中,用户态提供的 va
值会被直接与内核的 struct page
指针比较。同时由于 ioctl
的返回值会根据比较结果(成功(0)/-ENXIO
/-ETOOMANYREFS
)不同,攻击者可以对与 fastrpc_map
对象相关联的 struct page
指针进行暴力猜测(brute force),导致内核地址信息的泄露。
如 sethjenkins 在三星 S23 上运行 PoC 的输出如下:
dm1q:/data/local/tmp $ ./poc
Detected address 0xfffffffe01c00000
Final address: 0xfffffffe01a24000
此外,由于 map->va
是 struct page
指针而非实际内存地址,代码中直接使用 map->va + map->len
进行范围判断是错误的。这可能导致同一调用参数匹配到多个映射对象,进一步引发逻辑混乱。
2 Patch
和 CVE-2024-33060 是同一个patch链接,find函数里只搜索 local map,不再搜索global map。
参考文献
- https://googleprojectzero.blogspot.com/2024/12/qualcomm-dsp-driver-unexpectedly-excavating-exploit.html
- https://project-zero.issues.chromium.org/issues/42451713
- https://git.codelinaro.org/clo/la/kernel/msm-5.4/-/commit/4056f87e3b347e0283234f56b9e9aaea681d1644