1

Linux 内核 nftables 子系统研究与漏洞分析

 1 year ago
source link: https://paper.seebug.org/1937/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

近期,开源安全社区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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK