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

前言

学二进制的第一步就是斩断情丝!

CVE-2024-1085

前置知识

为什么如此之多的前置知识,主要是在nftables子系统浅分析的分析并不彻底所以在遇到一个新的问题的时候还是有必要回过头去细致分析的。

C
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
/*
* Generic transaction helpers
*/

/* Check if this object is currently active. */
#define nft_is_active(__net, __obj) \
(((__obj)->genmask & nft_genmask_cur(__net)) == 0)

/* Check if this object is active in the next generation. */
#define nft_is_active_next(__net, __obj) \
(((__obj)->genmask & nft_genmask_next(__net)) == 0)

/* This object becomes active in the next generation. */
#define nft_activate_next(__net, __obj) \
(__obj)->genmask = nft_genmask_cur(__net)

/* This object becomes inactive in the next generation. */
#define nft_deactivate_next(__net, __obj) \
(__obj)->genmask = nft_genmask_next(__net)

/* After committing the ruleset, clear the stale generation bit. */
#define nft_clear(__net, __obj) \
(__obj)->genmask &= ~nft_genmask_next(__net)
#define nft_active_genmask(__obj, __genmask) \
!((__obj)->genmask & __genmask)

在看源码的过程中会看到很多上述宏,其实在nftables中genmask用于标记对象的状态,主要是有两个重要的标记,分别是该对象当前任务是否可用以及该对象下一次任务是否可以用。

当一个对象被删除时通常会被调用nft_deactivate_next函数来设置下次任务不可用。

漏洞分析

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int nft_setelem_catchall_deactivate(const struct net *net,
struct nft_set *set,
struct nft_set_elem *elem)
{
struct nft_set_elem_catchall *catchall;
struct nft_set_ext *ext;

list_for_each_entry(catchall, &set->catchall_list, list) {
ext = nft_set_elem_ext(set, catchall->elem);
if (!nft_is_active(net, ext))
continue;

kfree(elem->priv);
elem->priv = catchall->elem;
nft_set_elem_change_active(net, set, ext);
return 0;
}

return -ENOENT;
}

该漏洞发生于nft_setelem_catchall_deactivate函数,这里会遍历当前set中所有的catchall_list然后会通过nft_is_active函数判断element在当前任务是否可用,随后修改掉该element的active状态。

C
1
2
3
4
5
6
static inline void nft_set_elem_change_active(const struct net *net,
const struct nft_set *set,
struct nft_set_ext *ext)
{
ext->genmask ^= nft_genmask_next(net);
}

这里改变active状态所使用的函数内部实现是与nft_genmask_next进行异或,也就是其实修改的是其在下一次任务中的活跃状态,但是前面用于判断的是这一次任务,这也就导致可以两次del它,也就是Double Free。

C
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
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;
unsigned int base_seq, gc_seq;
LIST_HEAD(set_update_list);
struct nft_trans_elem *te;
struct nft_chain *chain;
struct nft_table *table;
LIST_HEAD(adl);
int err;

// ......

list_for_each_entry_safe(trans, next, &nft_net->commit_list, list) {
nf_tables_commit_audit_collect(&adl, trans->ctx.table,
trans->msg_type);
switch (trans->msg_type) {
// ......
case NFT_MSG_DELSETELEM:
case NFT_MSG_DESTROYSETELEM:
te = (struct nft_trans_elem *)trans->data;

nf_tables_setelem_notify(&trans->ctx, te->set,
te->elem_priv,
trans->msg_type);
nft_setelem_remove(net, te->set, te->elem_priv);
if (!nft_setelem_is_catchall(te->set, te->elem_priv)) {
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;
// ......
}
}

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);

nft_gc_seq_end(nft_net, gc_seq);
nft_net->validate_state = NFT_VALIDATE_SKIP;
nf_tables_commit_release(net);

return 0;
}

我们两次删除element的时候会两次进入如上case分支中,虽然在nft_setelem_remove函数中会free掉set->catchall但是并不影响第二次的删除,因为这里的elem_priv是从trans中拿出来的。

利用分析

目前已经可以实现Double Free了,所以利用方法就十分简单,通过DF我们可以控制table->udata指向该内存区域,然后在内存区域中放上其余堆块例如expr来泄漏出它的ops来获得内核基地址,随后在分配set来获取该内存获得内核堆地址,最后先使用expr占据该内存随后使用table->udata来修改其ops来劫持rip。

需要注意的是内核是存在Double Free的检测的。

C
1
2
3
4
5
6
7
8
9
10
11
static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
unsigned long freeptr_addr = (unsigned long)object + s->offset;

