CVE-2020-7460:FreeBSD内核特权提升漏洞
2020-09-18 10:34:02 Author: www.4hou.com(查看原文) 阅读量:255 收藏

0x00 概述

2020年8月,FreeBSD发布更新,修复了条件竞争(TOCTOU)漏洞,该漏洞可能会被不具有特权的用户空间恶意程序利用,从而实现特权提升。一位名为m00nbsd的研究人员将该漏洞报告给ZDI,并提供了该漏洞的详细描述和漏洞验证代码详情,该漏洞的编号为ZDI-20-949/CVE-2020-7460。

攻击者利用32位sendmsg()系统调用中存在的TOCTOU漏洞,可以以无特权的用户在FreeBSD上执行内核代码。该漏洞影响自2014年以来的所有FreeBSD内核,已经为该漏洞分配了CVE-2020-7460编号。在进行漏洞的详细分析之前,我们首先可以通过演示视频快速了解该漏洞的实际利用方式。

演示视频:https://youtu.be/LBFfX90Acw8

0x01 漏洞详情

我们直接跳到漏洞所在的位置,即freebsd32_copyin_control()函数。该函数由两个循环组成,如下所示:

//
// ----------------------- FIRST LOOP -----------------------
//
while (idx < buflen) {
    error = copyin(buf + idx, &msglen, sizeof(msglen));
    if (error)
        return (error);
    if (msglen < sizeof(struct cmsghdr))
        return (EINVAL);
    msglen = FREEBSD32_ALIGN(msglen);
    if (idx + msglen > buflen)
        return (EINVAL);
    idx += msglen;
    msglen += CMSG_ALIGN(sizeof(struct cmsghdr)) -
        FREEBSD32_ALIGN(sizeof(struct cmsghdr));
    len += CMSG_ALIGN(msglen);
}
 
if (len > MCLBYTES)
    return (EINVAL);
 
//
// ALLOCATE KERNEL MEMORY
//
m = m_get(M_WAITOK, MT_CONTROL);
if (len > MLEN)
    MCLGET(m, M_WAITOK);
m->m_len = len;
 
//
// ----------------------- SECOND LOOP -----------------------
//
md = mtod(m, void *);
while (buflen > 0) {
    error = copyin(buf, md, sizeof(struct cmsghdr));
    if (error)
        break;
    msglen = *(u_int *)md;
    msglen = FREEBSD32_ALIGN(msglen);
 
    /* Modify the message length to account for alignment. */
    *(u_int *)md = msglen + CMSG_ALIGN(sizeof(struct cmsghdr)) -
        FREEBSD32_ALIGN(sizeof(struct cmsghdr));
 
    md = (char *)md + CMSG_ALIGN(sizeof(struct cmsghdr));
    buf += FREEBSD32_ALIGN(sizeof(struct cmsghdr));
    buflen -= FREEBSD32_ALIGN(sizeof(struct cmsghdr));
 
    msglen -= FREEBSD32_ALIGN(sizeof(struct cmsghdr));
    if (msglen > 0) {
        error = copyin(buf, md, msglen); // <<-------- OVERFLOW
        if (error)
            break;
        md = (char *)md + CMSG_ALIGN(msglen);
        buf += msglen;
        buflen -= msglen;
    }
}

我们来看看这里发生了什么。第一个循环从用户区域获取数据。这个数据是一组连续的cmsghdr结构:

struct cmsghdr {
    socklen_t cmsg_len;       /* data byte count, including hdr */
    int       cmsg_level;     /* originating protocol */
    int       cmsg_type;      /* protocol-specific type */
    /* u_char cmsg_data[]; */
};

图1.png

第一个循环执行长度检查,并确保缓冲区的总长度(len字节)适合随后分配的内核缓冲区,该缓冲区的大小为MLEN字节。

一旦通过了长度检查,就会分配所述的内核缓冲区,第二个循环将用户空间数据复制到缓冲区中。内核执行了一些处理,将结构从32位转换为64位,但在这里是无关的,因此我们将忽略它。

在第二个循环结束之后,内核缓冲区将以以下格式结束:

图2.png

但是,在这里存在一个TOCTOU漏洞:在第一个循环和第二个循环之间,用户空间可能修改了其cmsghdr结构的cmsg_len字段,并且最终的总长度现在超过了MLEN。

假设在第一个循环之后,用户空间增加了最后一个cmsg_len字段的值:

