吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 17052|回复: 19
收起左侧

[漏洞分析] android内核漏洞cve-2014-3153分析笔记

  [复制链接]
inquisiter 发表于 2017-9-15 21:02
本帖最后由 inquisiter 于 2017-9-15 21:14 编辑

                                             Android内核提权cve-2014-3153研究笔记
   一、简介
   我这里把我自己的理解总结下,看别人的总是云山雾绕,不得要领。还是要有自己的思路。当然也希望自己写的通俗一些,那么又有一大批人能看懂了就。
文中图片修改了文尾链接处作者的图片,部分例子采用参考中所得。各位想做下实验的可以参考我上一篇的编译过程,也可以看我给出的链接。
  受影响的Linux内核系统可能被直接DOS,精心设计可以获取根权限。这个漏洞利用的核心就是,通过两个流程bug造成程序栈中变量没有清理,然后利用栈内存共用修改栈值,最终绕过地址读写限制实现提权。

  二、触发机制
  科普下锁的概念:锁就是由于多线程同时访问资源会造成资源更改混乱而增加的概念。简单说,有一个公共资源。一个人在用的时候,其他人就要等着。不能两个或多个人同时用。
   这个漏洞利用Futex(Fast UserspacemuTEX)(是一种锁机制)的不同唤醒方式,绕过了栈数据清理。从而控制了流程。
   如何绕过?
漏洞利用了futex_requeue,futex_lock_pi,futex_wait_requeue_pi三个函数存在的两个bug位于futex.c
git clone https://android.googlesource.com/kernel/goldfish.git -bgoldfish3.4
cd goldfish
git checkout e8c92d268b8b8feb550ca8d24a92c1c98ed65acekernel/futex.c
可以自行下载一下。
   2.1.relockBUG
  relock漏洞源于futex_lock_pi函数(由futex_lock_pi_atomic实现),futex_lock_pi(&uaddr)调用之后,调用地址uaddr被锁住,只有利用解锁futex_unlock_pi后,才能被其他线程利用。
futex_lock_pi_atomic又是由cmpxchg_futex_value_locked(&curval,uaddr, 0,newval)实现并尝试去锁住uaddr。它的实现的含义是如果uaddr中存储的值为0,那么就说明没有线程占用锁,成功的获取到了锁,并将当前线程的id写进去。(uaddr是用户空间的一个整形变量,被用于Futex系统架构中的futex互斥量。uaddr的值与其用户空间的地址都会被Futex用到。)
  但是问题来了,既然uaddr是用户变量,那我们就可以手动设置为0.这时候地址上的锁其实是释放了,但上锁后的堆栈里的内容没有被清理。而且没有唤醒阻塞在锁上的线程,修改pi_state等。
  这样就可以利用通过手动设置uaddr=0的方式使两个线程同时获得锁。这个叫relock。可以叫多重上锁。
   2.2 requeueBUG
  futex_wait_requeue_pi的功能是让调用线程阻塞在uaddr1上,然后等待futex_requeue的唤醒。唤醒过程将所有阻塞在uaddr1上的线程全部移动到uaddr2上去。

syscall(__NR_futex, &uaddr1,FUTEX_WAIT_REQUEUE_PI, 1, 0, &uaddr2, uaddr1); //在uaddr1上等待
syscall(__NR_futex, &uaddr1, FUTEX_CMP_REQUEUE_PI, 1, 0,&uaddr2, uaddr1);//尝试获取uaddr2上的锁,然后唤醒uaddr1上等待的线程。如果uaddr2锁获取失败,则将被唤醒线程添加到uaddr2的rt_waiter列表上,进入线程进入内核等待。啥时候进入内核等待,我们下面讲。

进入内核等待方式图

   这个时候如果我们再次调用
syscall(__NR_futex, &uaddr1, FUTEX_CMP_REQUEUE_PI, 1, 0,&uaddr2, uaddr1)
将失败而直接返回,并不会进入系统调用。
  而requeueBUG允许我们在以上两条语句执行之后,首先设置uaddr2=0,然后执行这样的语句:
syscall(__NR_futex, &uaddr2, FUTEX_CMP_REQUEUE_PI, 1, 0,&uaddr2, uaddr2);
这个语句中所有地址都变成了uaddr2,也就是说将等待在uaddr2上的线程重排到uaddr2上,这是不合逻辑的,但是Futex没有检查这样的调用,也就是说没有检查uaddr1 ==uaddr2的情况,从而造成了我们可以二次进入futex_requeue中进行唤醒操作。我们的线程进入内核等待后本来需要用内核唤醒的方式,现在被篡改成了普通的唤醒方式。致使一部分的栈没有被清空。就是栈上的rt_waiter依然被连在rt_mutex的waiterlist上。

   2.3漏洞触发
这里还要了解一下futex_requeue中唤醒futex_wait_requeue_pi线程的两种方式:
  1.futex_proxy_trylock_atomic调用尝试获取uaddr2上的锁,如果成功,则唤醒等待线程,函数返回,否则继续执行。注意,这一步没有进入内核互斥量中,如果成功,将不进入内核互斥量,而是直接返回到用户空间,从而减小内核互斥量的开销;
  2.rt_mutex_start_proxy_lock尝试获取uaddr2锁,如果成功,则唤醒等待线程,如果失败,则将线程阻塞到uaddr2的内核互斥量上,将rt_waiter加入rt_mutex的waiterlist。

   我们来总结下正常程序的执行状态。
futex_wait_requeue_pi(uaddr1,uaddr2)等待被唤醒,正常情况下我们唤醒的方式要么在内核唤醒,要么普通的唤醒。这个要看uaddr2的锁状态。
                           漏洞触发图

  但是我们这里利用uaddr2加锁使线程进入内核等待状态,然后relockBUG uaddr2=0,最后 requeueBUGfutex_wait_requeue_pi(uaddr2,uaddr2),使阻塞在内核等待的线程用普通方式唤醒。构造了程序的异常执行流。
   如何使一个线程按我们的方式执行如下图:
                          异常流程构造图

1.我们使用主线程1futex_lock_pi锁住uaddr2。
2、3.创建线程2,等待被唤醒futex_requeue(uaddr1,uaddr2),uaddr2被锁,所以进入内核等待,futex_wait_requeue_pi中的rt_waiter加入到rt_waiter的waiterlist上。
4.利用relockBUG,将uaddr2赋值为0,释放uaddr2上的锁.
5.利用requeue漏洞,调用futex_requeue(uaddr2,uaddr2),uaddr2没锁,触发的普通唤醒模式。
导致rt_waiter没有被清理。
而至于这个栈上的没有owner的rt_waiter被链接在rt_mutex上,如果线程2结束,内核清理环境的时候,会去尝试唤醒这个rt_waiter,结果就是造成内核崩溃。
  
二、提权过程
  上一节我们讲到了rt_waiter没有了owner,但是有什么用呢?
  这里我们会用一种机制来更改这个没有owner的rt_waiter的数据
   2.1栈内存共用问题
   #include  <stdio.h>

void A(int val)
{
   int local;
   local =val;
   printf("A locacladdr =0x%x\n",&local);
}

void B(int val2)
{
   int local;
   printf("B locacladdr =0x%x\n",&local);
   printf("B local =%d\n",local);
}

int main()
{
   A(6);
   B(2);
   return 0;
}
这里用GCC编译,不进行优化
gcc -m32 foo.c -o foo -g
./foo

A locacladdr = 0xffd119b8
B locacladdr = 0xffd119b8
B local = 6
            图栈

我们可以看到,这里A,B函数的局部变量的地址是一样的。有堆栈概念的人都知道,我们的每调用一个函数就会产生一个新的堆栈。但是上一个调用函数的栈中的数据如果没销毁,下一个函数构造的栈中就能利用。我们就可非法篡改数据。如上图的实例。
哈,有啥用,我们可以直接调用A函数修改B函数中的数据。
   2.2修改内核中的数据
  我们可以调用另一个结构相似的函数修改我们的rt_waiter结构数据。我们选取__sys_sendmmsg函数。
有其他选择么?有。有一个分析栈空间的脚本,checkstack.pl的脚本,断到futex_wait_requeue_pi上可以看到很多函数。这里选择可以完成rt_waiter所在栈深度修改的一个。
   这里我们要修改的是链表。
   rt_waiter结构
type = struct rt_mutex_waiter{
   struct plist_nodelist_entry;
   struct plist_nodepi_list_entry;
   struct task_struct*task;
   struct rt_mutex *lock
}
plist结构
struct plist_node{
int prio;
struct list_head prio_list;//有个next和prev的指针
struct list_head node_lsit;//有个next和prev的指针
}

endmmsg的函数声明及主要结构如下:
int sendmmsg(int sockfd, struct mmsghdr *msgvec, unsigned intvlen,
unsigned int flags);

struct mmsghdr {
struct msghdr msg_hdr;
unsigned int msg_len;
};

struct msghdr {
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
size_t msg_iovlen;
void *msg_control;
size_t msg_controllen;
int msg_flags;
};

struct iovec {
void *iov_base;
__kernel_size_t iov_len;
};
其中位置重叠部分如图

看下我们锁机制中链表的形式是这样的

我们利用内核锁的唤醒在内核中插入链表,这个插入的位置可以根据prio参数来选择,因为程序会按顺序排,我们只要适当的修改prio参数即可。虽然可以更改内核中的值了,但这个地址内核地址不可控。怎么利用?
这里分两步:
   (一)、内核任意地址写入值(写入的值不可控)
   我们在用户态地址上利用mmap构建一个rt_waiter的结构fake_node。如图
  
在内核中把rt_waiter指向用户态的fake_node.这时候我们我们在用户态的fake_node就可以随意指定内核地址.


假设我们要修改内核地址A的值,我们就把fake_node的node.prev指向(A-offset),这里offset=sizeof(prio)+sizeof(list_head);我们把A-offset当成了一个plist结构。其实没人知道是不是。这个时候再利用漏洞在A节点和fake_node之间插入一个内核节点,那么A节点的node.prev就指向了新节点的地址,虽然这个地址我们暂时不可控,但我们实现了任意内核地址A写入数据。
   (二).实现线程任意地址可读写
    我们这里需要找到特定线程的thread_info,方法很简单线程任意栈地址与上0xffffe000。这个位置是固定的。thread_info的地址,再定义正确的thread_info的结构,就可以得到addr_limit的地址了。addr_limit是限制我们访问空间地址位置的,限制在哪,我们就只能读小于它的地址,只要我们把它改成0xffffffff。我们就可以实现,任意地址的读写。
目测不容易。我们现在只能实现任意地址写,但地址上写了啥,还不知道。
   没关系,我们这里创建两个线程A,B.线程B循环创建。


A实现循环读取addr_limit的值,显然开始的时候读不到,就一直读着。线程B利用任意地址写值得方式把自己的不可控的rt_wait地址写到A的addr_limit中,由于内核中不同线程栈位置不同。我们的线程B不断的创建,总有机会得到一个比A线程高的地址。只要我们把这个高地址写到A线程的addr_limit中,那么线程A的addr_limit位置就能任意改写了。(不同线程使用不同位置的栈)
简单说就是先利用内核漏洞把addr_limit值的改到比本线程高的值,用户态可以改写了,然后直接在用户态addr_limit=0xffffffff.这下任意内核地址就都可以读写了。
  2.3、内核提权
  thread_info包含了线程的主要信息,当然也就包括了线程的task_struct。而task_struct结构体包含了该线程的所有信息。这其中就包括权限方面的重要证书信息cred,该结构体是线程权限的管理者,标识了当前线程的权限。我们只要如下更改:
credbuf.uid = 0;
credbuf.gid = 0;
credbuf.suid = 0;
credbuf.sgid = 0;
credbuf.euid = 0;
credbuf.egid = 0;
credbuf.fsuid = 0;
credbuf.fsgid = 0;
credbuf.cap_inheritable.cap[0] = 0xffffffff;
credbuf.cap_inheritable.cap[1] = 0xffffffff;
credbuf.cap_permitted.cap[0] = 0xffffffff;
credbuf.cap_permitted.cap[1] = 0xffffffff;
credbuf.cap_effective.cap[0] = 0xffffffff;
credbuf.cap_effective.cap[1] = 0xffffffff;
credbuf.cap_bset.cap[0] = 0xffffffff;
credbuf.cap_bset.cap[1] = 0xffffffff;
securitybuf.osid = 1;
securitybuf.sid = 1;
taskbuf.pid = 1;

三、总结
  本文利用自己的思路将CVE-2014-3153漏洞利用过程整理了下,整个过程所获很多,把自己很多连不起来的知识融汇了一下。下面总结下:
  1.分析得出两个BUG,利用bug实现了,实现了内核栈上残留有效数据。
  2.利用栈内存共用问题,实现了内核栈数据的更改。(修改的值不可控)
  3.通过多线程配合实现了内核数据任意读写从而提权。



参考:
1.http://blog.topsec.com.cn/ad_lab/cve2014-3153/
2.《漏洞战争》CVE-2014-3153Android内核Futex提取漏洞

免费评分

参与人数 15威望 +2 吾爱币 +26 热心值 +15 收起 理由
peter_king + 1 谢谢@Thanks!
开心果1122 + 1 + 1 小白看不懂啊..
淡じ☆vé嗼 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ddd23365 + 1 + 1 谢谢@Thanks!
ff7722422 + 1 + 1 谢谢@Thanks!
gunxsword + 1 + 1 看不懂,就是感觉很历害!
连晋 + 2 + 1 谢谢@Thanks!
52破解☆ + 1 + 1 我很赞同!
gray小灰 + 1 + 1 热心回复!
吾2破解 + 1 + 1 热心回复!
cc78947 + 2 + 1 小白表示看不懂,求指导,学习LINUX
A-_虚伪_! + 2 + 1 楼下都给你加分了,我也&#10133;
Hmily + 2 + 10 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
慵懒丶L先森 + 1 + 1 感谢经验分享,BTW,安卓的内核漏洞利用的条件都很苛刻
amoxuan + 1 + 1 --------

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| inquisiter 发表于 2017-9-16 09:09
chenjingyes 发表于 2017-9-16 00:13
写的挺好的  谢谢楼主分享  就是文章中一些图片太模糊了

以后会注意
naic 发表于 2017-10-17 16:55
xiawan 发表于 2017-9-17 12:19
看了LZ的帖子,我只想说一句很好很强大!

确实如此《楼主厉害了》
chenjingyes 发表于 2017-9-16 00:13
写的挺好的  谢谢楼主分享  就是文章中一些图片太模糊了
mythboy 发表于 2017-9-16 02:00
不错 学习了~
努力的小七 发表于 2017-9-16 09:43
厉害了,虽然不太懂!
M。 发表于 2017-9-16 11:46
学习大神   虽然不懂         
金樱子 发表于 2017-9-16 15:07
思路挺清晰的,学习学习
demon_lin 发表于 2017-9-16 17:23
感谢分享,要是有pdf下载来看就好了
xiawan 发表于 2017-9-17 12:19

看了LZ的帖子,我只想说一句很好很强大!
q110 发表于 2017-9-17 19:09
看不懂什么漏洞什么的.....
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-15 13:46

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表