nftables CVE复现系列【一】
196082 慢慢好起来

前言

CVE复现还是要趁热复现,不要因为其他事耽误了TvT,毕竟挖洞是重中之重特别是对现在的我来说,一个洞都没有TvT。

不知道算是运气好还是运气不好,在复现CVE-2023-4004的时候发现了另外一个漏洞,在一通分析之后欣喜若狂发现确实可以用来提权,但是在想搞明白这一行为的时候搜索发现已经有了CVE了也就是CVE-2024-1085,那这个CVE的复现就放在后面一篇文章吧。

在这个CVE复现系列就不会再写exp,如果不是特别新颖的利用方式也不会过多介绍了,这里主要分析漏洞成因以及梳理系统逻辑。

本来打算复现三个CVE无奈篇幅过长,漏洞的细节挺多的,所以只能容纳两篇。

CVE-2023-4004

前置知识

在想要彻底搞明白这个漏洞之前可能还需要补齐一点前两篇文章缺少的一些前置知识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct nft_set {
struct list_head list;
struct list_head bindings;
refcount_t refs;
struct nft_table *table;
possible_net_t net;
char *name;
u64 handle;
u32 ktype;
u32 dtype;
u32 objtype;
u32 size;
u8 field_len[NFT_REG32_COUNT];
u8 field_count;
u32 use;
atomic_t nelems;
u32 ndeact;
u64 timeout;
u32 gc_int;
u16 policy;
u16 udlen;
unsigned char *udata;
struct list_head pending_update;
/* runtime data below here */
const struct nft_set_ops *ops ____cacheline_aligned;
u16 flags:13,
dead:1,
genmask:2;
u8 klen;
u8 dlen;
u8 num_exprs;
struct nft_expr *exprs[NFT_SET_EXPR_MAX];
struct list_head catchall_list;
unsigned char data[]
__attribute__((aligned(__alignof__(u64))));
};

首先上面是set集合的结构体定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static int nf_tables_newset(struct net *net, struct sock *nlsk,
struct sk_buff *skb, const struct nlmsghdr *nlh,
const struct nlattr * const nla[],
struct netlink_ext_ack *extack)
{
// ......
if (nla[NFTA_SET_DESC] != NULL) {
err = nf_tables_set_desc_parse(&desc, nla[NFTA_SET_DESC]);
if (err < 0)
return err;

if (desc.field_count > 1) {
if (!(flags & NFT_SET_CONCAT))
return -EINVAL;
} else if (flags & NFT_SET_CONCAT) {
return -EINVAL;
}
} else if (flags & NFT_SET_CONCAT) {
return -EINVAL;
}
// ......
set->field_count = desc.field_count;
for (i = 0; i < desc.field_count; i++)
set->field_len[i] = desc.field_len[i];

err = ops->init(set, &desc, nla);
if (err < 0)
goto err_set_init;
// ......
}

在后面关于element的inert时会遇到set中的成员field_countfield_len

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int nf_tables_set_desc_parse(struct nft_set_desc *desc,
const struct nlattr *nla)
{
struct nlattr *da[NFTA_SET_DESC_MAX + 1];
int err;

err = nla_parse_nested_deprecated(da, NFTA_SET_DESC_MAX, nla,
nft_set_desc_policy, NULL);
if (err < 0)
return err;

if (da[NFTA_SET_DESC_SIZE] != NULL)
desc->size = ntohl(nla_get_be32(da[NFTA_SET_DESC_SIZE]));
if (da[NFTA_SET_DESC_CONCAT])
err = nft_set_desc_concat(desc, da[NFTA_SET_DESC_CONCAT]);

return err;
}

前面通过nla_parse_nested_deprecated将nla解析到da中,并执行nft_set_desc_concat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static int nft_set_desc_concat_parse(const struct nlattr *attr,
struct nft_set_desc *desc)
{
struct nlattr *tb[NFTA_SET_FIELD_MAX + 1];
u32 len;
int err;

if (desc->field_count >= ARRAY_SIZE(desc->field_len))
return -E2BIG;

err = nla_parse_nested_deprecated(tb, NFTA_SET_FIELD_MAX, attr,
nft_concat_policy, NULL);
if (err < 0)
return err;

if (!tb[NFTA_SET_FIELD_LEN])
return -EINVAL;

len = ntohl(nla_get_be32(tb[NFTA_SET_FIELD_LEN]));
if (!len || len > U8_MAX)
return -EINVAL;

desc->field_len[desc->field_count++] = len;

return 0;
}

static int nft_set_desc_concat(struct nft_set_desc *desc,
const struct nlattr *nla)
{
struct nlattr *attr;
u32 num_regs = 0;
int rem, err, i;

nla_for_each_nested(attr, nla, rem) {
if (nla_type(attr) != NFTA_LIST_ELEM)
return -EINVAL;

err = nft_set_desc_concat_parse(attr, desc);
if (err < 0)
return err;
}

for (i = 0; i < desc->field_count; i++)
num_regs += DIV_ROUND_UP(desc->field_len[i], sizeof(u32));

if (num_regs > NFT_REG32_COUNT)
return -E2BIG;

return 0;
}

可以看到的是这里的desc->field_count是根据循环次数来决定的,那么先分析一下能够循环多少次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* nla_data - head of payload
* @nla: netlink attribute
*/
static inline void *nla_data(const struct nlattr *nla)
{
return (char *) nla + NLA_HDRLEN;
}

/**
* nla_len - length of payload
* @nla: netlink attribute
*/
static inline int nla_len(const struct nlattr *nla)
{
return nla->nla_len - NLA_HDRLEN;
}

/**
* nla_next - next netlink attribute in attribute stream
* @nla: netlink attribute
* @remaining: number of bytes remaining in attribute stream
*
* Returns the next netlink attribute in the attribute stream and
* decrements remaining by the size of the current attribute.
*/
static inline struct nlattr *nla_next(const struct nlattr *nla, int *remaining)
{
unsigned int totlen = NLA_ALIGN(nla->nla_len);

*remaining -= totlen;
return (struct nlattr *) ((char *) nla + totlen);
}
/**
* nla_ok - check if the netlink attribute fits into the remaining bytes
* @nla: netlink attribute
* @remaining: number of bytes remaining in attribute stream
*/
static inline int nla_ok(const struct nlattr *nla, int remaining)
{
return remaining >= (int) sizeof(*nla) &&
nla->nla_len >= sizeof(*nla) &&
nla->nla_len <= remaining;
}
/**
* nla_for_each_attr - iterate over a stream of attributes
* @pos: loop counter, set to current attribute
* @head: head of attribute stream
* @len: length of attribute stream
* @rem: initialized to len, holds bytes currently remaining in stream
*/
#define nla_for_each_attr(pos, head, len, rem) \
for (pos = head, rem = len; \
nla_ok(pos, rem); \
pos = nla_next(pos, &(rem)))

/**
* nla_for_each_nested - iterate over nested attributes
* @pos: loop counter, set to current attribute
* @nla: attribute containing the nested attributes
* @rem: initialized to len, holds bytes currently remaining in stream
*/
#define nla_for_each_nested(pos, nla, rem) \
nla_for_each_attr(pos, nla_data(nla), nla_len(nla), rem)

所以这里根据这里的宏定义可以看到最终for循环语句是

1
for(attr = (nla + NLA_HDRLEN), rem = (nla->nla_len - NLA_HDRLEN); (rem >= 4) && (attr->nla_len >= 4) && (attr->nla_len) < rem); pos = nla_next(pos, &(rem)))

结合上下文可以得出这里desc->field_count是由NFTA_LIST_ELEM数量所决定的。并且这里的field_len也是有做相应限制的。

这里再一次回到上一篇文章提到的nft_add_set_elem函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
const struct nlattr *attr, u32 nlmsg_flags)
{
struct nft_expr *expr_array[NFT_SET_EXPR_MAX] = {};
struct nlattr *nla[NFTA_SET_ELEM_MAX + 1];
u8 genmask = nft_genmask_next(ctx->net);
u32 flags = 0, size = 0, num_exprs = 0;
struct nft_set_ext_tmpl tmpl;
struct nft_set_ext *ext, *ext2;
struct nft_set_elem elem;
struct nft_set_binding *binding;
struct nft_object *obj = NULL;
struct nft_userdata *udata;
struct nft_data_desc desc;
enum nft_registers dreg;
struct nft_trans *trans;
u64 timeout;
u64 expiration;
int err, i;
u8 ulen;

// ......
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_ACCOUNT);
if (IS_ERR(elem.priv)) {
err = PTR_ERR(elem.priv);
goto err_parse_data;
}

// ......
err = nft_setelem_insert(ctx->net, set, &elem, &ext2, flags);
if (err) {
if (err == -EEXIST) {
if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA) ^
nft_set_ext_exists(ext2, NFT_SET_EXT_DATA) ||
nft_set_ext_exists(ext, NFT_SET_EXT_OBJREF) ^
nft_set_ext_exists(ext2, NFT_SET_EXT_OBJREF))
goto err_element_clash;
if ((nft_set_ext_exists(ext, NFT_SET_EXT_DATA) &&
nft_set_ext_exists(ext2, NFT_SET_EXT_DATA) &&
memcmp(nft_set_ext_data(ext),
nft_set_ext_data(ext2), set->dlen) != 0) ||
(nft_set_ext_exists(ext, NFT_SET_EXT_OBJREF) &&
nft_set_ext_exists(ext2, NFT_SET_EXT_OBJREF) &&
*nft_set_ext_obj(ext) != *nft_set_ext_obj(ext2)))
goto err_element_clash;
else if (!(nlmsg_flags & NLM_F_EXCL))
err = 0;
} else if (err == -ENOTEMPTY) {
/* ENOTEMPTY reports overlapping between this element
* and an existing one.
*/
err = -EEXIST;
}
goto err_element_clash;
}

// ......
}

可以看到通过nft_set_elem_init函数申请的element其实给到的是elem.priv中的,最后调用nft_setelem_insert将其插入到set中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int nft_setelem_insert(const struct net *net,
struct nft_set *set,
const struct nft_set_elem *elem,
struct nft_set_ext **ext, unsigned int flags)
{
int ret;

if (flags & NFT_SET_ELEM_CATCHALL)
ret = nft_setelem_catchall_insert(net, set, elem, ext);
else
ret = set->ops->insert(net, set, elem, ext);

return ret;
}

这里先判断是否设置了NFT_SET_ELEM_CATCHALL标志位,这个会在后面的CVE复现中详细解释,这里不会出现这一标志位所以最终会调用set->ops->insert函数。

