Google Securitybulletins CVE-2023-48409 漏洞分析

0x01 具体背景

很少能看到有关GPU 的漏洞披露信息,因此比较感兴趣的跟了一下这个漏洞。

看到有关pixel GPU 的漏洞利用: https://github.com/0x36/Pixel_GPU_Exploit , 这篇披露的文章中描述了对GPU 的漏洞利用情况,但我更感兴趣这个漏洞是具体的入口调用到目标漏洞函数的完整流程。

在Google 安全公告中有这个编号 CVE-2023-48409, 但是通过公告的信息是没办法获取到详细的修复缓解措施,还好披露文章中给出了修复的commitID 的链接。
https://android.googlesource.com/kernel/google-modules/gpu/+/68073dce197709c025a520359b66ed12c5430914%5E%21/#F0

https://android.googlesource.com/kernel/google-modules/gpu/+/68073dce197709c025a520359b66ed12c5430914/mali_kbase/mali_kbase_core_linux.c

0x02 详情分析

存在漏洞的函数

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
int gpu_pixel_handle_buffer_liveness_update_ioctl(struct kbase_context* kctx,
struct kbase_ioctl_buffer_liveness_update* update)
{
int err = 0;
struct gpu_slc_liveness_update_info info;
u64* buff;


/* Compute the sizes of the user space arrays that we need to copy */
u64 const buffer_info_size = sizeof(u64) * update->buffer_count; // 计算需要复制的buffer_count 个u64类型元素的大小。
u64 const live_ranges_size =
sizeof(struct kbase_pixel_gpu_slc_liveness_mark) * update->live_ranges_count; // 获取总ranges 需要的大小,根据传入的live_ranges_count 进行计算。

/* Nothing to do */
if (!buffer_info_size || !live_ranges_size)
goto done;

/* Allocate the memory we require to copy from user space */
buff = kmalloc(buffer_info_size * 2 + live_ranges_size, GFP_KERNEL); // 给缓冲区分配大小
if (buff == NULL) {
dev_err(kctx->kbdev->dev, "pixel: failed to allocate buffer for liveness update");
err = -ENOMEM;
goto done;
}
/* Set up the info struct by pointing into the allocation. All 8 byte aligned */
info = (struct gpu_slc_liveness_update_info){
.buffer_va = buff,
.buffer_sizes = buff + update->buffer_count,
.live_ranges = (struct kbase_pixel_gpu_slc_liveness_mark*)(buff + update->buffer_count * 2),
.live_ranges_count = update->live_ranges_count,
};
/* Copy the data from user space */
err =
copy_from_user(info.live_ranges, u64_to_user_ptr(update->live_ranges_address), live_ranges_size);
if (err) {
dev_err(kctx->kbdev->dev, "pixel: failed to copy live ranges");
err = -EFAULT;
goto done;
}
err = copy_from_user(
info.buffer_sizes, u64_to_user_ptr(update->buffer_sizes_address), buffer_info_size);
if (err) {
dev_err(kctx->kbdev->dev, "pixel: failed to copy buffer sizes");
err = -EFAULT;
goto done;
}
err = copy_from_user(info.buffer_va, u64_to_user_ptr(update->buffer_va_address), buffer_info_size);
if (err) {
dev_err(kctx->kbdev->dev, "pixel: failed to copy buffer addresses");
err = -EFAULT;
goto done;
}
/* Execute an slc update */
gpu_slc_liveness_update(kctx, &info);
done:
kfree(buff);
return err;
}

函数原型copy_from_user
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
第三个参数的值是无符号的长整型整数。

kmalloc()
static inline void *kmalloc(size_t size, gfp_t flags); /*返回的是虚拟地址*/
其中 size_t 类型是 C 中任何对象所能达到的最大长度,是无符号整数。

结构体

1
2
3
4
5
6
7
struct kbase_ioctl_buffer_liveness_update {
__u64 live_ranges_address;
__u64 live_ranges_count;
__u64 buffer_va_address;
__u64 buffer_sizes_address;
__u64 buffer_count;
};

结合修复的注释与gpu_pixel_handle_buffer_liveness_update_ioctl 函数来一起分析

    1. buffer_info_size 和 live_ranges_size 的值都是由用户传入的数据来定义
    1. buff 局部变量会根据 buffer_info_size * 2 + live_ranges_size 来计算要通过kmalloc 分配所需的内存大小。
    • 如果 buffer_info_size 和 live_ranges_size 都很大,会导致计算的值非常大。
    • kmalloc 第一个参数的值是无符号的数,因此会产生整数上溢的问题,进而分配一个很小的内存空间给buff
    1. copy_from_user(info.live_ranges, u64_to_user_ptr(update->live_ranges_address), live_ranges_size); 这段代码会出现缓冲区溢出
    • 因为 info.live_ranges 是由(buff + update->buffer_count * 2) 定义的指针,因此在正常情况下,info.live_ranges是不会超过buffer内存区域的,但实际上已经指向了错误的内存区域。
    • 由于buffer 的内存区域很小,而从用户空间要拷贝的数据live_ranges_size 很大,因此在内核中会造成缓冲区溢出的问题。
  • kmalloc 调用实际上可能会分配比预期少的内存。

  • 这可能导致 info 结构体的指针指向错误的内存区域。

  • 当尝试从用户空间复制数据到这些指针指向的内存时,可能会访问到未分配或不允许的内存区域,导致程序崩溃或安全漏洞。

函数调用流程

在分析完漏洞成因之后,再分析一下这个漏洞该如何通过用户空间来触发,根据披露的信息来看,该利用访问了 /dev/mail0 这个字符设备。下面梳理一下正向的函数之间的调用关系。

首先是驱动注册,这里将驱动注册成了一个平台设备驱动,这种驱动一般用在各种集成在系统芯片上的硬件,比如过SOC 上的GPU,DSP等,并且使用platform_device 结构体来注册,这个结构体包含了设备名称和驱动程序需要的资源,另外平台设备驱动的加载依赖于系统启动时的自动发现机制,并且这个机制是根据 of_match_table 的硬件id 来进行匹配并加载。

/mali_kbase/mali_kbase_core_linux.c

平台设备的probe 注册函数, 这个函数会在驱动加载的时候,自动执行。
kbase_platform_device_probe

mali_kbase/mali_kbase_core_linux.c

kbase_device_init 这个函数会
- 1. 调用 kbase_device_id_init 函数来初始化驱动的名称
- 2. 初始化kbdev 设备结构体,然后执行dev_init 结构体中的一系列的初始化函数,其中包括kbase_sysfs_init,这个函数再后面会用到
{ kbase_sysfs_init, kbase_sysfs_term, "SysFS group creation failed" },
kbase_device_init

mali_kbase/device/backend/mali_kbase_device_csf.c

kbase_device_id_init函数是给 kbdev->devname 赋值,也就是poc 中的 mail0 驱动名称。
kbase_device_id_init

/device/mali_kbase_device.c

kbase_sysfs_init 函数也是驱动设备初始化的函数,将驱动设备名称 mail0 和 kbase_fops 进行绑定,函数是在 kbase_device_init 阶段进行初始化的。
kbase_sysfs_init

/mali_kbase/mali_kbase_core_linux.c

到这里有关整体的驱动加载注册的过程已经结束了。

调用驱动字符设备驱动,很关键的函数时ioctl,因此找到 kbase_fops
这里会实现file_operations 结构体,提供系统调用的接口。
kbase_fops

mali_kbase/mali_kbase_core_linux.c

kbase_ioctl

mali_kbase/mali_kbase_core_linux.c

kbase_kfile_ioctl 函数才是真正的 ioctl 的实现函数,里面会获取用户传入的结构体数据以及对应要处理的cmd 对应的case。

  • 在 KBASE_IOCTL_BUFFER_LIVENESS_UPDATE case 中,会调用kbase_api_buffer_liveness_update 函数处理用户传入过来的 kbase_ioctl_buffer_liveness_update结构体数据。
    kbase_kfile_ioctl()

    /mali_kbase/mali_kbase_core_linux.c

    1
    2
    3
    4
    5
    6
    7
    case KBASE_IOCTL_BUFFER_LIVENESS_UPDATE:
    KBASE_HANDLE_IOCTL_IN(KBASE_IOCTL_BUFFER_LIVENESS_UPDATE,
    kbase_api_buffer_liveness_update,
    struct kbase_ioctl_buffer_liveness_update,
    kctx);
    break;
    }

其中调用的宏 KBASE_HANDLE_IOCTL_IN 的定义如下所示,会调用函数来处理用户空间传递过来的数据,

最终会走到漏洞触发函数中。
kbase_api_buffer_liveness_update

mali_kbase/mali_kbase_core_linux.c

0x03 修复分析

为什么要这么修复

buffer_info_size * 2 + live_ranges_size < U64_MAX ,防止buff 分配的 buffer_info_size * 2 + live_ranges_size 过大,从而引发整数上溢,那么给buffer 分配的内存大小就会过小,引发其他的溢出问题。


Google Securitybulletins CVE-2023-48409 漏洞分析
https://tig3rhu.github.io/2024/08/07/59__Google Securitybulletins CVE-2023-48409 漏洞分析/
Author
Tig3rHu
Posted on
August 7, 2024
Licensed under