#ifdef CONFIG_SLAB_FREELIST_HARDENED
BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif

freeptr_addr = (unsigned long)kasan_reset_tag((void *)freeptr_addr);
*(freeptr_t *)freeptr_addr = freelist_ptr_encode(s, fp, freeptr_addr);
}

所以我们在两次删除element的中间可以穿插一个删除其他结构体来避免检测。

CVE-2023-31248

漏洞分析

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static struct nft_chain *nft_chain_lookup_byid(const struct net *net,
const struct nft_table *table,
const struct nlattr *nla)
{
struct nftables_pernet *nft_net = nft_pernet(net);
u32 id = ntohl(nla_get_be32(nla));
struct nft_trans *trans;

list_for_each_entry(trans, &nft_net->commit_list, list) {
struct nft_chain *chain = trans->ctx.chain;

if (trans->msg_type == NFT_MSG_NEWCHAIN &&
chain->table == table &&
id == nft_trans_chain_id(trans))
return chain;
}
return ERR_PTR(-ENOENT);
}

漏洞出现在这个函数,当通过id寻找chain时是不会判断当前chain的活跃状态的,如果此时在同一个batch中,该chain已被删除这里依旧可以拿到目标chain,例如在chain2中有一个expr会goto到chain1但是chain1在当前batch中已被删除,那么chain2依旧会引用chain1,不过这里虽然会删除chain1但是其并不会被free掉,因为chain1->use会因为引用加一。

C
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);
}

在commit最终处理release时最后其实是通过schedule_work新起一个线程来调用调度任务。

所以这里可以使用条件竞争来扩大漏洞影响到UAF,也就是在执行trans_destroy_work之前,第二个batch进入到内核中并开始运行。

第一个batch如上面出发漏洞那样做,最后会正确执行调用trans_destroy_work,但是最后在其中不会free掉目标chain1。

第二个batch主要做的事就是删除掉chain2中的rule,并且在chain2中错误创建一个rule,那么会因为第二次的错误创建而导致不会进入到nf_tables_commit中,也就不会真正的删除掉chain2中原本的rule(因为真正删除是在commit中完成的),但是会因为存在删除chain2的行为导致chain1->use减一,从而使其为0。

那么如果能够在进入nf_tables_chain_destroy函数前执行完batch2那么在真正进入该函数时chain1的use依旧变为了0但是chain1依旧被chain2的rule所引用从而实现uaf。

因为这一漏洞的利用方式和前面的cve过于类似就不再赘述。

CVE-2023-3390

前置知识

C
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_lookup_init(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[])
{
struct nft_lookup *priv = nft_expr_priv(expr);
u8 genmask = nft_genmask_next(ctx->net);
struct nft_set *set;
u32 flags;
int err;

if (tb[NFTA_LOOKUP_SET] == NULL ||
tb[NFTA_LOOKUP_SREG] == NULL)
return -EINVAL;

set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],
tb[NFTA_LOOKUP_SET_ID], genmask);
if (IS_ERR(set))
return PTR_ERR(set);

err = nft_parse_register_load(tb[NFTA_LOOKUP_SREG], &priv->sreg,
set->klen);
if (err < 0)
return err;

if (tb[NFTA_LOOKUP_FLAGS]) {
flags = ntohl(nla_get_be32(tb[NFTA_LOOKUP_FLAGS]));

if (flags & ~NFT_LOOKUP_F_INV)
return -EINVAL;

if (flags & NFT_LOOKUP_F_INV) {
if (set->flags & NFT_SET_MAP)
return -EINVAL;
priv->invert = true;
}
}

if (tb[NFTA_LOOKUP_DREG] != NULL) {
if (priv->invert)
return -EINVAL;
if (!(set->flags & NFT_SET_MAP))
return -EINVAL;

err = nft_parse_register_store(ctx, tb[NFTA_LOOKUP_DREG],
&priv->dreg, NULL, set->dtype,
set->dlen);
if (err < 0)
return err;
} else if (set->flags & NFT_SET_MAP)
return -EINVAL;

priv->binding.flags = set->flags & NFT_SET_MAP;

err = nf_tables_bind_set(ctx, set, &priv->binding);
if (err < 0)
return err;

priv->set = set;
return 0;
}

这次接触的新的一个expr类型,这里是lookup类型,该类型会绑定一个set。

C
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
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);

这里就是简单的set绑定到expr上,并且在最后对set的use成员加一。

漏洞分析

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{
// ......

err_destroy_flow_rule:
if (flow)
nft_flow_rule_destroy(flow);
err_release_rule:
nf_tables_rule_release(&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;
}

漏洞又发生在nf_tables_newrule内,在nf_tables_rule_release函数内部。

C
1
2
3
4
5
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);
}

