Dig into Netfilter (五) —— CVE-2022-32250

CVE-2022-32250

漏洞分析

完整的表达式析构过程 / nft_set_binding

一些规则可以与集合绑定, 但其实真正绑定的是表达式和集合, 这类表达式有nft_lookup, nft_dynset, nft_objref.

代表这一关系的结构是nft_set_binding.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* struct nft_set_binding - nf_tables set binding
*
* @list: set bindings list node
* @chain: chain containing the rule bound to the set
* @flags: set action flags
*
* A set binding contains all information necessary for validation
* of new elements added to a bound set.
*/
struct nft_set_binding {
struct list_head list;
const struct nft_chain *chain;
u32 flags;
};

实话说笔者暂时没太理解这个binding的作用, 能看出来的一是校验NFT_SET_MAP类型的集合中的NFT_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
int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set,
struct nft_set_binding *binding)
{
struct nft_set_binding *i;
struct nft_set_iter iter;

if (set->use == UINT_MAX)
return -EOVERFLOW;

if (!list_empty(&set->bindings) && nft_set_is_anonymous(set))
return -EBUSY;

if (binding->flags & NFT_SET_MAP) {
/* If the set is already bound to the same chain all
* jumps are already validated for that chain.
*/
list_for_each_entry(i, &set->bindings, list) {
if (i->flags & NFT_SET_MAP &&
i->chain == binding->chain)
goto bind;
}

iter.genmask = nft_genmask_next(ctx->net);
iter.skip = 0;
iter.count = 0;
iter.err = 0;
iter.fn = nf_tables_bind_check_setelem;

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

if (iter.err < 0)
return iter.err;
}
bind:
binding->chain = ctx->chain;
list_add_tail_rcu(&binding->list, &set->bindings);
nft_set_trans_bind(ctx, set);
set->use++;

return 0;
}
EXPORT_SYMBOL_GPL(nf_tables_bind_set);

static void nf_tables_unbind_set(const struct nft_ctx *ctx, struct nft_set *set,
struct nft_set_binding *binding, bool event)
{
list_del_rcu(&binding->list);

if (list_empty(&set->bindings) && nft_set_is_anonymous(set)) {
list_del_rcu(&set->list);
if (event)
nf_tables_set_notify(ctx, set, NFT_MSG_DELSET,
GFP_KERNEL);
}
}

这个绑定关系在表达式构造时建立, 在表达式析构时解除, 但在这条commit中 “netfilter: nf_tables: split set destruction in deactivate and destroy phase” , 将析构分成了两部分, deactivate(禁用)和destroy(销毁).

前者保证表达式在下次迭代时不会被遍历到, 后者使用rcu销毁表达式. 这么设计的原因和netfilter中使用的”事务”概念有关.

1
2
*	@deactivate: deactivate expression in next generation
* @destroy: destruction function, called after synchronize_rcu

一次完整的析构如下nf_tables_rule_release函数所示(deactivate + destroy).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void nf_tables_rule_destroy(const struct nft_ctx *ctx,
struct nft_rule *rule)
{
struct nft_expr *expr, *next;

/*
* Careful: some expressions might not be initialized in case this
* is called on error from nf_tables_newrule().
*/
expr = nft_expr_first(rule);
while (nft_expr_more(rule, expr)) {
next = nft_expr_next(expr);
nf_tables_expr_destroy(ctx, expr);
expr = next;
}
kfree(rule);
}

void nf_tables_rule_release(const struct nft_ctx *ctx, struct nft_rule *rule)
{
nft_rule_expr_deactivate(ctx, rule, NFT_TRANS_RELEASE);
nf_tables_rule_destroy(ctx, rule);
}

但其实并不是所有的表达式析构都需要有deactivate这一步骤, 使用如下正则可以搜索到需要deactivate的表达式.

1
^\s*\.deactivate\s*=
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
static 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);
}
}

static void nft_dynset_deactivate(const struct nft_ctx *ctx,
const struct nft_expr *expr,
enum nft_trans_phase phase)
{
struct nft_dynset *priv = nft_expr_priv(expr);

nf_tables_deactivate_set(ctx, priv->set, &priv->binding, phase);
}

