CVE-2015-1805 漏洞复现 - iovyroot
影响版本: < linux kernel 3.16
CVE信息:https://nvd.nist.gov/vuln/detail/CVE-2015-1805
补丁信息:
https://github.com/raymanfx/android-cve-checker/blob/master/patches/3.10/CVE-2015-1805.patch
这个漏洞漏洞点代码量不多,但poc由于涉及条件竞争故而较复杂,所以我们先看漏洞分析。
漏洞分析
漏洞的本质是,pipe_iov_copy_to_user()/pipe_iov_copy_from_user() 函数在拷贝中途出现错误返回时,未考虑如何将已拷贝的数据长度同步给上级函数,而上级函数也不考虑已拷贝的长度,将再次以相同的长度参数调用该函数,那么就会引发越界读写的问题。
漏洞出现在管道的内核实现代码中,管道读写处理函数 pipe_read()
和 pipe_write()
均存在该漏洞,但我们以 pipe_read()
为例进行代码分析。
pipe_read()
中会调用 pipe_iov_copy_to_user()
函数进行一段数据的拷贝:iov是用户态传入的iovec数组,from是管道中待读取数据的起始地址,len是当前管道段(pipe_buffer)可读取数据的长度,atomic为1表示iov中的地址已通过检查可走快速拷贝分支(__copy_to_user_inatomic()
)。
1 | static int pipe_iov_copy_to_user(struct iovec *iov, const void *from, unsigned long len, int atomic) |
拷贝成功时没什么问题,拷贝失败时父级函数会如何处理呢?我们来看一下 pipe_read() 函数:
1 | static ssize_t |
我们发现再次调用 pipe_iov_copy_to_user()
函数进行拷贝时,长度chars没有改变。也就是说即使第一次已经完成了一部分拷贝工作,但第二次的拷贝长度依然是chars。那么毫无疑问,会越界写iov中的地址(iov[idx].iov_base
),写的内容就是管道中的数据。
因此,如果某个 iov[a].iov_base
在 pipe_iov_copy_to_user()
函数中突然变得不可访问,然后再进入redo流程后又变得可访问,那么就会触发漏洞,越界往 iov[a+1].iov_base
或 iov[a+n].iov_base
中写入内容。
但此时在 pipe_iov_copy_to_user() 函数中走的是 copy_to_user() 分支,往任意用户态地址写的能力太弱了。那么,能否让 pipe_iov_copy_to_user() 再次进入快速拷贝分支呢?
考虑这么一种情况:假设用户态需要拷贝的数据长度,大于管道中的数据长度(total_len > chars
)。见如下代码分析
1 | static ssize_t |
以上流程说明,如果 total_len > chars
,在拷贝完chars长度后,有机会再次置atomic为1,走快速拷贝分支(__copy_to_user_inatomic()
)。也就是说,如果后续的 iov[].iov_base
中有内核地址,那么就可以完成任意内核地址写。
漏洞验证
环境搭建
下载goldfish源码,回滚到未patch版本,然后编译生成内核文件。
1 | git clone https://android.googlesource.com/kernel/goldfish.git -b android-goldfish-3.10 |
如何加载启动该内核文件可参考我之前的文章:android 模拟器 goldfish 环境搭建
poc验证
本着由简入难的想法,先构造一个任意用户态地址写,再构造一个任意内核地址写。
往任意用户态地址写
这个漏洞的触发并不简单,有以下几个关键点需要保证:
- 在 iov_fault_in_pages_write() 函数检查时,所有
iov[idx].iov_base
地址需要可写入 - 第一次执行 pipe_iov_copy_to_user() 函数时,拷贝一段数据后,某个iov_base突然不可写,于是流程进入redo
- 第二次执行 pipe_iov_copy_to_user() 函数时,上文这个iov_base又变成可写状态,于是完成页面内容的拷贝(大小为PAGE_SIZE/chars)。由于total_len大于chars,于是会走向continue分支
- 第三次执行 pipe_iov_copy_to_user() 函数时,基于第一次拷贝失败时已拷贝字节数(copied_len),这里会越界获取
iov[].iov_base
,往该地址写入一段管道中的数据(copied_len)
1 | for (;;) { // 一次for循环可以看作对一个 pipe_buffer(<4096 bytes)的操作 |
poc使用了三个线程来竞争触发漏洞:
- 控制某块空间时而可访问,时而不可访问(mmap,munmap)
- 往管道中写入内容(使用write,一次写一个PAGE_SIZE)
- 从管道中读取内容(使用readv,传入多个iovec结构体)
本小节的完整poc如下:
1 |
|
往任意内核地址写
上一小节是一个验证该漏洞实现任意地址写功能的poc,当前仅能往用户态写。因为使用 readv 传递 pipe_iovec 时,其中的 iov_base 会被检查,要求必须是用户态可访问的地址。
所以,我们需要将目标地址(内核地址)放入 pipe_iovec 在内核中申请的堆块的后面,然后利用越界访问,将后一个堆块中的内核地址作为 iov_base,完成任意地址写。如下构造
1 | --------heap1----------- |
让需要的数据刚好放置在目标堆块的后面,这当然需要使用堆喷技术来布置堆空间。需要考虑几个问题:
选择合适的堆块大小
首先选择目标堆块的大小,通过 cat /proc/slabinfo 发现 kmalloc-8192 的使用率是最低的,为了减少干扰,优先选择该大小。
使用何种方式布置堆空间
经过测试,有两种方式将 overun_iovec 布置到堆空间:
第一种,通过堆喷把 overflowcheck 等内容布置到堆头,这样只要heap1和heap2是相邻的两个 kmalloc-8192 就能完成任意地址写。如下布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20--------heap1-----------
pipe_iovec[0].iov_base = r_buf;
pipe_iovec[0].iov_len = sizeof(long) * 2;
pipe_iovec[1].iov_base = MMAP_ADDR;
pipe_iovec[1].iov_len = ((PAGE_SIZE * 2) - pipe_iovec[0].iov_len);
pipe_iovec[2].iov_base = 0;
pipe_iovec[2].iov_len = 0;
[...]
pipe_iovec[511].iov_base = 0;
pipe_iovec[511].iov_len = 0;
--------heap2-----------
overun_iovec[0].iov_base = (void*)&overflowcheck;
overun_iovec[0].iov_base = sizeof(overflowcheck);
overun_iovec[1].iov_base = kernel_addr;
overun_iovec[1].iov_base = sizeof(unsigned long);
overun_iovec[2].iov_base = 0;
overun_iovec[2].iov_len = 0;
[...]
overun_iovec[511].iov_base = 0;
overun_iovec[511].iov_len = 0;第二种,通过堆喷把 overflowcheck 等内容布置到堆中间靠后的部分,然后释放堆块(heap2)。这样只要 readv(heap1) 申请到heap2的同一个 kmalloc-8192 堆块,就可完成任意地址写。如下布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15--------heap1/heap2-----------
pipe_iovec[0].iov_base = r_buf;
pipe_iovec[0].iov_len = sizeof(long) * 2;
pipe_iovec[1].iov_base = MMAP_ADDR;
pipe_iovec[1].iov_len = ((PAGE_SIZE * 2) - pipe_iovec[0].iov_len);
pipe_iovec[2].iov_base = 0;
pipe_iovec[2].iov_len = 0;
[...]
pipe_iovec[257].iov_base = 0;
pipe_iovec[257].iov_len = 0;
pipe_iovec[258].iov_base = 0;
pipe_iovec[258].iov_len = 0;
[...]
pipe_iovec[511].iov_base = 0;
pipe_iovec[511].iov_len = 0;要求readv时指定的 iovec 大小为256+1,使其分配到kmalloc-8192。堆喷时指定的大小为512,使其分配的也是kmalloc-8192。
本小节的完整poc如下:
1 |
|
漏洞利用
准备用两种方法来做:
- 写
ptmx_fop->check_flags
,控制流劫持后泄露内核sp信息,然后利用任意地址写写addr limit 和selinux,通过pipe任意读写内核,提权 - 用KSMA的方法,改页表,将内核镜像重新映射成可写的,然后改
setresuid()
函数,使普通用户可以将自己变成root
ptmx_fop→check_flags
如何找到
ptmx_fops->check_flags
的地址?(check_flags第一个参数用户态可控)通过调用链
sys_fcntl->do_fcntl->setfl
,结合 bzImage.elf 和源码来确认check_flags 在 ptmx_fops 中的偏移。本次实验环境中,ptmx_fops 的地址是 0xFFFFFFC0006F0F28,check_flags 的偏移是 0xA8,所以
ptmx_fops->check_flags
的地址为 0xFFFFFFC0006F0FD0。1
2
3
4
5
6
7
8
9
10v27 = *(__int64 (__fastcall **)(_QWORD))(v26 + 0xA8);
// ptmx_fops->check_flags
if ( !v27 )
{
if ( (((unsigned int)a3 ^ *(_DWORD *)(v9 + 64)) & 0x2000) == 0 )
goto LABEL_92;
goto LABEL_90;
}
v16 = v27((unsigned int)a3); // a3是用户态传入参数
if ( v16 )找一条泄露内核sp的gadget
如下四条gadget均能满足要求
1
2
3
4
5
6
70xffffffc00027ad14 : ldr x1, [x0, #0x20] ; add x0, x29, #0x50 ; blr x1
0xffffffc000198d50 : ldr x1, [x0, #8] ; cbz x1, #0xffffffc000198d60 ; add x0, x29, #0x10 ; blr x1
0xffffffc000198e20 : ldr x1, [x0, #8] ; cbz x1, #0xffffffc000198e30 ; add x0, x29, #0x30 ; blr x1
0xffffffc000211d20 : ldr x1, [x21, #0x18] ; add x0, x29, #0x88 ; blr x1 ;令 x1 为 check_flags 被调用后的返回地址即 0xFFFFFFC00015EC34,x0中存储当前进程的内核栈地址。由于 w0 非0,将会跳转到 0xFFFFFFC00015EA3C 处继续执行即返回用户态。用户态会得到w0的值,缺点是这是一个 int 类型的返回值,只有低8位。不过影响不大,栈地址的前几位是
0xffffffc0
,补上即可。1
2
3
4.kernel:FFFFFFC00015EC2C MOV W0, W22
.kernel:FFFFFFC00015EC30 BLR X1 # 这里x1是ptmx_fops->check_flags
.kernel:FFFFFFC00015EC34 CBNZ W0, loc_FFFFFFC00015EA3C
.kernel:FFFFFFC00015EC38 LDR W0, [X19,#0x40]
完整exp如下:
1 |
|
执行效果:
KSMA
如何找到一级页表对应的虚拟地址
bzImage恢复符号后,找到 init_mm.pgd,如下示例中
0xFFFFFFC00007D000
就是一级页表对应的虚拟地址1
2
3
4
5
6
7
8
9
10.kernel:FFFFFFC0006A5AB0 EXPORT init_mm
.kernel:FFFFFFC0006A5AB0 init_mm DCQ 0 ; DATA XREF: show_pte:loc_FFFFFFC0000930AC↑o
.kernel:FFFFFFC0006A5AB0 ; setup_mm_for_reboot+4↑o ...
.kernel:FFFFFFC0006A5AB8 DCQ 0, 0, 0
.kernel:FFFFFFC0006A5AD0 DCB 0, 0, 0, 0
.kernel:FFFFFFC0006A5AD4 dword_FFFFFFC0006A5AD4 DCD 0 ; DATA XREF: .kernel:FFFFFFC0005DA5C8↑o
.kernel:FFFFFFC0006A5AD8 ALIGN 0x40
.kernel:FFFFFFC0006A5B00 off_FFFFFFC0006A5B00 DCQ 0xFFFFFFC00007D000
.kernel:FFFFFFC0006A5B00 ; DATA XREF: show_pte:loc_FFFFFFC000093054↑r
.kernel:FFFFFFC0006A5B00 ; show_pte+34↑r ...
该部分完整exp
1 | // ksma.c |
执行效果:
参考文章
看明白漏洞点:some points on CVE-2015-1805
有个dosomder/iovyroot代码和ppt:Iovyroot (CVE-2015-1805) 分析
讲解dosomder/iovyroot代码:https://wenboshen.org/posts/2016-04-25-1805-cve
一个分析,在多个真机上复现:[原创][首发]CVE-2015-1805 安卓手机提权ROOT漏洞 分析
keenlab-mosec2016-PPT:https://github.com/retme7/My-Slides/blob/master/Keenlab-mosec2016.pdf
exp: