CVE-2022-34918 netfilter nf_tables 本地提权分析


影响范围

简介

在netfilter模块的nft_setelem_parse_data()中存在一处类型混淆(type confusion),从而在nft_set_elem_init()中产生了堆越界问题。

修复方案

更新内核或使用下面命令禁止普通用户改变user namspace。

sysctl kernel.unprivileged_userns_clone=0

漏洞分析

漏洞的patch如链接:https://github.com/torvalds/linux/commit/7e6bc1f6cab.diff

diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 51144fc66889b5..d6b59beab3a986 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -5213,13 +5213,20 @@ static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
                  struct nft_data *data,
                  struct nlattr *attr)
 {
+   u32 dtype;
    int err;

    err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
    if (err < 0)
        return err;

-   if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
+   if (set->dtype == NFT_DATA_VERDICT)
+       dtype = NFT_DATA_VERDICT;
+   else
+       dtype = NFT_DATA_VALUE;
+
+   if (dtype != desc->type ||
+       set->dlen != desc->len) {
        nft_data_release(data, desc->type);
        return -EINVAL;
    }

变化不是很大。仔细看的话,原代码其实强制把set->dtype当做NFT_DATA_VERDICT,但实际上set->dtype有其他类型的可能性(类型混淆)。

这个函数其实是在5.8加入的。在5.8之前,调用逻辑是这样的:

```c
// >>> net/netfilter/nf_tables_api.c:4488
/ 4488 / static int nft_add_set_elem(struct nft_ctx ctx, struct nft_set set,
/ 4489 / const struct nlattr attr, u32 nlmsg_flags)
/
4490 */ {


/ 4500 / struct nft_data data;

/ 4595 / if (nla[NFTA_SET_ELEM_DATA] != NULL) {
/ 4596 / err = nft_data_init(ctx, &data, sizeof(data), &d2,
/ 4597 / nla[NFTA_SET_ELEM_DATA]);
/ 4598 / if (err < 0)
/ 4599 / goto err2;
/ 4600 /
/ 4601 / err = -EINVAL;
/ 4602 / if (set->dtype != NFT_DATA_VERDICT && d2.len != set->dlen)
/ 4603 / goto err3;


/ 4629 /
/ 4630 / nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, d2.len);
/ 4631 / }

```

在5.8之后添加了nft_setelem_parse_data(),逻辑变成如下:

```c
// >>> net/netfilter/nf_tables_api.c:5697
/ 5697 / static int nft_add_set_elem(struct nft_ctx ctx, struct nft_set set,
/ 5698 / const struct nlattr attr, u32 nlmsg_flags)
/
5699 */ {


/ 5706 / struct nft_set_elem elem;

/ 5884 / if (nla[NFTA_SET_ELEM_DATA] != NULL) {
/ 5885 / err = nft_setelem_parse_data(ctx, set, &desc, &elem.data.val,
/ 5886 / nla[NFTA_SET_ELEM_DATA]);
/ 5887 / if (err < 0)
/ 5888 / goto err_parse_key_end;


/ 5914 /
/ 5915 / nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len);
/ 5916 / }

// >>> net/netfilter/nf_tables_api.c:5116
/ 5116 / static int nft_setelem_parse_data(struct nft_ctx ctx, struct nft_set set,
/ 5117 / struct nft_data_desc desc,
/
5118 / struct nft_data data,
/ 5119 / struct nlattr attr)
/
5120 / {
/
5121 / int err;
/
5122 /
/
5123 / err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
/
5124 / if (err < 0)
/
5125 / return err;
/
5126 /
/
5127 / if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
/
5128 / nft_data_release(data, desc->type);
/
5129 / return -EINVAL;
/
5130 / }
/
5131 /
/
5132 / return 0;
/
5133 */ }
```

仔细看!5.8之前调用nft_data_init()时第3个参数是值为sizeof(data),即16bytes。但到了5.8之后第3个参数莫名变成了NFT_DATA_VALUE_MAXLEN,即64bytes。其实patch修复的那一行在5.8之前已经存在,但5.8前却不受影响,因为nft_data_init()的第三个参数保证了datalen不会大于16bytes,从而不会导致溢出。

先看nft_setelem_parse_data()函数的参数部分,attr是用户可控的数据,将attr传入nft_data_init()中对datadesc进行初始化。因此datadesc也是用户可控的。

// >>> net/netfilter/nf_tables_api.c:5116
/* 5116 */ static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
/* 5117 */                struct nft_data_desc *desc,
/* 5118 */                struct nft_data *data,
/* 5119 */                struct nlattr *attr)
/* 5120 */ {
/* 5121 */  int err;
/* 5122 */ 
/* 5123 */  err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);

下面是初始化的过程。

先进入nla_parse_nested_deprecated()nla(即上层的attr)中的数据解析到tb中。然后根据tb中的值在设置datadesc(Line 9543 和 Line 9546)

// >>> net/netfilter/nf_tables_api.c:9530
/* 9530 */ int nft_data_init(const struct nft_ctx *ctx,
/* 9531 */        struct nft_data *data, unsigned int size,
/* 9532 */        struct nft_data_desc *desc, const struct nlattr *nla)
/* 9533 */ {
/* 9534 */  struct nlattr *tb[NFTA_DATA_MAX + 1];
------
            // 通过 nla 初始化 tb
/* 9537 */  err = nla_parse_nested_deprecated(tb, NFTA_DATA_MAX, nla,
/* 9538 */                    nft_data_policy, NULL);
------
/* 9540 */      return err;
------
            // 初始化 data 和 desc
/* 9542 */  if (tb[NFTA_DATA_VALUE])
/* 9543 */      return nft_value_init(ctx, data, size, desc,
/* 9544 */                    tb[NFTA_DATA_VALUE]);
/* 9545 */  if (tb[NFTA_DATA_VERDICT] && ctx != NULL)
/* 9546 */      return nft_verdict_init(ctx, data, desc, tb[NFTA_DATA_VERDICT]);
// >>> net/netfilter/nf_tables_api.c:9486
/* 9486 */ static int nft_value_init(const struct nft_ctx *ctx,
/* 9487 */            struct nft_data *data, unsigned int size,
/* 9488 */            struct nft_data_desc *desc, const struct nlattr *nla)
/* 9489 */ {
------
/* 9495 */  if (len > size)
/* 9496 */      return -EOVERFLOW;
------
/* 9498 */  nla_memcpy(data->data, nla, len);
/* 9499 */  desc->type = NFT_DATA_VALUE;
/* 9500 */  desc->len  = len;

初始化完datadesc后就是下面这段判断了。

// >>> net/netfilter/nf_tables_api.c:5116
/* 5116 */ static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
/* 5117 */                struct nft_data_desc *desc,
/* 5118 */                struct nft_data *data,
/* 5119 */                struct nlattr *attr)
/* 5120 */ {
------
/* 5123 */  err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
------
            // 判断是否畸形,但可以绕过
/* 5127 */  if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
/* 5128 */      nft_data_release(data, desc->type);
/* 5129 */      return -EINVAL;
/* 5130 */  }

由于上面说了,descdata是通过解析用户提供的attr得到的,而set->dlen又是用户在创建netfilter的set时可以控制的(但上层存在一个限制,即set->dlen < 64)。

重点来了,假设此时set->dtype == NFT_DATA_VALUEdesc->type == NFT_DATA_VERDICT,则desc->len == 16set->dlendesc->len可以不同,但由于这两个判断中间是“与”逻辑,因此并不会走到错误分支。

nft_setelem_parse_data()外层由nft_add_set_elem()调用,在其下还调用nft_set_elem_init()

// >>> net/netfilter/nf_tables_api.c:5697
/* 5697 */ static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
/* 5698 */              const struct nlattr *attr, u32 nlmsg_flags)
/* 5699 */ {
------
/* 5704 */  struct nft_set_ext_tmpl tmpl;
------
/* 5710 */  struct nft_data_desc desc;
------
/* 5884 */  if (nla[NFTA_SET_ELEM_DATA] != NULL) {
                // 调用`nft_setelem_parse_data`,存在类型混淆问题
/* 5885 */      err = nft_setelem_parse_data(ctx, set, &desc, &elem.data.val,
/* 5886 */                       nla[NFTA_SET_ELEM_DATA]);
/* 5887 */      if (err < 0)
                    // err就挂了,直接走到return
/* 5888 */          goto err_parse_key_end;
------
                // 根据 desc.len 修改 tmpl
/* 5915 */      nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len);
/* 5916 */  }
------
            // 调用`nft_set_elem_init`,存在由类型混淆导致堆越界写
/* 5931 */  elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,
/* 5932 */                    elem.key_end.val.data, elem.data.val.data,
/* 5933 */                    timeout, expiration, GFP_KERNEL);
------
/* 6010 */ err_parse_key_end:
------
/* 6018 */  return err;
/* 6019 */ }

nft_set_elem_init()中存在一处memcpy,来源为data,长度为set->dlen,但ext长度和分配和参数中的tmpl有关,tmpl在上层函数中根据desc进行修改。又因为在nft_setelem_parse_data()中我们提到,set->dlendesc->len可以不同,前者最大可到64,而后者只为16定值,因此此处memcpy会造成堆越界写。

// >>> net/netfilter/nf_tables_api.c:5364
/* 5364 */ void *nft_set_elem_init(const struct nft_set *set,
/* 5365 */          const struct nft_set_ext_tmpl *tmpl,
/* 5366 */          const u32 *key, const u32 *key_end,
/* 5367 */          const u32 *data, u64 timeout, u64 expiration, gfp_t gfp)
/* 5368 */ {
/* 5369 */  struct nft_set_ext *ext;
/* 5370 */  void *elem;
/* 5371 */ 
            // 根据 tmpl->len 分配空间
/* 5372 */  elem = kzalloc(set->ops->elemsize + tmpl->len, gfp);
------
            // 初始化 ext
/* 5376 */  ext = nft_set_elem_ext(set, elem);
/* 5377 */  nft_set_ext_init(ext, tmpl);
------
/* 5383 */  if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
                // 触发堆越界写
/* 5384 */      memcpy(nft_set_ext_data(ext), data, set->dlen);

根据数据的不同,分配的elem结构体可以位于kmalloc-64、kmalloc-128、kmalloc-192中。

漏洞利用

先观察一下这个发生堆越界写的对象:elem。

如下设置结构体可以在kmalloc-64中发生堆溢出:

image-20220802165516046.png

如下设置结构体可以在kmalloc-128中发生堆溢出:

image-20220802165924035.png

如下设置结构体可以在kmalloc-192中发生堆溢出:

image-20220802170139279.png

这里可以选择kmalloc-64的排布,当然其他的也可以利用成功。

之后我们堆喷结构体user_key_payload,做linux内核利用的朋友应该很熟悉这个结构体了。

struct user_key_payload {
    struct rcu_head rcu;        /* RCU destructor */
    unsigned short  datalen;    /* length of this data */
    char        data[] __aligned(__alignof__(u64)); /* actual data */
};

pwndbg> pt/o struct user_key_payload
/* offset      |    size */  type = struct user_key_payload {
/*      0      |      16 */    struct callback_head {
/*      0      |       8 */        struct callback_head *next;
/*      8      |       8 */        void (*func)(struct callback_head *);

                                   /* total size (bytes):   16 */
                               } rcu;
/*     16      |       2 */    unsigned short datalen;
/* XXX  6-byte hole      */
/*     24      |       0 */    char data[];

                               /* total size (bytes):   24 */
                             }

结构体中的datalen字段描述了后面data的长度。通过堆越界写修改它就能通过keyctlsyscall 来越界读取data中的数据。

我们先堆喷一些user_key_payload来简单的做一下堆风水。然后再喷一些user_key_payload,并隔空释放几个。之后触发nft_add_set_elem()中的kzalloc时大概率会得到如下的堆布局:

image-20220802170923279.png

之后越界写就可以修改user_key_payload中的 datalen字段。我们可以使用keyctl遍历读取所有的key,并检查读取的长度。如果长度变成我们修改的长度,则说明修改成功,否则需要重复这一步。

成功后我们把其他所有的user_key_payload通过keyctl的KEYCTL_REVOKE删掉。需要注意的是,这里的删掉并不是直接调用kfree将堆块释放,而是通过向user_key_payload中的rcu写入func值,并让这个key对用户不可见,之后在垃圾回收中将其释放。man中是这样描述的:

KEYCTL_REVOKE (since Linux 2.6.10)
       Revoke the key with the ID provided in arg2 (cast to key_serial_t).  The key is scheduled for  garbage  collection;  it will no longer be findable, and will be unavailable for further operations.  Further attempts to use the key will  fail with the error EKEYREVOKED.

此时我们通过corrupted的user_key_payload做越界读,就能读到rcu.func中的user_free_payload_rcu这个函数指针,从而泄露出内核代码段地址。

稍微提一嘴,原作者博客中使用了另一个结构体作为泄露来源,即通过io_uring来分配percpu_ref_data结构体。但我看了眼这个结构体是在Linux 5.10中加入的,这意味着受影响的5.8~5.9并不能使用这个方法来leak。

有了leak就是想办法通过越界写来提权了。这边我打算试试360在今年blackhat上提到的新利用思路USMA

在raw_packet中存在这样一条路径:

// >>> linux-5.13/net/packet/af_packet.c:3695
/* 3695 */ static int
/* 3696 */ packet_setsockopt(struct socket *sock, int level, int optname, sockptr_t optval,
/* 3697 */        unsigned int optlen)
/* 3698 */ {
------
/* 3706 */  switch (optname) {
------
/* 3711 */      int len = optlen;
------
/* 3728 */  case PACKET_RX_RING:
/* 3729 */  case PACKET_TX_RING:
/* 3730 */  {
------
/* 3735 */      switch (po->tp_version) {
------
/* 3740 */      case TPACKET_V3:
------
                        // 调用
/* 3751 */              ret = packet_set_ring(sk, &req_u, 0,
/* 3752 */                          optname == PACKET_TX_RING);


// >>> linux-5.13/net/packet/af_packet.c:4306
/* 4306 */ static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
/* 4307 */      int closing, int tx_ring)
/* 4308 */ {
------
/* 4331 */  if (req->tp_block_nr) {
------
/* 4376 */      order = get_order(req->tp_block_size);
                // 调用
/* 4377 */      pg_vec = alloc_pg_vec(req, order);


// >>> linux-5.13/net/packet/af_packet.c:4281
/* 4281 */ static struct pgv *alloc_pg_vec(struct tpacket_req *req, int order)
/* 4282 */ {
/* 4283 */  unsigned int block_nr = req->tp_block_nr;
------
            // 在slab中申请一段内存在存放pg_vec
/* 4287 */  pg_vec = kcalloc(block_nr, sizeof(struct pgv), GFP_KERNEL | __GFP_NOWARN);
------
            // 申请 n 个 page
/* 4291 */  for (i = 0; i < block_nr; i++) {
/* 4292 */      pg_vec[i].buffer = alloc_one_pg_vec_page(order);

平时我们只是拿它来做方便的page level 风水,即4292行的功能。而USMA使用的是4287行分配的数组。

首先我们设置block_nr为5~8,这样pg_vec就能分配到kmalloc-64中。之后我们触发nft_add_set_elem()中的堆溢出就能覆盖到pg_vec中的虚拟地址。

之后通过packet_mmap就能将这些page映射到用户态进行读写。

其中在mmap时还存在如下的校验:

// >>> mm/memory.c:1752
/* 1752 */ static int validate_page_before_insert(struct page *page)
/* 1753 */ {
/* 1754 */  if (PageAnon(page) || PageSlab(page) || page_has_type(page))
/* 1755 */      return -EINVAL;
/* 1756 */  flush_dcache_page(page);
/* 1757 */  return 0;
/* 1758 */ }

即检查page是否为匿名页,是否为Slab子系统分配的页,以及page是否含有type,而内存页的type总共有以下四种。

#define PG_buddy    0x00000080
#define PG_offline  0x00000100
#define PG_table    0x00000200
#define PG_guard    0x00000400

PG_buddy为伙伴系统中的页,PG_offline为内存交换出去的页,PG_table为用作页表的页,PG_guard为用作内存屏障的页。可以看到如果传入的page为内核代码段的页,以上的检查全都可以绕过。

例如我们可以将pg_vec中的页修改为__sys_setresuid()所在的页,从而直接patch它的代码让任意用户都可以直接提权到root。

// >>> kernel/sys.c:652
/* 652 */ long __sys_setresuid(uid_t ruid, uid_t euid, uid_t suid)
/* 653 */ {
------
            // patch 这个判断
/* 680 */   if (!ns_capable_setid(old->user_ns, CAP_SETUID)) {
/* 681 */       if (ruid != (uid_t) -1        && !uid_eq(kruid, old->uid) &&
/* 682 */           !uid_eq(kruid, old->euid) && !uid_eq(kruid, old->suid))
/* 683 */           goto error;
/* 684 */       if (euid != (uid_t) -1        && !uid_eq(keuid, old->uid) &&
/* 685 */           !uid_eq(keuid, old->euid) && !uid_eq(keuid, old->suid))
/* 686 */           goto error;
/* 687 */       if (suid != (uid_t) -1        && !uid_eq(ksuid, old->uid) &&
/* 688 */           !uid_eq(ksuid, old->euid) && !uid_eq(ksuid, old->suid))
/* 689 */           goto error;
/* 690 */   }

稍微提一嘴,原作者博客中使用这篇文章中所描述的手法来实现任意地址写。简单来说是借助了simple_xattr结构体中对list_head的unlink操作,从而修改modprobe_path。

image-20220802174129707.png

代码见: https://github.com/veritas501/CVE-2022-34918

参考资料

评论

V

veritas501

这个人很懒,没有留下任何介绍

随机分类

业务安全 文章:29 篇
Ruby安全 文章:2 篇
SQL注入 文章:39 篇
memcache安全 文章:1 篇
渗透测试 文章:154 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

草莓

收藏一下,目前见过最全的资料了

Y4tacker

Ps:纠错不是每个环境都是那样,之前很多时候/proc/self这些都可以,但是

苦咖啡

感谢大佬分享 863558996@qq.com

m1yuu

H

HaCky

stomppe 这部分有点问题,和官方文档有出入,文档中介绍开启这项功能,则会混

目录