【kernel exploit】CVE-2019-15666 xfrm UAF 8字节写NULL提权分析

文章首发于安全客:CVE-2019-15666 xfrm UAF 8字节写NULL提权分析

CVE-2019-15666是个被低估的漏洞,NVD给的评分只有4.4分。但是2020年5月份,据外媒报道,英国、德国、瑞士和西班牙等国超级计算机中心纷纷报告被加密货币恶意软件感染,导致多个高性能计算集群关闭。根据英国网络安全公司Cado Secutiry调查分析,黑客很有可能是通过利用非法得来的SSH凭证获得了超级计算机集群的权限,然后利用CVE-2019-15666漏洞进行了提权,然后部署了恶意挖矿程序。这次受影响的超级计算机有不少都是用于新冠肺炎研究的,因此这个时候尝试利用这些超级计算机来挖矿真的是非常不道德的。

影响版本:Linux v5.0.19 以前。v5.0.19已修补,v5.0.18未修补。 4.4分。

测试版本:Ubuntu 18.04(4.15.0-20-generic #21) exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项CONFIG_USER_NS=y CONFIG_SLAB=y 所有含NETLINKXFRM的配置全部选上。

General setup ---> Choose SLAB allocator (SLUB (Unqueued Allocator)) ---> SLAB

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

CONFIG_USER_NS=y
CONFIG_USERFAULTFD=y
CONFIG_DEBUG_INFO=y
// debug on running kernel with gdb
CONFIG_GDB_SCRIPTS=y
CONFIG_FRAME_POINTER=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KDB_KEYBOARD=y

漏洞描述net/xfrm/xfrm_user.c中的 verify_newpolicy_info() 错误处理了dir验证,导致__xfrm_policy_unlink()中出现越界访问。dir的范围本来是0~2,但从index计算dir时错误(policy2),导致dir超过3,后续将所有policy重新加入bydst list时漏掉该policy2,释放bydst后但该policy2上的pprev指针仍指向释放后的policy1,将该policy2进行unlink时会导致空闲块8字节写null*policy1->next = 0),见[12]

如果不构造policy1,直接对policy2进行unlink,只会导致越界减1,见[8]

补丁:patch 这里检查过后,后续还会通过p->index来计算p->dirp->dir = xfrm_policy_id2dir(p->index) -> p->dir = p->index & 7),p->dir前后不一致,导致未正确处理。


diff --git a/net/xfrm/xfrm_user.c b/net/xfrm/xfrm_user.c
index a131f9ff979e1..8d4d52fd457b2 100644
--- a/net/xfrm/xfrm_user.c
+++ b/net/xfrm/xfrm_user.c
@@ -1424,7 +1424,7 @@ static int verify_newpolicy_info(struct xfrm_userpolicy_info *p)
    ret = verify_policy_dir(p->dir);
    if (ret)
        return ret;
-   if (p->index && ((p->index & XFRM_POLICY_MAX) != p->dir))       // p->index=4, p->dir=0即可绕过该检查。 
+   if (p->index && (xfrm_policy_id2dir(p->index) != p->dir))
        return -EINVAL;
 
    return 0;

保护机制:开启KASLR/SMEP/SMAP。

利用总结:漏洞原本是个越界减1,可以利用别的路径来构造UAF,可以往空闲块上的8字节写null。漏洞对象xfrm_policy位于kmalloc-1024,cred结构位于kmalloc-192。首先利用setxattr+userfaultfdpolicy0周围都喷射kmalloc-1024堆块,释放policy0后同时释放喷射块,促使该slab释放后被c子进程的red复用,然后触发UAF 空闲块8字节NULL写来修改cred中的gid/suid,再将当前用户添加到sudoers,即可提权。


1. 漏洞分析

1.1 越界访问漏洞

创建xfrm_policy:用户采用特定的index和timer set来创建policy,参数是XFRM_MSG_NEWSA

漏洞链:xfrm_add_policy() -> verify_newpolicy_info()

