Linux内核nftables子系统研究与漏洞分析
发布时间 2022-07-18一、背景
近期,开源安全社区oss-security披露了多个Linux内核netfilter模块相关漏洞,漏洞均出现在netfilter子系统nftables中,其中两个漏洞在内核中存在多年,并且均可用于内核权限提升。漏洞编号分别为:CVE-2022-32250,该漏洞类型为释放重引用;CVE-2022-1972,该漏洞类型为越界读写;CVE-2022-34918,该漏洞类型为堆溢出。
二、相关介绍
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的容器,chain为rule的容器,rule为expression的容器,expression响应action。构造成由table->chain->rule->expression四级组成的数据结构。
三、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的创建过程。
四、相关漏洞分析
其实,回调客户端中还提供了其他元素的创建操作,比如创建set(集合)。set只需要依附于table即可,可构成table->set->expression数据结构。
该漏洞是释放重引用漏洞,出现在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()函数实现。