作者:阿里安全 谢君
公众号:vessial的安全Trash Can

背景

今天看到腾讯玄武实验室推送的一篇国外的安全公司zimperium的研究人员写的一篇他们分析发现的高通的QSEECOM接口漏洞文章,https://blog.zimperium.com/multiple-kernel-vulnerabilities-affecting-all-qualcomm-devices/其中一个 Use-After-Free 的漏洞(CVE-2019-14041)我觉得挺有意思,但是原文有些部分写的比较生涩或者没有提到关键点上,所以我想稍微续叼写的更具体一些,以及我对这种类型漏洞的一些思考或者是对我的启发,以及安全研究人员和产品开发人员对安全的理解方式。

这名叫TamirZahavi-Brunner的安全研究者在2019年的7月底发现两个高通QSEECOM接口的漏洞,一个是条件竞争的漏洞CVE-2019-14041,一个就是我今天要讲的内核内存映射相关的Use-After-Free漏洞CVE-2019-14040。

简单介绍一下这个QSEECOM接口,它是一个内核驱动连接用户态Normal world和Secure world的一个桥梁,Secure world就是我们常说的Trustzone/TEE/Security Enclav安全运行环境,Normalworld就是非安全运行环境,这个高通的QSEECOM接口可以实现一些从用户态加载/卸载一些安全的TA(TrustApplcation)到TrustZone中去运行,比如我们手机常用的指纹/人脸识别的应用,这些应用都是在TrustZone中运行的,在这种运行环境下,可以保证我们用户的关键隐私不被窃取。

要想了解这个漏洞的成因,需要先了解这个QSEECOM接口的功能处理逻辑,用户态通过ION设备(一个内存管理器,可以通过打开/dev/ion进行访问)申请的内存可以通过QSEECOM接口映射到内核地址空间,可供内核或者TrustZone访问,而对于QSEECOM驱动模型中(/dev/qseecom)提供给用户的接口有open/close/ioctl,对应着QSEECOM内核处理函数为qseecom_open/qseecom_ioctl/qseecom_release

漏洞成因

说到Use-After-Free漏洞,我们需要先了解内存在哪里Free掉的,然后是在哪里Use的,如何Use的。

Free操作过程

用户态每次打开qseecom设备(/dev/qseecom),都会在内核态生成一个qseecom_dev_handle的结构指针,这个结构指针会被关闭qseecom设备(用户态通过close函数)或者来自用户的IO操作号QSEECOM_IOCTL_UNLOAD_APP_REQ请求予以销毁,需要了解这个结构指针的销毁过程,那么得先了解这个指针的初始化过程。

打开qseecom设备时会调用qseecom_open分配一个qseecom_dev_handle结构体

static int qseecom_open(struct inode *inode, struct file*file)
{
  int ret = 0;
  structqseecom_dev_handle *data;
  data = kzalloc(sizeof(*data), GFP_KERNEL);
  if (!data)
    return -ENOMEM;
  file->private_data= data;
  data->abort = 0;
  …

用户通过QSEECOM_IOCTL_SET_MEM_PARAM_REQ ioctl请求通过函数qseecom_set_client_mem_param来建立用户态ion内存在内核地址空间的映射,而qseecom_set_client_mem_param函数通过copy_from_user函数来获取用户传递的ion用户内存的地址信息以及这个内存的长度信息,我把关键的代码标示出来。

staticint qseecom_set_client_mem_param(struct qseecom_dev_handle data,
          void __user argp)
{
  ion_phys_addr_t pa;
  int32_t ret;
  struct qseecom_set_sb_mem_param_req req;
  size_t len;
  /* Copy the relevant information needed forloading the image */
  if (copy_from_user(&req, (void __user*)argp, sizeof(req)))
    return -EFAULT;
  ...
  data->client.ihandle =ion_import_dma_buf_fd(qseecom.ion_clnt,
          req.ifd_data_fd);
  ...
  /* Get the physical address of the ION BUF*/
  ret =ion_phys(qseecom.ion_clnt, data->client.ihandle, &pa, &len);
  if (ret) {
    pr_err("Cannot get phys_addr for theIon Client, ret = %d\n",
     ret);
return ret;
  }
  if (len < req.sb_len) {
    pr_err("Requested length (0x%x) is> allocated (%zu)\n",
      req.sb_len, len);
    return -EINVAL;
  }
  /* Populate the structure for sending scmcall to load image */
  data->client.sb_virt = (char *)ion_map_kernel(qseecom.ion_clnt,
             data->client.ihandle);
  if (IS_ERR_OR_NULL(data->client.sb_virt)){
    pr_err("ION memory mapping forclient shared buf failed\n");
    return -ENOMEM;
  }
  data->client.sb_phys = (phys_addr_t)pa;
  data->client.sb_length = req.sb_len;
  data->client.user_virt_sb_base =(uintptr_t)req.virt_sb_base;
  return 0;
}

这个代码流程如下:

我们从qseecom_dev_handle结构体上能够发现client是它的子成员结构体

struct qseecom_dev_handle {
  enumqseecom_client_handle_type type;
  union {
    structqseecom_client_handle client;//这个指针没有置空
    structqseecom_listener_handle listener;
  };
  bool released;

struct qseecom_client_handle {
  u32 app_id;
  u8 *sb_virt;
  phys_addr_t sb_phys;
  unsigned longuser_virt_sb_base;
  size_t sb_length;
  struct ion_handle *ihandle;   /*Retrieve phy addr */
  charapp_name[MAX_APP_NAME_SIZE];
  u32 app_arch;
  structqseecom_sec_buf_fd_info sec_buf_fd[MAX_ION_FD];
  bool from_smcinvoke;
};

Copy: 而销毁qseecom_dev_handle结构指针的时候只是把子成员结构体client的子成员ion_handle结构指针ihandle给置空了,client结构体的其它成员并没有置空,也就是说client结构体中的sb_virt地址还sb_length的值还是残留的,这也为后续的freed的内存重新use提供了前提。

static int qseecom_unmap_ion_allocated_memory(struct qseecom_dev_handle*data)
{
    int ret = 0;
   if(!IS_ERR_OR_NULL(data->client.ihandle)) {
      ion_unmap_kernel(qseecom.ion_clnt,data->client.ihandle);//解除用户态 ion内存到内核态的映射
       ion_free(qseecom.ion_clnt,data->client.ihandle);//
       data->client.ihandle= NULL; //只是把这个指针置空了
   }
    return ret;
}

Use的过程

上面我们已经讲了qseecom_dev_handle的销毁的过程,接下来我们看看攻击者是如何使用释放掉的内存的。

我们知道当释放掉的内存被以同样大小以及同样的内存分配式来申请的时候,之前释放掉的内存是很容易被重新命中的,同理常见于浏览器use-after-free漏洞通过heap spray的方式进行大量内存申请来命中之前被释放掉的对象。攻击者的目标就是重用qseecom_unmap_ion_allocated_memory释放掉用户态ion分配的内存,PoC里面的做法通过ion分配一段0x1000内存后,最后释放掉,然后再同样的操作申请同样大小的ion内存,将命中之前释放掉的ion内存,这段内存并没有被memset清0,里面会有之前的数据残留。

接下来就是use过程的关键了,我们的目标就是能够使用这些free掉的结构中残留的数据,如何能够保证残留数据可用,第一,残留的关键数据不被接下来的流程所覆盖,第二,保护流程正常走下去,现有的qseecom_dev_handle结构不被无效的操作释放,满足这两条,后续的正常业务处理逻辑就会use之前残留的free掉的内存完成free掉内存的use。为了保证满足第二条,我们需要满足qseecom_dev_handle成员client的ihandle指针不能为空(__validate_send_service_cmd_inputs会检查),因为之前释放的时候这里被置空了。好的,现在只需要保证第一条,关键的残留数据不被覆盖就好了。

为了达到这个ion申请的且还没有初始化并有残留数据的内存不被覆盖的目标,只需要用户态发送一个QSEECOM_IOCTL_SET_MEM_PARAM_REQ ioctl请求,且用户提交的ION内存分配的长度信息大于实际用户所分配的大小即可(例如用户只分配了0x1000字节内存,但是用户提交给内核说我分配了0x2000个字节,当然内核也不是傻子,你说多少就多少,内核说我要检查一下,检查发现,好小子你才分配了0x1000字节的内存,你却告诉我有0x2000字节,是不是当我傻,内核就立即返回操作出错的信息给用户),然后用户通过发送一个ioctl号QSEECOM_IOCTL_SEND_MODFD_CMD_64_REQ通过传递畸形的用户请求数据来use之前的内存数据

static int __qseecom_send_modfd_cmd(struct qseecom_dev_handle *data,
                    void __user *argp,
                    bool is_64bit_addr)
{
    int ret = 0;
    int i;
    struct qseecom_send_modfd_cmd_req req;
    struct qseecom_send_cmd_req send_cmd_req;
    ret = copy_from_user(&req, argp, sizeof(req));//用户传递进来畸形的请求数据
    if (ret) {
        pr_err("copy_from_user failed\n");
        return ret;
    }
    send_cmd_req.cmd_req_buf = req.cmd_req_buf;
    send_cmd_req.cmd_req_len = req.cmd_req_len;
    send_cmd_req.resp_buf = req.resp_buf;
    send_cmd_req.resp_len = req.resp_len;
    if (__validate_send_cmd_inputs(data, &send_cmd_req))//成功绕过检查
        return -EINVAL;
    /* validate offsets */
    for (i = 0; i < MAX_ION_FD; i++) {
        if (req.ifd_data[i].cmd_buf_offset >= req.cmd_req_len) {
            pr_err("Invalid offset %d = 0x%x\n",
                i, req.ifd_data[i].cmd_buf_offset);
            return -EINVAL;
        }
    }
    req.cmd_req_buf = (void *)__qseecom_uvirt_to_kvirt(data,  
                        (uintptr_t)req.cmd_req_buf);
    req.resp_buf = (void *)__qseecom_uvirt_to_kvirt(data,
                        (uintptr_t)req.resp_buf);
    if (!is_64bit_addr) {  //接下来开始use
        ret = __qseecom_update_cmd_buf(&req, false, data);
        if (ret)
            return ret;
        ret = __qseecom_send_cmd(data, &send_cmd_req);
        if (ret)
            return ret;
        ret = __qseecom_update_cmd_buf(&req, true, data);
        if (ret)
            return ret;
    } else {
        ret = __qseecom_update_cmd_buf_64(&req, false, data);
        if (ret)
            return ret;
        ret = __qseecom_send_cmd(data, &send_cmd_req);
        if (ret)
            return ret;
        ret = __qseecom_update_cmd_buf_64(&req, true, data);
        if (ret)
            return ret;
    }
    return ret;
}

当然最后这个漏洞的修补过程也比较简单,把client结构成员全部清空即可。

写到这里漏洞分析过程就结束了,这个漏洞的利用危害,我觉得比较容易实现的一点可能是泄露一些内存信息,这个需要关联上下文深入研究,作者说可能用于提权获取root权限,我觉得还是挺麻烦的,而且需要把不太可控的读写转化成可控的读写,比较复杂,最终也有可能利用不成功,因为越是复杂的系统掺杂的噪音越多,需要排查的东西也越多。

最后的一些思考

也是我觉得比较有意思的一点,这个漏洞的根源当然是释放的内存没有清空,但是有一个很重要点就是内核态和用户态的状态机制不同步造成的(不知道这样说对不对),比如内核返回给用户说,我判断了,你给我的信息不对,你的行为不对,我警告过你了,但是用户根本不管,我继续做我认为是正确的事情,从这里可以看出安全研究人员与开发人员对于安全风险视角的不同了,或者可以看出安全研究人员是如何定位攻击面,如何挖掘漏洞的。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1179/