CVE-2022-34918 nftables堆溢出

  • 影响版本:原作者在Ubuntu 22.04(5.15.0)上成功提权,本篇使用5.17.15
  • 利用条件:CAP_NET_ADMIN

参考:【kernel exploit】CVE-2022-34918 nftable堆溢出漏洞利用(list_head任意写)

背疼o(╥﹏╥)o

漏洞分析

set,map,vmap

  • set

    • 操作行为
      • add
      • delete
      • destroy
      • list
      • flush
      • reset
    • 集合规格
      • type(string):ipv4_addr,ipv6_addr,ether_addr,inet_proto,inet_service,mark
      • typeof(expression)
      • flags(string):constant,dynamic,interval,timeout
      • timeout: string, decimal followed by unit (d, h, m, s)
      • gc-interval:string, decimal followed by unit (d, h, m, s)
      • elements:depends on ‘type’
      • size:unsigned integer(64 bit)
      • policy(string):performance(default),memory
      • auto-merge:boolean or specific parameters
    1
    2
    3
    4
    5
    $ add set [family] table set { type type | typeof expression ; [flags flags ;] [timeout timeout ;] [gc-interval gc-interval ;] [elements = { element[, ...] } ;] [size size ;] [comment comment ;] [policy policy ;] [auto-merge ;] }
    $ {delete | destroy | list | flush | reset } set [family] table set
    $ list sets [family]
    $ delete set [family] table handle handle
    $ {add | delete | destroy } element [family] table set { element[, ...] }
  • map

    1
    2
    3
    4
    5
    6
    7
    $ add map [family] table map { type type | typeof expression [flags flags ;] [elements = { element[, ...] } ;] [size size ;] [comment comment ;] [policy policy ;] }
    $ {delete | destroy | list | flush | reset } map [family] table map
    $ list maps [family]
    $ {add | create | delete | destroy | get | reset } element [family] table set { ELEMENT[, ...] }
    ELEMENT := key_expression OPTIONS [: value_expression]
    OPTIONS := [timeout TIMESPEC] [expires TIMESPEC] [comment string]
    TIMESPEC := [numd][numh][numm][num[s]]
  • vmaps:将元素直接映射到裁决(verdict)语句上,裁决语句决定了当匹配到特定规则时应该采取的动作,比如accept、reject或drop

    1
    $ nft add map table-name map-name { type inet_proto : verdict \; }

    其实就是一种特殊的map

相关源码分析

以上三个都是用set实现的

nftables创建集合

1
2
3
4
5
6
7
$ nft add set inet my_table my_set { type ipv4_addr \; }
$ nft list sets
table inet my_table {
set my_set {
type ipv4_addr
}
}

向集合中添加元素

1
2
3
4
5
6
7
8
$ nft add element inet my_table my_set { 10.10.10.22, 10.10.10.33 }
$ nft list set inet my_table my_set
table inet my_table {
set my_set {
type ipv4_addr
elements = { 10.10.10.22, 10.10.10.33 }
}
}

引用集合

1
2
3
4
5
6
7
8
9
10
11
12
$ nft insert rule inet my_table my_filter_chain ip saddr @my_set drop
$ nft list chain inet my_table my_filter_chain
table inet my_table {
chain my_filter_chain {
type filter hook input priority 0; policy accept;
ip saddr @my_set drop
tcp dport http accept
tcp dport nfs accept
tcp dport ssh accept
ip saddr { 10.10.10.123, 10.10.10.231 } accept
}
}

详情见另一篇博客

创建集合元素的回调函数

1
2
3
4
5
6
7
8
9
10
static const struct nfnl_callback nf_tables_cb[NFT_MSG_MAX] = {
// ……
[NFT_MSG_NEWSETELEM] = {
.call = nf_tables_newsetelem,
.type = NFNL_CB_BATCH,
.attr_count = NFTA_SET_ELEM_LIST_MAX,
.policy = nft_set_elem_list_policy,
},
// ……
}

分析使用的命令

1
2
3
$ nft add table inet my_table
$ nft add set inet my_table my_set {type ipv4_addr \;}
$ nft add element inet my_table my_set {10.10.10.22}

nla

nla相关数据处理