首先看较为熟悉的nft_rule_expr_deactivate函数,这个函数的主要作用就是调用expr对应的deactivate函数。

C
1
2
3
4
5
6
7
8
static void nft_lookup_deactivate(const struct nft_ctx *ctx,
const struct nft_expr *expr,
enum nft_trans_phase phase)
{
struct nft_lookup *priv = nft_expr_priv(expr);

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

函数内部直接调用了nf_tables_deactivate_set函数。

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}
}
EXPORT_SYMBOL_GPL(nf_tables_deactivate_set);

在这个函数中会根据不同的标志位进入到不同分支,这里的漏洞就是因为给了错误的标志位所引起的,这里的标志位为NFT_TRANS_RELEASE,这会直接对set的use成员进行减一操作随后进入到nf_tables_unbind_set函数内部。

C
1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

该函数首先对binding解绑,然后判断set的bindings是否为空,如果为空并且该集合为匿名集合那么就会对该集合的list进行脱链(也就是从table中取出)。

回到前面的init函数,其中是通过nft_set_lookup_global函数找到对应的集合的。

C
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
static struct nft_set *nft_set_lookup_byid(const struct net *net,
const struct nft_table *table,
const struct nlattr *nla, u8 genmask)
{
struct nftables_pernet *nft_net = nft_pernet(net);
u32 id = ntohl(nla_get_be32(nla));
struct nft_trans *trans;

list_for_each_entry(trans, &nft_net->commit_list, list) {
if (trans->msg_type == NFT_MSG_NEWSET) {
struct nft_set *set = nft_trans_set(trans);

if (id == nft_trans_set_id(trans) &&
set->table == table &&
nft_active_genmask(set, genmask))
return set;
}
}
return ERR_PTR(-ENOENT);
}

struct nft_set *nft_set_lookup_global(const struct net *net,
const struct nft_table *table,
const struct nlattr *nla_set_name,
const struct nlattr *nla_set_id,
u8 genmask)
{
struct nft_set *set;

set = nft_set_lookup(table, nla_set_name, genmask);
if (IS_ERR(set)) {
if (!nla_set_id)
return set;

set = nft_set_lookup_byid(net, table, nla_set_id, genmask);
}
return set;
}
EXPORT_SYMBOL_GPL(nft_set_lookup_global);

从上述可以看到,如果我们通过名字找失败的话就会直接通过id去找,并且这里通过id去找事在trans中去寻找最后关于table的校验也只是从set中的table成员拿来对比,所以他也是能够被继续拿到的。

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}

回到nf_tables_rule_release函数,后续会接着调用nf_tables_rule_destroy函数,这个函数还是比较眼熟,又回调用expr->ops->destroy函数。

C
1
2
3
4
5
6
7
static void nft_lookup_destroy(const struct nft_ctx *ctx,
const struct nft_expr *expr)
{
struct nft_lookup *priv = nft_expr_priv(expr);

nf_tables_destroy_set(ctx, priv->set);
}
C
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_set_destroy(const struct nft_ctx *ctx, struct nft_set *set)
{
int i;

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

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

set->ops->destroy(set);
nft_set_catchall_destroy(ctx, set);
kfree(set->name);
kvfree(set);
}

void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set)
{
if (list_empty(&set->bindings) && nft_set_is_anonymous(set))
nft_set_destroy(ctx, set);
}
EXPORT_SYMBOL_GPL(nf_tables_destroy_set);

这里会首先验证set的bindings是否为空,随后检测是否是匿名集合。在进入nft_set_destroy后回西安判断use成员是否为0,最终直接free掉set和set->name

所以这里存在明显的UAF,不过利用起来稍显麻烦,这里简单介绍一下。

利用分析

其实也不是利用起来麻烦,主要是了解为什么可以这样用这样很麻烦。

CONFIG_KMALLOC_SPLIT_VARSIZE

首先是CONFIG_KMALLOC_SPLIT_VARSIZE标志位,该标志位会将可变大小的对象从dyn-kmalloc-cg-xx中申请,但是在原作者的利用过程中利用的全是可变大小的结构体,并且可变大小的结构体挺多的。

CONFIG_DEBUG_LIST

再就是这次利用最为核心的标志位CONFIG_DEBUG_LIST,因为可见的是在前面如果想要完成Double Free那么第二次free的时候会再一次对set进行脱链操作,而没有该标志位系统会直接kernel panic

