作者:阿里安全 謝君
公眾號: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 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1179/