xfrm_policy创建链:xfrm_add_policy() -> xfrm_policy_construct() -> xfrm_policy_alloc()

static int xfrm_add_policy(struct sk_buff *skb, struct nlmsghdr *nlh,
        struct nlattr **attrs)
{
    struct net *net = sock_net(skb->sk);
    struct xfrm_userpolicy_info *p = nlmsg_data(nlh);
    struct xfrm_policy *xp;
    struct km_event c;
    int err;
    int excl;

    err = verify_newpolicy_info(p);                             // [1] 检查用户参数—xfrm_userpolicy_info结构
    if (err)
        return err;
    err = verify_sec_ctx_len(attrs);
    if (err)
        return err;

    xp = xfrm_policy_construct(net, p, attrs, &err);
    if (!xp)
        return err;

    /* shouldn't excl be based on nlh flags??
     * Aha! this is anti-netlink really i.e  more pfkey derived
     * in netlink excl is a flag and you wouldnt need
     * a type XFRM_MSG_UPDPOLICY - JHS */
    excl = nlh->nlmsg_type == XFRM_MSG_NEWPOLICY;
    err = xfrm_policy_insert(p->dir, xp, excl);                 // [2] 检查通过则将新的policy对象插入到全局policy list中
    xfrm_audit_policy_add(xp, err ? 0 : 1, true);
    ... ...
    return 0;
}
// [1] verify_newpolicy_info() 参数检查
static int verify_newpolicy_info(struct xfrm_userpolicy_info *p)
{
  ... ...
    ret = verify_policy_dir(p->dir);                            // [3] 检查 p->dir,保证其值只能是0,1,2
    if (ret)
        return ret;
    if (p->index && ((p->index & XFRM_POLICY_MAX) != p->dir))   // [4] XFRM_POLICY_MAX=3,设置p->index=4 p->dir=0即可绕过本限制
        return -EINVAL;

    return 0;
}

如果policy插入时包含timer set,则触发OOB后、超时后会执行以下timer函数:

xfrm_policy_timer() -> xfrm_policy_delete() -> __xfrm_policy_unlink()

static void xfrm_policy_timer(struct timer_list *t)
{
    struct xfrm_policy *xp = from_timer(xp, t, timer);
    time64_t now = ktime_get_real_seconds();
    time64_t next = TIME64_MAX;
    int warn = 0;
    int dir;

    read_lock(&xp->lock);

    if (unlikely(xp->walk.dead))
        goto out;

    dir = xfrm_policy_id2dir(xp->index);            // [5] 根据用户提供的index重新计算: return index & 7 ->  dir=4&7=4     dir之前是用 XFRM_POLICY_MAX 来限制的,本应该小于3,现在却用7来计算,导致取值超过3。
    ... ...
expired:
    read_unlock(&xp->lock);
    if (!xfrm_policy_delete(xp, dir))                   // [6]  
        km_policy_expired(xp, dir, 1, 0);
    xfrm_pol_put(xp);
}
// [6] xfrm_policy_delete()
int xfrm_policy_delete(struct xfrm_policy *pol, int dir)
{
    struct net *net = xp_net(pol);

    spin_lock_bh(&net->xfrm.xfrm_policy_lock);
    pol = __xfrm_policy_unlink(pol, dir);        // [7] 
    spin_unlock_bh(&net->xfrm.xfrm_policy_lock);
    if (pol) {
        xfrm_policy_kill(pol);
        return 0;
    }
    return -ENOENT;
}
EXPORT_SYMBOL(xfrm_policy_delete);
// [7] __xfrm_policy_unlink()
static struct xfrm_policy *__xfrm_policy_unlink(struct xfrm_policy *pol,
                        int dir)
{
    struct net *net = xp_net(pol);

    if (list_empty(&pol->walk.all))
        return NULL;

    /* Socket policies are not hashed. */
    if (!hlist_unhashed(&pol->bydst)) {
        hlist_del_rcu(&pol->bydst);
        hlist_del_init(&pol->bydst_inexact_list);
        hlist_del(&pol->byidx);
    }

    list_del_init(&pol->walk.all);
    net->xfrm.policy_count[dir]--;              // [8] dir=4, 导致越界减一

    return pol;
}

1.2 构造UAF 空闲块8字节写NULL

步骤

  • (1)插入policy1对象,index=0, direction=0, priority=0
  • (2)插入policy2对象,index=4, direction=0, priority=1 (>0), a timer set
  • (3)请求XFRM_SPD_IPV4_HTHRESH,触发 policy rehashing
  • (4)请求XFRM_FLUSH_POLICY,触发释放policy1
  • (5)policy2 超时,触发 policy1 的UAF

第(1)(2)步后,两个policy对象被插入到同一list(direction 0),第(3)步执行以下函数,将已有的policy逆序重新插入 bydst list。xfrm_hash_rebuild()

static void xfrm_hash_rebuild(struct work_struct *work)
{
  ... ...
    /* re-insert all policies by order of creation */
    list_for_each_entry_reverse(policy, &net->xfrm.policy_all, walk.all) {
        if (policy->walk.dead)
            continue;
        dir = xfrm_policy_id2dir(policy->index);                            // [9] 将 policy1 重新插入到 bydst list,由于 policy2 的index>=3,所以跳过不插入。
        if (dir >= XFRM_POLICY_MAX) {
            /* skip socket policies */
            continue;
        }
        newpos = NULL;
        chain = policy_hash_bysel(net, &policy->selector,
                      policy->family, dir);

        hlist_del_rcu(&policy->bydst);

        if (!chain) {
            void *p = xfrm_policy_inexact_insert(policy, dir, 0);

            WARN_ONCE(IS_ERR(p), "reinsert: %ld\n", PTR_ERR(p));
            continue;
        }

        hlist_for_each_entry(pol, chain, bydst) {
            if (policy->priority >= pol->priority)
                newpos = &pol->bydst;
            else
                break;
        }
        if (newpos)
            hlist_add_behind_rcu(&policy->bydst, newpos);
        else
            hlist_add_head_rcu(&policy->bydst, chain);
    }
  ... ...
}

第(3)步,[9]之后,两个policy不再连贯,但policy2仍然指向policy1

1-kmalloc-2014-layout.png

第(4)步,释放[9]bydstpolicy1,由于policy2不在bydst中,所以没有被释放,且policy2的pprev指针仍然指向释放后的 policy1

xfrm_policy_flush()

int xfrm_policy_flush(struct net *net, u8 type, bool task_valid)
{
    int dir, err = 0, cnt = 0;
    struct xfrm_policy *pol;
    ... ...
again:
    list_for_each_entry(pol, &net->xfrm.policy_all, walk.all) {
        dir = xfrm_policy_id2dir(pol->index);
        if (pol->walk.dead ||
            dir >= XFRM_POLICY_MAX ||
            pol->type != type)
            continue;

        __xfrm_policy_unlink(pol, dir);
        spin_unlock_bh(&net->xfrm.xfrm_policy_lock);
        cnt++;
        xfrm_audit_policy_delete(pol, 1, task_valid);
        xfrm_policy_kill(pol);                                              // [10] 释放
        spin_lock_bh(&net->xfrm.xfrm_policy_lock);
        goto again;
    }
    ... ...
}
EXPORT_SYMBOL(xfrm_policy_flush);

static void xfrm_policy_kill(struct xfrm_policy *policy)
{
    policy->walk.dead = 1;

    atomic_inc(&policy->genid);

    if (del_timer(&policy->polq.hold_timer))
        xfrm_pol_put(policy);
    skb_queue_purge(&policy->polq.hold_queue);

    if (del_timer(&policy->timer))
        xfrm_pol_put(policy);

    xfrm_pol_put(policy);
}

第(5)步,当policy2超时后,将对policy2进行unlink操作:__xfrm_policy_unlink() -> __hlist_del()

static struct xfrm_policy *__xfrm_policy_unlink(struct xfrm_policy *pol,
                        int dir)
{
    struct net *net = xp_net(pol);

    if (list_empty(&pol->walk.all))
        return NULL;

    /* Socket policies are not hashed. */
    if (!hlist_unhashed(&pol->bydst)) {
        hlist_del_rcu(&pol->bydst);                 // [11] 对bydst list上 policy2 调用 __hlist_del() 
        hlist_del_init(&pol->bydst_inexact_list);
        hlist_del(&pol->byidx);
    }

    list_del_init(&pol->walk.all);
    net->xfrm.policy_count[dir]--;                  // [8] dir=4, 导致越界减一

    return pol;
}
// [11] __hlist_del()
static inline void __hlist_del(struct hlist_node *n)
{
    struct hlist_node *next = n->next;
    struct hlist_node **pprev = n->pprev;

    WRITE_ONCE(*pprev, next);                       // [12] policy2 的pprev指针仍然指向释放后的 policy1,这样导致 policy1 的next指针被写为null。导致8字节写
    if (next)
        next->pprev = pprev;
}

1-3 unlink过程分析

2-unlink.png

插入到bydst后,效果如图。总的来说,最后能往空闲块pol1(xfrm_policy 结构)中的xfrm_policy->bydst.next写NULL,也就是空闲块的第2个8字节。

// 删除 pol1
next = pol1->next = NULL;
pprev = pol1->pprev = pol2;

*pprev = next       ==>     pol2->next = NULL;
next->pprev = pprev  // 没有操作        最终 pol2->pprev = pol1。pol2还引用这释放后的pol1值。可堆喷站位pol1。
// 删除 pol2
next = pol2->next =NULL
pprev = pol2->pprev =pol1

pprev = next        ==>     pol1->next = NULL
next->pprev = pprev // 没有操作

xfrm_policy 结构如下,大小为0x358(v4.10.7版本),cred结构大小为0xa8。能将cred结构中的gid/suid覆盖为NULL,拥有suid = 0的进程就可以成功的利用seteuid, setresuid提权成功。

// 能将空闲块 xfrm_policy 结构的第2个8字节写NULL
struct xfrm_policy {
    possible_net_t      xp_net;     // 8 bytes
    struct hlist_node   bydst;      
    struct hlist_node   byidx;
    ...
}
typedef struct {        // possible_net_t
#ifdef CONFIG_NET_NS
    struct net *net;
#endif
} possible_net_t;
struct hlist_node {     // hlist_node
    struct hlist_node *next, **pprev;
};
// 对比 cred 结构, 能将对应的 gid/suid 覆盖为0
struct cred {
    atomic_t    usage;                                      // 4 bytes
    uid_t        uid;        /* real UID of the task */     // 4 bytes
    gid_t        gid;        /* real GID of the task */     // 4 bytes
    uid_t        suid;        /* saved UID of the task */   // 4 bytes
    gid_t        sgid;        /* saved GID of the task */
    uid_t        euid;        /* effective UID of the task */
    gid_t        egid;        /* effective GID of the task */
    uid_t        fsuid;        /* UID for VFS ops */
    gid_t        fsgid;        /* GID for VFS ops */
    unsigned    securebits;    /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;    /* caps we're permitted */
    kernel_cap_t    cap_effective;    /* caps we can actually use */
    kernel_cap_t    cap_bset;    /* capability bounding set */
......
};

2. 漏洞利用

堆喷:堆喷用到了setxattr+userfaultfd堆喷。setxattr往内核拷贝的是用户栈上未初始化的0x1000字节,所以利用的关键并非布置数据,而是用作堆占位。那么kmalloc-1024xfrm_policy对象)是如何覆盖kmalloc-192cred对象,位于 dedicated cache-cred_jar)堆块的呢?