void nf_tables_deactivate_set(const struct nft_ctx *ctx, struct nft_set *set,
struct nft_set_binding *binding,
enum nft_trans_phase phase)
{
switch (phase) {
case NFT_TRANS_PREPARE:
set->use--;
return;
case NFT_TRANS_ABORT:
case NFT_TRANS_RELEASE:
set->use--;
fallthrough;
default:
nf_tables_unbind_set(ctx, set, binding,
phase == NFT_TRANS_COMMIT);
}
}
有状态表达式

在 nft_set中, 有一个expr字段, 用来存放与该set相关的有状态表达式(stateful expression). 常用的有状态表达式有 limit, counter等

Stateful objects is nftables’s umbrella term for objects that maintain information about packet flows and connection states, that are updated by each packet that “hits” them, and that share a common syntax. Strictly speaking, stateful object refers to a named object that is attached to a table. More loosely, anonymous stateful objects can also be used, e.g. an unnamed counter used in a rule. Anonymous stateful objects exist only in the context of the object (i.e. rule) in which they are used.

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
/**
* struct nft_set - nf_tables set instance
*
* @list: table set list node
* @bindings: list of set bindings
* @table: table this set belongs to
* @net: netnamespace this set belongs to
* @name: name of the set
* @handle: unique handle of the set
* @ktype: key type (numeric type defined by userspace, not used in the kernel)
* @dtype: data type (verdict or numeric type defined by userspace)
* @objtype: object type (see NFT_OBJECT_* definitions)
* @size: maximum set size
* @field_len: length of each field in concatenation, bytes
* @field_count: number of concatenated fields in element
* @use: number of rules references to this set
* @nelems: number of elements
* @ndeact: number of deactivated elements queued for removal
* @timeout: default timeout value in jiffies
* @gc_int: garbage collection interval in msecs
* @policy: set parameterization (see enum nft_set_policies)
* @udlen: user data length
* @udata: user data
* @expr: stateful expression
* @ops: set ops
* @flags: set flags
* @genmask: generation mask
* @klen: key length
* @dlen: data length
* @data: private set data
*/
struct nft_set {
struct list_head list;
struct list_head bindings;
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;
/* runtime data below here */
const struct nft_set_ops *ops ____cacheline_aligned;
u16 flags:14,
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))));
};

该字段在nf_tables_newset时根据nla[NFTA_SET_EXPR]或nla[NFTA_SET_EXPRESSIONS]进行创建. 如果创建失败, 跳转到err_set_expr_alloc进行销毁工作.

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
	if (nla[NFTA_SET_EXPR]) {
expr = nft_set_elem_expr_alloc(&ctx, set, nla[NFTA_SET_EXPR]);
if (IS_ERR(expr)) {
err = PTR_ERR(expr);
goto err_set_expr_alloc;
}
set->exprs[0] = expr;
set->num_exprs++;
} else if (nla[NFTA_SET_EXPRESSIONS]) {
struct nft_expr *expr;
struct nlattr *tmp;
int left;

if (!(flags & NFT_SET_EXPR)) {
err = -EINVAL;
goto err_set_expr_alloc;
}
i = 0;
nla_for_each_nested(tmp, nla[NFTA_SET_EXPRESSIONS], left) {
if (i == NFT_SET_EXPR_MAX) {
err = -E2BIG;
goto err_set_expr_alloc;
}
if (nla_type(tmp) != NFTA_LIST_ELEM) {
err = -EINVAL;
goto err_set_expr_alloc;
}
expr = nft_set_elem_expr_alloc(&ctx, set, tmp);
if (IS_ERR(expr)) {
err = PTR_ERR(expr);
goto err_set_expr_alloc;
}
set->exprs[i++] = expr;
set->num_exprs++;
}
}

......
err_set_expr_alloc:
for (i = 0; i < set->num_exprs; i++)
nft_expr_destroy(&ctx, set->exprs[i]);

ops->destroy(set);
err_set_init:
kfree(set->name);
err_set_name:
kvfree(set);
return err;