C
1
2
3
4
5
static inline void list_del_rcu(struct list_head *entry)
{
__list_del_entry(entry);
entry->prev = LIST_POISON2;
}

上面就是删除链的操作,主要逻辑还是在__list_del_entry中。

C
1
2
3
4
5
6
7
static inline void __list_del_entry(struct list_head *entry)
{
if (!__list_del_entry_valid(entry))
return;

__list_del(entry->prev, entry->next);
}

这里关注__list_del_entry_valid函数,其主要用于检测是否合法。

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifdef CONFIG_DEBUG_LIST
extern bool __list_add_valid(struct list_head *new,
struct list_head *prev,
struct list_head *next);
extern bool __list_del_entry_valid(struct list_head *entry);
#else
static inline bool __list_add_valid(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
return true;
}
static inline bool __list_del_entry_valid(struct list_head *entry)
{
return true;
}
#endif

该函数定义由是否存在该标志位决定,如果存在那么就是外部引用的。

C
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
bool __list_del_entry_valid(struct list_head *entry)
{
struct list_head *prev, *next;

prev = entry->prev;
next = entry->next;

if (CHECK_DATA_CORRUPTION(next == NULL,
"list_del corruption, %px->next is NULL\n", entry) ||
CHECK_DATA_CORRUPTION(prev == NULL,
"list_del corruption, %px->prev is NULL\n", entry) ||
CHECK_DATA_CORRUPTION(next == LIST_POISON1,
"list_del corruption, %px->next is LIST_POISON1 (%px)\n",
entry, LIST_POISON1) ||
CHECK_DATA_CORRUPTION(prev == LIST_POISON2,
"list_del corruption, %px->prev is LIST_POISON2 (%px)\n",
entry, LIST_POISON2) ||
CHECK_DATA_CORRUPTION(prev->next != entry,
"list_del corruption. prev->next should be %px, but was %px. (prev=%px)\n",
entry, prev->next, prev) ||
CHECK_DATA_CORRUPTION(next->prev != entry,
"list_del corruption. next->prev should be %px, but was %px. (next=%px)\n",
entry, next->prev, next))
return false;

return true;

}
EXPORT_SYMBOL(__list_del_entry_valid);

因为在第一次脱链结束后,entry->prev就被赋值为LIST_POISON2了,所以这里会直接返回flase,从而不会进行地址操作也就不会出现kernel panic。

CONFIG_SLAB_FREELIST_HARDENED

这里有点怪的是,在原作者文档中给出了freelist_ptr_decode函数的定义。

C
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
static inline void *freelist_ptr_decode(const struct kmem_cache *s,
freeptr_t ptr, unsigned long ptr_addr,
struct slab *slab)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED
void *decoded;
/* TODO: maybe let slab_to_virt load a virtual address from
* struct slab instead of using arithmetic for the translation?
*/
unsigned long slab_base = (unsigned long)slab_to_virt(slab);

/*
* When CONFIG_KASAN_SW/HW_TAGS is enabled, ptr_addr might be tagged.
* Normally, this doesn't cause any issues, as both set_freepointer()
* and get_freepointer() are called with a pointer with the same tag.
* However, there are some issues with CONFIG_SLUB_DEBUG code. For
* example, when __free_slub() iterates over objects in a cache, it
* passes untagged pointers to check_object(). check_object() in turns
* calls get_freepointer() with an untagged pointer, which causes the
* freepointer to be restored incorrectly.
*/
decoded = (void *)(ptr.v ^ s->random ^
swab((unsigned long)kasan_reset_tag((void *)ptr_addr)));

/*
* This verifies that the SLUB freepointer does not point outside the
* slab. Since at that point we can basically do it for free, it also
* checks that the pointer alignment looks vaguely sane.
* However, we probably don't want the cost of a proper division here,
* so instead we just do a cheap check whether the bottom bits that are
* clear in the size are also clear in the pointer.
* So for kmalloc-32, it does a perfect alignment check, but for
* kmalloc-192, it just checks that the pointer is a multiple of 32.
* This should probably be reconsidered - is this a good tradeoff, or
* should that part be thrown out, or do we want a proper accurate
* alignment check (and can we make it work with acceptable performance
* cost compared to the security improvement - probably not)?
*
* NULL freepointer must be special-cased.
* Write it in a way that gives the compiler a chance to avoid adding
* an unpredictable branch.
*/
slab_base = decoded ? slab_base : 0;
if (CHECK_DATA_CORRUPTION(
((unsigned long)decoded & slab->align_mask) != slab_base,
"bad freeptr (encoded %lx, ptr %px, base %lx, mask %lx",
ptr.v, decoded, slab_base, slab->align_mask))
return NULL;
return decoded;
#else
return (void*)ptr.v;
#endif
}

这里会检测地址是否在slab_base中,但是我在elixir.bootlin.com中切换了很多版本,死活找不到这段代码,不过这里有没有这段代码其实影响不大,因为原作者在这里使用了错位的堆块来利用(感觉是不必要的),所以是需要考虑一下这个对齐是否会引起panic,不过这里使用的是CHECK_DATA_CORRUPTION也只是会引起警告,并且如果不是对齐的堆地址只会返回NULL故在后面的利用结束后虽然freelist的内容依旧损坏但是依旧不会出现kernel panic。

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,
unsigned long ptr_addr)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED
/*
* When CONFIG_KASAN_SW/HW_TAGS is enabled, ptr_addr might be tagged.
* Normally, this doesn't cause any issues, as both set_freepointer()
* and get_freepointer() are called with a pointer with the same tag.
* However, there are some issues with CONFIG_SLUB_DEBUG code. For
* example, when __free_slub() iterates over objects in a cache, it
* passes untagged pointers to check_object(). check_object() in turns
* calls get_freepointer() with an untagged pointer, which causes the
* freepointer to be restored incorrectly.
*/
return (void *)((unsigned long)ptr ^ s->random ^
swab((unsigned long)kasan_reset_tag((void *)ptr_addr)));
#else
return ptr;
#endif
}

不过在我看到的内容里是没有上面那些代码的,所以在利用结束后freelist被破坏的数据被申请时就极有可能出现kernel panic的问题,不过这里我能想到的解决办法就是开始申请一部分会污染掉freelist对应大小的堆块,最后放入其中避免出现panic。

这个CVE的具体利用方法可以参考CVE-2021-22555利用方法分析文章,利用方式基本一致(并且我觉得错位堆块是无意义的,当然可能是我没有理解到原作者使用此方式的背后含意)。

CVE-2023-3777

漏洞分析

该漏洞类似于上一篇中的CVE-2023-4015漏洞。

C
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
static int nf_tables_delrule(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 nft_chain *chain = NULL;
struct net *net = info->net;
struct nft_table *table;
struct nft_rule *rule;
struct nft_ctx ctx;
int err = 0;

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

if (nla[NFTA_RULE_CHAIN]) {
chain = nft_chain_lookup(net, table, nla[NFTA_RULE_CHAIN],
genmask);
if (IS_ERR(chain)) {
if (PTR_ERR(chain) == -ENOENT &&
NFNL_MSG_TYPE(info->nlh->nlmsg_type) == NFT_MSG_DESTROYRULE)
return 0;

NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_CHAIN]);
return PTR_ERR(chain);
}
if (nft_chain_is_bound(chain))
return -EOPNOTSUPP;
}

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

if (chain) {
if (nla[NFTA_RULE_HANDLE]) {
rule = nft_rule_lookup(chain, nla[NFTA_RULE_HANDLE]);
if (IS_ERR(rule)) {
if (PTR_ERR(rule) == -ENOENT &&
NFNL_MSG_TYPE(info->nlh->nlmsg_type) == NFT_MSG_DESTROYRULE)
return 0;

NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_HANDLE]);
return PTR_ERR(rule);
}

err = nft_delrule(&ctx, rule);
} else if (nla[NFTA_RULE_ID]) {
rule = nft_rule_lookup_byid(net, chain, nla[NFTA_RULE_ID]);
if (IS_ERR(rule)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_ID]);
return PTR_ERR(rule);
}

err = nft_delrule(&ctx, rule);
} else {
err = nft_delrule_by_chain(&ctx);
}
} else {
list_for_each_entry(chain, &table->chains, list) {
if (!nft_is_active_next(net, chain))
continue;

ctx.chain = chain;
err = nft_delrule_by_chain(&ctx);
if (err < 0)
break;
}
}

return err;
}

该漏洞发生于nf_tables_delrule函数中,可以看到在给定chain时,会在最后对chain是否绑定做检测,但是在没有给定chain时会直接遍历chain删除所有rule。

删除rule的流程就不过多介绍了,前面已经分析过很多次了,所以很容易想到这里会出现use成员下溢的现象最终转化为uaf。


参考链接:

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

https://starlabs.sg/blog/2023/09-nftables-adventures-bug-hunting-and-n-day-exploitation/#cve-2023-31248

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

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

 评论
评论插件加载失败
Powered By Valine
v1.5.2
由 Hexo 驱动 & 主题 Keep v4.2.2
本站由 提供部署服务
总字数 335.6k 访客数 4771 访问量 7174