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

2017-08-28 263447人围观 漏洞

*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。

目录

前言

背景知识

漏洞成因

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个驱动

3个驱动

背景知识

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

背景知识

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

4.png

哈希运算服务的核心结构体是 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 进行处理。

CVE-2016-6738漏洞成因

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

CVE-2016-6738漏洞成因

在函数 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 的值是从用户态地址拷贝过来的,可以被控制,但是,还没完

CVE-2016-6738漏洞成因

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

CVE-2016-6738漏洞成因

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

CVE-2016-6738漏洞成因

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

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

CVE-2016-6738漏洞补丁

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

下面看第二个漏洞

CVE-2016-3935漏洞成因

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

CVE-2016-3935漏洞成因

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

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

CVE-2016-3935漏洞成因

注意到:

32 bit 系统,  UINT_MAX= ULONG_MAX

64 bit 系统,  UINT_MAX = ULONG_MAX

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

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

CVE-2016-3935漏洞成因

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

CVE-2016-3935漏洞成因

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

CVE-2016-3935漏洞补丁

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

下面进入漏洞利用分析

漏洞利用

androidkernel 漏洞利用基础

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

什么是提权

什么是提权

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

222222.jpg

所谓提权,就是修改进程的 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)/)

基本的 linux kernel ROP  技巧

可以看到这两篇文章的方法是搜索一些 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-default 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, 先在用户态构造一个伪结构,如下

在用户态构造一个伪结构

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

CVE-2016-6738漏洞利用

下面是核心的函数

CVE-2016-6738漏洞利用

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

CVE-2016-6738漏洞利用

比如,我们找到一段 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, 这就是一个内核任意地址写任意值函数。

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

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

CVE-2016-6738漏洞利用

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

CVE-2016-3935漏洞利用

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

CVE-2016-3935漏洞利用

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

CVE-2016-3935漏洞利用

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

CVE-2016-3935漏洞利用 

CVE-2016-3935漏洞利用

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

CVE-2016-3935漏洞利用

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

参考 

android_run_root_shell

xairy

NewReliable Android Kernel Root Exploitation Techniques

 *本文作者:360冰刃团队;转载请注明来自 FreeBuf.COM

相关推荐
取消
Loading...

特别推荐

推荐关注

活动预告

填写个人信息

姓名
电话
邮箱
公司
行业
职位
css.php