nf_tables_newsetelem

  • 进入时的参数nla,一个nlattr指针的数组,nlattr的结构如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /*
    * <------- NLA_HDRLEN ------> <-- NLA_ALIGN(payload)-->
    * +---------------------+- - -+- - - - - - - - - -+- - -+
    * | Header | Pad | Payload | Pad |
    * | (struct nlattr) | ing | | ing |
    * +---------------------+- - -+- - - - - - - - - -+- - -+
    * <-------------- nlattr->nla_len -------------->
    */

    struct nlattr {
    __u16 nla_len;
    __u16 nla_type;
    };

    nla数组的idx和内容对应

    1
    2
    3
    4
    5
    6
    7
    8
    9
    enum nft_set_elem_list_attributes {
    NFTA_SET_ELEM_LIST_UNSPEC,
    NFTA_SET_ELEM_LIST_TABLE,
    NFTA_SET_ELEM_LIST_SET,
    NFTA_SET_ELEM_LIST_ELEMENTS,
    NFTA_SET_ELEM_LIST_SET_ID,
    __NFTA_SET_ELEM_LIST_MAX
    };
    #define NFTA_SET_ELEM_LIST_MAX (__NFTA_SET_ELEM_LIST_MAX - 1)
    • nla

    • table

    • set

    • elements,后面会送解析

    • set_id

  • 接下来就是惯例查找table查找set,调用nft_add_set_elem添加set元素

nft_add_set_elem

  • 进入时的参数attr就是上面的elements,先送解析为nla

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    enum nft_set_elem_attributes {
    NFTA_SET_ELEM_UNSPEC,
    NFTA_SET_ELEM_KEY,
    NFTA_SET_ELEM_DATA,
    NFTA_SET_ELEM_FLAGS,
    NFTA_SET_ELEM_TIMEOUT,
    NFTA_SET_ELEM_EXPIRATION,
    NFTA_SET_ELEM_USERDATA,
    NFTA_SET_ELEM_EXPR,
    NFTA_SET_ELEM_PAD,
    NFTA_SET_ELEM_OBJREF,
    NFTA_SET_ELEM_KEY_END,
    NFTA_SET_ELEM_EXPRESSIONS,
    __NFTA_SET_ELEM_MAX
    };
    #define NFTA_SET_ELEM_MAX (__NFTA_SET_ELEM_MAX - 1)

    只有key有数据,ip地址

  • 后面就是nla解析,填入nft_set_elem,这里只有key需要解析,elem.key内容见下

    elem的结构

    • key
    • key_end
    • data
    • priv

    前三个成员是一个buf或一个熟悉的nft_data,寄存器也是用这个表示的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct nft_set_elem {
    union {
    u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
    struct nft_data val;
    } key;
    union {
    u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
    struct nft_data val;
    } key_end;
    union {
    u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
    struct nft_data val;
    } data;
    void *priv;
    };
    1
    2
    3
    4
    5
    6
    struct nft_data {
    union {
    u32 data[4];
    struct nft_verdict verdict;
    };
    } __attribute__((aligned(__alignof__(u64))));

    然后key,key_end和data传递给nft_set_elem_init,返回一个priv

tmpl

tmpl相关数据处理

nft_add_set_elem

nft_set_ext_tmpl结构及相关enum,一个用于准备nft_set_ext结构体的模板结构体,用于记录各个数据的偏移

1
2
3
4
struct nft_set_ext_tmpl {
u16 len;
u8 offset[NFT_SET_EXT_NUM];
};
1
2
3
4
5
6
7
8
9
10
11
12
enum nft_set_extensions {
NFT_SET_EXT_KEY,
NFT_SET_EXT_KEY_END,
NFT_SET_EXT_DATA,
NFT_SET_EXT_FLAGS,
NFT_SET_EXT_TIMEOUT,
NFT_SET_EXT_EXPIRATION,
NFT_SET_EXT_USERDATA,
NFT_SET_EXT_EXPRESSIONS,
NFT_SET_EXT_OBJREF,
NFT_SET_EXT_NUM
};

先prepare一个tmpl,就是进行一个结构体的初始化,尤其是初始化len

1
2
3
4
5
static inline void nft_set_ext_prepare(struct nft_set_ext_tmpl *tmpl)
{
memset(tmpl, 0, sizeof(*tmpl));
tmpl->len = sizeof(struct nft_set_ext);
}

之后的数据解析都会更新tmpl的长度和对应偏移

  • 调用nft_set_ext_add_length更新

    1
    2
    3
    4
    5
    6
    nft_set_ext_add_length(&tmpl, NFT_SET_EXT_KEY, set->klen);
    nft_set_ext_add_length(&tmpl, NFT_SET_EXT_KEY_END, set->klen);
    nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len);
    nft_set_ext_add_length(&tmpl, NFT_SET_EXT_EXPRESSIONS,
    sizeof(struct nft_set_elem_expr) +
    size);
    1
    2
    3
    4
    5
    6
    7
    8
    static inline void nft_set_ext_add_length(struct nft_set_ext_tmpl *tmpl, u8 id,
    unsigned int len)
    {
    tmpl->len = ALIGN(tmpl->len, nft_set_ext_types[id].align);
    BUG_ON(tmpl->len > U8_MAX);
    tmpl->offset[id] = tmpl->len;
    tmpl->len += nft_set_ext_types[id].len + len;
    }
  • 调用nft_set_ext_add更新

    1
    2
    3
    4
    nft_set_ext_add(&tmpl, NFT_SET_EXT_FLAGS);
    nft_set_ext_add(&tmpl, NFT_SET_EXT_EXPIRATION);
    nft_set_ext_add(&tmpl, NFT_SET_EXT_TIMEOUT);
    nft_set_ext_add(&tmpl, NFT_SET_EXT_OBJREF);
    1
    2
    3
    4
    static inline void nft_set_ext_add(struct nft_set_ext_tmpl *tmpl, u8 id)
    {
    nft_set_ext_add_length(tmpl, id, 0);
    }

然后就进入到了nft_set_elem_init

1
2
3
elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,
elem.key_end.val.data, elem.data.val.data,
timeout, expiration, GFP_KERNEL);

nft_set_elem_init

开始填充nft_set_ext

1
2
3
4
5
struct nft_set_ext {
u8 genmask;
u8 offset[NFT_SET_EXT_NUM];
char data[];
};

先申请空间,加的这个elemsize不知道是什么,这时候tmpl->len包括nft_set_ext结构体头的大小+数据长度

1
elem = kzalloc(set->ops->elemsize + tmpl->len, gfp);

nft_set_ext_init就是在复制offset

1
2
3
4
5
static inline void nft_set_ext_init(struct nft_set_ext *ext,
const struct nft_set_ext_tmpl *tmpl)
{
memcpy(ext->offset, tmpl->offset, sizeof(ext->offset));
}

然后就是复制数据

1
2
3
memcpy(nft_set_ext_key(ext), key, set->klen);
memcpy(nft_set_ext_key_end(ext), key_end, set->klen);
memcpy(nft_set_ext_data(ext), data, set->dlen);

漏洞分析

可以看到在处理data的时候

  • tmpl更新用的是desc.len

    1
    nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len);
  • memcpy使用的是set->dlen

    1
    memcpy(nft_set_ext_data(ext), data, set->dlen);

desc.len的赋值

desc的赋值

1
2
3
if (nla[NFTA_SET_ELEM_DATA] != NULL) {
err = nft_setelem_parse_data(ctx, set, &desc, &elem.data.val,
nla[NFTA_SET_ELEM_DATA]);

nft_setelem_parse_data中会检查desc->len==set->dlen,但如果是verdict则不会进行这个检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
struct nft_data_desc *desc,
struct nft_data *data,
struct nlattr *attr)
{
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) { // !
nft_data_release(data, desc->type);
return -EINVAL;
}

return 0;
}

set->dlen的赋值

nf_table_newset如果dtype不是verdict可以自定义dlen,只要不超过64,如果是verdict应该是16

1
2
3
4
5
6
7
8
9
10
if (dtype != NFT_DATA_VERDICT) {
if (nla[NFTA_SET_DATA_LEN] == NULL)
return -EINVAL;
desc.dlen = ntohl(nla_get_be32(nla[NFTA_SET_DATA_LEN]));
if (desc.dlen == 0 || desc.dlen > NFT_DATA_VALUE_MAXLEN)
return -EINVAL;
} else
desc.dlen = sizeof(struct nft_verdict);
// ……
set->dlen = desc.dlen;

漏洞点

如果往一个NFT_DATA_VALUE的set里添加一个NFT_DATA_VERDICT就可以进行溢出,最多48字节,似乎没有进行type一致的检查

  • src的data是nft_add_set_elem的栈上的数据,也没有进行初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct nft_set_elem {
    union {
    u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
    struct nft_data val;
    } key;
    union {
    u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
    struct nft_data val;
    } key_end;
    union {
    u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
    struct nft_data val;
    } data;
    void *priv;
    };
  • dest是kzalloc得到的堆块

    1
    2
    3
    4
    elem = kzalloc(set->ops->elemsize + tmpl->len, gfp);
    // ……
    if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
    memcpy(nft_set_ext_data(ext), data, set->dlen);

可以通过先add一个NFT_DATA_VALUE布置栈上数据,再add一个NFT_DATA_VERDICT就可以保证溢出的数据是可控的

漏洞利用

