目录 
前言              
背景知识 
漏洞成因 
CVE-2016-6738 漏洞成因 
CVE-2016-6738 漏洞补丁 
 CVE-2016-3935 漏洞成因 
 CVE-2016-3935 漏洞补丁 
 漏洞利用 
什么是提权 
 利用方法回顾 
本文使用的方法 
CVE-2016-6738 漏洞利用 
CVE-2016-3935 漏洞利用 
参考 
 
前言    
CVE-2016-3935 和CVE-2016-6738 是 360冰刃实验室发现的高通加解密引擎(    Qualcomm crypto engine)的两个提权漏洞,分别在2016 年10月 和    11月 的谷歌android 漏洞榜被公开致谢,同时高通也在    2016年 10月     和 11月     的漏洞公告里进行了介绍和公开致谢。这两个漏洞报告给谷歌的时候都提交了 exploit 并且被采纳,这篇文章介绍一下这两个漏洞的成因和利用。 
背景知识    
高通芯片提供了硬件加解密功能,并提供驱动给内核态和用户态程序提供高速加解密服务,我们在这里收获了多个漏洞,主要有 3 个驱动 
qcedev driver       就是本文两个漏洞发生的地方,这个驱动通过  ioctl  接口为用户层提供加解密和哈希运算服务。 
加解密服务的核心结构体是  struct qcedev_cipher_op_req,  其中 ,  待加     / 解密数据存放在  vbuf  变量里, enckey      是秘钥,   alg  是算法,这个结构将控制内核 qce 引擎的加解密行为。 
哈希运算服务的核心结构体是  struct qcedev_sha_op_req,  待处理数据存放在  data  数组里,     entries  是待处理数据的份数, data_len  是总长度。 
漏洞成因  
可以通过下面的方法获取本文的漏洞代码 
 
CVE-2016-6738漏洞成因 
现在,我们来看第一个漏洞  cve-2016-6738 
介绍漏洞之前,先科普一下 linux kernel  的两个小知识点 
1) linuxkernel  的用户态空间和内核态空间是怎么划分的? 
简单来说,在一个进程的地址空间里,比  thread_info->addr_limit  大的属于内核态地址,比它小的属于用户态地址 
2) linuxkernel  用户态和内核态之间数据怎么传输? 
不可以直接赋值或拷贝,需要使用规定的接口进行数据拷贝,主要是 4 个接口: 
copy_from_user/copy_to_user/get_user/put_user 
这 4 个接口会对目标地址进行合法性校验,比如: 
copy_to_user = access_ok +__copy_to_user  // __copy_to_user  可以理解为是 memcpy 
下面看漏洞代码 
当用户态通过  ioctl  函数进入  qcedev  驱动后,如果      command  是  QCEDEV_IOCTL_ENC_REQ  (加密)或者  QCEDEV_IOCTL_DEC_REQ      (解密),最后都会调用函数  qcedev_vbuf_ablk_cipher   进行处理。 
在  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  的校验,这是一个疑似漏洞 
在函数  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,      这个地址可以被用户态控制,    这样漏洞就坐实了,我们得到了一个内核任意地址写漏洞。 
接下去我们看一下能写什么值 
再看一下漏洞触发的地方,源地址是  k_align_dst   ,这是一个局部变量,下面看这个地址的内容能否控制。 
在函数  qcedev_vbuf_ablk_cipher_max_xfer   的行  1160  可以看到,变量      k_align_dst   的值是从用户态地址拷贝过来的,可以被控制,但是,还没完 
行 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     进行加密 
2)  然后将 B 作为参数传入  qcedev_vbuf_ablk_cipher_max_xfer      函数触发漏洞,同时参数设置为解密操作,并且传入同样的解密算法和 key 
这样的话,经过  submit_req   操作后,  line 1204  得到的      k_align_dst  就是我们需要的数据。 
至此,我们得到了一个 任意地址写任意值的漏洞 。 
CVE-2016-6738漏洞补丁 
这个  漏洞的修复            很直观,将  in_place_op   的判断去掉了,对  creq->vbuf.src        和  creq->vbuf.dst  两个数组里的地址挨个进行  access_ok      校验 
下面看第二个漏洞 
CVE-2016-3935漏洞成因 
在  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 
 