nft_pipapo_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const struct nft_set_type nft_set_pipapo_type = {
.features = NFT_SET_INTERVAL | NFT_SET_MAP | NFT_SET_OBJECT |
NFT_SET_TIMEOUT,
.ops = {
.lookup = nft_pipapo_lookup,
.insert = nft_pipapo_insert,
.activate = nft_pipapo_activate,
.deactivate = nft_pipapo_deactivate,
.flush = nft_pipapo_flush,
.remove = nft_pipapo_remove,
.walk = nft_pipapo_walk,
.get = nft_pipapo_get,
.privsize = nft_pipapo_privsize,
.estimate = nft_pipapo_estimate,
.init = nft_pipapo_init,
.destroy = nft_pipapo_destroy,
.gc_init = nft_pipapo_gc_init,
.commit = nft_pipapo_commit,
.abort = nft_pipapo_abort,
.elemsize = offsetof(struct nft_pipapo_elem, ext),
},
};

这里具有漏洞的set类型为上述类型,并且可以看到的是在nf_tables_newset函数在完成对set成员的赋值操作之后就会调用ops->init(set, &desc, nla)来进行初始化,所以这里关注他的init即nft_pipapo_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
static int nft_pipapo_init(const struct nft_set *set,
const struct nft_set_desc *desc,
const struct nlattr * const nla[])
{
struct nft_pipapo *priv = nft_set_priv(set);
struct nft_pipapo_match *m;
struct nft_pipapo_field *f;
int err, i, field_count;

field_count = desc->field_count ? : 1;

if (field_count > NFT_PIPAPO_MAX_FIELDS)
return -EINVAL;

m = kmalloc(sizeof(*priv->match) + sizeof(*f) * field_count,
GFP_KERNEL);
if (!m)
return -ENOMEM;

m->field_count = field_count;
m->bsize_max = 0;

m->scratch = alloc_percpu(unsigned long *);
if (!m->scratch) {
err = -ENOMEM;
goto out_scratch;
}
for_each_possible_cpu(i)
*per_cpu_ptr(m->scratch, i) = NULL;

#ifdef NFT_PIPAPO_ALIGN
m->scratch_aligned = alloc_percpu(unsigned long *);
if (!m->scratch_aligned) {
err = -ENOMEM;
goto out_free;
}
for_each_possible_cpu(i)
*per_cpu_ptr(m->scratch_aligned, i) = NULL;
#endif

rcu_head_init(&m->rcu);

nft_pipapo_for_each_field(f, i, m) {
int len = desc->field_len[i] ? : set->klen;

f->bb = NFT_PIPAPO_GROUP_BITS_INIT;
f->groups = len * NFT_PIPAPO_GROUPS_PER_BYTE(f);

priv->width += round_up(len, sizeof(u32));

f->bsize = 0;
f->rules = 0;
NFT_PIPAPO_LT_ASSIGN(f, NULL);
f->mt = NULL;
}

/* Create an initial clone of matching data for next insertion */
priv->clone = pipapo_clone(m);
if (IS_ERR(priv->clone)) {
err = PTR_ERR(priv->clone);
goto out_free;
}

priv->dirty = false;

rcu_assign_pointer(priv->match, m);

return 0;

out_free:
#ifdef NFT_PIPAPO_ALIGN
free_percpu(m->scratch_aligned);
#endif
free_percpu(m->scratch);
out_scratch:
kfree(m);

return err;
}

首先使用nft_set_priv函数去除set的data段当作nft_pipapo结构体使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* struct nft_pipapo - Representation of a set
* @match: Currently in-use matching data
* @clone: Copy where pending insertions and deletions are kept
* @width: Total bytes to be matched for one packet, including padding
* @dirty: Working copy has pending insertions or deletions
* @last_gc: Timestamp of last garbage collection run, jiffies
*/
struct nft_pipapo {
struct nft_pipapo_match __rcu *match;
struct nft_pipapo_match *clone;
int width;
bool dirty;
unsigned long last_gc;
};

上述有关于该结构体成员的描述,较为重要以及见的较多的是clone成员,他用于暂时存放要被insert获得delete的element。

1
2
3
4
static inline void *nft_set_priv(const struct nft_set *set)
{
return (void *)set->data;
}

随后为m创建内存,可以看到创建内存的大小由nft_pipapo_match结构体本身以及前面的field_count决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* struct nft_pipapo_match - Data used for lookup and matching
* @field_count Amount of fields in set
* @scratch: Preallocated per-CPU maps for partial matching results
* @scratch_aligned: Version of @scratch aligned to NFT_PIPAPO_ALIGN bytes
* @bsize_max: Maximum lookup table bucket size of all fields, in longs
* @rcu Matching data is swapped on commits
* @f: Fields, with lookup and mapping tables
*/
struct nft_pipapo_match {
int field_count;
#ifdef NFT_PIPAPO_ALIGN
unsigned long * __percpu *scratch_aligned;
#endif
unsigned long * __percpu *scratch;
size_t bsize_max;
struct rcu_head rcu;
struct nft_pipapo_field f[];
};

这里简单看一下这个结构体,可以发现后面就是一个结构为nft_pipapo_field动态数组。

1
2
3
4
#define nft_pipapo_for_each_field(field, index, match)		\
for ((field) = (match)->f, (index) = 0; \
(index) < (match)->field_count; \
(index)++, (field)++)

接着就是对该结构体也就是m的赋值初始化,较为重要的是后续会进入到上述循环中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* struct nft_pipapo_field - Lookup, mapping tables and related data for a field
* @groups: Amount of bit groups
* @rules: Number of inserted rules
* @bsize: Size of each bucket in lookup table, in longs
* @bb: Number of bits grouped together in lookup table buckets
* @lt: Lookup table: 'groups' rows of buckets
* @lt_aligned: Version of @lt aligned to NFT_PIPAPO_ALIGN bytes
* @mt: Mapping table: one bucket per rule
*/
struct nft_pipapo_field {
int groups;
unsigned long rules;
size_t bsize;
int bb;
#ifdef NFT_PIPAPO_ALIGN
unsigned long *lt_aligned;
#endif
unsigned long *lt;
union nft_pipapo_map_bucket *mt;
};

在该循环中主要是对上述结构体赋值操作。这里主要对bb做了赋值为8,然后就是groups赋值为len,witdh则是赋值为len对4向上取整的倍数。

最后通过pipapo_clone函数创建一个新的nft_pipapo_match结构赋值给((struct nft_pipapo *)set->data)->clone成员,最后直接将m赋值给((struct nft_pipapo *)set->data)->match成员。

nft_pipapo_insert

前面主要关注了set的申请及初始过程这里来关注一个element是怎么被链到set中的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
static int nft_pipapo_insert(const struct net *net, const struct nft_set *set,
const struct nft_set_elem *elem,
struct nft_set_ext **ext2)
{
const struct nft_set_ext *ext = nft_set_elem_ext(set, elem->priv);
union nft_pipapo_map_bucket rulemap[NFT_PIPAPO_MAX_FIELDS];
const u8 *start = (const u8 *)elem->key.val.data, *end;
struct nft_pipapo_elem *e = elem->priv, *dup;
struct nft_pipapo *priv = nft_set_priv(set);
struct nft_pipapo_match *m = priv->clone;
u8 genmask = nft_genmask_next(net);
struct nft_pipapo_field *f;
const u8 *start_p, *end_p;
int i, bsize_max, err = 0;

if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY_END))
end = (const u8 *)nft_set_ext_key_end(ext)->data;
else
end = start;

dup = pipapo_get(net, set, start, genmask);
if (!IS_ERR(dup)) {
/* Check if we already have the same exact entry */
const struct nft_data *dup_key, *dup_end;

dup_key = nft_set_ext_key(&dup->ext);
if (nft_set_ext_exists(&dup->ext, NFT_SET_EXT_KEY_END))
dup_end = nft_set_ext_key_end(&dup->ext);
else
dup_end = dup_key;

if (!memcmp(start, dup_key->data, sizeof(*dup_key->data)) &&
!memcmp(end, dup_end->data, sizeof(*dup_end->data))) {
*ext2 = &dup->ext;
return -EEXIST;
}

return -ENOTEMPTY;
}

if (PTR_ERR(dup) == -ENOENT) {
/* Look for partially overlapping entries */
dup = pipapo_get(net, set, end, nft_genmask_next(net));
}

if (PTR_ERR(dup) != -ENOENT) {
if (IS_ERR(dup))
return PTR_ERR(dup);
*ext2 = &dup->ext;
return -ENOTEMPTY;
}

/* Validate */
start_p = start;
end_p = end;
nft_pipapo_for_each_field(f, i, m) {
if (f->rules >= (unsigned long)NFT_PIPAPO_RULE0_MAX)
return -ENOSPC;

if (memcmp(start_p, end_p,
f->groups / NFT_PIPAPO_GROUPS_PER_BYTE(f)) > 0)
return -EINVAL;

start_p += NFT_PIPAPO_GROUPS_PADDED_SIZE(f);
end_p += NFT_PIPAPO_GROUPS_PADDED_SIZE(f);
}

/* Insert */
priv->dirty = true;

bsize_max = m->bsize_max;

nft_pipapo_for_each_field(f, i, m) {
int ret;

rulemap[i].to = f->rules;

ret = memcmp(start, end,
f->groups / NFT_PIPAPO_GROUPS_PER_BYTE(f));
if (!ret)
ret = pipapo_insert(f, start, f->groups * f->bb);
else
ret = pipapo_expand(f, start, end, f->groups * f->bb);

if (f->bsize > bsize_max)
bsize_max = f->bsize;

rulemap[i].n = ret;

start += NFT_PIPAPO_GROUPS_PADDED_SIZE(f);
end += NFT_PIPAPO_GROUPS_PADDED_SIZE(f);
}

if (!*get_cpu_ptr(m->scratch) || bsize_max > m->bsize_max) {
put_cpu_ptr(m->scratch);

err = pipapo_realloc_scratch(m, bsize_max);
if (err)
return err;

m->bsize_max = bsize_max;
} else {
put_cpu_ptr(m->scratch);
}

*ext2 = &e->ext;

pipapo_map(m, rulemap, e);

return 0;
}

在看这里代码的时候最好结合前文中的nft_add_set_elem配合着看,这里的参数elem并不是真正的element而只是在栈上的结构,这里真正的element是elem->priv。

函数开头首先是通过nft_set_elem_ext函数拿出真是elem中的ext段,也就是数据段key、key_end、data等都在此段中。

随后创建一个结构为nft_pipapo_map_bucket的数组,这个结构很眼熟因为在前面的nft_pipapo_field结构体中也看到过此结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* union nft_pipapo_map_bucket - Bucket of mapping table
* @to: First rule number (in next field) this rule maps to
* @n: Number of rules (in next field) this rule maps to
* @e: If there's no next field, pointer to element this rule maps to
*/
union nft_pipapo_map_bucket {
struct {
#if BITS_PER_LONG == 64
static_assert(NFT_PIPAPO_MAP_TOBITS <= 32);
u32 to;

static_assert(NFT_PIPAPO_MAP_NBITS <= 32);
u32 n;
#else
unsigned long to:NFT_PIPAPO_MAP_TOBITS;
unsigned long n:NFT_PIPAPO_MAP_NBITS;
#endif
};
struct nft_pipapo_elem *e;
};

