背景
近期,开源安全社区oss-security披露了多个Linux内核netfilter模块相关漏洞,漏洞均出现在netfilter子系统nftables中,其中两个漏洞在内核中存在多年,并且均可用于内核权限提升。漏洞编号分别为:CVE-2022-32250,该漏洞类型为释放重引用;CVE-2022-1972,该漏洞类型为越界读写;CVE-2022-34918,该漏洞类型为堆溢出。
02 相关介绍
2.1 netfilter简单介绍
netfilter是一个开源项目,用于执行数据包过滤,也就是Linux防火墙。这个项目经常被提到iptables,它是用于配置防火墙的用户级应用程序。2014年,netfilter 防火墙添加了一个新子系统,称为nftables,可以通过nftables用户级应用程序进行配置。
2.2 nftables简单介绍
nftables取代了流行的{ip,ip6,arp,eb}表。该软件提供了一个新的内核数据包分类框架,该框架基于特定于网络的虚拟机 (VM) 和新的nft用户空间命令行工具。nftables重用了现有的netfilter子系统,例如现有的钩子基础设施、连接跟踪系统、NAT、用户空间队列和日志子系统。对于nftables,只需要扩展expression即可,用户自行编写expression,然后让nftables虚拟机执行它。nftables框架的数据结构如下所示:
Table{
Chain[
Rule
(expression1,expression2,expression3,...)
| ||--> expression_action
||--> expression_action
|-->expression_action
Rule
(expression,expression,expression,...)
...
],
Chain[
...
],
...
}
Table为chain的容器,chain为rule的容器,rule为expression的容器,expression响应action。构造成由table->chain->rule->expression四级组成的数据结构。
03 nftables子系统实现分析
通过发送netlink消息数据包来操作nftables,从netlink到nftables的调用过程如下所示:
在nfnetlink_rcv_batch()函数中对netlink消息进行操作。从netlink消息中剥离出nftable载荷,并依次进行对应处理。进入nfnetlink_rcv_batch()函数,首先根据subsys_id获得nfnetlink_subsystem。
这里nftables类型为0xa。获得subsystem后,然后拿到子系统对应的回调客户端。通过nfnetlink_find_client()实现该功能。
对应nftables回调客户端,在\\net\\netfilter\\nf_tables_api.c直接找到定义:
.cb数据域便是回调客户端。可以看到针对不同的nftables操作,定义了多个回调客户端,例如table的增删改查操作。
然后再从netlink消息中剥离出netlink载荷,根据不同的消息类型进行不同的分发处理,消息类型如下所示:
依次调用nc->call_batch()进一步处理。
开始剥洋葱式分析,第一层操作创建一个table,响应函数为nf_tables_newtable()。
先通过nla[NFTA_TABLE_NAME]来查找是否存在该table,如果存在,调用nf_tables_updtable(),如果不存在就创建该表。
创建完成后,然后就是必要的初始化操作。
初始化table->chains链表,table->sets链表,table->objects链表,table->flowtables链表。然后将table加到nftbales上下文中,最后将table链到net->nft.tables中。
第二步操作创建一个chain,响应函数为nf_tables_newchain()。首先先找table,无table直接退出。
找到table后,就找chain是否存在,存在进入update,不存在则添加一个新chain。
这里提供了两种方式寻找chain,通过nla[NFTA_CHAIN_HANDLE]和nla[NFTA_CHAIN_NAME]进行寻找。未找到就调用nf_tables_addchain()创建之。
具体看该函数实现,首先分配一个chain,然后初始化chain->rules链表,并设置chain->hanle和chain->table。随后进行初始化chain->name等操作,并将chain链到table->chains中。
第三步操作创建一个rule,响应函数为nf_tables_newrule(),首先相继获取table和chain,如果设置了nla[NFTA_RULE_EXPRESSIONS],会先把所有的expression遍历出来,计算其总值放在size中。
如果设置了nla[NFTA_RULE_USERDATA],获取userdata的大小放在usize中,最后分配内存,创建一个rule,随即初始化相关数据域。
第四步操作创建expression,其实这一步和创建rule是连在一起的。都在nf_tables_newrule()函数中实现。expresssion总共有如下多种类型。
将用户层传进来的expression剥离出来后,依次放在rule中。
这里调用了nf_tables_newexpr()函数,会根据expression类型对其进行初始化。
以上就是一个完整的table->chain->rule->expression的创建过程。
04 相关漏洞分析
其实,回调客户端中还提供了其他元素的创建操作,比如创建set(集合)。set只需要依附于table即可,可构成table->set->expression数据结构。
4.1 CVE-2022-32250
该漏洞是释放重引用漏洞,出现在nf_tables_newset()函数中,该函数是创建一个set。set中也可以包含各种expression。首先看下nf_tables_newset()函数实现。
同样地,先获取table,再获取set,如果set不存在就创建之。接下来根据set类型获取对应的ops操作集,并确定set的大小,并分配之。
接下来,如果设置了nla[NFTA_SET_EXPR],并进入调用nft_set_elem_expr_alloc()函数进行处理。
进入该函数看具体实现。
行5128,首先进入nft_expr_init()函数分配一个expr,该函数具体实现如下所示。
行2686,调用kzalloc()分配一个expr,这是第一次操作,nft_expr结构体定义为:
data为动态数组,nft_expr本身是个不固定大小的结构体,使用时候才确定的大小,data处可以存放多种类型的expression,并匹配对应的ops操作集合。这里以nft_lookup为例子,nft_lookup结构体定义如下:
将expr合起拼接等于nft_lookup_expr,分配完expr后,就进入nf_tables_newexpr()函数,初始化expr(nft_lookup_expr),该函数实现如下所示:
行2652,调用对应的ops->init()函数进行初始化,具体看对应的nft_lookup_init()函数实现。
行64,首先获得priv指针,即expr->data,也即是nft_lookup,进行一些初始化操作后,最后进行绑定操作。
行113,调用nf_tables_bind_set()进行绑定,具体看该函数实现。
行4490,宏list_add_tail_rcu将priv即nft_lookup绑定到set->binding中,即set->binding链表中引用了nft_lookup的地址,一路正常从nft_expr_init()函数返回后,具体看如下操作。
行5136,判断expr->ops->type->flags是否为NFT_EXPR_STATEFUL,如果不是,那就直接跳到nft_expr_destory()函数进行释放,具体看该函数实现。
行2727,首先调用nf_tables_expr_destroy()释放set。
最后调用expr->ops->destroy()函数,这里对应函数是nft_lookup_destory()。
获取priv,进一步调用nf_tables_destroy_set()函数,具体看该函数实现。
行4532,判断set->bindings链表是否为空同时是否为匿名set,如果否,不释放set并退出。
接下来回到nft_expr_destroy()函数中,释放expr,这是第二步操作。这就出现了问题,expr被释放了,但是set->bindings链表中却引用了expr->nft_lookup->binding的地址指针,其调用流程如下所示:
nf_tables_newset
nft_set_elem_expr_alloc
nft_expr_init
kzalloc 分配expr
nf_tables_newexpr
nft_lookup_init
nf_tables_bind_set
list_add_tail_rcu set->bindings引用expr->nft_lookup->binding
nft_expr_destroy
nf_tables_expr_destroy
nft_lookup_destroy
nf_tables_destroy_set 未释放set
kfree 释放expr
set->binding链表保存了对释放后内存的指针引用,导致UAF。
4.2 CVE-2022-1972
该漏洞是一个越界读写漏洞,可以越界读写多个字节,继续看nf_tables_newset()函数实现。
如果设置了nla[NFTA_SET_DESC],会调用nf_tables_set_desc_parse()函数解析nla[NFTA_SET_DESC]对应的数据。具体看该函数实现。
首先通过nla_parse_nested_deprecated()函数循环解析出数据中各种属性标签地址。如果设置了nla[NFTA_SET_DESC_CONCAT],进入nft_set_desc_concat()函数进行解析。
在nla_for_each_nested()循环中,依次遍历出nlattar并调用nft_set_desc_concat_parse()函数处理,具体看该函数实现。
行4070,从nlattr中读取len,行4075,将len写入到desc->field_len数组中。该desc结构体定义如下:
其field_len数组大小为16,同时desc->field_count做自增操作,因此很容易发生溢出。如果这里溢出后,可以覆盖到field_count,不过len最大为0x40。如果覆盖到field_count后,返回到nf_tables_newset()函数中,后续操作如下:
会把set->field_count设置为desc.field_count,这个已经被覆盖了,因此set->field_count也被修改了,然后就是循环将desc.field_len数组中数据写到set->field_len数组中,看set结构体定义如下:
可以将desc->field_len数组中越界读取的数据越界回写到set->field_len数组后面的数据域中。这里可进行信息泄露获得内核指针数据。通过nf_tables_fill_set()函数进行泄露,当用户读取set相关属性时会调用该函数。两种方式,较为方便一种:
将set->timeout,set->gc_init和set->policy返回给用户。其desc定义在栈内存中,很容易越界读取栈上数据,泄露栈数据。另一种方式为覆盖set->udlen,它被覆盖成一个较大的数值,然后通过set->udata读取,这样可以泄露更多数据。
4.3 CVE-2022-34918
该漏洞为堆溢出漏洞,出现在响应函数nf_tables_newsetelem()中,该函数部分实现如下:
首先判断nla[NFTA_SET_ELEM_LIST_ELEMENTS]是否为空,然后根据table获取set。行5622,通过nla_for_each_nested宏遍历nla[NFTA_SET_ELEM_LIST_ELEMENTS]对应的数据,调用nft_add_set_elem()函数进行设置。该函数部分实现如下:
进一步剥离出nla[NFTA_SET_ELEM_DATA]对应的数据,调用nft_setelem_parse_data()函数进行处理,该函数实现如下:
调用nft_data_init()函数将data读出来,同时回写desc,其中包括desc->len。行4951,如果type不是NFT_DATA_VERDICT同时desc->len不等于set->dlen,释放data并退出。如果将type设置为NFT_DATA_VERDICT,那么该判断语句不会成立,并不会判断desc->len和set->dlen是否相同,并成功返回。这就可以构造data_len不超过NFT_DATA_VALUE_MAXLEN,其为64,但是同时不等于set->dlen即可,在后续操作中就可以发生堆溢出。
这里说明一下set->dlen和desc->len的关系。在nf_table_newset()函数中,当设置了nla[NFTA_SET_DATA_TYPE]时,type不是NFT_DTAA_VERDICT并且nla[NFTA_SET_DATA_LEN]不为空时,会将nla[NFTA_SET_DATA_LEN]对应的数据赋值给desc.dlen。
后面初始化set时,并将desc.dlen赋值给set.dlen。
回到nft_add_set_elem()函数中,后续实现如下:
该拷贝的对应数据都拷贝完成后,调用nft_set_elem_init()函数进行初始化。具体看该函数实现。
行5159,调用kzalloc()函数分配内存,大小为elemsize+tmpl->len,这里的tmpl->len已经包括了desc.dlen。正常情况下desc.dlen应该是等于set->dlen的。行5170,如果存在nla[NFT_SET_EXT_DATA]时,并调用memcpy将data拷贝到elem中,长度为set->dlen,我们清楚set->dlen是不等于desc.dlen的,tmpl->len小了,因此发生溢出。
参考链接:
[1] https://blog.51cto.com/dog250/1583015
[2] https://www.openwall.com/lists/oss-security/2022/05/31/1
[3] https://www.openwall.com/lists/oss-security/2022/06/02/1
[4] https://www.openwall.com/lists/oss-security/2022/07/02/2
来源:ADLab