cred_jar 是一个 kmem_cache ,每次 释放一个无用的 cred 的时候不会直接释放占用的内存 而是放入 cred_jar,高频使用的数据结构都有这样一个缓存机制。

cache转化:SLAB/SLUB分配器采用slabs来管理物理内存页,slabs有三种状态,分别是empty(所有对象都空闲)、partial(包含使用的和空闲的对象)、full(所有对象都被使用)。当Linux内核打算释放 empty slab时,相应的物理页返回到可用的页池中,所以某些情况下,这些页可用从一种cache转化为另一种cache。

3-slabs.png

利用思路:这里我们按照exp中的说法,将这两个policy称为policy0policy1。最开始policy0policy1都位于kmalloc-1024,当policy0被释放后,相应的内存被另一种cache kmalloc-192也即cred结构占用,再利用policy1对空闲块进行8字节NULL写。详细EXP的步骤如下图所示:

4-exp_process.png
(1) 父进程—触发漏洞 (2) Process 0~249—堆喷射 (3) Process 250~299—堆喷射 (4) Process 300~1999—提权
sleep(2) 设置userfaultfd() 设置userfaultfd() sleep(8) 等待policy0被释放
sleep(2) pthread_create(handler)错误处理子线程 pthread_create()错误处理子线程
setxattr()喷射 1024 字节 wait_for_pol0[]时阻塞,等(1)
handler()wait_for_bug[]时阻塞,等(1)
创建policy0__xfrm_add_policy0()
sleep(1)
close(wait_for_pol0[])解除阻塞
setxattr()喷射 1024 字节
handler()wait_for_bug[]时阻塞,等(1)
policy0kmalloc-1024包围
创建policy1__xfrm_add_policy1() timer = 7 -> sleep(7)
__xfrm_hash_rebuild()
sleep(1)
__xfrm_flush_policy0() 释放policy0
close(wait_for_bug[])解除阻塞
handler()拷贝数据,释放kmalloc-1024 handler()拷贝数据,释放kmalloc-1024
子进程调用setgid()促使内核分配cred
sleep(5)
policy1超时触发漏洞policy0->next被写NULL
检查cred是否被覆盖为NULL
如果为root则将当前用户添加到sudo组
exit(1)通知父进程,提权成功!

利用步骤

    1. 父进程创建policy0,用setattr()policy0之前和之后喷射kmalloc-1024,这样就能保证policy0policy1相距很远,且不在同一slab中(便于之后policy0所在的slab被释放,接着被cred使用);
    1. 父进程创建policy1,并调用__xfrm_hash_rebuild()__xfrm_flush_policy0(),等待policy1超时触发漏洞;
    1. setxattr()所喷射的kmalloc-1024对象被释放,很有可能与policy0相邻,这样相应的slab为empty也被释放;
    1. 子进程300~1999调用setgid()促使分配子进程的cred结构,很有可能复用了policy0的内存。调用链——setgid() -> prepare_creds()
    1. 如果子进程的gidsuid被覆盖为NULL(调用seteuid(0)来检测),则有了root权限,可以更新/etc/sudoers,添加当前用户为sudoer,使当前用户具备持久的root权限。

成功提权

5-succeed.png

参考

CVE-2019-15666 Ubuntu / CentOS / RHEL Linux Kernel 4.4 - 4.18 privilege escalation

[原创]cve-2019-15666 xfrm_policy 提权漏洞

Exploiting CVE-2019-15666 by reversing the binary PoC

Linux kernel (3.x-5.x) use-after-free in the XFRM subsystem

CVE-2019-15666-Exploit

信号量释放和等待函数sem_post()和sem_wait()

The Slab Allocator in the Linux kernel

因黑客尝试利用超算来挖矿,部分超算被逼下线

欧洲多国超级计算机被挖矿软件“团灭”

你可能感兴趣的:(【kernel exploit】CVE-2019-15666 xfrm UAF 8字节写NULL提权分析)