可以看到结构体中存在一个成员结构正好为nft_pipapo_elem,不难猜到这个结构体就是最终用于存放element的结构体。

接着从栈上的elem取出key赋值给start,接着将真实的element赋值给e,随后和前面的init类似,取出set中的data字段当作nft_pipapo使用。

接着会判断是否存在key_end如果有则取出,如果没有则直接将end指向start。

随后就是对key的判断是否以及存在之类的,最后的for循环才是真正的插入过程。

首先会让rulemap[i].to等于f->rules,这里的简单介绍一下上面的结构体中,to的含义为下一个rule的标号,n表示的是下一个rule的个数(后面看到了上面其实并不是结构体是union哈)。知道了这两个是干什么的再去看insert就会很好理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static int pipapo_insert(struct nft_pipapo_field *f, const uint8_t *k,
int mask_bits)
{
int rule = f->rules++, group, ret, bit_offset = 0;

ret = pipapo_resize(f, f->rules - 1, f->rules);
if (ret)
return ret;

for (group = 0; group < f->groups; group++) {
int i, v;
u8 mask;

v = k[group / (BITS_PER_BYTE / f->bb)];
v &= GENMASK(BITS_PER_BYTE - bit_offset - 1, 0);
v >>= (BITS_PER_BYTE - bit_offset) - f->bb;

bit_offset += f->bb;
bit_offset %= BITS_PER_BYTE;

if (mask_bits >= (group + 1) * f->bb) {
/* Not masked */
pipapo_bucket_set(f, rule, group, v);
} else if (mask_bits <= group * f->bb) {
/* Completely masked */
for (i = 0; i < NFT_PIPAPO_BUCKETS(f->bb); i++)
pipapo_bucket_set(f, rule, group, i);
} else {
/* The mask limit falls on this group */
mask = GENMASK(f->bb - 1, 0);
mask >>= mask_bits - group * f->bb;
for (i = 0; i < NFT_PIPAPO_BUCKETS(f->bb); i++) {
if ((i & ~mask) == (v & ~mask))
pipapo_bucket_set(f, rule, group, i);
}
}
}

pipapo_lt_bits_adjust(f);

return 1;
}

这里面大多是对数据做处理我们暂时先不关心,主要看其中的pipapo_resize函数,这个函数的作用就是将lt和mt根据当前的rule数量进行重新申请,因为在前面init时这俩都为0,即便是在clone之后也是如此,所以这里会先在函数开头重新分配。最后中间则是根据maskbit进行处理,最后返回1,这里返回的1是给到了rulemap[i].n的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static int pipapo_expand(struct nft_pipapo_field *f,
const u8 *start, const u8 *end, int len)
{
int step, masks = 0, bytes = DIV_ROUND_UP(len, BITS_PER_BYTE);
u8 base[NFT_PIPAPO_MAX_BYTES];

memcpy(base, start, bytes);
while (memcmp(base, end, bytes) <= 0) {
int err;

step = 0;
while (pipapo_step_diff(base, step, bytes)) {
if (pipapo_step_after_end(base, end, step, bytes))
break;

step++;
if (step >= len) {
if (!masks) {
pipapo_insert(f, base, 0);
masks = 1;
}
goto out;
}
}

err = pipapo_insert(f, base, len - step);

if (err < 0)
return err;

masks++;
pipapo_base_sum(base, step, bytes);
}
out:
return masks;
}

这里在简单看一下expand,这种情况就是start和end不一致时会产生的,这表明这个field中存在多条规则,最终返回masks。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void pipapo_map(struct nft_pipapo_match *m,
union nft_pipapo_map_bucket map[NFT_PIPAPO_MAX_FIELDS],
struct nft_pipapo_elem *e)
{
struct nft_pipapo_field *f;
int i, j;

for (i = 0, f = m->f; i < m->field_count - 1; i++, f++) {
for (j = 0; j < map[i].n; j++) {
f->mt[map[i].to + j].to = map[i + 1].to;
f->mt[map[i].to + j].n = map[i + 1].n;
}
}

/* Last field: map to ext instead of mapping to next field */
for (j = 0; j < map[i].n; j++)
f->mt[map[i].to + j].e = e;
}

然后就是这里比较重要的pipapo_map函数,这里会根据前面的结果对f->mt赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
field_count = 2;
f1->rules = 0;
f2->rules = 0;
rulemap = [
{to:0, n:1},
{to:0, n:1}
];

↓↓↓↓↓↓ 变为

f1->rules = 1;
f2->rules = 1;
f1->mt = [
{to:0, n:1}
];
f2->mt = [
{e: element}
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
field_count = 3;
f1->rules = 0;
f2->rules = 0;
f3->rules = 0;
rulemap = [
{to:0, n:2},
{to:2, n:1},
{to:3, n:2}
];

↓↓↓↓↓↓ 变为

f1->rules = 2;
f2->rules = 1;
f3->rules = 2;
f1->mt = [
{to:2, n:1},
{to:2, n:1}
];
f2->mt = [
{to:3, n:2};
];
f3->mt = [
{e: element},
{e: element}
]

最终会形成如上形式,目前element存在于set的这样一条链中((struct nft_pipapo *)set->data)->clone->f->mt[i].e中,在开头我们也说了clone成员代表的是临时存放的,所以最后还会通过commit提交进行进一步处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case NFT_MSG_NEWSETELEM:
te = (struct nft_trans_elem *)trans->data;

nft_setelem_activate(net, te->set, &te->elem);
nf_tables_setelem_notify(&trans->ctx, te->set,
&te->elem,
NFT_MSG_NEWSETELEM);
if (te->set->ops->commit &&
list_empty(&te->set->pending_update)) {
list_add_tail(&te->set->pending_update,
&set_update_list);
}
nft_trans_destroy(trans);
break;

最后来到nf_tables_commit函数这里主要是对trans和element进行处理,这里主要就是将set加入到更新列表中,对trans处理就不过多提及对element处理会在复现前面提到的CVE-2024-1085详细分析。函数最后会调用nft_set_commit_update函数对这里加入更新列表的进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void nft_set_commit_update(struct list_head *set_update_list)
{
struct nft_set *set, *next;

list_for_each_entry_safe(set, next, set_update_list, pending_update) {
list_del_init(&set->pending_update);

if (!set->ops->commit)
continue;

set->ops->commit(set);
}
}

最后这里又一次会调用ops中的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void nft_pipapo_commit(const struct nft_set *set)
{
struct nft_pipapo *priv = nft_set_priv(set);
struct nft_pipapo_match *new_clone, *old;

if (time_after_eq(jiffies, priv->last_gc + nft_set_gc_interval(set)))
pipapo_gc(set, priv->clone);

if (!priv->dirty)
return;

new_clone = pipapo_clone(priv->clone);
if (IS_ERR(new_clone))
return;

priv->dirty = false;

old = rcu_access_pointer(priv->match);
rcu_assign_pointer(priv->match, priv->clone);
if (old)
call_rcu(&old->rcu, pipapo_reclaim_match);

priv->clone = new_clone;
}

可以看到这里主要干的事就是将clone移到match成员上,最后调用pipapo_reclaim_match去free掉clone成员。

nft_del_setelem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
static int nft_del_setelem(struct nft_ctx *ctx, struct nft_set *set,
const struct nlattr *attr)
{
struct nlattr *nla[NFTA_SET_ELEM_MAX + 1];
struct nft_set_ext_tmpl tmpl;
struct nft_set_elem elem;
struct nft_set_ext *ext;
struct nft_trans *trans;
u32 flags = 0;
int err;

err = nla_parse_nested_deprecated(nla, NFTA_SET_ELEM_MAX, attr,
nft_set_elem_policy, NULL);
if (err < 0)
return err;

err = nft_setelem_parse_flags(set, nla[NFTA_SET_ELEM_FLAGS], &flags);
if (err < 0)
return err;

if (!nla[NFTA_SET_ELEM_KEY] && !(flags & NFT_SET_ELEM_CATCHALL))
return -EINVAL;

if (!nft_setelem_valid_key_end(set, nla, flags))
return -EINVAL;

nft_set_ext_prepare(&tmpl);

if (flags != 0) {
err = nft_set_ext_add(&tmpl, NFT_SET_EXT_FLAGS);
if (err < 0)
return err;
}

if (nla[NFTA_SET_ELEM_KEY]) {
err = nft_setelem_parse_key(ctx, set, &elem.key.val,
nla[NFTA_SET_ELEM_KEY]);
if (err < 0)
return err;

err = nft_set_ext_add_length(&tmpl, NFT_SET_EXT_KEY, set->klen);
if (err < 0)
goto fail_elem;
}

if (nla[NFTA_SET_ELEM_KEY_END]) {
err = nft_setelem_parse_key(ctx, set, &elem.key_end.val,
nla[NFTA_SET_ELEM_KEY_END]);
if (err < 0)
goto fail_elem;

err = nft_set_ext_add_length(&tmpl, NFT_SET_EXT_KEY_END, set->klen);
if (err < 0)
goto fail_elem_key_end;
}

err = -ENOMEM;
elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,
elem.key_end.val.data, NULL, 0, 0,
GFP_KERNEL_ACCOUNT);
if (IS_ERR(elem.priv)) {
err = PTR_ERR(elem.priv);
goto fail_elem_key_end;
}

ext = nft_set_elem_ext(set, elem.priv);
if (flags)
*nft_set_ext_flags(ext) = flags;

trans = nft_trans_elem_alloc(ctx, NFT_MSG_DELSETELEM, set);
if (trans == NULL)
goto fail_trans;

err = nft_setelem_deactivate(ctx->net, set, &elem, flags);
if (err < 0)
goto fail_ops;

nft_setelem_data_deactivate(ctx->net, set, &elem);

nft_trans_elem(trans) = elem;
nft_trans_commit_list_add_tail(ctx->net, trans);
return 0;

fail_ops:
kfree(trans);
fail_trans:
kfree(elem.priv);
fail_elem_key_end:
nft_data_release(&elem.key_end.val, NFT_DATA_VALUE);
fail_elem:
nft_data_release(&elem.key.val, NFT_DATA_VALUE);
return err;
}

可以看到这里和add有点子像,也是会在nla中拿值只是没那么多,这里会取出key和key_end来和已存在的进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static int nft_set_flush(struct nft_ctx *ctx, struct nft_set *set, u8 genmask)
{
struct nft_set_iter iter = {
.genmask = genmask,
.fn = nft_setelem_flush,
};

set->ops->walk(ctx, set, &iter);
if (!iter.err)
iter.err = nft_set_catchall_flush(ctx, set);

return iter.err;
}