图3.png

随后就发生内核堆溢出。

0x02 触发漏洞

我们可以通过32位sendmsg()系统调用触发这个堆溢出,其格式如下:

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
 
struct msghdr {
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */
    struct iovec *msg_iov;        /* scatter/gather array */
    int           msg_iovlen;     /* # elements in msg_iov */
    void         *msg_control;    /* ancillary data, see below */
    socklen_t     msg_controllen; /* ancillary data buffer len */
    int           msg_flags;      /* flags on received message */
};

msg_control就是用户内存缓冲区的开始位置,也是连续的cmsghdr结构所在的位置。

要触发此漏洞,我们需要:

1、创建连续有效的cmsghdr结构块;

2、在我们的代码块中,循环生成一个调用sendmsg()的线程。

3、派生出另一个线程,该线程增加最后一个cmsg_len字段的值,然后循环返回到正确的值。

完成后,我们只需要等待几秒钟,两个线程就可以运行了。很快,就会发生内核崩溃(Kernel Panic)。我们已经有了一个比较好的开始。

0x03 改进原语

我们希望知道何时触发堆溢出,以便让我们了解什么时候停止两个竞争线程。也就是说,我们不想让线程保持太长时间的运行,这样会以一种极端的方式实现内核内存溢出,可能会导致内核崩溃(Panic)。与此不同,我们希望在第一次成功溢出后就立即停止,以提高漏洞利用的可靠性,并限制对内存的破坏。

为此,我们可以利用以下技巧:

图4.png

这个思路包括利用copyin()的行为,该函数用于将用户数据复制到内核内存中。为了防止用户区域给出未映射的页面(或一般意义上的垃圾),copyin()会适当处理内核中的页面错误,并在复制过程中遇到未映射的页面时安全地返回EFAULT。EFAULT依次由系统调用返回。

在堆溢出的情况下,我们可以将未映射的页面恰好放置在我们希望复制结束的位置。然后,copyin()将复制所有内容,直到这个未映射的页面,在该页面上将出现错误,然后返回EFAULT,这将导致sendmsg()也返回EFAULT。

因此,如果sendmsg()返回EFAULT,则未映射的页面被命中,此时溢出就已经被触发。

我们现在可以选择写入的内容、写入的大小,并且能够确切知道溢出触发的时间。这样一来,原本的漏洞利用原语就得到了改进。

0x04 Mbufs和FreeBSD中的堆布局

我们溢出的内核缓冲区是mbuf。Mbuf是一种特殊的结构,使用FreeBSD的常规区域分配器分配。这里不需要知道太多细节,只需要知道mbuf在大页面上一个接一个的定位。但由于堆溢出的存在,我们很有可能覆盖内存中的下一个mbuf。

从漏洞利用的角度来看,mbuf结构有一个非常有意思的字段。在这里,我们将使用m->m_ext.ext_free field字段,它是指向函数的指针,并且具有以下原型:

void (*ext_free)(struct mbuf *the_current_mbuf_being_freed);

当内核希望释放mbuf时,它将检查mbuf中是否包含某些标志。如果这个检查成功,内核将调用mbuf的ext_free函数,并期望该函数释放mbuf。

我们将在漏洞利用中使用它。

0x05 控制mbufs的释放

当前的事务状态是堆溢出,让我们可以覆盖内存中的下一个mbuf,因此,我们可以修补下一个mbuf的ext_free字段。

那么,究竟如何让ext_free被调用呢?一旦检测到堆溢出成功,就必须迅速触发内存中下一个mbuf的释放。

这个过程可以通过在本地启动的简单UDP服务器/客户端配对来实现:

1、客户端使用常规的sendto()将数据包发送到服务器。这会导致mbuf在内核中分配。可以将其视为是PushMbuf()原语,该原语基本上分配mbuf。

2、服务器使用常规的recvfrom()接收这些数据包。这将导致释放已经分配的mbuf,并调用ext_free。这可以看作是释放mbuf的PopMbuf()原语。

现在,让我们看看如何使用这些原语:

1、使用PushMbuf()将大量mbuf PUSH到内核中。这样一来,就填满了堆。

2、使用PopMbuf() POP出刚刚PUSH的mbuf的50%,这样一来,会在分配映射中产生漏洞。

3、触发堆溢出,并覆盖紧随其后的一些mbuf的ext_free。

4、一旦我们得知触发了堆溢出,就可以使用PopMbuf() POP出剩下50%的mbuf。

5、如果幸运的话,这样就会释放我们刚刚重写了ext_free的mubf。因此,ext_free被调用,并跳转到我们控制的地址。如果这样无效,那么就只能返回步骤1,然后重试,直到成功为止。

这个过程通常在不到1秒的时间内成功完成,并且内核跳转到我们完全控制的ext_free地址。在这里,COP/JOP/ROP链开始。

0x06 利用链小工具

我们刚刚设法让内核跳转到我们控制的地址中。寄存器的状态非常简单:

%rdi = 释放的mbuf的地址

在这里,%rdi指向我们重写的mbuf。如果我们基于特定偏移量(%rdi)利用COP/JOP链,那么实际上就是位于我们能控制内容的缓冲区中,我们之前已经设法溢出了这个缓冲区。

图5.png

但遗憾的是,很少有可以链接在一起的小工具,而且由于mbuf内容已经被需要保证有效的字段消耗了一部分,因此我们的空间非常有限。

可以看到:

1、由于缺少比较理想的小工具,我们不得不使用一些其他的COP/JOP小工具。

2、我们很快就转向了ROP,因为ROP更容易,而且我们不能将COP/JOP维持太长时间。

3、考虑到空间不足的问题,ROP必须迅速退出内核,并跳转到用户空间Shellcode,这样我们就可以不受约束地获得完整的执行。

考虑到上述情况,我们还是选择使用利用链。在这里,我最终得到了由13个小工具组成的利用链,它们可以执行以下操作:

1、保存寄存器上下文,以便随后正确地还原。

2、将用户空间页表标记为可执行。这是FreeBSD对Meltdown漏洞缓解措施中添加的一个约束措施。执行内核时,用户页表有一个“不执行”(NX)位,因此内核无法跳转到任何用户地址。所以,利用链必须修补页表,设置NX,鉴于FreeBSD不会随机化其PML4 Slot,所以这并不是很复杂。

3、在CPU上禁用SMEP和SMAP。

4、最后,跳转到我们构建的用户级别Shellcode。

这并不是一个直接的漏洞利用方式,但它确实有效。

0x07 获取root权限

我们终于在内核模式下执行了Shellcode。现在,我们可以任意进行操作了。

为了使这个过程尽可能简单,我修补了线程的UID字段,将其UID设置为0。然后,我继续恢复最初保存为链的一部分的内核状态,然后内核继续执行,好像什么都没发生一样。最后,我们就成功调用了setuid(0),这意味着已经获取了root权限。

当然,要实现这一点,在没有实际内核代码执行程序的情况下,直接将UID作为任意位置/任意内容写入的一部分进行修补也可以,这样就不需要实际的内核代码执行,但是我还是希望尝试能否实现代码执行。

0x08 完整步骤

我们回顾一下:

1、在本地创建UDP服务器/客户端配对。

2、创建用户空间Shellcode。

3、使用UDP客户端,我们分配了许多mbuf,并使用服务器在内核堆分配映射中创建了漏洞。

4、我们将两个线程相互竞争,以触发sendmsg()中的堆溢出,这样一来就可以覆盖mbuf。在溢出的mbuf中,我们编写了一个新的ext_free指针和COP/JOP/ROP链的数据。

5、我们检测到成功溢出,并使用UDP服务器释放分配的mbuf,这将导致ext_free被调用并启动链。

6、利用链执行,禁用内存/CPU保护,并跳转到我们的用户空间Shellcode。

7、成功实现漏洞利用。

0x09 漏洞利用

在这里提供了概念验证代码,可以从无特权的用户实现root shell。我们观察到该概念验证的可靠性为90%。

https://github.com/thezdi/PoC/tree/master/CVE-2020-7460

0x0A 总结

根据FreeBSD提供的信息,i386以及其他32位平台不受此漏洞威胁。对于易受攻击的系统,建议尽快更新到0-day披露日期之后发布的FreeBSD稳定版本或发行版本,然后重新启动。通过这种方式,理论上可以实现漏洞修复。FreeBSD团队仅用两个星期就构建和发布了补丁,效率相对较高。

本文翻译自:https://www.thezdi.com/blog/2020/9/1/cve-2020-7460-freebsd-kernel-privilege-escalation如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/MolQ
如有侵权请联系:admin#unsafe.sh