创建工作由nft_set_elem_expr_alloc函数完成.

  • 先调用nft_expr_init创建表达式
    • nf_tables_expr_parse从nla中解析出对应expr_info
    • 为表达式分配空间
    • nf_tables_newexpr根据expr_info构造实际表达式
  • 检查是否是有状态表达式(NFT_EXPR_STATEFUL), 如果不是调用nft_expr_destroy销毁表达式.
  • 检查是否具有垃圾回收标志, 如果有继续检查一些相关信息, 如相关信息不符则调用nft_expr_destroy销毁表达式.
    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
    static struct nft_expr *nft_expr_init(const struct nft_ctx *ctx,
    const struct nlattr *nla)
    {
    struct nft_expr_info expr_info;
    struct nft_expr *expr;
    struct module *owner;
    int err;

    err = nf_tables_expr_parse(ctx, nla, &expr_info);
    if (err < 0)
    goto err1;

    err = -ENOMEM;
    expr = kzalloc(expr_info.ops->size, GFP_KERNEL);
    if (expr == NULL)
    goto err2;

    err = nf_tables_newexpr(ctx, &expr_info, expr);
    if (err < 0)
    goto err3;
    ......
    }

    struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
    const struct nft_set *set,
    const struct nlattr *attr)
    {
    struct nft_expr *expr;
    int err;

    expr = nft_expr_init(ctx, attr);
    if (IS_ERR(expr))
    return expr;

    err = -EOPNOTSUPP;
    if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
    goto err_set_elem_expr;

    if (expr->ops->type->flags & NFT_EXPR_GC) {
    if (set->flags & NFT_SET_TIMEOUT)
    goto err_set_elem_expr;
    if (!set->ops->gc_init)
    goto err_set_elem_expr;
    set->ops->gc_init(set);
    }

    return expr;

    err_set_elem_expr:
    nft_expr_destroy(ctx, expr);
    return ERR_PTR(err);
    }
    表达式的destroy分两步, 调用表达式本身的析构函数和释放表达式占用的内存.
    1
    2
    3
    4
    5
    void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr)
    {
    nf_tables_expr_destroy(ctx, expr);
    kfree(expr);
    }
缺失的deactivate / 任意表达式构造

综合以上两个主题, 可以发现在err_set_elem_expr中进行的析构操作并不完整, 因为其只有destroy而缺少了deactivate. 但正如之前所说, 并不是所有的表达式都需要deactivate操作, 于是查看所有的有状态表达式, 均不需要deactivate, 所以这里的析构操作没问题.

但和nft_set_elem_expr_alloc配合起来就有问题了, 该函数是先构造表达式, 再进行类型check, 这给了任意表达式构造的原语. 如果构造一个需要deactivate操作的表达式, 这里将只会destroy释放其的空间, 而该表达式依旧悬垂在set->bindings链表中.

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
struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
const struct nft_set *set,
const struct nlattr *attr)
{
struct nft_expr *expr;
int err;

expr = nft_expr_init(ctx, attr);
if (IS_ERR(expr))
return expr;

err = -EOPNOTSUPP;
if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
goto err_set_elem_expr;

if (expr->ops->type->flags & NFT_EXPR_GC) {
if (set->flags & NFT_SET_TIMEOUT)
goto err_set_elem_expr;
if (!set->ops->gc_init)
goto err_set_elem_expr;
set->ops->gc_init(set);
}

return expr;

err_set_elem_expr:
nft_expr_destroy(ctx, expr);
return ERR_PTR(err);
}

利用分析

笔者看完文章后认为原作者的利用方式略显繁琐, 但调繁琐的exp也算是一种锻炼吧. 所以利用过程基本复现原作者的文章.

对于UAF漏洞, 肯定是先看我们还能对UAF的对象进行什么操作. 搜索对set->bindings的引用, 发现大部分使用的地方都是一些遍历读取和check, 仅有的两处写入是nf_tables_bind_set函数中的入链操作和nf_tables_unbind_set函数中的脱链操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
list_add_tail_rcu(&binding->list, &set->bindings);

list_del_rcu(&binding->list);


/*
* Insert a new entry between two known consecutive entries.
*
* This is only for internal list manipulation where we know
* the prev/next entries already!
*/
static inline void __list_add_rcu(struct list_head *new,
struct list_head *prev, struct list_head *next)
{
if (!__list_add_valid(new, prev, next))
return;

new->next = next;
new->prev = prev;
rcu_assign_pointer(list_next_rcu(prev), new);
next->prev = new;
}

这让我们能构造出类似如下的原语, 即在UAF的表达式的binding链表位置写上另一个表达式或nft_set链表的地址.

1
2
3
nft_xxxx0.nft_set_binding.list_head.prev = &nft_xxxx1.nft_set_binding.list_head

nft_xxxx0.nft_set_binding.list_head.next = &nft_set.bindings

查看链表结构在对应表达式中的位置, nft_lookup的偏移在0x18和0x20, nft_dynset的偏移在0x40和0x48.
(排版可能有点难受, maybe你可以将它复制到文本框里查看)

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
struct nft_lookup {
struct nft_set * set; /* 0 0x8 */
/* typedef u8 -> __u8 */ unsigned char sreg; /* 0x8 0x1 */
/* typedef u8 -> __u8 */ unsigned char dreg; /* 0x9 0x1 */
/* typedef bool */ _Bool invert; /* 0xa 0x1 */

/* XXX 5 bytes hole, try to pack */

struct nft_set_binding {
struct list_head {
struct list_head * next; /* 0x10 0x8 */
struct list_head * prev; /* 0x18 0x8 */
}list; /* 0x10 0x10 */
const struct nft_chain * chain; /* 0x20 0x8 */
/* typedef u32 -> __u32 */ unsigned int flags; /* 0x28 0x4 */
}binding; /* 0x10 0x20 */
};

struct nft_dynset {
struct nft_set * set; /* 0 0x8 */
struct nft_set_ext_tmpl {
/* typedef u16 -> __u16 */ short unsigned int len; /* 0x8 0x2 */
/* typedef u8 -> __u8 */ unsigned char offset[9]; /* 0xa 0x9 */
}tmpl; /* 0x8 0xc */

/* XXX last struct has 1 byte of padding */

enum nft_dynset_ops op:8; /* 0x14: 0 0x4 */

/* Bitfield combined with next fields */

/* typedef u8 -> __u8 */ unsigned char sreg_key; /* 0x15 0x1 */
/* typedef u8 -> __u8 */ unsigned char sreg_data; /* 0x16 0x1 */
/* typedef bool */ _Bool invert; /* 0x17 0x1 */
/* typedef bool */ _Bool expr; /* 0x18 0x1 */
/* typedef u8 -> __u8 */ unsigned char num_exprs; /* 0x19 0x1 */

/* XXX 6 bytes hole, try to pack */

/* typedef u64 -> __u64 */ long long unsigned int timeout; /* 0x20 0x8 */
struct nft_expr * expr_array[2]; /* 0x28 0x10 */
struct nft_set_binding {
struct list_head {
struct list_head * next; /* 0x38 0x8 */
/* --- cacheline 1 boundary (64 bytes) --- */
struct list_head * prev; /* 0x40 0x8 */
}list; /* 0x38 0x10 */
const struct nft_chain * chain; /* 0x48 0x8 */
/* typedef u32 -> __u32 */ unsigned int flags; /* 0x50 0x4 */
}binding; /* 0x38 0x20 */

};

struct nft_expr {
const struct nft_expr_ops * ops; /* 0 0x8 */
unsigned char data[] __attribute__((__aligned__(8))); /* 0x8 0 */
} __attribute__((__aligned__(8)));

以dynset为例, 原作者使用类似如下codeql查询偏移0x40,0x48,0x20,0x28处有指针字段的从kmalloc-(cg)-96中分配的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @name kmalloc-96
* @kind problem
* @problem.severity warning
*/
import cpp


from FunctionCall fc, Type t, Variable v, Field f, Type t2
where (fc.getTarget().hasName("kmalloc") or
fc.getTarget().hasName("kzalloc") or
fc.getTarget().hasName("kcalloc"))
and
exists(Assignment assign | assign.getRValue() = fc and
assign.getLValue() = v.getAnAccess() and
v.getType().(PointerType).refersToDirectly(t)) and
t.getSize() <= 96 and t.getSize() > 64 and t.fromSource() and
f.getDeclaringType() = t and
(f.getType().(PointerType).refersTo(t2) and t2.getSize() <= 8) and
(f.getByteOffset() = 72)
select fc, t, fc.getLocation()

最终锁定到了cgroup_fs_context对象, 用cgroup_fs_context占位我们释放的表达式, 再触发入链或脱链操作, 即可修改name或release_agent指针, 再触发cgroup_fs_context_free即可释放掉修改后指针指向的空间.

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
struct cgroup_fs_context {
struct kernfs_fs_context kfc; /* 0 32 */

/* XXX last struct has 7 bytes of padding */

struct cgroup_root * root; /* 32 8 */
struct cgroup_namespace * ns; /* 40 8 */
unsigned int flags; /* 48 4 */
bool cpuset_clone_children; /* 52 1 */
bool none; /* 53 1 */
bool all_ss; /* 54 1 */

/* XXX 1 byte hole, try to pack */

u16 subsys_mask; /* 56 2 */

/* XXX 6 bytes hole, try to pack */

/* --- cacheline 1 boundary (64 bytes) --- */
char * name; /* 64 8 */
char * release_agent; /* 72 8 */

/* size: 80, cachelines: 2, members: 10 */
/* sum members: 73, holes: 2, sum holes: 7 */
/* paddings: 1, sum paddings: 7 */
/* last cacheline: 16 bytes */
};

static void cgroup_fs_context_free(struct fs_context *fc)
{
struct cgroup_fs_context *ctx = cgroup_fc2context(fc);

kfree(ctx->name);
kfree(ctx->release_agent);
put_cgroup_ns(ctx->ns);
kernfs_free_fs_context(fc);
kfree(ctx);
}

而如前所述, 该指针有两种可能, &nft_xxxx.nft_set_binding.list_head 和 &nft_set.bindings.

释放前者可以造出下图中的空洞, 再通过如setxattr之类的写入原语即可部分覆写与表达式相邻的目标对象.

但按原作者的想法, 这并不稳定因为不一定能控制target与expression正好相邻.
(依我贫瘠的kernel pwn经验, 这种情形其实还算比较稳定, 而且在覆写不致命的情况下完全可以多次覆写, 如CVE-2022-34918的利用)

it means we can potentially replace and corrupt the contents of an adjacent target object. It is somewhat bad in that the randomized layout of slabs doesn’t necessarily let us know exactly which target object we will be able to corrupt, which adds extra complexity. We can’t leak what target object is adjacent or test whether or not the expression we are freeing is the last of one slab cache, so using this approach would be blind.

于是作者决定释放后者, &nft_set.bindings. 这将在nft_set结构中挖出一个0x200大小的空洞. 由于nft_set.bindings仅位于nft_set+0x10的位置, 所以我们近似拥有整个set的控制能力.

其中我们感兴趣的字段有:

  1. udata, udlen, 配合可以完成任意虚拟地址读(set没有更新udata的功能, 所以没办法写).
  2. ops, 可以劫持RIP.

在任意虚拟地址读之前, 我们需要泄露一些虚拟地址信息. 通过使用user_key_payload来占位UAF的表达式, 触发脱链操作即可在data区域写入一个set地址.

然后用任意虚拟地址读原语读出set的内容即可泄露内核基址.由于测试环境中的nf_tables是直接编译到内核中的, 所以可以直接拿到内核基址. 如果是以模块形式的话, 可能还得修改udlen进行堆上越界读泄露内核基址.

接下来是劫持ops, 其实随意选择即可. 原作者选用gc_init指针, 调用时rdi指向内容可控的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
struct nft_set_ops {
bool (*lookup)(const struct net *net,
const struct nft_set *set,
const u32 *key,
const struct nft_set_ext **ext);
bool (*update)(struct nft_set *set,
const u32 *key,
void *(*new)(struct nft_set *,
const struct nft_expr *,
struct nft_regs *),
const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_set_ext **ext);
bool (*delete)(const struct nft_set *set,
const u32 *key);

int (*insert)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem,
struct nft_set_ext **ext);
void (*activate)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem);
void * (*deactivate)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem);
bool (*flush)(const struct net *net,
const struct nft_set *set,
void *priv);
void (*remove)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem);
void (*walk)(const struct nft_ctx *ctx,
struct nft_set *set,
struct nft_set_iter *iter);
void * (*get)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem,
unsigned int flags);

u64 (*privsize)(const struct nlattr * const nla[],
const struct nft_set_desc *desc);
bool (*estimate)(const struct nft_set_desc *desc,
u32 features,
struct nft_set_estimate *est);
int (*init)(const struct nft_set *set,
const struct nft_set_desc *desc,
const struct nlattr * const nla[]);
void (*destroy)(const struct nft_set *set);
void (*gc_init)(const struct nft_set *set);

unsigned int elemsize;
};

gc_init函数在nft_set_elem_expr_alloc中触发, 且表达式需要带有NFT_EXPR_GC标志, 在该版本内核中仅有nft_connlimit表达式带有这一标志. 添加一个nft_connlimit表达式的集合元素, 即可在nf_tables_new_setelem时触发. 注意nft_connlimit表达式对应的内核代码默认是以模块形式编译的, 所以需要提前申请一次, 触发该模块的加载.

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
struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
const struct nft_set *set,
const struct nlattr *attr)
{
struct nft_expr *expr;
int err;

expr = nft_expr_init(ctx, attr);
if (IS_ERR(expr))
return expr;

err = -EOPNOTSUPP;
if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
goto err_set_elem_expr;

if (expr->ops->type->flags & NFT_EXPR_GC) {
if (set->flags & NFT_SET_TIMEOUT)
goto err_set_elem_expr;
if (!set->ops->gc_init)
goto err_set_elem_expr;
set->ops->gc_init(set);
}

return expr;

err_set_elem_expr:
nft_expr_destroy(ctx, expr);
return ERR_PTR(err);
}

原作者将gc_init指向perf_swevent_del函数, 制造一个任意地址写改modprobe_path提权. 这会造成oops但一般情况下(!panic_on_oops)不会导致内核崩溃.

笔者在这里选择经典的work_for_cpu_fn, 调用一次commit_creds(init_cred)提权.
在unshare后这样能否提到”真正”的root笔者暂时还不太确定, 但至少完成user_namespace的切换是没问题的, 因为user_namespace的指针在cred结构中而不是nsproxy. (maybe 接下来会去学一下linux权限控制和namespace的东西).

笔者也尝试了一些传统意义上需要root权限的操作, 也确实都成功了.

EXP

写得比较杂乱(临近期末+写这个exp的用时超出预期, 没时间再改), 这里不贴了, 讲一下流程和细节.
完整EXP及测试环境可在这里获取: CVE-2022-32250

  1. 第一次触发UAF write, 将binding_set的地址写入user_key_payload再读出.
  2. 第二次触发UAF write, 将binding_set2的地址写入cgroup_fs_context->release_agent, 释放掉binding_set2.
  3. 通过setxattr+fuse修改binding_set2, 使得udata指向刚泄露出的binding_set的地址, nf_tables_getset读出binding_set的内容, 其中可以泄露binding_set.list.prev指向的binding_set2的地址, 以及binding_set.ops. (一个细节, 为了能dump binding_set2, 需要提前在binding_set的userdata中存放一个name字符串)
  4. 再次通过setxattr+fuse修改binding_set2, 劫持ops, 使得在调用gc_init时调用到work_for_cpu_fn(rdi为binding_set2的地址), 调用commit_creds(init_cred)完成提权.

something

  1. theori在 这篇文章中采用了与原作者不同的利用方式,主要使用了posix_msg_tree_node结构. 先制造一个UAF的expr1, 使用posix_msg_tree_node占据expr1的空间,通过一次UAF write将posix_msg_tree_node.msg_list.next指向另一个UAF的表达式expr2, 用user_key_payload占据expr2空间. 此时user_key_payload.data区域就会被当作msg_msg结构来解释. 这给了我们越界读的机会(由于hardened user copy , 还是只能读相邻的泄露).泄露基址后, 通过msg_msg脱链时进行unlink attack, 改写modprobe_path.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct posix_msg_tree_node {
struct rb_node rb_node;
struct list_head msg_list;
int priority;
};

struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};

struct user_key_payload {
struct rcu_head rcu; /* 0 10*/
unsigned short datalen; /* 10 2 */
/* padding */
char data[] __aligned(__alignof__(u64)); /* 18 -- */
};

参考文章

https://www.nccgroup.com/us/research-blog/settlers-of-netlink-exploiting-a-limited-uaf-in-nf_tables-cve-2022-32250/
https://web.archive.org/web/20221126100444/https://blog.theori.io/research/CVE-2022-32250-linux-kernel-lpe-2022/

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信