static int nf_tables_delsetelem(struct sk_buff *skb,
const struct nfnl_info *info,
const struct nlattr * const nla[])
{
struct netlink_ext_ack *extack = info->extack;
u8 genmask = nft_genmask_next(info->net);
u8 family = info->nfmsg->nfgen_family;
struct net *net = info->net;
const struct nlattr *attr;
struct nft_table *table;
struct nft_set *set;
struct nft_ctx ctx;
int rem, err = 0;

table = nft_table_lookup(net, nla[NFTA_SET_ELEM_LIST_TABLE], family,
genmask, NETLINK_CB(skb).portid);
if (IS_ERR(table)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_SET_ELEM_LIST_TABLE]);
return PTR_ERR(table);
}

set = nft_set_lookup(table, nla[NFTA_SET_ELEM_LIST_SET], genmask);
if (IS_ERR(set))
return PTR_ERR(set);

if (!list_empty(&set->bindings) &&
(set->flags & (NFT_SET_CONSTANT | NFT_SET_ANONYMOUS)))
return -EBUSY;

nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, nla);

if (!nla[NFTA_SET_ELEM_LIST_ELEMENTS])
return nft_set_flush(&ctx, set, genmask);

nla_for_each_nested(attr, nla[NFTA_SET_ELEM_LIST_ELEMENTS], rem) {
err = nft_del_setelem(&ctx, set, attr);
if (err < 0) {
NL_SET_BAD_ATTR(extack, attr);
break;
}
}
return err;
}

当然这里也可以选择直接偷懒不设置NFTA_SET_ELEM_LIST_ELEMENTS然后走nft_set_flush

1
2
3
4
5
6
7
8
9
10
11
12
13
static int nft_set_flush(struct nft_ctx *ctx, struct nft_set *set, u8 genmask)
{
struct nft_set_iter iter = {
.genmask = genmask,
.fn = nft_setelem_flush,
};

set->ops->walk(ctx, set, &iter);
if (!iter.err)
iter.err = nft_set_catchall_flush(ctx, set);

return iter.err;
}

这里会直接调用set->ops->walk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static void nft_pipapo_walk(const struct nft_ctx *ctx, struct nft_set *set,
struct nft_set_iter *iter)
{
struct nft_pipapo *priv = nft_set_priv(set);
struct net *net = read_pnet(&set->net);
struct nft_pipapo_match *m;
struct nft_pipapo_field *f;
int i, r;

rcu_read_lock();
if (iter->genmask == nft_genmask_cur(net))
m = rcu_dereference(priv->match);
else
m = priv->clone;

if (unlikely(!m))
goto out;

for (i = 0, f = m->f; i < m->field_count - 1; i++, f++)
;

for (r = 0; r < f->rules; r++) {
struct nft_pipapo_elem *e;
struct nft_set_elem elem;

if (r < f->rules - 1 && f->mt[r + 1].e == f->mt[r].e)
continue;

if (iter->count < iter->skip)
goto cont;

e = f->mt[r].e;
if (nft_set_elem_expired(&e->ext))
goto cont;

elem.priv = e;

iter->err = iter->fn(ctx, set, iter, &elem);
if (iter->err < 0)
goto out;

cont:
iter->count++;
}

out:
rcu_read_unlock();
}

结合前面的分析可以清晰的看到这里会拿到element给到elem.priv。最后调用iter->fn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int nft_setelem_flush(const struct nft_ctx *ctx,
struct nft_set *set,
const struct nft_set_iter *iter,
struct nft_set_elem *elem)
{
struct nft_trans *trans;
int err;

trans = nft_trans_alloc_gfp(ctx, NFT_MSG_DELSETELEM,
sizeof(struct nft_trans_elem), GFP_ATOMIC);
if (!trans)
return -ENOMEM;

if (!set->ops->flush(ctx->net, set, elem->priv)) {
err = -ENOENT;
goto err1;
}
set->ndeact++;

nft_setelem_data_deactivate(ctx->net, set, elem);
nft_trans_elem_set(trans) = set;
nft_trans_elem(trans) = *elem;
nft_trans_commit_list_add_tail(ctx->net, trans);

return 0;
err1:
kfree(trans);
return err;
}

这里大部分就不用多提,就是创建trans准备commit,这里有个对set->ops->flush的判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void nft_pipapo_activate(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem)
{
struct nft_pipapo_elem *e;

e = pipapo_get(net, set, (const u8 *)elem->key.val.data, 0);
if (IS_ERR(e))
return;

nft_set_elem_change_active(net, set, &e->ext);
nft_set_elem_clear_busy(&e->ext);
}

static bool nft_pipapo_flush(const struct net *net, const struct nft_set *set,
void *elem)
{
struct nft_pipapo_elem *e = elem;

return pipapo_deactivate(net, set, (const u8 *)nft_set_ext_key(&e->ext),
&e->ext);
}

这里会修改element的活跃状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case NFT_MSG_DELSETELEM:
te = (struct nft_trans_elem *)trans->data;

nf_tables_setelem_notify(&trans->ctx, te->set,
&te->elem,
NFT_MSG_DELSETELEM);
nft_setelem_remove(net, te->set, &te->elem);
if (!nft_setelem_is_catchall(te->set, &te->elem)) {
atomic_dec(&te->set->nelems);
te->set->ndeact--;
}
if (te->set->ops->commit &&
list_empty(&te->set->pending_update)) {
list_add_tail(&te->set->pending_update,
&set_update_list);
}
break;

前面的逻辑比较简单这里直接看commit的内容,这里主要看nft_setelem_remove函数。

1
2
3
4
5
6
7
8
9
static void nft_setelem_remove(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem)
{
if (nft_setelem_is_catchall(set, elem))
nft_setelem_catchall_remove(net, set, elem);
else
set->ops->remove(net, set, elem);
}

因为我们并不是catchall类型的(看到这个就烦!!!)所以最终会调用set->ops->remove

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static void nft_pipapo_remove(const struct net *net, const struct nft_set *set,
const struct nft_set_elem *elem)
{
struct nft_pipapo *priv = nft_set_priv(set);
struct nft_pipapo_match *m = priv->clone;
struct nft_pipapo_elem *e = elem->priv;
int rules_f0, first_rule = 0;
const u8 *data;

data = (const u8 *)nft_set_ext_key(&e->ext);

e = pipapo_get(net, set, data, 0);
if (IS_ERR(e))
return;

while ((rules_f0 = pipapo_rules_same_key(m->f, first_rule))) {
union nft_pipapo_map_bucket rulemap[NFT_PIPAPO_MAX_FIELDS];
const u8 *match_start, *match_end;
struct nft_pipapo_field *f;
int i, start, rules_fx;

match_start = data;
match_end = (const u8 *)nft_set_ext_key_end(&e->ext)->data;

start = first_rule;
rules_fx = rules_f0;

nft_pipapo_for_each_field(f, i, m) {
if (!pipapo_match_field(f, start, rules_fx,
match_start, match_end))
break;

rulemap[i].to = start;
rulemap[i].n = rules_fx;

rules_fx = f->mt[start].n;
start = f->mt[start].to;

match_start += NFT_PIPAPO_GROUPS_PADDED_SIZE(f);
match_end += NFT_PIPAPO_GROUPS_PADDED_SIZE(f);
}

if (i == m->field_count) {
priv->dirty = true;
pipapo_drop(m, rulemap);
return;
}

first_rule += rules_f0;
}
}

这里的漏洞就发生在nft_pipapo_remove函数中,所以在分析这个函数的同时就一起把漏洞给分析了。

首先函数开头使用pipapo_get函数通过key拿到对应的element,然后进入到循环中,注意的是这里会直接强制拿key_end但是在前面insert的时候key_end是可有可无的,如果没有的话key_end会直接等于key

1
2
3
4
5
6
7
8
9
10
11
12
13
static bool pipapo_match_field(struct nft_pipapo_field *f,
int first_rule, int rule_count,
const u8 *start, const u8 *end)
{
u8 right[NFT_PIPAPO_MAX_BYTES] = { 0 };
u8 left[NFT_PIPAPO_MAX_BYTES] = { 0 };

pipapo_get_boundaries(f, first_rule, rule_count, left, right);

return !memcmp(start, left,
f->groups / NFT_PIPAPO_GROUPS_PER_BYTE(f)) &&
!memcmp(end, right, f->groups / NFT_PIPAPO_GROUPS_PER_BYTE(f));
}

所以在这个函数中是永远不会返回true也就是会直接break,无法将目标element给丢弃掉,也就会造成UAF!

commit_release详细分析

可以看到在前面会创建trans送入到commit中去,并且最后会调用

1
nft_trans_commit_list_add_tail(ctx->net, trans);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void nft_trans_commit_list_add_tail(struct net *net, struct nft_trans *trans)
{
struct nftables_pernet *nft_net = nft_pernet(net);

switch (trans->msg_type) {
case NFT_MSG_NEWSET:
if (!nft_trans_set_update(trans) &&
nft_set_is_anonymous(nft_trans_set(trans)))
list_add_tail(&trans->binding_list, &nft_net->binding_list);
break;
case NFT_MSG_NEWCHAIN:
if (!nft_trans_chain_update(trans) &&
nft_chain_binding(nft_trans_chain(trans)))
list_add_tail(&trans->binding_list, &nft_net->binding_list);
break;
}

list_add_tail(&trans->list, &nft_net->commit_list);
}

可以看到这个函数会将trans添加到&nft_net->commit_list双向链表中去。

1
2
3
4
5
6
nft_set_commit_update(&set_update_list);

nft_commit_notify(net, NETLINK_CB(skb).portid);
nf_tables_gen_notify(net, skb, NFT_MSG_NEWGEN);
nf_tables_commit_audit_log(&adl, nft_net->base_seq);
nf_tables_commit_release(net);

中间几个函数暂时没有遇到,我也没有详细分析他们的含义,所以这里主要看第一个和最后一个,这段代码是nf_tables_commit函数最后的几行代码,第一函数我们比较熟悉,在前面也是分析过了其内部会调用set->ops->commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static void nf_tables_commit_release(struct net *net)
{
struct nftables_pernet *nft_net = nft_pernet(net);
struct nft_trans *trans;

/* all side effects have to be made visible.
* For example, if a chain named 'foo' has been deleted, a
* new transaction must not find it anymore.
*
* Memory reclaim happens asynchronously from work queue
* to prevent expensive synchronize_rcu() in commit phase.
*/
if (list_empty(&nft_net->commit_list)) {
nf_tables_module_autoload_cleanup(net);
mutex_unlock(&nft_net->commit_mutex);
return;
}

trans = list_last_entry(&nft_net->commit_list,
struct nft_trans, list);
get_net(trans->ctx.net);
WARN_ON_ONCE(trans->put_net);

trans->put_net = true;
spin_lock(&nf_tables_destroy_list_lock);
list_splice_tail_init(&nft_net->commit_list, &nf_tables_destroy_list);
spin_unlock(&nf_tables_destroy_list_lock);

nf_tables_module_autoload_cleanup(net);
schedule_work(&trans_destroy_work);

mutex_unlock(&nft_net->commit_mutex);
}