qcedev_check_sha_params   对用户态传入的数据做多种校验,其中一项是对传入的数据数组挨个累加长度,并对总长度做整数溢出校验 
问题在于,  req->data[i].len   是  uint32_t  类型,       总长度  total   也是  uint32_t      类型,  uint32_t  的上限是  UINT_MAX,  而这里使用了      ULONG_MAX   来做校验 
注意到: 
32 bit  系统,   UINT_MAX= ULONG_MAX  
64 bit  系统,   UINT_MAX   ! = ULONG_MAX 
 
所以这里的整数溢出校验   在 64bit 系统是无效的     ,即在  64bit  系统, req->data  数组项的总长度可以整数溢出,这里还无法确定这个整数溢出能造成什么后果。 
下面看看有何影响,我们选取  case QCEDEV_IOCTL_SHA_UPDATE_REQ  
qcedev_areq.sha_op_req.alg   的值也是应用层控制的,当等于 QCEDEV_ALG_AES_CMAC  时,进入函数 qcedev_hash_cmac      
在函数  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   年的一类常见漏洞 
下面进入漏洞利用分析 
漏洞利用  
androidkernel 漏洞利用基础 
在介绍本文两个漏洞的利用之前,先回顾一下  android kernel  漏洞利用的基础知识 
什么是提权 
linuxkernel  里,进程由  struct task_struct   表示,进程的权限由该结构体的两个成员      real_cred   和  cred   表示 
所谓提权,就是修改进程的  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  非常简单 ,  利用漏洞改写内核某个函数指针     ( 最常见的就是  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  技巧有几种, 
1.       下面两篇文章讲了基本的  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 , 是对上一阶段思路的自然延伸。 
1.       使用  rop  改写   addr_limit           的值,破除本进程的系统调用  access_ok   校验,然后通过一些函数如                ptrace_write_value_at_address   直接读写内核来提权  ,              将   selinux_enforcing  变量写 0  关闭   selinux; 
2.       大名鼎鼎的           Ret2dir  bypassPXN; 
3.       还有就是本文使用的思路,用漏洞重定向内核驱动的  xxx_operations    结构体指针到应用层,再用          rop  地址填充应用层的伪  xxx_operations    里的函数实现;  
4.       还有一些  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 kernelstack (Done: x86, arm64, s390. Needed on arm, powerpc and others?) * Movekernel stack to vmap area (Done: x86, s390. Needed on arm, arm64, powerpc andothers?) 
·         Implement kernel relocation andKASLR for ARM 
·         Write a plugin to clear structpadding 
·         Write a plugin to do formatstring warnings correctly (gcc’s -Wformat-security is bad about const strings) 
·         Make CONFIG_STRICT_KERNEL_RWX andCONFIG_STRICT_MODULE_RWX mandatory (done for arm64 and x86, other archs stillneed it) 
·         Convert remaining BPF JITs toeBPF JIT (with blinding) (In progress: arm) 
·         Write lib/test_bpf.c tests foreBPF constant blinding 
·         Further restriction ofperf_event_open (e.g. perf_event_paranoid=3) 
·         Extend HARDENED_USERCOPY to useslab whitelisting (in progress) 
·         Extend HARDENED_USERCOPY to splituser-facing malloc()s and in-kernel malloc()svmalloc stack guard pages (inprogress) 
·         protect ARM vector table asfixed-location kernel target 
·         disable kuser helpers on arm 
·         rename CONFIG_DEBUG_LIST betterand default=y 
·         add WARN path for page-spanningusercopy checks (instead of the separate CONFIG) 
·         create UNEXPECTED(), like BUG()but without the lock-busting, etc 
·         create defconfig “make” targetfor by-defaul 
t hardened Kconfigs (using guidelines below) 
·         provide mechanism to check forro_after_init memory areas, and reject structures not marked ro_after_init invmbus_register() 
·         expand use of __ro_after_init,especially in arch/arm64 
·         Add stack-frame walking tousercopy implementations (Done: x86. In progress: arm64. Needed on arm,others?) 
·         restrict autoloading of kernelmodules (like GRKERNSEC_MODHARDEN) (In progress: Timgad LSM) 
 
有兴趣的同学可以进入该项目看看代码,提前了解一下缓解措施, 
比如   KASLR for ARM ,  将大部分内核对象的地址做了随机化处理,这是以后      androidkernel exploit  必须面对的 ; 
另外比如   __ro_after_init   ,内核启动完成初始化之后大部分      fops   全局变量都变成  readonly  的,这造成了本文这种利用方法失效 ,      所幸的是,目前  android kernel  还是可以用的。 
本文使用的利用方法  
对照   Kernel_Self_Protection_Project        的利用分类,本文的利用思路属于       Userspace data usage 
Sometimesan attacker won’t be able to control the instruction pointer directly, but theywill be able to redirect the dereference a structure or other pointer. In thesecases, it is easiest to aim at malicious structures that have been built inuserspace 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,  先在用户态构造一个伪结构,如下 
根据前面的分析,伪结构的值需要先做一次加密,再使用 
下面是核心的函数 
参数  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  如上,其地址是  0xffffffc000671a58 ,       其指令是  str w1, [x2] ; ret ; 
这段  rop  作为一个函数去执行的话,其效果相当于将第二个参数的值写入第三个参数指向的地址。 
我们用这段  rop  构造一个用户态函数,如下 
9*8  是  ioctl  函数在  file_operations      结构体里的偏移,  
*(unsignedlong*)(fake_ptmx_fops + 9 * 8) = ROP_WRITE; 
的效果就是  ioctl  的函数实现替换成  ROP_WRITE ,  这样我们调用      ptmx  的  ioctl  函数时,最后真实执行的是  ROP_WRITE ,     这就是一个内核任意地址写任意值函数。 
同样的原理,我们封装读任意内核地址的函数。 
有了任意内核地址读写函数之后,我们通过以下方法完成最终提权: 
搜索到本进程的  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   这个结构 
在我做利用的设备里,这个结构是从  kmalloc-4096  堆里分配的,其偏移  24Byte  的地方是一个      struct tty_operations   的指针,我们溢出后重写这个结构体,用一个用户态地址覆盖这个指针。 
4128 + 536867423 + 7 *0x1fffffff = 632 
溢出的方法如上,我们让  entry  的数目为  9  个,第一个长度为      4128,  第二个为  536867423 ,       其他  7 个为 0x1fffffff 
这样他们加起来溢出之后的值就是  632 ,   这个长度刚好是      structtty_struct   的长度,我们用  qcedev_sha_op_req.data[0].vaddr[4096]   这个数据来填充被溢出的      tty_struct   的内容 
主要是填充两个地方,一个是最开头的  tty magic ,  另一个就是偏移  24Bype  的      tty_operations   指针,我们将这个指针覆盖为伪指针  fake_ptm_fops . 
之后的提权操作与  cve-2016-6738  类似, 
如上, ioctl  函数在  tty_operations   结构体里偏移      12  个指针,当我们用  ROP_WRITE   覆盖这个位置时,可以得到一个内核地址写函数。 
  
  
同理,当我们用  ROP_READ   覆盖这个位置时,可以得到一个内核地址写函数。 
最后,用封装好的内核读写函数,修改内核的  cred  等结构体完成提权。 
参考    
android_run_root_shell 
xairy 
NewReliable Android Kernel Root Exploitation Techniques 
 
 *