高通加解密引擎提权漏洞解析

华盟原创文章投稿奖励计划

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

背景知识

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

1

1

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

1

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

1

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

漏洞成因 

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

1

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

下面看漏洞代码

1

在 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 的校验,这是一个疑似漏洞

图片1

在函数 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, 这个地址可以被用户态控制, 这样漏洞就坐实了,我们得到了一个内核任意地址写漏洞。

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

图片2

图10

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

 

图片3

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

图片4

行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进行加密

图片5

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

图片6

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

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

CVE-2016-6738 漏洞补丁

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

下面看第二个漏洞

CVE-2016-3935 漏洞成因

图片7

在 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

图片8

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

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

图片9

注意到:

  • 32 bit 系统,UINT_MAX = ULONG_MAX
  • 64 bit 系统,UINT_MAX != ULONG_MAX

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

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

图片10

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

图片11

在函数 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 漏洞补丁 

图片12

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

下面进入漏洞利用分析

漏洞利用

android kernel 漏洞利用基础

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

什么是提权

图片13

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

图片14

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

  • 怎么找到目标 cred ?
  • cred 所在内存页面是否可写?
  • 如何利用漏洞往 cred 所在地址写值?

利用方法回顾

图片15

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

  • 09 ~ 10 年的时候,由于没有对 mmap 的地址范围做任何限制,应用层可以映射0页面,null pointer deref 漏洞在当时也是可以做利用的,后面针对这种漏洞推出了mmap_min_addr 限制,目前 null pointer deref 漏洞一般只能造成
  • 11 ~ 13 年的时候,常用的提权套路是从/proc/kallsyms 搜索符号 commit_creds 和 prepare_kernel_cred 的地址,然后在用户态通过这两个符号构造一个提权函数(如下),

图片16

可以看到,这个阶段的用户态 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)/)

图片17

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

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

  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, 先在用户态构造一个伪结构,如下

图片18

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

图片19

下面是核心的函数

图片20

参数 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 片段来实现伪函数,就可以被内核直接调用。

图片21

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

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

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

1

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

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

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

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

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

图片22

搜索到本进程的 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 这个结构

图片23

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

图片24

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 类似,

图片25

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

图片26

图片27

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

图35

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

参考 
android_run_root_shell

xairy

New Reliable Android Kernel Root Exploitation Techniques

本文原创,作者:congtou,其版权均为华盟网所有。如需转载,请注明出处:https://www.77169.net/html/166826.html

发表评论