这里重点关注最后一行代码调用的函数即nf_tables_commit_release函数,可以看到在函数尾部通过list_splice_tail_init函数将&nft_net->commit_list添加至&nf_tables_destroy_list中去了。

最后注意最后这里会调用schedule_work函数将trans_destroy_work提交至任务队列中去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void nf_tables_trans_destroy_work(struct work_struct *w);
static DECLARE_WORK(trans_destroy_work, nf_tables_trans_destroy_work);

static void nf_tables_trans_destroy_work(struct work_struct *w)
{
struct nft_trans *trans, *next;
LIST_HEAD(head);

spin_lock(&nf_tables_destroy_list_lock);
list_splice_init(&nf_tables_destroy_list, &head);
spin_unlock(&nf_tables_destroy_list_lock);

if (list_empty(&head))
return;

synchronize_rcu();

list_for_each_entry_safe(trans, next, &head, list) {
nft_trans_list_del(trans);
nft_commit_release(trans);
}
}

而在其内部则是将&nf_tables_destroy_list赋值给了head并在后面循环调用了nft_commit_release函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static void nft_commit_release(struct nft_trans *trans)
{
switch (trans->msg_type) {
case NFT_MSG_DELTABLE:
nf_tables_table_destroy(&trans->ctx);
break;
case NFT_MSG_NEWCHAIN:
free_percpu(nft_trans_chain_stats(trans));
kfree(nft_trans_chain_name(trans));
break;
case NFT_MSG_DELCHAIN:
nf_tables_chain_destroy(&trans->ctx);
break;
case NFT_MSG_DELRULE:
nf_tables_rule_destroy(&trans->ctx, nft_trans_rule(trans));
break;
case NFT_MSG_DELSET:
nft_set_destroy(&trans->ctx, nft_trans_set(trans));
break;
case NFT_MSG_DELSETELEM:
nf_tables_set_elem_destroy(&trans->ctx,
nft_trans_elem_set(trans),
nft_trans_elem(trans).priv);
break;
case NFT_MSG_DELOBJ:
nft_obj_destroy(&trans->ctx, nft_trans_obj(trans));
break;
case NFT_MSG_DELFLOWTABLE:
if (nft_trans_flowtable_update(trans))
nft_flowtable_hooks_destroy(&nft_trans_flowtable_hooks(trans));
else
nf_tables_flowtable_destroy(nft_trans_flowtable(trans));
break;
}

if (trans->put_net)
put_net(trans->ctx.net);

kfree(trans);
}

可以看到这里就是真正处理delete的位置了,这里以setelem为例,会调用nf_tables_set_elem_destroy函数。

1
2
3
4
5
6
7
8
9
10
void nf_tables_set_elem_destroy(const struct nft_ctx *ctx,
const struct nft_set *set, void *elem)
{
struct nft_set_ext *ext = nft_set_elem_ext(set, elem);

if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPRESSIONS))
nft_set_elem_expr_destroy(ctx, nft_set_ext_expr(ext));

kfree(elem);
}

最终在这里调用了kfree(elem)

利用分析

该漏洞的利用方法比较简单,因为element的大小是用户态可控的,所以在有UAF的加持下可以很轻松的进行利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct nft_object {
struct list_head list;
struct rhlist_head rhlhead;
struct nft_object_hash_key key;
u32 genmask:2,
use:30;
u64 handle;
u16 udlen;
u8 *udata;
/* runtime data below here */
const struct nft_object_ops *ops ____cacheline_aligned;
unsigned char data[]
__attribute__((aligned(__alignof__(u64))));
};

原作者是利用UAF然后使用堆喷table来占据位置并转化为table的UAF接着使用obj来占用位置,通过get_table来泄露出obj中的ops,最后修改ops来劫持rip,最后使用rop提权。

CVE-2023-4015

前置知识

没错,这又是一个需要一点点前置知识才能完全搞清楚的漏洞。TvT

在上面那个cve的时候可能有人会有疑惑commit到底是在什么时候被调用的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
static void nfnetlink_rcv_batch(struct sk_buff *skb, struct nlmsghdr *nlh,
u16 subsys_id, u32 genid)
{
struct sk_buff *oskb = skb;
struct net *net = sock_net(skb->sk);
const struct nfnetlink_subsystem *ss;
const struct nfnl_callback *nc;
struct netlink_ext_ack extack;
LIST_HEAD(err_list);
u32 status;
int err;

// ......

{
int min_len = nlmsg_total_size(sizeof(struct nfgenmsg));
struct nfnl_net *nfnlnet = nfnl_pernet(net);
struct nlattr *cda[NFNL_MAX_ATTR_COUNT + 1];
struct nlattr *attr = (void *)nlh + min_len;
u8 cb_id = NFNL_MSG_TYPE(nlh->nlmsg_type);
int attrlen = nlh->nlmsg_len - min_len;
struct nfnl_info info = {
.net = net,
.sk = nfnlnet->nfnl,
.nlh = nlh,
.nfmsg = nlmsg_data(nlh),
.extack = &extack,
};

/* Sanity-check NFTA_MAX_ATTR */
if (ss->cb[cb_id].attr_count > NFNL_MAX_ATTR_COUNT) {
err = -ENOMEM;
goto ack;
}

err = nla_parse_deprecated(cda,
ss->cb[cb_id].attr_count,
attr, attrlen,
ss->cb[cb_id].policy, NULL);
if (err < 0)
goto ack;

err = nc->call(skb, &info, (const struct nlattr **)cda);

/* The lock was released to autoload some module, we
* have to abort and start from scratch using the
* original skb.
*/
if (err == -EAGAIN) {
status |= NFNL_BATCH_REPLAY;
goto done;
}
}
ack:
if (nlh->nlmsg_flags & NLM_F_ACK || err) {
/* Errors are delivered once the full batch has been
* processed, this avoids that the same error is
* reported several times when replaying the batch.
*/
if (err == -ENOMEM ||
nfnl_err_add(&err_list, nlh, err, &extack) < 0) {
/* We failed to enqueue an error, reset the
* list of errors and send OOM to userspace
* pointing to the batch header.
*/
nfnl_err_reset(&err_list);
netlink_ack(oskb, nlmsg_hdr(oskb), -ENOMEM,
NULL);
status |= NFNL_BATCH_FAILURE;
goto done;
}
/* We don't stop processing the batch on errors, thus,
* userspace gets all the errors that the batch
* triggers.
*/
if (err)
status |= NFNL_BATCH_FAILURE;
}

msglen = NLMSG_ALIGN(nlh->nlmsg_len);
if (msglen > skb->len)
msglen = skb->len;
skb_pull(skb, msglen);
}
done:
if (status & NFNL_BATCH_REPLAY) {
ss->abort(net, oskb, NFNL_ABORT_AUTOLOAD);
nfnl_err_reset(&err_list);
kfree_skb(skb);
module_put(ss->owner);
goto replay;
} else if (status == NFNL_BATCH_DONE) {
err = ss->commit(net, oskb);
if (err == -EAGAIN) {
status |= NFNL_BATCH_REPLAY;
goto done;
} else if (err) {
ss->abort(net, oskb, NFNL_ABORT_NONE);
netlink_ack(oskb, nlmsg_hdr(oskb), err, NULL);
}
} else {
enum nfnl_abort_action abort_action;

if (status & NFNL_BATCH_FAILURE)
abort_action = NFNL_ABORT_NONE;
else
abort_action = NFNL_ABORT_VALIDATE;

err = ss->abort(net, oskb, abort_action);
if (err == -EAGAIN) {
nfnl_err_reset(&err_list);
kfree_skb(skb);
module_put(ss->owner);
status |= NFNL_BATCH_FAILURE;
goto replay_abort;
}
}

nfnl_err_deliver(&err_list, oskb);
kfree_skb(skb);
module_put(ss->owner);
}

这个函数在nftables子系统浅分析大概介绍过,不过在那篇文章主要介绍的是如何找到对应的子系统的过程,下面的内容是没怎么介绍。可以看到done分支中当status为NFNL_BATCH_DONE时就会调用commit了,如果commit发生错误就会调用abort,或者是status不仅为NFNL_BATCH_DONE则会进入else分支调用abort。

常见结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* struct nft_table - nf_tables table
*
* @list: used internally
* @chains_ht: chains in the table
* @chains: same, for stable walks
* @sets: sets in the table
* @objects: stateful objects in the table
* @flowtables: flow tables in the table
* @hgenerator: handle generator state
* @handle: table handle
* @use: number of chain references to this table
* @flags: table flag (see enum nft_table_flags)
* @genmask: generation mask
* @afinfo: address family info
* @name: name of the table
* @validate_state: internal, set when transaction adds jumps
*/
struct nft_table {
struct list_head list;
struct rhltable chains_ht;
struct list_head chains;
struct list_head sets;
struct list_head objects;
struct list_head flowtables;
u64 hgenerator;
u64 handle;
u32 use;
u16 family:6,
flags:8,
genmask:2;
u32 nlpid;
char *name;
u16 udlen;
u8 *udata;
u8 validate_state;
};

如上是nft_table结构体,这里关注其use成员,其含义为有多少chain引用此table。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* struct nft_chain - nf_tables chain
*
* @rules: list of rules in the chain
* @list: used internally
* @rhlhead: used internally
* @table: table that this chain belongs to
* @handle: chain handle
* @use: number of jump references to this chain
* @flags: bitmask of enum nft_chain_flags
* @name: name of the chain
*/
struct nft_chain {
struct nft_rule_blob __rcu *blob_gen_0;
struct nft_rule_blob __rcu *blob_gen_1;
struct list_head rules;
struct list_head list;
struct rhlist_head rhlhead;
struct nft_table *table;
u64 handle;
u32 use;
u8 flags:5,
bound:1,
genmask:2;
char *name;
u16 udlen;
u8 *udata;

/* Only used during control plane commit phase: */
struct nft_rule_blob *blob_next;
};

这里的nft_chain结构体中的use成员的含义为有多少条转到了此chain。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask,
u8 policy, u32 flags,
struct netlink_ext_ack *extack)
{
const struct nlattr * const *nla = ctx->nla;
struct nft_table *table = ctx->table;
struct nft_base_chain *basechain;
struct net *net = ctx->net;
char name[NFT_NAME_MAXLEN];
struct nft_rule_blob *blob;
struct nft_trans *trans;
struct nft_chain *chain;
int err;

// .......

if (!nft_use_inc(&table->use)) {
err = -EMFILE;
goto err_use;
}

// ......
}

上述为chain的添加函数,可以看到每添加一个chain就会增加一次table的use成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{
struct nftables_pernet *nft_net = nft_pernet(info->net);
struct netlink_ext_ack *extack = info->extack;
unsigned int size, i, n, ulen = 0, usize = 0;
u8 genmask = nft_genmask_next(info->net);
struct nft_rule *rule, *old_rule = NULL;
struct nft_expr_info *expr_info = NULL;
u8 family = info->nfmsg->nfgen_family;
struct nft_flow_rule *flow = NULL;
struct net *net = info->net;
struct nft_userdata *udata;
struct nft_table *table;
struct nft_chain *chain;
struct nft_trans *trans;
u64 handle, pos_handle;
struct nft_expr *expr;
struct nft_ctx ctx;
struct nlattr *tmp;
int err, rem;

// ......

err = -ENOMEM;
rule = kzalloc(sizeof(*rule) + size + usize, GFP_KERNEL_ACCOUNT);
if (rule == NULL)
goto err_release_expr;

nft_activate_next(net, rule);

rule->handle = handle;
rule->dlen = size;
rule->udata = ulen ? 1 : 0;

if (ulen) {
udata = nft_userdata(rule);
udata->len = ulen - 1;
nla_memcpy(udata->data, nla[NFTA_RULE_USERDATA], ulen);
}

expr = nft_expr_first(rule);
for (i = 0; i < n; i++) {
err = nf_tables_newexpr(&ctx, &expr_info[i], expr);
if (err < 0) {
NL_SET_BAD_ATTR(extack, expr_info[i].attr);
goto err_release_rule;
}

if (expr_info[i].ops->validate)
nft_validate_state_update(table, NFT_VALIDATE_NEED);

expr_info[i].ops = NULL;
expr = nft_expr_next(expr);
}

if (chain->flags & NFT_CHAIN_HW_OFFLOAD) {
flow = nft_flow_rule_create(net, rule);
if (IS_ERR(flow)) {
err = PTR_ERR(flow);
goto err_release_rule;
}
}

if (!nft_use_inc(&chain->use)) {
err = -EMFILE;
goto err_release_rule;
}

// .......

return 0;

err_destroy_flow_rule:
nft_use_dec_restore(&chain->use);
if (flow)
nft_flow_rule_destroy(flow);
err_release_rule:
nft_rule_expr_deactivate(&ctx, rule, NFT_TRANS_PREPARE_ERROR);
nf_tables_rule_destroy(&ctx, rule);
err_release_expr:
for (i = 0; i < n; i++) {
if (expr_info[i].ops) {
module_put(expr_info[i].ops->type->owner);
if (expr_info[i].ops->type->release_ops)
expr_info[i].ops->type->release_ops(expr_info[i].ops);
}
}
kvfree(expr_info);

return err;
}

上述为rule的申请函数可以看到虽然在内核注释中写的use成员的含义为跳转到此chain的个数但是实际申请一个rule也会对use进行加一操作。

这里再关注一下注释所写的跳转吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static int nft_immediate_init(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[])
{
struct nft_immediate_expr *priv = nft_expr_priv(expr);
struct nft_data_desc desc = {
.size = sizeof(priv->data),
};
int err;

if (tb[NFTA_IMMEDIATE_DREG] == NULL ||
tb[NFTA_IMMEDIATE_DATA] == NULL)
return -EINVAL;

desc.type = nft_reg_to_type(tb[NFTA_IMMEDIATE_DREG]);
err = nft_data_init(ctx, &priv->data, &desc, tb[NFTA_IMMEDIATE_DATA]);
if (err < 0)
return err;

priv->dlen = desc.len;

err = nft_parse_register_store(ctx, tb[NFTA_IMMEDIATE_DREG],
&priv->dreg, &priv->data, desc.type,
desc.len);
if (err < 0)
goto err1;

if (priv->dreg == NFT_REG_VERDICT) {
struct nft_chain *chain = priv->data.verdict.chain;

switch (priv->data.verdict.code) {
case NFT_JUMP:
case NFT_GOTO:
err = nf_tables_bind_chain(ctx, chain);
if (err < 0)
return err;
break;
default:
break;
}
}

return 0;

err1:
nft_data_release(&priv->data, desc.type);
return err;
}

上述代码是immediate类型的expr的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int nft_data_init(const struct nft_ctx *ctx, struct nft_data *data,
struct nft_data_desc *desc, const struct nlattr *nla)
{
struct nlattr *tb[NFTA_DATA_MAX + 1];
int err;

if (WARN_ON_ONCE(!desc->size))
return -EINVAL;

err = nla_parse_nested_deprecated(tb, NFTA_DATA_MAX, nla,
nft_data_policy, NULL);
if (err < 0)
return err;

if (tb[NFTA_DATA_VALUE]) {
if (desc->type != NFT_DATA_VALUE)
return -EINVAL;

err = nft_value_init(ctx, data, desc, tb[NFTA_DATA_VALUE]);
} else if (tb[NFTA_DATA_VERDICT] && ctx != NULL) {
if (desc->type != NFT_DATA_VERDICT)
return -EINVAL;

err = nft_verdict_init(ctx, data, desc, tb[NFTA_DATA_VERDICT]);
} else {
err = -EINVAL;
}

return err;
}
EXPORT_SYMBOL_GPL(nft_data_init);

随后调用nft_data_init函数对expr的data段做初始化,因为这里是做跳转操作所以tb的索引为NFTA_DATA_VERDICT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data,
struct nft_data_desc *desc, const struct nlattr *nla)
{
u8 genmask = nft_genmask_next(ctx->net);
struct nlattr *tb[NFTA_VERDICT_MAX + 1];
struct nft_chain *chain;
int err;

err = nla_parse_nested_deprecated(tb, NFTA_VERDICT_MAX, nla,
nft_verdict_policy, NULL);
if (err < 0)
return err;

if (!tb[NFTA_VERDICT_CODE])
return -EINVAL;

/* zero padding hole for memcmp */
memset(data, 0, sizeof(*data));
data->verdict.code = ntohl(nla_get_be32(tb[NFTA_VERDICT_CODE]));

switch (data->verdict.code) {
default:
switch (data->verdict.code & NF_VERDICT_MASK) {
case NF_ACCEPT:
case NF_DROP:
case NF_QUEUE:
break;
default:
return -EINVAL;
}
fallthrough;
case NFT_CONTINUE:
case NFT_BREAK:
case NFT_RETURN:
break;
case NFT_JUMP:
case NFT_GOTO:
if (tb[NFTA_VERDICT_CHAIN]) {
chain = nft_chain_lookup(ctx->net, ctx->table,
tb[NFTA_VERDICT_CHAIN],
genmask);
} else if (tb[NFTA_VERDICT_CHAIN_ID]) {
chain = nft_chain_lookup_byid(ctx->net, ctx->table,
tb[NFTA_VERDICT_CHAIN_ID],
genmask);
if (IS_ERR(chain))
return PTR_ERR(chain);
} else {
return -EINVAL;
}

if (IS_ERR(chain))
return PTR_ERR(chain);
if (nft_is_base_chain(chain))
return -EOPNOTSUPP;
if (nft_chain_is_bound(chain))
return -EINVAL;
if (desc->flags & NFT_DATA_DESC_SETELEM &&
chain->flags & NFT_CHAIN_BINDING)
return -EINVAL;
if (!nft_use_inc(&chain->use))
return -EMFILE;

data->verdict.chain = chain;
break;
}

desc->len = sizeof(data->verdict);

return 0;
}

这里首先是解析code,这里就是jump或goto,随后会通过chain的id或者名字找到对应的chain,随后判断chain是否为base chain,chain是否为binding并且已经bound了,然后又对desc做判断是否存在标志位NFT_DATA_DESC_SETELEM,最后对目标chain的use成员加一操作,并且将目标chain放到expr->data->verdict.chain中去。

回到nft_immediate_init函数,会调用nft_parse_register_store函数,这里只需要设置NFTA_IMMEDIATE_DREGNFT_REG_VERDICT即可进入到后续if分支,并且在nft_parse_register_store只会对goto或jump是否构成死循环做判断。

继续看nft_immediate_init函数,在进入到最后的if分支中后回先拿到目标chain,然后如果是goto或是jump则会进入nf_tables_bind_chain对chain进行绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int nf_tables_bind_chain(const struct nft_ctx *ctx, struct nft_chain *chain)
{
if (!nft_chain_binding(chain))
return 0;

if (nft_chain_binding(ctx->chain))
return -EOPNOTSUPP;

if (chain->bound)
return -EBUSY;

if (!nft_use_inc(&chain->use))
return -EMFILE;

chain->bound = true;
nft_chain_trans_bind(ctx, chain);

return 0;
}

首先这里如果是目标chain不带有binding标志位则直接退出,接着如果当前chain带有binding也会直接退出,随后就是检查目标chain是否已经绑定,随后对目标chain的use成员加一操作并且将其标记为已绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
chain1->flags no NFT_CHAIN_BINDING;
chain2->flags = NFT_CHAIN_BINDING;

当 chain1 => chain2 时:
chain1->use = 1
chain2->use = 2



chain1->flags = NFT_CHAIN_BINDING;
chain2->flags no NFT_CHAIN_BINDING;

当 chain1 => chain2 时:
chain1->use = 1
chain2->use = 1



chain1->flags no NFT_CHAIN_BINDING;
chain2->flags no NFT_CHAIN_BINDING;

当 chain1 => chain2 时:
chain1->use = 1
chain2->use = 1

从上述分析可以看到如果让chain1去引用chain2则会引起上述效果。

nft_rule_expr_deactivate

在删除一个rule时最终会调用nft_rule_expr_deactivate函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
void nft_rule_expr_deactivate(const struct nft_ctx *ctx, struct nft_rule *rule,
enum nft_trans_phase phase)
{
struct nft_expr *expr;

expr = nft_expr_first(rule);
while (nft_expr_more(rule, expr)) {
if (expr->ops->deactivate)
expr->ops->deactivate(ctx, expr, phase);

expr = nft_expr_next(expr);
}
}

这里会首先拿到rule中的第一个expr然后进行循环直到拿完rule中的所有expr。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static void nft_immediate_deactivate(const struct nft_ctx *ctx,
const struct nft_expr *expr,
enum nft_trans_phase phase)
{
const struct nft_immediate_expr *priv = nft_expr_priv(expr);
const struct nft_data *data = &priv->data;
struct nft_ctx chain_ctx;
struct nft_chain *chain;
struct nft_rule *rule;

if (priv->dreg == NFT_REG_VERDICT) {
switch (data->verdict.code) {
case NFT_JUMP:
case NFT_GOTO:
chain = data->verdict.chain;
if (!nft_chain_binding(chain))
break;

chain_ctx = *ctx;
chain_ctx.chain = chain;

list_for_each_entry(rule, &chain->rules, list)
nft_rule_expr_deactivate(&chain_ctx, rule, phase);

switch (phase) {
case NFT_TRANS_PREPARE_ERROR:
nf_tables_unbind_chain(ctx, chain);
fallthrough;
case NFT_TRANS_PREPARE:
nft_deactivate_next(ctx->net, chain);
break;
default:
nft_chain_del(chain);
chain->bound = false;
nft_use_dec(&chain->table->use);
break;
}
break;
default:
break;
}
}

if (phase == NFT_TRANS_COMMIT)
return;

return nft_data_release(&priv->data, nft_dreg_to_type(priv->dreg));
}

上述ops中的函数即为nft_immediate_deactivate函数。首先函数内部进入if分支之后回先拿到目标chain,然后如果目标chain不为binding则直接break出这个switch,随后会通过list_for_each_entry循环递归的沿着goto或jump的目标链去deactivate目标chain的rule。

随后根据不同类型进入到不同分支,当类型为NFT_TRANS_PREPARE_ERROR时会先解绑定目标chain,然后设置目标chain为deactivate。

如果类型为NFT_TRANS_PREPARE则不会解绑定只会将目标chain设置为deactivate。

最后就是其余类型的话,会直接将目标chain给del掉,接着对table的use成员减一操作。

1
2
3
4
5
6
7
8
9
10
11
12
void nft_data_release(const struct nft_data *data, enum nft_data_types type)
{
if (type < NFT_DATA_VERDICT)
return;
switch (type) {
case NFT_DATA_VERDICT:
return nft_verdict_uninit(data);
default:
WARN_ON(1);
}
}
EXPORT_SYMBOL_GPL(nft_data_release);

然后注意的是在函数末尾会调用nft_data_release函数,该函数的主要作用也就是对目标chain的use给减一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
chain1->flags no NFT_CHAIN_BINDING;
chain2->flags = NFT_CHAIN_BINDING;
chain2->bound = true;

当 chain1 => chain2 时:
chain1->use = 1
chain2->use = 1
chain2->bound = true;



chain1->flags = NFT_CHAIN_BINDING;
chain2->flags no NFT_CHAIN_BINDING;
chain2->bound = true;

当 chain1 => chain2 时:
chain1->use = 1
chain2->use = 1
chain2->bound = true;



chain1->flags no NFT_CHAIN_BINDING;
chain2->flags no NFT_CHAIN_BINDING;
choun2->bound = true;

当 chain1 => chain2 时:
chain1->use = 1
chain2->use = 1
chain2->bound = true;

漏洞分析

漏洞发生在nf_tables_newrule函数内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
err_destroy_flow_rule:
nft_use_dec_restore(&chain->use);
if (flow)
nft_flow_rule_destroy(flow);
err_release_rule:
nft_rule_expr_deactivate(&ctx, rule, NFT_TRANS_PREPARE_ERROR);
nf_tables_rule_destroy(&ctx, rule);
err_release_expr:
for (i = 0; i < n; i++) {
if (expr_info[i].ops) {
module_put(expr_info[i].ops->type->owner);
if (expr_info[i].ops->type->release_ops)
expr_info[i].ops->type->release_ops(expr_info[i].ops);
}
}
kvfree(expr_info);

return err;

可以看到的是在发生不正常退出的时候会调用nft_rule_expr_deactivate函数,即上面分析的函数。

可以知道的是这个函数会致使chain2->use - 1

回到前置知识nfnetlink_rcv_batch中,因为我们在nf_tables_newrule发生了不正确退出导致会给err = ss->call(...)返回负数,最终status不会等于NFNL_BATCH_DONE,最终会直接调用abort。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action)
{
struct nftables_pernet *nft_net = nft_pernet(net);
struct nft_trans *trans, *next;
LIST_HEAD(set_update_list);
struct nft_trans_elem *te;

if (action == NFNL_ABORT_VALIDATE &&
nf_tables_validate(net) < 0)
return -EAGAIN;

list_for_each_entry_safe_reverse(trans, next, &nft_net->commit_list,
list) {
switch (trans->msg_type) {
// ......
case NFT_MSG_NEWRULE:
if (nft_trans_rule_bound(trans)) {
nft_trans_destroy(trans);
break;
}
nft_use_dec_restore(&trans->ctx.chain->use);
list_del_rcu(&nft_trans_rule(trans)->list);
nft_rule_expr_deactivate(&trans->ctx,
nft_trans_rule(trans),
NFT_TRANS_ABORT);
if (trans->ctx.chain->flags & NFT_CHAIN_HW_OFFLOAD)
nft_flow_rule_destroy(nft_trans_flow_rule(trans));
break;
// ......
}
}

nft_set_abort_update(&set_update_list);

synchronize_rcu();

list_for_each_entry_safe_reverse(trans, next,
&nft_net->commit_list, list) {
nft_trans_list_del(trans);
nf_tables_abort_release(trans);
}

if (action == NFNL_ABORT_AUTOLOAD)
nf_tables_module_autoload(net);
else
nf_tables_module_autoload_cleanup(net);

return 0;
}

因为我们在前面的分析过程中会看到在当chain2的flags为NFT_CHAIN_BINDING时chain2的bound会等于false,当chain2内部有所依就不会进入if分之内,而是进入后面的分支,可是后面会又一次的调用nft_rule_expr_deactivate函数,此时,如果chain2有goto到chain3的rule的话则会导致chain3的use成员出现下溢的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
chain1 => chain2 => chain3;
chain2->rule->expr0 => chain3 [OK];
chain1->rule->expr1 => chain2 [OK];

此时各chain状态:
chain1->use = 0;
chain2->use = 2;
chain2->bound = true;
chain3->use = 1;
chain3->bound = true;


chain1->rule->expr2 => chain2 [ERR]; // 会调用一次 nf_tables_rule_destroy 函数会将 chain2->use--

此时各chain状态:
chain1->use = 0;
chain2->use = 0;
chain3->use = 0;


nf_tables_abort; // 因为此时只有chain2的rule能够走到 case NFT_MSG_NEWRULE: 分支。

此时各chain状态:
chain1->use = 0;
chain2->use = 0;
chain3->use = -1;

如上就是整个过程中各个chain的rule变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void nf_tables_abort_release(struct nft_trans *trans)
{
switch (trans->msg_type) {
case NFT_MSG_NEWTABLE:
nf_tables_table_destroy(&trans->ctx);
break;
case NFT_MSG_NEWCHAIN:
if (nft_trans_chain_update(trans))
nft_hooks_destroy(&nft_trans_chain_hooks(trans));
else
nf_tables_chain_destroy(&trans->ctx);
break;
case NFT_MSG_NEWRULE:
nf_tables_rule_destroy(&trans->ctx, nft_trans_rule(trans));
break;
case NFT_MSG_NEWSET:
nft_set_destroy(&trans->ctx, nft_trans_set(trans));
break;
case NFT_MSG_NEWSETELEM:
nft_set_elem_destroy(nft_trans_elem_set(trans),
nft_trans_elem(trans).priv, true);
break;
case NFT_MSG_NEWOBJ:
nft_obj_destroy(&trans->ctx, nft_trans_obj(trans));
break;
case NFT_MSG_NEWFLOWTABLE:
if (nft_trans_flowtable_update(trans))
nft_hooks_destroy(&nft_trans_flowtable_hooks(trans));
else
nf_tables_flowtable_destroy(nft_trans_flowtable(trans));
break;
}
kfree(trans);
}

最终走到nf_tables_abort_release函数,会在NFT_MSG_NEWCHAIN分支中调用nf_tables_chain_destroy函数,最终会将chain1、chain2给free掉,chain3则保留并且其use成员为-1,如果此时再次创建一个chain引用chain3那么chain3的use成员会变为0,最后调用nf_tables_delchain即可实现uaf。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void nf_tables_chain_destroy(struct nft_ctx *ctx)
{
struct nft_chain *chain = ctx->chain;
struct nft_hook *hook, *next;

if (WARN_ON(chain->use > 0))
return;

/* no concurrent access possible anymore */
nf_tables_chain_free_chain_rules(chain);

if (nft_is_base_chain(chain)) {
struct nft_base_chain *basechain = nft_base_chain(chain);

if (nft_base_chain_netdev(ctx->family, basechain->ops.hooknum)) {
list_for_each_entry_safe(hook, next,
&basechain->hook_list, list) {
list_del_rcu(&hook->list);
kfree_rcu(hook, rcu);
}
}
module_put(basechain->type->owner);
if (rcu_access_pointer(basechain->stats)) {
static_branch_dec(&nft_counters_enabled);
free_percpu(rcu_dereference_raw(basechain->stats));
}
kfree(chain->name);
kfree(chain->udata);
kfree(basechain);
} else {
kfree(chain->name);
kfree(chain->udata);
kfree(chain);
}
}

因为其被漏洞修改为-1所以一开始才是没有被free掉的。

利用分析

现状是chain4 => chain3(freed)所以只能通过chain4去访问chain3的内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
static int nf_tables_getrule(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{
struct netlink_ext_ack *extack = info->extack;
u8 genmask = nft_genmask_cur(info->net);
u8 family = info->nfmsg->nfgen_family;
const struct nft_chain *chain;
const struct nft_rule *rule;
struct net *net = info->net;
struct nft_table *table;
struct sk_buff *skb2;
bool reset = false;
int err;

if (info->nlh->nlmsg_flags & NLM_F_DUMP) {
struct netlink_dump_control c = {
.start= nf_tables_dump_rules_start,
.dump = nf_tables_dump_rules,
.done = nf_tables_dump_rules_done,
.module = THIS_MODULE,
.data = (void *)nla,
};

return nft_netlink_dump_start_rcu(info->sk, skb, info->nlh, &c);
}

table = nft_table_lookup(net, nla[NFTA_RULE_TABLE], family, genmask, 0);
if (IS_ERR(table)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_TABLE]);
return PTR_ERR(table);
}

chain = nft_chain_lookup(net, table, nla[NFTA_RULE_CHAIN], genmask);
if (IS_ERR(chain)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_CHAIN]);
return PTR_ERR(chain);
}

rule = nft_rule_lookup(chain, nla[NFTA_RULE_HANDLE]);
if (IS_ERR(rule)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_HANDLE]);
return PTR_ERR(rule);
}

skb2 = alloc_skb(NLMSG_GOODSIZE, GFP_ATOMIC);
if (!skb2)
return -ENOMEM;

if (NFNL_MSG_TYPE(info->nlh->nlmsg_type) == NFT_MSG_GETRULE_RESET)
reset = true;

err = nf_tables_fill_rule_info(skb2, net, NETLINK_CB(skb).portid,
info->nlh->nlmsg_seq, NFT_MSG_NEWRULE, 0,
family, table, chain, rule, 0, reset);
if (err < 0)
goto err_fill_rule_info;

return nfnetlink_unicast(skb2, net, NETLINK_CB(skb).portid);

err_fill_rule_info:
kfree_skb(skb2);
return err;
}

这里使用getrule,最终会调用nf_tables_fill_rule_info函数去填充rule信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static int nf_tables_fill_rule_info(struct sk_buff *skb, struct net *net,
u32 portid, u32 seq, int event,
u32 flags, int family,
const struct nft_table *table,
const struct nft_chain *chain,
const struct nft_rule *rule, u64 handle,
bool reset)
{
struct nlmsghdr *nlh;
const struct nft_expr *expr, *next;
struct nlattr *list;
u16 type = nfnl_msg_type(NFNL_SUBSYS_NFTABLES, event);

nlh = nfnl_msg_put(skb, portid, seq, type, flags, family, NFNETLINK_V0,
nft_base_seq(net));
if (!nlh)
goto nla_put_failure;

if (nla_put_string(skb, NFTA_RULE_TABLE, table->name))
goto nla_put_failure;
if (nla_put_string(skb, NFTA_RULE_CHAIN, chain->name))
goto nla_put_failure;
if (nla_put_be64(skb, NFTA_RULE_HANDLE, cpu_to_be64(rule->handle),
NFTA_RULE_PAD))
goto nla_put_failure;

if (event != NFT_MSG_DELRULE && handle) {
if (nla_put_be64(skb, NFTA_RULE_POSITION, cpu_to_be64(handle),
NFTA_RULE_PAD))
goto nla_put_failure;
}

if (chain->flags & NFT_CHAIN_HW_OFFLOAD)
nft_flow_rule_stats(chain, rule);

list = nla_nest_start_noflag(skb, NFTA_RULE_EXPRESSIONS);
if (list == NULL)
goto nla_put_failure;
nft_rule_for_each_expr(expr, next, rule) {
if (nft_expr_dump(skb, NFTA_LIST_ELEM, expr, reset) < 0)
goto nla_put_failure;
}
nla_nest_end(skb, list);

if (rule->udata) {
struct nft_userdata *udata = nft_userdata(rule);
if (nla_put(skb, NFTA_RULE_USERDATA, udata->len + 1,
udata->data) < 0)
goto nla_put_failure;
}

nlmsg_end(skb, nlh);
return 0;

nla_put_failure:
nlmsg_trim(skb, nlh);
return -1;
}

然后这里会调用nft_expr_dump函数获得expr。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static int nf_tables_fill_expr_info(struct sk_buff *skb,
const struct nft_expr *expr, bool reset)
{
if (nla_put_string(skb, NFTA_EXPR_NAME, expr->ops->type->name))
goto nla_put_failure;

if (expr->ops->dump) {
struct nlattr *data = nla_nest_start_noflag(skb,
NFTA_EXPR_DATA);
if (data == NULL)
goto nla_put_failure;
if (expr->ops->dump(skb, expr, reset) < 0)
goto nla_put_failure;
nla_nest_end(skb, data);
}

return skb->len;

nla_put_failure:
return -1;
};

int nft_expr_dump(struct sk_buff *skb, unsigned int attr,
const struct nft_expr *expr, bool reset)
{
struct nlattr *nest;

nest = nla_nest_start_noflag(skb, attr);
if (!nest)
goto nla_put_failure;
if (nf_tables_fill_expr_info(skb, expr, reset) < 0)
goto nla_put_failure;
nla_nest_end(skb, nest);
return 0;

nla_put_failure:
return -1;
}

这里最后会调用ops的dump。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int nft_data_dump(struct sk_buff *skb, int attr, const struct nft_data *data,
enum nft_data_types type, unsigned int len)
{
struct nlattr *nest;
int err;

nest = nla_nest_start_noflag(skb, attr);
if (nest == NULL)
return -1;

switch (type) {
case NFT_DATA_VALUE:
err = nft_value_dump(skb, data, len);
break;
case NFT_DATA_VERDICT:
err = nft_verdict_dump(skb, NFTA_DATA_VERDICT, &data->verdict);
break;
default:
err = -EINVAL;
WARN_ON(1);
}

nla_nest_end(skb, nest);
return err;
}
EXPORT_SYMBOL_GPL(nft_data_dump);

static int nft_immediate_dump(struct sk_buff *skb,
const struct nft_expr *expr, bool reset)
{
const struct nft_immediate_expr *priv = nft_expr_priv(expr);

if (nft_dump_register(skb, NFTA_IMMEDIATE_DREG, priv->dreg))
goto nla_put_failure;

return nft_data_dump(skb, NFTA_IMMEDIATE_DATA, &priv->data,
nft_dreg_to_type(priv->dreg), priv->dlen);

nla_put_failure:
return -1;
}

这里最终会走到nft_verdict_dump函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int nft_verdict_dump(struct sk_buff *skb, int type, const struct nft_verdict *v)
{
struct nlattr *nest;

nest = nla_nest_start_noflag(skb, type);
if (!nest)
goto nla_put_failure;

if (nla_put_be32(skb, NFTA_VERDICT_CODE, htonl(v->code)))
goto nla_put_failure;

switch (v->code) {
case NFT_JUMP:
case NFT_GOTO:
if (nla_put_string(skb, NFTA_VERDICT_CHAIN,
v->chain->name))
goto nla_put_failure;
}
nla_nest_end(skb, nest);
return 0;

nla_put_failure:
return -1;
}

最后可以拿到chain的name,原作者是通过堆喷struct nft_expr,通过其ops拿到内核基地址,在通过对喷struct nft_rule拿到内核堆地址,最终通过控制table->udata去修改掉。

关于如何控制RIP这里又需要一点点前置知识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct nft_chain {
struct nft_rule_blob __rcu *blob_gen_0;
struct nft_rule_blob __rcu *blob_gen_1;
struct list_head rules;
struct list_head list;
struct rhlist_head rhlhead;
struct nft_table *table;
u64 handle;
u32 use;
u8 flags:5,
bound:1,
genmask:2;
char *name;
u16 udlen;
u8 *udata;

/* Only used during control plane commit phase: */
struct nft_rule_blob *blob_next;
};

在看到nft_chain结构体中还存在blob_next成员blob_gen_0以及blob_gen_0成员叫人难以理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static int nf_tables_commit(struct net *net, struct sk_buff *skb)
{
struct nftables_pernet *nft_net = nft_pernet(net);
struct nft_trans *trans, *next;
LIST_HEAD(set_update_list);
struct nft_trans_elem *te;
struct nft_chain *chain;
struct nft_table *table;
unsigned int base_seq;
LIST_HEAD(adl);
int err;

// ......

/* 1. Allocate space for next generation rules_gen_X[] */
list_for_each_entry_safe(trans, next, &nft_net->commit_list, list) {
int ret;

ret = nf_tables_commit_audit_alloc(&adl, trans->ctx.table);
if (ret) {
nf_tables_commit_chain_prepare_cancel(net);
nf_tables_commit_audit_free(&adl);
return ret;
}
if (trans->msg_type == NFT_MSG_NEWRULE ||
trans->msg_type == NFT_MSG_DELRULE) {
chain = trans->ctx.chain;

ret = nf_tables_commit_chain_prepare(net, chain);
if (ret < 0) {
nf_tables_commit_chain_prepare_cancel(net);
nf_tables_commit_audit_free(&adl);
return ret;
}
}
}

/* step 2. Make rules_gen_X visible to packet path */
list_for_each_entry(table, &nft_net->tables, list) {
list_for_each_entry(chain, &table->chains, list)
nf_tables_commit_chain(net, chain);
}

// ......

return 0;
}

可以注意到这里会在进入后面的switch之前对newrule一点操作,具体操作因为篇幅问题就不过多介绍,大概就是前面的for循环是将新生成的rule放到这里,后面则是放到另外两个结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
unsigned int
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
const struct nft_chain *chain = priv, *basechain = chain;
const struct net *net = nft_net(pkt);
const struct nft_expr *expr, *last;
const struct nft_rule_dp *rule;
struct nft_regs regs = {};
unsigned int stackptr = 0;
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];
bool genbit = READ_ONCE(net->nft.gencursor);
struct nft_rule_blob *blob;
struct nft_traceinfo info;

// ......
do_chain:
if (genbit)
blob = rcu_dereference(chain->blob_gen_1);
else
blob = rcu_dereference(chain->blob_gen_0);

rule = (struct nft_rule_dp *)blob->data;
next_rule:
regs.verdict.code = NFT_CONTINUE;
for (; !rule->is_last ; rule = nft_rule_next(rule)) {
nft_rule_dp_for_each_expr(expr, last, rule) {
if (expr->ops == &nft_cmp_fast_ops)
nft_cmp_fast_eval(expr, &regs);
else if (expr->ops == &nft_cmp16_fast_ops)
nft_cmp16_fast_eval(expr, &regs);
else if (expr->ops == &nft_bitwise_fast_ops)
nft_bitwise_fast_eval(expr, &regs);
else if (expr->ops != &nft_payload_fast_ops ||
!nft_payload_fast_eval(expr, &regs, pkt))
expr_call_ops_eval(expr, &regs, pkt);

if (regs.verdict.code != NFT_CONTINUE)
break;
}

// ......
EXPORT_SYMBOL_GPL(nft_do_chain);

函数nft_do_chain会在评估数据包的时候调用,这里会从blob_gen_0中拿到blob最终拿到rule和expr,然后根据ops调用对应的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void expr_call_ops_eval(const struct nft_expr *expr,
struct nft_regs *regs,
struct nft_pktinfo *pkt)
{
#ifdef CONFIG_RETPOLINE
unsigned long e;

if (nf_skip_indirect_calls())
goto indirect_call;

e = (unsigned long)expr->ops->eval;
#define X(e, fun) \
do { if ((e) == (unsigned long)(fun)) \
return fun(expr, regs, pkt); } while (0)

X(e, nft_payload_eval);
X(e, nft_cmp_eval);
X(e, nft_counter_eval);
X(e, nft_meta_get_eval);
X(e, nft_lookup_eval);
#if IS_ENABLED(CONFIG_NFT_CT)
X(e, nft_ct_get_fast_eval);
#endif
X(e, nft_range_eval);
X(e, nft_immediate_eval);
X(e, nft_byteorder_eval);
X(e, nft_dynset_eval);
X(e, nft_rt_get_eval);
X(e, nft_bitwise_eval);
X(e, nft_objref_eval);
X(e, nft_objref_map_eval);
#undef X
indirect_call:
#endif /* CONFIG_RETPOLINE */
expr->ops->eval(expr, regs, pkt);
}

所以这里原作者的思路就是控制到blob即可控制RIP了。


参考链接:

https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2023-4004_lts_cos_mitigation/docs/exploit.md#rop-detail

https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2023-4015_lts/docs/exploit.md

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量