資安

高通TrustZone接口QSEECOM Use-After-Free漏洞分析

高通QSEECOM接口漏洞(CVE-2019-14040)分析

阿里安全(侯客)

背景:

 上週五看到一篇國外的安全公司zimperium的研究人員寫的一篇他們分析發現的高通的QSEECOM接口漏洞文章,[https://blog.zimperium.com/multiple-kernel-vulnerabilities-affecting-all-qualcomm-devices/] 其中一個Use-After-Free的漏洞(CVE-2019-14041)我覺得挺有意思,但是原文有些部分寫的比較生澀或者沒有提到關鍵點上,所以我想稍微續叼寫的更具體一些,以及我對這種類型漏洞的一些思考或者是對我的啟發,以及安全研究人員和產品開發人員對安全的理解方式。

 這名叫Tamir Zahavi-Brunner的安全研究在2019年的7月底發現兩個高通QSEECOM接口的漏洞,一個是條件競爭的漏洞CVE-2019-14041,一個就是我今天要講的內核內存映射相關的Use-After-Free漏洞CVE-2019-14040。

 簡單介紹一下這個QSEECOM接口,它是一個內核驅動連接用戶態Normal world和Secure world的一個橋樑,Secure world就是我們常說的Trustzone/TEE/Security Enclave安全運行環境,Normal world就是非安全運行環境,這個高通的QSEECOM接口可以實現一些從用戶態加載/卸載一些安全的TA(Trust Applcation)到TrustZone中去運行,比如我們手機常用的指紋/人臉識別的應用,這些應用都是在TrustZone中運行的,在這種運行環境下,可以保證我們用戶的關鍵隱私不被竊取,這個QSEECOM架構如下。

 要想了解這個漏洞的成因,需要先了解這個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;
    struct qseecom_dev_handle *data;
        data = kzalloc(sizeof(*data), GFP_KERNEL);//分配qseecom_dev_handle結構體內存
    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用戶內存的地址信息以及這個內存的長度信息,我把關鍵的代碼標示出來(markdown語法好像無法標示代碼塊裡面的特定行的代碼)。

static int 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 for loading 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);//獲取client的ihandle信息
    ...
    /* Get the physical address of the ION BUF */
    ret = ion_phys(qseecom.ion_clnt, data->client.ihandle, &pa, &len);//獲取用戶態提交ion虛擬內存所映射的物理內存址與真實長度信息
    if (ret) {
        pr_err("Cannot get phys_addr for the Ion 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 scm call 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 for client 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;
}

這個代碼流程如下:
image.png

我們從qseecom_dev_handle結構體上能夠發現client是它的子成員結構體

struct qseecom_dev_handle {
    enum qseecom_client_handle_type type;
    union {
        struct qseecom_client_handle client;//這個指針沒有置空
        struct qseecom_listener_handle listener;
    };
    bool released;
…

struct qseecom_client_handle {
    u32  app_id;
    u8 *sb_virt;
    phys_addr_t sb_phys;
    unsigned long user_virt_sb_base;
    size_t sb_length;
    struct ion_handle *ihandle;        /* Retrieve phy addr */
    char app_name[MAX_APP_NAME_SIZE];
    u32  app_arch;
    struct qseecom_sec_buf_fd_info sec_buf_fd[MAX_ION_FD];
    bool from_smcinvoke;
};

 而銷燬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_open打開qseecom設備的時候會分配一個qseecom_dev_handle結構體,但是很不幸的是這個初始化過程也沒有完全把這片內存給清0。

static int qseecom_open(struct inode *inode, struct file *file)
{
    int ret = 0;
    struct qseecom_dev_handle *data;
    data = kzalloc(sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    file->private_data = data;
    data->abort = 0;
    data->type = QSEECOM_GENERIC;
    data->released = false;
    memset((void *)data->client.app_name, 0, MAX_APP_NAME_SIZE);//似乎還差一點點

這個初始化前後的內存對比是這樣的
image.png

 接下來就是use過程的關鍵了,我們的目標就是能夠使用這些free掉的結構中殘留的數據,如何能夠保證殘留數據可用,第一,殘留的關鍵數據不被接下來的流程所覆蓋,第二,保護流程正常走下去,現有的qseecom_dev_handle結構不被無效的操作釋放,滿足這兩條,後續的正常業務處理邏輯就會use之前殘留的free掉的內存完成free掉內存的use。為了保證滿足第二條,我們需要滿足qseecom_dev_handle成員client的ihandle指針不能為空(__validate_send_service_cmd_inputs會檢查),因為之前釋放的時候這裡被置空了。好的,現在只需要保證第一條,關鍵的殘留數據不被覆蓋就好了。

 為了達到這個殘留數據不被覆蓋的目標,只需要用戶態發送一個QSEECOM_IOCTL_SET_MEM_PARAM_REQ ioctl請求,且用戶提交的ION內存分配的長度信息大於實際用戶所分配的大小即可(例如用戶只分配了0x1000字節內存,但是用戶提交給內核說我分配了0x2000個字節,當然內核也不是傻子,你說多少就多少,內核說我要檢查一下,檢查發現,好小子你才分配了0x1000字節的內存,你卻告訴我有0x2000字節,是不是當我傻,內核就立即返回操作出錯的信息給用戶),還記得上面提到的qseecom_set_client_mem_param函數處理流程嗎? 雖然內核直接返回操作錯誤告之給用戶態,但是最重要的是qseecom_dev_handle指針沒有被銷燬,而且就是因為這個錯誤的操作,那個殘留數據也沒有被覆蓋,且結構體裡面的ihandle也賦值了不為空,兩個條件都滿足了,然後接下來的正常業務處理邏輯將會把之前殘留的sb_virt/sb_phys地址用於內存讀寫操作,完成真正的use操作。

當然最後這個漏洞的修補過程也比較簡單,把client結構成員全部清空即可。
image.png

 寫到這裡漏洞分析過程就結束了,這個漏洞的利用危害,我覺得比較容易實現的一點可能是洩露一些敏感信息,這個需要關聯上下文深入研究,作者提到可能用於提權獲取root權限,我覺得還是挺麻煩的,而且需要把不太可控的讀寫轉化成可控的讀寫,比較複雜,最終也有可能利用不成功,因為越是複雜的系統摻雜的噪音越多,排查起來比較麻煩,加上內核態的調試困難以及,而且對內存佈局要求也非常高。

最後的一些思考:
 也是我覺得比較有意思的一點,這個漏洞的根源當然是釋放的內存沒有清空,但是有一個很重要點就是內核態和用戶態的狀態機制不同步造成的(不知道這樣說對不對),比如內核返回給用戶說,我判斷了,你給我的信息不對,你的行為不對,我警告過你了,但是用戶根本不管,我繼續做我認為是正確的事情,從這裡可以看出安全研究人員與開發人員對於安全風險視角的不同了,或者可以看出安全研究人員是如何定位攻擊面,如何挖掘漏洞的。

參考

https://android.googlesource.com/kernel/msm/+/2786ec57c52839f02802c01b0a12f24255064b10/drivers/misc/qseecom.c
https://source.codeaurora.org/quic/la/kernel/msm-3.18/commit/?id=c4f42c24e02ce82392d8f8fe215570568380c8ab
https://github.com/tamirzb/CVE-2019-14040

Leave a Reply

Your email address will not be published. Required fields are marked *