漏洞对象的大小取决于用户选项

  • 20字节的head,elemsize(8)+ nft_set_ext_tmpl(2+9)+ 对齐(1)
  • NFT_SET_ELEM_KEY填充28字节
  • NFT_DATA_VERDICT填充16字节

这样elem位于kmalloc-64,可以溢出48字节

地址泄露

泄露内核基址

内核中有一个用于密钥管理的子系统,提供了add_key系统调用进行密钥创建,keyctl系统调用进行密钥的读取、更新、销毁等功能

1
2
3
4
5
SYSCALL_DEFINE5(add_key, const char __user *, _type,
const char __user *, _description,
const void __user *, _payload,
size_t, plen,
key_serial_t, ringid)
  • sys_add_key中会先申请两块临时内存存储从用户空间复制过来的description和payload

    • description:size = 4096

      1
      description = strndup_user(_description, KEY_MAX_DESC_SIZE);
    • payload:size = 用户传来的plen

      1
      payload = kvmalloc(plen, GFP_KERNEL);
  • 调用key_create_or_update

    • 调用user_preparse其中为payload申请user_key_payload结构体

      1
      ret = index_key.type->preparse(&prep); // user_preparse
    • 调用key_alloc

      • 申请key结构体(从独立的key_jar中分配),这是密钥的主结构体

      • 为description申请空间

        1
        key->index_key.description = kmemdup(desc, desclen + 1, GFP_KERNEL);
  • 最后释放description和payload的临时空间

    1
    2
    3
    4
    error3:
    kvfree_sensitive(payload, plen);
    error2:
    kfree(description);

user_key_payload结构体

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

可以通过改大datalen越界读

泄露对象

percpu_ref_data也位于kmalloc-64

1
2
3
4
5
6
7
8
9
struct percpu_ref_data {
atomic_long_t count;
percpu_ref_func_t *release;
percpu_ref_func_t *confirm_switch;
bool force_atomic:1;
bool allow_reinit:1;
struct rcu_head rcu;
struct percpu_ref *ref;
};

io_uring使用这个结构体,此时

  • confirm_switch和release可以泄露内核基址
  • ref可以泄露physmap地址

percpu_ref_data由percpu_ref_init申请,io_ring_ctx_alloc函数申请了这个结构体

1
2
3
4
5
6
7
8
static __cold struct io_ring_ctx *io_ring_ctx_alloc(struct io_uring_params *p)
{
// ……
if (percpu_ref_init(&ctx->refs, io_ring_ctx_ref_free,
PERCPU_REF_ALLOW_REINIT, GFP_KERNEL))
goto err;
// ……
}

任意写

simple_xattr

用于存储in-memory filesystems(tmpfs)的扩展属性(xattrs - extended attribute),通过simple_xattr_alloc申请

1
2
3
4
5
6
struct simple_xattr {
struct list_head list;
char *name;
size_t size;
char value[];
};
1
2
3
struct list_head {
struct list_head *next, *prev;
};
  • kmalloc-32以上
  • 无法修改,修改会将旧的simple_xattr unlink,再link一个新的上去
  • 所以不能通过修改next或size进行越界写
  • 非特权用户无法设置simple_xattr,但系统支持user namespace也可以

unlinking attack

和堆的unsafe unlink差不多,且内核的double link没有任何检查,只要求next和prev都是合法地址

unlink的流程无非就是

1
2
p->next->prev = p->prev;
p->prev->next = p->next;

我们能够修改p->next和p->prev,令

1
2
p->next = modprobe_path - 8 + 1;	// modeprobe_path : /sbin/modeprobe
p->prev = 0xffffxxxx2f706d74; // b'tmp/\xXX\xXX\xff\xff'

则unlink过程

1
2
p->next->prev = p->prev;	// modeprobe_path : b'/tmp/\xXX\xXX\xff\xffprobe'
p->prev->next = p->next; // *0xffffxxxx2f706d74 = modprobe_path - 8 + 1

prev指针的合法地址由physmap提供,及0xffffxxxx2f706d74是一个合法的physmap地址

识别被覆盖对象

unlink一个simple_xattr时需要一个name,因此我们需要知道哪个结构体被覆盖了,我们可以通过分配0x100大小的name,这样name指针的最低字节为\x00,在伪造list的同时覆盖name指针的低字节来识别覆盖对象

1
2
3
4
5
6
struct simple_xattr {
struct list_head list;
char *name;
size_t size;
char value[];
};

Exp

自己搓了一个,太长了不贴了


CVE-2022-34918 nftables堆溢出
http://akaieurus.github.io/2025/01/07/cve-2022-34918/
作者
Eurus
发布于
2025年1月7日
许可协议