Linux 内核 nftables 子系统研究与漏洞分析
2022-7-20 14:16:0 Author: paper.seebug.org(查看原文) 阅读量:25 收藏

作者:启明星辰ADLab
原文链接:https://mp.weixin.qq.com/s/ILyBUq--PK01TvNF8Vh9KQ

1 背景

近期,开源安全社区oss-security披露了多个Linux内核netfilter模块相关漏洞,漏洞均出现在netfilter子系统nftables中,其中两个漏洞在内核中存在多年,并且均可用于内核权限提升。漏洞编号分别为:CVE-2022-32250,该漏洞类型为释放重引用;CVE-2022-1972,该漏洞类型为越界读写;CVE-2022-34918,该漏洞类型为堆溢出。

2 相关介绍

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四级组成的数据结构。

3 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的创建过程。

4 相关漏洞分析

其实,回调客户端中还提供了其他元素的创建操作,比如创建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


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1937/


文章来源: https://paper.seebug.org/1937/
如有侵权请联系:admin#unsafe.sh